Until recently I had not solved any hacker101 CTF challenges. Last week I was working through them and one of them was BugDB v2. Here's the flow of how I got the flag and hope it helps.

Starting Point

Whenever I see a GraphQL API, the first thing I do is run an introspection query. Basically asking the API to describe itself. It came back with a handful of queries. There was also a mutation called modifyBug.

{
  __schema {
    types {
      name
      fields {
        name
        args {
          name
          type {
            name
            kind
          }
        }
        type {
          name
          kind
          ofType {
            name
            kind
          }
        }
      }
    }
  }
}
None

The Bugs type had a private field on it, which immediately got my attention. There's probably something being hidden right?

So I queried allBugs and got one result back. A single public bug, posted by admin, nothing interesting in the text. But the ID stood out QnVnczox. I threw it into a base64 decoder and got Bugs:1.

None

So there's probably a `Bugs:2` somewhere. That would be QnVnczoy in base64.

I tried findBug with that ID. Nothing. I tried several IDs. All null. I also tried the modifyBug mutation to flip a bug from private to public, but that didn't get me anywhere either.

Real Progress

The schema also had this node query. It's a pretty standard GraphQL thing, you give it any ID and it returns whatever object that ID points to. I hadn't tried it yet, so I gave it a shot:

{
  node(id: "QnVnczoy") {
    id
    ... on Bugs {
      text
      private
      reporter {
        username
      }
    }
  }
}
None
Flag retrieved

And finally it returned with what I've been looking for. The flag. The private bug, owned by a user called victim, with the flag in the text field.

What Actually Went Wrong for me here was:

The allBugs and findBug queries both checked whether a bug was private before returning it. But that was not the same case with the node query. It's a generic utility that fetches any object by ID, and was not gatekeeping anything.

So even though the flag was private, you could just ask for it directly by ID. And since the IDs were just sequential numbers wrapped in base64, guessing Bugs:2 took about five seconds.

Authorization in GraphQL needs to live at the resolver level, on every single query that touches sensitive data. It's not enough to protect the obvious endpoints if there's a back door hiding in a utility query that nobody thought to check.

Thanks for reading. Happy hacking.