From a9e2edc572938585289f432db502abc7fe9eb316 Mon Sep 17 00:00:00 2001 From: Noratrieb <48135649+Noratrieb@users.noreply.github.com> Date: Fri, 30 Aug 2024 18:44:09 +0200 Subject: [PATCH] bootstrap SFTP subsystem --- Cargo.lock | 20 ++++++++++++++ bin/cluelessh-faked/src/main.rs | 5 ++++ bin/cluelesshd-sftp-server/Cargo.toml | 12 +++++++++ bin/cluelesshd-sftp-server/README.md | 3 +++ bin/cluelesshd-sftp-server/src/main.rs | 17 ++++++++++++ bin/cluelesshd/cluelesshd.toml | 4 +++ bin/cluelesshd/src/config.rs | 35 +++++++++++++++++++++--- bin/cluelesshd/src/connection.rs | 24 ++++++++++++++--- bin/cluelesshd/src/main.rs | 11 +++++--- bin/cluelesshd/src/rpc.rs | 37 +++++++++++++++++++++----- lib/cluelessh-connection/src/lib.rs | 20 ++++++++++++++ lib/cluelessh-sftp-proto/Cargo.toml | 10 +++++++ lib/cluelessh-sftp-proto/src/lib.rs | 1 + lib/cluelessh-sftp/Cargo.toml | 9 +++++++ lib/cluelessh-sftp/src/lib.rs | 14 ++++++++++ 15 files changed, 205 insertions(+), 17 deletions(-) create mode 100644 bin/cluelesshd-sftp-server/Cargo.toml create mode 100644 bin/cluelesshd-sftp-server/README.md create mode 100644 bin/cluelesshd-sftp-server/src/main.rs create mode 100644 lib/cluelessh-sftp-proto/Cargo.toml create mode 100644 lib/cluelessh-sftp-proto/src/lib.rs create mode 100644 lib/cluelessh-sftp/Cargo.toml create mode 100644 lib/cluelessh-sftp/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index a6fcda1..a21c207 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -422,6 +422,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "cluelessh-sftp" +version = "0.1.0" + +[[package]] +name = "cluelessh-sftp-proto" +version = "0.1.0" +dependencies = [ + "cluelessh-transport", +] + [[package]] name = "cluelessh-tokio" version = "0.1.0" @@ -489,6 +500,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "cluelesshd-sftp-server" +version = "0.1.0" +dependencies = [ + "eyre", + "tracing", + "tracing-subscriber", +] + [[package]] name = "cobs" version = "0.2.3" diff --git a/bin/cluelessh-faked/src/main.rs b/bin/cluelessh-faked/src/main.rs index a10d351..7ed9b10 100644 --- a/bin/cluelessh-faked/src/main.rs +++ b/bin/cluelessh-faked/src/main.rs @@ -230,6 +230,11 @@ async fn handle_session_channel( channel.send(ChannelOperationKind::Eof).await?; channel.send(ChannelOperationKind::Close).await?; } + ChannelRequest::Subsystem { want_reply, .. } => { + if want_reply { + channel.send(ChannelOperationKind::Failure).await?; + } + } ChannelRequest::ExitStatus { .. } => {} ChannelRequest::Env { .. } => {} }; diff --git a/bin/cluelesshd-sftp-server/Cargo.toml b/bin/cluelesshd-sftp-server/Cargo.toml new file mode 100644 index 0000000..4012d5f --- /dev/null +++ b/bin/cluelesshd-sftp-server/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "cluelesshd-sftp-server" +version = "0.1.0" +edition = "2021" + +[dependencies] +eyre.workspace = true +tracing.workspace = true +tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } + +[lints] +workspace = true diff --git a/bin/cluelesshd-sftp-server/README.md b/bin/cluelesshd-sftp-server/README.md new file mode 100644 index 0000000..a7384cd --- /dev/null +++ b/bin/cluelesshd-sftp-server/README.md @@ -0,0 +1,3 @@ +# cluelesshd-sftp-server + +SFTP server for cluelesshd. diff --git a/bin/cluelesshd-sftp-server/src/main.rs b/bin/cluelesshd-sftp-server/src/main.rs new file mode 100644 index 0000000..0ec0e9b --- /dev/null +++ b/bin/cluelesshd-sftp-server/src/main.rs @@ -0,0 +1,17 @@ +use eyre::Result; +use tracing::info; +use tracing_subscriber::EnvFilter; + +fn main() -> Result<()> { + let env_filter = + EnvFilter::try_from_env("SFTP_LOG").unwrap_or_else(|_| EnvFilter::new("debug")); + + tracing_subscriber::fmt() + .with_writer(std::io::stderr) + .with_env_filter(env_filter) + .init(); + + info!("mroooow!"); + + Ok(()) +} diff --git a/bin/cluelesshd/cluelesshd.toml b/bin/cluelesshd/cluelesshd.toml index 3ce6d0a..dc8b342 100644 --- a/bin/cluelesshd/cluelesshd.toml +++ b/bin/cluelesshd/cluelesshd.toml @@ -1,5 +1,9 @@ log_level = "info" + +[subsystem.sftp] +path = "../../target/debug/cluelesshd-sftp-server" + [net] ip = "0.0.0.0" port = 2223 diff --git a/bin/cluelesshd/src/config.rs b/bin/cluelesshd/src/config.rs index f6fa339..8d55f0e 100644 --- a/bin/cluelesshd/src/config.rs +++ b/bin/cluelesshd/src/config.rs @@ -1,12 +1,15 @@ use eyre::{Context, Result}; use serde::{Deserialize, Serialize}; use std::{ + collections::HashMap, net::{IpAddr, Ipv4Addr}, path::PathBuf, }; use crate::Args; +// TODO: validate config and user nicer structs to consume it + #[derive(Clone, Serialize, Deserialize)] #[serde(deny_unknown_fields)] pub struct Config { @@ -15,6 +18,8 @@ pub struct Config { pub net: NetConfig, pub auth: AuthConfig, pub security: SecurityConfig, + #[serde(default)] + pub subsystem: HashMap, } #[derive(Clone, Serialize, Deserialize)] @@ -52,8 +57,21 @@ pub struct SecurityConfig { pub experimental_seccomp: bool, } +/// Add arbitrary subsystems. +/// # Subsystem Protocol +/// Every subsystem process gets spawned in the home directory of the user, as the user. +/// Several FDs are guaranteed to be open. +/// - stdin (0): data from the client channel +/// - stdout (1): data to the client channel +/// - stderr (2): data to the client channel extended stderr (used for debugging) +#[derive(Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct SubsystemConfig { + pub path: PathBuf, +} + impl Config { - pub fn find(args: &Args) -> Result { + pub fn load(args: &Args) -> Result { let path = std::env::var("CLUELESSHD_CONFIG") .map(PathBuf::from) .or(args.config.clone().ok_or(std::env::VarError::NotPresent)) @@ -63,8 +81,19 @@ impl Config { format!("failed to open config file '{}', refusing to start. you can change the config file path with the --config arg or the CLUELESSHD_CONFIG environment variable", path.display()) })?; - toml::from_str(&content) - .wrap_err_with(|| format!("invalid config file '{}'", path.display())) + let mut config: Config = toml::from_str(&content) + .wrap_err_with(|| format!("invalid config file '{}'", path.display()))?; + + for sub in config.subsystem.values_mut() { + sub.path = sub.path.canonicalize().wrap_err_with(|| { + format!( + "error canonicalizing subsystem path: {}", + sub.path.display() + ) + })?; + } + + Ok(config) } } diff --git a/bin/cluelesshd/src/connection.rs b/bin/cluelesshd/src/connection.rs index 07a1296..ba57dea 100644 --- a/bin/cluelesshd/src/connection.rs +++ b/bin/cluelesshd/src/connection.rs @@ -292,7 +292,7 @@ impl SessionState { } } } - ChannelRequest::Shell { want_reply } => match self.shell(None).await { + ChannelRequest::Shell { want_reply } => match self.shell(None, None).await { Ok(()) => { if want_reply { self.channel.send(ChannelOperationKind::Success).await?; @@ -309,7 +309,7 @@ impl SessionState { want_reply, command, } => match String::from_utf8(command) { - Ok(command) => match self.shell(Some(&command)).await { + Ok(command) => match self.shell(Some(command), None).await { Ok(()) => { if want_reply { self.channel.send(ChannelOperationKind::Success).await?; @@ -330,6 +330,21 @@ impl SessionState { } } }, + ChannelRequest::Subsystem { want_reply, name } => { + match self.shell(None, Some(name)).await { + Ok(()) => { + if want_reply { + self.channel.send(ChannelOperationKind::Success).await?; + } + } + Err(err) => { + debug!(%err, "Failed to spawn subsystem"); + if want_reply { + self.channel.send(ChannelOperationKind::Failure).await?; + } + } + } + } ChannelRequest::Env { name, value, @@ -394,11 +409,12 @@ impl SessionState { Ok(()) } - async fn shell(&mut self, shell_command: Option<&str>) -> Result<()> { + async fn shell(&mut self, shell_command: Option, subsystem: Option) -> Result<()> { let mut fds = self .rpc_client .shell( - shell_command.map(ToOwned::to_owned), + shell_command, + subsystem, self.pty_term.clone(), self.envs.clone(), ) diff --git a/bin/cluelesshd/src/main.rs b/bin/cluelesshd/src/main.rs index 7fb7acb..12143d7 100644 --- a/bin/cluelesshd/src/main.rs +++ b/bin/cluelesshd/src/main.rs @@ -25,7 +25,7 @@ use eyre::{bail, eyre, Context, Result}; use rustix::fs::MemfdFlags; use serde::{Deserialize, Serialize}; use tokio::net::{TcpListener, TcpStream}; -use tracing::{error, info, warn}; +use tracing::{ error, info, warn}; use tracing_subscriber::EnvFilter; @@ -51,10 +51,14 @@ fn main() -> eyre::Result<()> { // Initial setup let args = Args::parse(); - let config = config::Config::find(&args)?; + let config = config::Config::load(&args)?; setup_tracing(&config); + for (name, system) in &config.subsystem { + info!(%name, path = %system.path.display(), "Loaded subsystem"); + } + if !rustix::process::getuid().is_root() { warn!("Daemon not started as root. This disables several security mitigations and permits logging in as any other user"); } @@ -197,7 +201,8 @@ async fn spawn_connection_child( ) -> Result<()> { let stream_fd = stream.as_raw_fd(); - let mut rpc_server = rpc::Server::new(host_keys).wrap_err("creating RPC server")?; + let mut rpc_server = + rpc::Server::new(config.clone(), host_keys).wrap_err("creating RPC server")?; let rpc_client_fd = rpc_server.client_fd().as_raw_fd(); diff --git a/bin/cluelesshd/src/rpc.rs b/bin/cluelesshd/src/rpc.rs index 982605b..eedc2cf 100644 --- a/bin/cluelesshd/src/rpc.rs +++ b/bin/cluelesshd/src/rpc.rs @@ -41,6 +41,8 @@ use users::os::unix::UserExt; use users::User; use zeroize::Zeroizing; +use crate::config::Config; + #[derive(Debug, Serialize, Deserialize)] enum Request { /// Performs the key exchange by generating a private key, deriving the shared secret, @@ -136,6 +138,7 @@ struct ShellRequest { /// Whether a PTY is used and if yes, the TERM env var. pty_term: Option, command: Option, + subsystem: Option, env: Vec<(String, String)>, } @@ -162,17 +165,20 @@ pub struct Server { host_keys: Vec, authenticated_user: Option, + config: Config, + pty_user: Option, shell_process: Option, } impl Server { - pub fn new(host_keys: Vec) -> Result { + pub fn new(config: Config, host_keys: Vec) -> Result { let (server, client) = UnixDatagram::pair().wrap_err("creating socketpair")?; Ok(Self { server, client, + config, host_keys, authenticated_user: None, pty_user: None, @@ -366,13 +372,28 @@ impl Server { } async fn shell(&mut self, user: &User, req: ShellRequest) -> Result> { + let subsystem = match req.subsystem.as_deref() { + Some(subsystem) => match self.config.subsystem.get(subsystem) { + Some(system) => Some(system.path.clone()), + None => bail!("unsupported subsystem: {subsystem}"), + }, + None => None, + }; + let shell = user.shell(); - let mut cmd = Command::new(shell); - if let Some(shell_command) = req.command { - cmd.arg("-c"); - cmd.arg(shell_command); - } + let cmd_arg0 = subsystem.as_deref().unwrap_or(shell); + + // TODO: the SSH RFC mentions subsystems going through shell... should we? + let mut cmd = Command::new(cmd_arg0); + + if subsystem.is_none() { + if let Some(shell_command) = req.command { + cmd.arg("-c"); + cmd.arg(shell_command); + } + }; + cmd.env_clear(); let has_pty = req.pty_term.is_some(); @@ -404,7 +425,7 @@ impl Server { cmd.env(k, v); } - debug!(cmd = %shell.display(), uid = %user.uid(), gid = %user.primary_group_id(), "Executing process"); + debug!(cmd = %cmd_arg0.display(), uid = %user.uid(), gid = %user.primary_group_id(), "Executing process"); let mut shell = cmd.spawn()?; @@ -533,12 +554,14 @@ impl Client { pub async fn shell( &self, command: Option, + subsystem: Option, pty_term: Option, env: Vec<(String, String)>, ) -> Result> { self.send_request(&Request::Shell(ShellRequest { pty_term, command, + subsystem, env, })) .await?; diff --git a/lib/cluelessh-connection/src/lib.rs b/lib/cluelessh-connection/src/lib.rs index 0d7fa3f..89f0f28 100644 --- a/lib/cluelessh-connection/src/lib.rs +++ b/lib/cluelessh-connection/src/lib.rs @@ -105,6 +105,11 @@ pub enum ChannelRequest { command: Vec, }, + Subsystem { + want_reply: bool, + + name: String, + }, Env { want_reply: bool, @@ -471,6 +476,19 @@ impl ChannelsState { command: command.to_owned(), } } + "subsystem" => { + if !self.is_server { + return Err(peer_error!("server tried to set environment var")); + } + + let name = p.utf8_string()?; + + info!(channel = %our_channel, %name, "Starting subsystem"); + ChannelRequest::Subsystem { + want_reply, + name: name.to_owned(), + } + } "env" => { if !self.is_server { return Err(peer_error!("server tried to set environment var")); @@ -632,6 +650,7 @@ impl ChannelsState { Packet::new_msg_channel_request_shell(peer, b"shell", want_reply) } ChannelRequest::Exec { .. } => todo!("exec"), + ChannelRequest::Subsystem { .. } => todo!("subsystem"), ChannelRequest::Env { .. } => todo!("env"), ChannelRequest::ExitStatus { status } => { Packet::new_msg_channel_request_exit_status( @@ -787,6 +806,7 @@ impl ChannelOperation { ChannelRequest::PtyReq { .. } => "pty-req", ChannelRequest::Shell { .. } => "shell", ChannelRequest::Exec { .. } => "exec", + ChannelRequest::Subsystem { .. } => "subsystem", ChannelRequest::Env { .. } => "env", ChannelRequest::ExitStatus { .. } => "exit-status", }, diff --git a/lib/cluelessh-sftp-proto/Cargo.toml b/lib/cluelessh-sftp-proto/Cargo.toml new file mode 100644 index 0000000..672268e --- /dev/null +++ b/lib/cluelessh-sftp-proto/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "cluelessh-sftp-proto" +version = "0.1.0" +edition = "2021" + +[dependencies] +cluelessh-transport = { path = "../cluelessh-transport" } + +[lints] +workspace = true diff --git a/lib/cluelessh-sftp-proto/src/lib.rs b/lib/cluelessh-sftp-proto/src/lib.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/lib/cluelessh-sftp-proto/src/lib.rs @@ -0,0 +1 @@ + diff --git a/lib/cluelessh-sftp/Cargo.toml b/lib/cluelessh-sftp/Cargo.toml new file mode 100644 index 0000000..a0d9a1e --- /dev/null +++ b/lib/cluelessh-sftp/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "cluelessh-sftp" +version = "0.1.0" +edition = "2021" + +[dependencies] + +[lints] +workspace = true diff --git a/lib/cluelessh-sftp/src/lib.rs b/lib/cluelessh-sftp/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/lib/cluelessh-sftp/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +}