sh4d0wup 0.11.0

Signing-key abuse and update exploitation framework
Documentation
use crate::args;
use crate::errors::*;
use crate::httpd;
use crate::keygen::EmbeddedKey;
use crate::plot;
use crate::plot::Cmd;
use crate::utils;
use nix::sched::CloneFlags;
use nix::sys::wait::{WaitPidFlag, WaitStatus};
use std::ffi::OsStr;
use std::fmt;
use std::net::SocketAddr;
use std::process::Stdio;
use std::sync::Arc;
use tokio::fs;
use tokio::io::AsyncWriteExt;
use tokio::net::TcpListener;
use tokio::net::TcpStream;
use tokio::process::Command;
use tokio::signal;
use tokio::time::{Duration, sleep};

const PODMAN_BINARY: &str = utils::compile_env!("SH4D0WUP_PODMAN_BINARY", "podman");

pub async fn wait_for_server(addr: &SocketAddr) -> Result<()> {
    debug!("Waiting for server to start up...");
    for _ in 0..5 {
        sleep(Duration::from_millis(100)).await;
        if TcpStream::connect(addr).await.is_ok() {
            debug!("Successfully connected to tcp port");
            return Ok(());
        }
    }
    bail!("Failed to connect to server");
}

pub fn test_userns_clone() -> Result<()> {
    let cb = Box::new(|| 0);
    let stack = &mut [0; 1024];
    let flags = CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWUSER;

    let pid = unsafe { nix::sched::clone(cb, stack, flags, None) }
        .context("Failed to create user namespace")?;
    let status = nix::sys::wait::waitpid(pid, Some(WaitPidFlag::__WCLONE))
        .context("Failed to reap child")?;

    if status != WaitStatus::Exited(pid, 0) {
        bail!("Unexpected wait result: {:?}", status);
    }

    Ok(())
}

pub async fn test_for_unprivileged_userns_clone() -> Result<()> {
    debug!("Testing if user namespaces can be created");
    if let Err(err) = test_userns_clone() {
        match fs::read("/proc/sys/kernel/unprivileged_userns_clone").await {
            Ok(buf) => {
                if buf == b"0\n" {
                    warn!(
                        "User namespaces are not enabled in /proc/sys/kernel/unprivileged_userns_clone"
                    )
                }
            }
            Err(err) => warn!(
                "Failed to check if unprivileged_userns_clone are allowed: {:#}",
                err
            ),
        }

        Err(err)
    } else {
        debug!("Successfully tested for user namespaces");
        Ok(())
    }
}

pub async fn podman<I, S>(args: I, capture_stdout: bool, stdin: Option<&[u8]>) -> Result<Vec<u8>>
where
    I: IntoIterator<Item = S>,
    S: AsRef<OsStr> + fmt::Debug,
{
    let mut cmd = Command::new(PODMAN_BINARY);
    let args = args.into_iter().collect::<Vec<_>>();
    cmd.args(&args);
    if stdin.is_some() {
        cmd.stdin(Stdio::piped());
    }
    if capture_stdout {
        cmd.stdout(Stdio::piped());
    }
    debug!("Spawning child process: podman {:?}", args);
    let mut child = cmd
        .spawn()
        .with_context(|| anyhow!("Failed to execute podman binary: {PODMAN_BINARY:?}"))?;

    if let Some(data) = stdin {
        debug!("Sending {} bytes to child process...", data.len());
        let mut stdin = child.stdin.take().unwrap();
        stdin.write_all(data).await?;
        stdin.flush().await?;
    }

    let out = child.wait_with_output().await?;
    debug!("Podman command exited: {:?}", out.status);
    if !out.status.success() {
        bail!(
            "Podman command ({:?}) failed to execute: {:?}",
            args,
            out.status
        );
    }
    Ok(out.stdout)
}

#[derive(Debug)]
pub struct Container {
    id: String,
    addr: SocketAddr,
}

impl Container {
    pub async fn create(
        image: &str,
        init: &[String],
        addr: SocketAddr,
        expose_fuse: bool,
    ) -> Result<Container> {
        let bin = init
            .first()
            .context("Command for container can't be empty")?;
        let cmd_args = &init[1..];
        let entrypoint = format!("--entrypoint={bin}");
        let mut podman_args = vec![
            "container",
            "run",
            "--detach",
            "--rm",
            "--network=host",
            "-v=/usr/bin/catatonit:/__:ro",
        ];
        if expose_fuse {
            debug!("Mapping /dev/fuse into the container");
            podman_args.push("--device=/dev/fuse");
        }

        podman_args.extend([&entrypoint, "--", image]);
        podman_args.extend(cmd_args.iter().map(|s| s.as_str()));

        let mut out = podman(&podman_args, true, None).await?;
        if let Some(idx) = memchr::memchr(b'\n', &out) {
            out.truncate(idx);
        }
        let id = String::from_utf8(out)?;
        Ok(Container { id, addr })
    }

    pub async fn exec<I, S>(
        &self,
        args: I,
        stdin: Option<&[u8]>,
        env: &[String],
        user: Option<String>,
    ) -> Result<()>
    where
        I: IntoIterator<Item = S>,
        S: AsRef<str> + fmt::Debug + Clone,
    {
        let args = args.into_iter().collect::<Vec<_>>();
        let mut a = vec!["container".to_string(), "exec".to_string()];
        if let Some(user) = user {
            a.extend(["-u".to_string(), user]);
        }
        if stdin.is_some() {
            a.push("-i".to_string());
        }
        for env in env {
            a.push(format!("-e={env}"));
        }
        a.extend(["--".to_string(), self.id.to_string()]);
        a.extend(args.iter().map(|x| x.as_ref().to_string()));
        podman(&a, false, stdin)
            .await
            .with_context(|| anyhow!("Failed to execute in container: {:?}", args))?;
        Ok(())
    }

    pub async fn kill(self) -> Result<()> {
        podman(&["container", "kill", &self.id], true, None)
            .await
            .context("Failed to remove container")?;
        Ok(())
    }

    pub async fn exec_cmd_stdin(
        &self,
        cmd: &Cmd,
        stdin: Option<&[u8]>,
        user: Option<String>,
    ) -> Result<()> {
        let args = match cmd {
            Cmd::Shell(cmd) => vec!["sh", "-c", cmd],
            Cmd::Exec(cmd) => cmd.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
        };
        info!("Executing process in container: {:?}", args);
        self.exec(
            &args,
            stdin,
            &[
                format!("SH4D0WUP_BOUND_ADDR={}", self.addr),
                format!("SH4D0WUP_BOUND_IP={}", self.addr.ip()),
                format!("SH4D0WUP_BOUND_PORT={}", self.addr.port()),
            ],
            user,
        )
        .await
        .map_err(|err| {
            error!("Command failed: {:#}", err);
            err
        })
        .context("Command failed")?;
        Ok(())
    }

    pub async fn exec_cmd(&self, cmd: &Cmd, user: Option<String>) -> Result<()> {
        self.exec_cmd_stdin(cmd, None, user).await
    }

    pub async fn run_check(
        &self,
        config: &plot::Check,
        plot_extras: &plot::PlotExtras,
        tls: Option<&httpd::Tls>,
        keep: bool,
    ) -> Result<()> {
        info!("Finishing setup in container...");
        if let (Some(tls), Some(cmd)) = (tls, &config.install_certs) {
            info!("Installing certificates...");
            self.exec_cmd_stdin(cmd, Some(&tls.cert), Some("0".to_string()))
                .await
                .context("Failed to install certificates")?;
        }
        for install in &config.install_keys {
            info!("Installing key {:?} with {:?}...", install.key, install.cmd);
            let key = plot_extras
                .signing_keys
                .get(&install.key)
                .context("Invalid reference to signing key")?;

            let cert = match key {
                EmbeddedKey::Pgp(pgp) => pgp.to_cert(install.binary)?,
                EmbeddedKey::Ssh(ssh) => ssh.to_cert()?,
                EmbeddedKey::Openssl(openssl) => openssl.to_cert(install.binary)?,
                EmbeddedKey::InToto(_in_toto) => {
                    bail!("Installing in-toto keys into the container isn't supported yet")
                }
            };

            self.exec_cmd_stdin(&install.cmd, Some(&cert), None)
                .await
                .context("Failed to install certificates")?;
        }
        for host in &config.register_hosts {
            info!(
                "Installing /etc/hosts entry, {:?} => {}",
                host,
                self.addr.ip()
            );
            let cmd = format!("echo \"{} {}\" >> /etc/hosts", self.addr.ip(), host);
            self.exec_cmd(&Cmd::Shell(cmd), Some("0".to_string()))
                .await
                .context("Failed to register /etc/hosts entry")?;
        }

        info!("Starting test...");
        for cmd in &config.cmds {
            self.exec_cmd(cmd, None)
                .await
                .context("Attack failed to execute on test environment")?;
        }
        info!("Test completed successfully");

        if keep {
            info!("Keeping container around until ^C...");
            futures::future::pending().await
        } else {
            Ok(())
        }
    }
}

pub async fn run(
    addr: SocketAddr,
    check: args::Check,
    tls: Option<&httpd::Tls>,
    ctx: Arc<plot::Ctx>,
) -> Result<()> {
    let check_config = ctx
        .plot
        .check
        .as_ref()
        .context("No test configured in this plot")?;
    wait_for_server(&addr).await?;

    let image = &check_config.image;
    let init = check_config
        .init
        .clone()
        .unwrap_or_else(|| vec!["/__".to_string(), "-P".to_string()]);

    if check.pull
        || podman(&["image", "exists", "--", image], false, None)
            .await
            .is_err()
    {
        info!("Pulling container image...");
        podman(&["image", "pull", "--", image], false, None).await?;
    }

    info!("Creating container...");
    let container = Container::create(image, &init, addr, check_config.expose_fuse).await?;
    let container_id = container.id.clone();
    let result = tokio::select! {
        result = container.run_check(check_config, &ctx.extras, tls, check.keep) => result,
        _ = signal::ctrl_c() => Err(anyhow!("Ctrl-c received")),
    };
    info!("Removing container...");
    if let Err(err) = container.kill().await {
        warn!("Failed to kill container {:?}: {:#}", container_id, err);
    }
    info!("Cleanup complete");

    result
}

pub async fn spawn(check: args::Check, ctx: plot::Ctx) -> Result<()> {
    test_for_unprivileged_userns_clone().await?;

    let addr = if let Some(addr) = check.bind {
        addr
    } else {
        let sock = TcpListener::bind("127.0.0.1:0").await?;
        sock.local_addr()?
    };

    let tls = if let Some(tls) = ctx.plot.tls.clone() {
        Some(httpd::Tls::try_from(tls)?)
    } else {
        None
    };

    let ctx = Arc::new(ctx);
    let httpd = httpd::run(addr, tls.clone(), ctx.clone());
    let check = run(addr, check, tls.as_ref(), ctx);

    tokio::select! {
        httpd = httpd => httpd.context("httpd thread terminated")?,
        check = check => check?,
    };

    Ok(())
}