🏡 index : ~doyle/pisshoff.git

author Jordan Doyle <jordan@doyle.la> 2023-07-12 1:45:40.0 +00:00:00
committer Jordan Doyle <jordan@doyle.la> 2023-07-12 1:55:38.0 +00:00:00
commit
7b10c579e287b88c94b1f62f497b20214438d7b2 [patch]
tree
27db11804ca035ebb894cb1082a4caa8c6cda805
parent
76bbb3c268c1522e291d2e7282ff4a816825bb8c
download
7b10c579e287b88c94b1f62f497b20214438d7b2.tar.gz

Hack together support for legacy scp protocol



Diff

 Cargo.lock                             |   4 +-
 pisshoff-server/src/command.rs         |  55 ++++++-
 pisshoff-server/src/command/scp.rs     | 276 ++++++++++++++++++++++++++++++++++-
 pisshoff-server/src/server.rs          |  61 ++------
 pisshoff-server/src/subsystem/mod.rs   |   5 +-
 pisshoff-server/src/subsystem/sftp.rs  |  25 +--
 pisshoff-server/src/subsystem/shell.rs |  82 ++++++++++-
 pisshoff-types/Cargo.toml              |   1 +-
 pisshoff-types/src/audit.rs            |   3 +-
 9 files changed, 456 insertions(+), 56 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock
index e213c04..2cce4d9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -198,6 +198,9 @@ name = "bytes"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be"
dependencies = [
 "serde",
]

[[package]]
name = "cc"
@@ -1104,6 +1107,7 @@ dependencies = [
name = "pisshoff-types"
version = "0.1.0"
dependencies = [
 "bytes",
 "serde",
 "strum",
 "time",
diff --git a/pisshoff-server/src/command.rs b/pisshoff-server/src/command.rs
index b6f754f..8446ae6 100644
--- a/pisshoff-server/src/command.rs
+++ b/pisshoff-server/src/command.rs
@@ -1,6 +1,8 @@
pub mod scp;
pub mod uname;

use crate::server::Connection;
use crate::{command::scp::Scp, server::Connection};
use async_trait::async_trait;
use itertools::{Either, Itertools};
use std::{f32, fmt::Write, str::FromStr, time::Duration};
use thrussh::{server::Session, ChannelId};
@@ -10,9 +12,9 @@ pub async fn run_command(
    channel: ChannelId,
    session: &mut Session,
    conn: &mut Connection,
) {
) -> Option<ConcreteLongRunningCommand> {
    let Some(command) = args.get(0) else {
        return;
        return None;
    };

    match command.as_str() {
@@ -61,7 +63,7 @@ pub async fn run_command(
                    channel,
                    "-bash: cd: too many arguments\n".to_string().into(),
                );
                return;
                return None;
            }

            conn.file_system().cd(args.get(1).map(String::as_str));
@@ -85,6 +87,10 @@ pub async fn run_command(
            let out = uname::execute(&args[1..]);
            session.data(channel, out.into());
        }
        "scp" => match Scp::new(&args[1..], channel, session) {
            Ok(v) => return Some(ConcreteLongRunningCommand::Scp(v)),
            Err(e) => session.data(channel, e.to_string().into()),
        },
        other => {
            // TODO: fix stderr displaying out of order
            session.data(
@@ -93,6 +99,47 @@ pub async fn run_command(
            );
        }
    }

    None
}

#[async_trait]
pub trait LongRunningCommand: Sized {
    fn new(
        params: &[String],
        channel: ChannelId,
        session: &mut Session,
    ) -> Result<Self, &'static str>;

    async fn data(
        self,
        connection: &mut Connection,
        channel: ChannelId,
        data: &[u8],
        session: &mut Session,
    ) -> Option<Self>;
}

#[derive(Debug, Clone)]
pub enum ConcreteLongRunningCommand {
    Scp(Scp),
}

impl ConcreteLongRunningCommand {
    pub async fn data(
        self,
        connection: &mut Connection,
        channel: ChannelId,
        data: &[u8],
        session: &mut Session,
    ) -> Option<Self> {
        match self {
            Self::Scp(cmd) => cmd
                .data(connection, channel, data, session)
                .await
                .map(Self::Scp),
        }
    }
}

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
diff --git a/pisshoff-server/src/command/scp.rs b/pisshoff-server/src/command/scp.rs
new file mode 100644
index 0000000..9e47f33
--- /dev/null
+++ b/pisshoff-server/src/command/scp.rs
@@ -0,0 +1,276 @@
use crate::{
    command::{Arg, LongRunningCommand},
    server::Connection,
};
use async_trait::async_trait;
use bytes::{Buf, BytesMut};
use nom::{
    bytes::complete::{tag, take, take_until},
    character::complete::{digit1, u64},
    combinator::{map, map_res},
    IResult,
};
use pisshoff_types::audit::{AuditLogAction, WriteFileEvent};
use std::{path::PathBuf, str::FromStr};
use thrussh::{server::Session, ChannelId};
use tracing::warn;

const HELP: &str = "usage: scp [-346ABCOpqRrsTv] [-c cipher] [-D sftp_server_path] [-F ssh_config]
           [-i identity_file] [-J destination] [-l limit] [-o ssh_option]
           [-P port] [-S program] [-X sftp_option] source ... target\n";

const AMBIGUOUS_TARGET: &str = "scp: ambiguous target\n";

const SUCCESS: &str = "\0";

// https://web.archive.org/web/20170215184048/https://blogs.oracle.com/janp/entry/how_the_scp_protocol_works
#[derive(Debug, Clone)]
pub struct Scp {
    path: PathBuf,
    pending_data: BytesMut,
    state: State,
}

#[async_trait]
impl LongRunningCommand for Scp {
    fn new(
        params: &[String],
        channel: ChannelId,
        session: &mut Session,
    ) -> Result<Self, &'static str> {
        let mut path = None;
        let mut transfer = false;

        for param in super::argparse(params) {
            match param {
                Arg::Short('t') => {
                    transfer = true;
                }
                Arg::Short('r' | 'v') => {
                    // this is an allowed param, do nothing
                }
                Arg::Operand(p) => {
                    path = Some(p);
                }
                _ => {
                    return Err(HELP);
                }
            }
        }

        let Some(path) = path else {
            return Err(AMBIGUOUS_TARGET);
        };

        if !transfer {
            return Err(HELP);
        }

        // signal to the client we've started listening
        session.data(channel, SUCCESS.to_string().into());

        Ok(Self {
            path: PathBuf::new().join(path),
            pending_data: BytesMut::new(),
            state: State::Waiting,
        })
    }

    async fn data(
        mut self,
        connection: &mut Connection,
        channel: ChannelId,
        data: &[u8],
        session: &mut Session,
    ) -> Option<Self> {
        self.pending_data.extend_from_slice(data);

        let mut exit = false;
        while !self.pending_data.is_empty() && !exit {
            let next_state = match self.state {
                State::Waiting => {
                    match Receive::parse(&self.pending_data) {
                        Ok((rest, res)) => {
                            let mut state = State::Waiting;

                            match res {
                                Receive::FileCopy {
                                    length, file_name, ..
                                } => {
                                    state = State::ReceivingFile(length, self.path.join(file_name));
                                }
                                Receive::DirectoryCopy { directory_name, .. } => {
                                    self.path.push(directory_name);
                                }
                                Receive::EndDirectory => {
                                    self.path.pop();
                                }
                                Receive::AccessTime { .. } => {}
                            }

                            self.pending_data
                                .advance(self.pending_data.len() - rest.len());

                            // signal to the client we received their message and we're now listening for
                            // more data
                            session.data(channel, SUCCESS.to_string().into());

                            state
                        }
                        Err(error) => {
                            warn!(%error, "Rejecting scp modes payload");
                            return None;
                        }
                    }
                }
                State::ReceivingFile(length, path) => {
                    if self.pending_data.len() < length {
                        // keep waiting for more data...
                        exit = true;
                        State::ReceivingFile(length, path)
                    } else {
                        // we've received the whole file, lets print and start waiting again
                        let data = self.pending_data.split_to(length);

                        connection
                            .audit_log()
                            .push_action(AuditLogAction::WriteFile(WriteFileEvent {
                                path: Box::from(path.to_string_lossy().into_owned()),
                                content: data.freeze(),
                            }));

                        State::AwaitingSeparator
                    }
                }
                State::AwaitingSeparator => {
                    if self.pending_data.starts_with(&[0]) {
                        self.pending_data.advance(1);

                        // signal to the client we received their message and we're now listening for
                        // more data
                        session.data(channel, SUCCESS.to_string().into());
                    }

                    State::Waiting
                }
            };

            self.state = next_state;
        }

        Some(self)
    }
}

#[derive(Clone, Debug)]
enum State {
    Waiting,
    ReceivingFile(usize, PathBuf),
    AwaitingSeparator,
}

#[derive(Debug)]
#[allow(dead_code)]
enum Receive<'a> {
    FileCopy {
        mode: &'a str,
        length: usize,
        file_name: &'a str,
    },
    DirectoryCopy {
        mode: &'a str,
        length: u64,
        directory_name: &'a str,
    },
    EndDirectory,
    AccessTime {
        modified_time: u64,
        modified_time_micros: u64,
        access_time: u64,
        access_time_micros: u64,
    },
}

enum ReceiveType {
    FileCopy,
    DirectoryCopy,
    EndDirectory,
    AccessTime,
}

impl<'a> Receive<'a> {
    fn parse(rest: &'a [u8]) -> IResult<&'a [u8], Receive<'a>> {
        let (rest, typ) = nom::branch::alt((
            map(tag("C"), |_| ReceiveType::FileCopy),
            map(tag("D"), |_| ReceiveType::DirectoryCopy),
            map(tag("E"), |_| ReceiveType::EndDirectory),
            map(tag("T"), |_| ReceiveType::AccessTime),
        ))(rest)?;

        match typ {
            ReceiveType::FileCopy => {
                let (rest, mode) = map_res(take(4_usize), std::str::from_utf8)(rest)?;
                let (rest, _) = tag(" ")(rest)?;
                let (rest, length) =
                    map_res(map_res(digit1, std::str::from_utf8), usize::from_str)(rest)?;
                let (rest, _) = tag(" ")(rest)?;
                let (rest, file_name) = map_res(take_until("\n"), std::str::from_utf8)(rest)?;
                let (rest, _) = tag("\n")(rest)?;

                Ok((
                    rest,
                    Receive::FileCopy {
                        mode,
                        length,
                        file_name,
                    },
                ))
            }
            ReceiveType::DirectoryCopy => {
                let (rest, mode) = map_res(take(4_usize), std::str::from_utf8)(rest)?;
                let (rest, _) = tag(" ")(rest)?;
                let (rest, length) = u64(rest)?;
                let (rest, _) = tag(" ")(rest)?;
                let (rest, directory_name) = map_res(take_until("\n"), std::str::from_utf8)(rest)?;
                let (rest, _) = tag("\n")(rest)?;

                Ok((
                    rest,
                    Receive::DirectoryCopy {
                        mode,
                        length,
                        directory_name,
                    },
                ))
            }
            ReceiveType::EndDirectory => {
                let (rest, _) = tag("\n")(rest)?;
                Ok((rest, Receive::EndDirectory))
            }
            ReceiveType::AccessTime => {
                let (rest, modified_time) =
                    map_res(map_res(digit1, std::str::from_utf8), u64::from_str)(rest)?;
                let (rest, _) = tag(" ")(rest)?;
                let (rest, modified_time_micros) =
                    map_res(map_res(digit1, std::str::from_utf8), u64::from_str)(rest)?;
                let (rest, _) = tag(" ")(rest)?;
                let (rest, access_time) =
                    map_res(map_res(digit1, std::str::from_utf8), u64::from_str)(rest)?;
                let (rest, _) = tag(" ")(rest)?;
                let (rest, access_time_micros) =
                    map_res(map_res(digit1, std::str::from_utf8), u64::from_str)(rest)?;
                let (rest, _) = tag("\n")(rest)?;

                Ok((
                    rest,
                    Receive::AccessTime {
                        modified_time,
                        modified_time_micros,
                        access_time,
                        access_time_micros,
                    },
                ))
            }
        }
    }
}
diff --git a/pisshoff-server/src/server.rs b/pisshoff-server/src/server.rs
index 04109a2..e390492 100644
--- a/pisshoff-server/src/server.rs
+++ b/pisshoff-server/src/server.rs
@@ -4,15 +4,13 @@ use crate::{
        PtyRequestEvent, X11RequestEvent,
    },
    audit::{
        ExecCommandEvent, SignalEvent, SubsystemRequestEvent, TcpIpForwardEvent,
        WindowAdjustedEvent, WindowChangeRequestEvent,
        SignalEvent, SubsystemRequestEvent, TcpIpForwardEvent, WindowAdjustedEvent,
        WindowChangeRequestEvent,
    },
    command::run_command,
    config::Config,
    file_system::FileSystem,
    state::State,
    subsystem,
    subsystem::Subsystem as SubsystemTrait,
    subsystem::{self, shell::Shell, Subsystem as SubsystemTrait},
};
use futures::{
    future::{BoxFuture, InspectErr},
@@ -38,7 +36,6 @@ use tracing::{debug, error, info, info_span, instrument::Instrumented, Instrumen

pub static KEYBOARD_INTERACTIVE_PROMPT: &[(Cow<'static, str>, bool)] =
    &[(Cow::Borrowed("Password: "), false)];
pub const SHELL_PROMPT: &str = "bash-5.1$ ";

#[derive(Clone)]
pub struct Server {
@@ -107,6 +104,10 @@ impl Connection {
        self.file_system.as_mut().unwrap()
    }

    pub fn audit_log(&mut self) -> &mut AuditLog {
        &mut self.audit_log
    }

    fn try_login(&mut self, user: &str, password: &str) -> bool {
        self.username = Some(user.to_string());

@@ -255,15 +256,13 @@ impl thrussh::server::Handler for Connection {
        let _entered = span.enter();

        if self.subsystem.remove(&channel).is_some() {
            session.exit_status_request(channel, 0);
            session.channel_success(channel);
        } else {
            session.channel_failure(channel);
        }

        if self.subsystem.is_empty() {
            session.exit_status_request(channel, 0);
            session.close(channel);
        }
        session.close(channel);

        self.finished(session).boxed().wrap(Span::current())
    }
@@ -332,22 +331,11 @@ impl thrussh::server::Handler for Connection {
            let mut subsystem = subsystem.lock().await;

            match &mut *subsystem {
                Subsystem::Shell => {
                    let data = shlex::split(String::from_utf8_lossy(&data).as_ref());
                    if let Some(args) = data {
                        run_command(&args, channel, &mut session, &mut self).await;
                        self.audit_log
                            .push_action(AuditLogAction::ExecCommand(ExecCommandEvent {
                                args: Box::from(args),
                            }));
                    }

                    session.data(channel, SHELL_PROMPT.to_string().into());
                Subsystem::Shell(ref mut inner) => {
                    inner.data(&mut self, channel, &data, &mut session).await;
                }
                Subsystem::Sftp(ref mut inner) => {
                    inner
                        .data(&mut self.audit_log, channel, &data, &mut session)
                        .await;
                    inner.data(&mut self, channel, &data, &mut session).await;
                }
            }

@@ -476,9 +464,9 @@ impl thrussh::server::Handler for Connection {

        self.audit_log.push_action(AuditLogAction::ShellRequested);

        session.data(channel, SHELL_PROMPT.to_string().into());
        let shell = Shell::new(true, channel, &mut session);
        self.subsystem
            .insert(channel, Arc::new(Mutex::new(Subsystem::Shell)));
            .insert(channel, Arc::new(Mutex::new(Subsystem::Shell(shell))));

        session.channel_success(channel);
        self.finished(session).boxed().wrap(Span::current())
@@ -493,21 +481,16 @@ impl thrussh::server::Handler for Connection {
        let span = info_span!(parent: &self.span, "exec_request");
        let _entered = span.enter();

        let data = shlex::split(String::from_utf8_lossy(data).as_ref());
        let data = data.to_vec();

        async move {
            if let Some(args) = data {
                run_command(&args, channel, &mut session, &mut self).await;
                self.audit_log
                    .push_action(AuditLogAction::ExecCommand(ExecCommandEvent {
                        args: Box::from(args),
                    }));

                session.channel_success(channel);
            } else {
                session.channel_failure(channel);
            }
            let mut shell = Shell::new(false, channel, &mut session);
            shell.data(&mut self, channel, &data, &mut session).await;

            self.subsystem
                .insert(channel, Arc::new(Mutex::new(Subsystem::Shell(shell))));

            session.channel_success(channel);
            self.finished(session).await
        }
        .boxed()
@@ -639,7 +622,7 @@ impl Drop for Connection {

#[derive(Debug, Clone)]
pub enum Subsystem {
    Shell,
    Shell(subsystem::shell::Shell),
    Sftp(subsystem::sftp::Sftp),
}

diff --git a/pisshoff-server/src/subsystem/mod.rs b/pisshoff-server/src/subsystem/mod.rs
index 55e9bda..395a06e 100644
--- a/pisshoff-server/src/subsystem/mod.rs
+++ b/pisshoff-server/src/subsystem/mod.rs
@@ -1,9 +1,10 @@
use crate::server::Connection;
use async_trait::async_trait;
use pisshoff_types::audit::AuditLog;
use thrussh::server::Session;
use thrussh::ChannelId;

pub mod sftp;
pub mod shell;

#[async_trait]
pub trait Subsystem {
@@ -11,7 +12,7 @@ pub trait Subsystem {

    async fn data(
        &mut self,
        audit_log: &mut AuditLog,
        connection: &mut Connection,
        channel: ChannelId,
        data: &[u8],
        session: &mut Session,
diff --git a/pisshoff-server/src/subsystem/sftp.rs b/pisshoff-server/src/subsystem/sftp.rs
index 9c12069..83d5370 100644
--- a/pisshoff-server/src/subsystem/sftp.rs
+++ b/pisshoff-server/src/subsystem/sftp.rs
@@ -1,5 +1,6 @@
use crate::subsystem::Subsystem;
use crate::{server::Connection, subsystem::Subsystem};
use async_trait::async_trait;
use bytes::Bytes;
use nom::{
    bytes::complete::take,
    combinator::{map_res, opt},
@@ -7,7 +8,7 @@ use nom::{
    number::complete::{be_u32, be_u64, be_u8},
    IResult,
};
use pisshoff_types::audit::{AuditLog, AuditLogAction, MkdirEvent, WriteFileEvent};
use pisshoff_types::audit::{AuditLogAction, MkdirEvent, WriteFileEvent};
use std::{collections::HashMap, io::Write, mem::size_of, str::FromStr};
use strum::FromRepr;
use thrussh::{server::Session, ChannelId};
@@ -28,7 +29,7 @@ impl Subsystem for Sftp {
    #[allow(clippy::too_many_lines)]
    async fn data(
        &mut self,
        audit_log: &mut AuditLog,
        connection: &mut Connection,
        channel: ChannelId,
        data: &[u8],
        session: &mut Session,
@@ -128,10 +129,12 @@ impl Subsystem for Sftp {
                        write_packet.offset, write_packet.data
                    );

                    audit_log.push_action(AuditLogAction::WriteFile(WriteFileEvent {
                        path: path.to_string().into_boxed_str(),
                        content: write_packet.data.to_string().into_boxed_str(),
                    }));
                    connection
                        .audit_log()
                        .push_action(AuditLogAction::WriteFile(WriteFileEvent {
                            path: path.to_string().into_boxed_str(),
                            content: Bytes::copy_from_slice(write_packet.data.as_bytes()),
                        }));

                    session.data(
                        channel,
@@ -205,9 +208,11 @@ impl Subsystem for Sftp {

                    trace!("SFTP mkdir packet: {mkdir:?}");

                    audit_log.push_action(AuditLogAction::Mkdir(MkdirEvent {
                        path: mkdir.path.to_string().into_boxed_str(),
                    }));
                    connection
                        .audit_log()
                        .push_action(AuditLogAction::Mkdir(MkdirEvent {
                            path: mkdir.path.to_string().into_boxed_str(),
                        }));

                    session.data(
                        channel,
diff --git a/pisshoff-server/src/subsystem/shell.rs b/pisshoff-server/src/subsystem/shell.rs
new file mode 100644
index 0000000..0776c34
--- /dev/null
+++ b/pisshoff-server/src/subsystem/shell.rs
@@ -0,0 +1,82 @@
use crate::{
    command::{run_command, ConcreteLongRunningCommand},
    server::Connection,
    subsystem::Subsystem,
};
use async_trait::async_trait;
use pisshoff_types::audit::{AuditLogAction, ExecCommandEvent};
use thrussh::{server::Session, ChannelId};

pub const SHELL_PROMPT: &str = "bash-5.1$ ";

#[derive(Clone, Debug)]
pub struct Shell {
    interactive: bool,
    state: State,
}

impl Shell {
    pub fn new(interactive: bool, channel: ChannelId, session: &mut Session) -> Self {
        if interactive {
            session.data(channel, SHELL_PROMPT.to_string().into());
        }

        Self {
            interactive,
            state: State::Prompt,
        }
    }
}

#[async_trait]
impl Subsystem for Shell {
    const NAME: &'static str = "shell";

    async fn data(
        &mut self,
        connection: &mut Connection,
        channel: ChannelId,
        data: &[u8],
        session: &mut Session,
    ) {
        let next = match std::mem::take(&mut self.state) {
            State::Prompt => {
                let Some(args) = shlex::split(String::from_utf8_lossy(data).as_ref()) else {
                    return;
                };

                connection
                    .audit_log()
                    .push_action(AuditLogAction::ExecCommand(ExecCommandEvent {
                        args: Box::from(args.clone()),
                    }));

                run_command(&args, channel, session, connection)
                    .await
                    .map_or(State::Prompt, State::Running)
            }
            State::Running(command) => command
                .data(connection, channel, data, session)
                .await
                .map_or(State::Prompt, State::Running),
        };

        if matches!(next, State::Prompt) {
            if self.interactive {
                session.data(channel, SHELL_PROMPT.to_string().into());
            } else {
                session.exit_status_request(channel, 0);
                session.close(channel);
            }
        }

        self.state = next;
    }
}

#[derive(Debug, Clone, Default)]
enum State {
    #[default]
    Prompt,
    Running(ConcreteLongRunningCommand),
}
diff --git a/pisshoff-types/Cargo.toml b/pisshoff-types/Cargo.toml
index e224dc1..1987279 100644
--- a/pisshoff-types/Cargo.toml
+++ b/pisshoff-types/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
bytes = { version = "1.4", features = ["serde"] }
uuid = "1.3"
time = { version = "0.3", features = ["serde", "formatting", "parsing"] }
serde = { version = "1.0", features = ["derive"] }
diff --git a/pisshoff-types/src/audit.rs b/pisshoff-types/src/audit.rs
index 834abbc..99dcad0 100644
--- a/pisshoff-types/src/audit.rs
+++ b/pisshoff-types/src/audit.rs
@@ -1,3 +1,4 @@
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::{
@@ -92,7 +93,7 @@ pub struct MkdirEvent {
#[derive(Debug, Serialize, Deserialize)]
pub struct WriteFileEvent {
    pub path: Box<str>,
    pub content: Box<str>,
    pub content: Bytes,
}

#[derive(Debug, Serialize, Deserialize)]