This is a full walkthrough of the Damn Vulnerable GraphQL Application (DVGA), a deliberately vulnerable app that you can use to test your GraphQL API hacking skills.
You will find a list of vulnerabilities in DVGA’s main interface, on the Solutions page. With every vulnerability, there is a button that displays a very short solution.
This present post will give you a more detailed explanation of each of them and a run down of the method I used. To be fully honest, this is more for myself to keep track of what I did on DVGA for future reference, but you’re welcome to read on. 😉
To host your DVGA instance, you need a linux system with Docker installed. As with all deliberately vulnerable practice apps, make sure you don’t install it on an Internet facing server, VPS or VM.
I usually install my target apps on a VM separate from my attacker system.
If you want to do the same, start by creating a fresh VM on your favourite hypervisor software (I use VirtualBox) and install a Ubuntu system. This will be your target system.
Now install Docker (see instructions here).
To check the Docker engine is properly installed, run the hello-world image by typing:
sudo docker run hello-world
Now install and run DVGA:
sudo docker pull dolevf/dvga
sudo docker run -t -p 5013:5013 -e WEB_HOST=0.0.0.0 dolevf/dvga
Make sure your terminal displays the following (server version may be different):
DVGA Server Version: 2.1.2 Running…
The DVGA app will be accessible locally from a browser at:
The app will be accessible from other VMs (including most importantly your attacker VM) at:
(this is assuming the target VM’s address on your network is
http://192.168.1.26, so adjust accordingly).
Installing hacking tools
Detecting and fingerprinting GraphQL
The first step is to determine if the app provides a GraphQL API.
Your first bet is to try accessing the most common GraphQL endpoint by pointing your attacker machine’s browser to:
Sure enough, this shows us this is indeed a GraphQL endpoint.
But let’s imagine this hadn’t been the case. We would need to try out a list of possible target endpoints using a graphw00f scan and fingerprint the GraphQL implementation at the same time :
This confirms the target endpoint and also indicates the GraphQL engine is Graphene, so we are up against a GraphQL implementation written in Python.
Let’s check the attack surface matrix at https://github.com/nicholasaleks/graphql-threat-matrix/blob/master/implementations/graphene.md
This gives us the following info:
Let’s keep this close at hand for later.
Now let’s check if introspection in enabled:
So the app is indeed open to introspection. That’s good news.
Now lets get the schema using the introspection query available here:
Copy the query and run it in Altair.
This results in a pretty long response. Rather than review manually, let’s get a graphical representation with GraphQL Voyager.
Go to https://ivangoncharov.github.io/graphql-voyager then click on
CHANGE SCHEMA (top left) then click on the
INTROSPECTION tab, paste the response we got from Altair and click
DISPLAY. This gives us a good idea of how the schema works.
DoS: Batch query attack
OK, let’s go on the offensive. The DVGA Solutions page lists a series of DoS attacks to run.
The problem statement tells us the
systemUpdate query is resource intensive and could be used for a batch query attack.
This can be done by grouping a series of commands in an array sent to the GraphQL endpoint. However, GraphQL IDEs such as Altair, GraphQL Playground, and GraphiQL Explorer don’t support array-based queries. This means we need to work from the command line.
Also, the Graphene attack surface matrix we checked earlier mentions batch requests are disabled by default. So we need to first check if this really is the case with DVGA:
This shows batch queries are enabled. Once again, that’s good news for us.
Now let’s try the same command using the resource intensive
systemUpdate query than we can run ten times in an array :
This makes the app unresponsive. So our DoS attack is successful.
You can stop the
curl command with
Ctrl-C, but you will also need to restart DVGA on your target VM (unless you want to wait until the ten
systemUpdate commands are done, which may take a while).
DoS: Deep recursion query attack
This time we want to leverage a recursion by building a circular query that uses two object types that cross reference each other. Let’s first check the visual representation of the schema we have in GraphQL Voyager.
Here we can see the
OwnerObject object types cross reference each other so let’s use that.
As a side note, we could also use InQL to automatically detect circular relationships in the schema:
Now let’s check the results:
This confirms what we found in GraphQL Voyager.
Let’s build a deep recursive query using the
This query is 30 lines deep. It returns a 1400 lines response. This is not enough to crash the app, though.
There is a similar query we can use that is 2003 lines long and that you can download here:
Run it in Altair and you will see it makes the app unresponsive:
So we have a successful attack.
And you’re good to restart your DVGA instance once again.
DoS: Resource intensive query attack
This one isn’t really a task we need to accomplish, but a reminder that some GraphQL servers implement query cost analysis. This system assigns values to fields in the schema according to the amount of resources they require to be processed by the GraphQL server.
This allows the server to set a threshold to determine which queries it will accept and which ones it will reject, in order to maintain a reasonable GraphQL server workload.
This means if such a system is in place, DoS attacks may not be as easy to conduct.
Also as a reminder, the attack surface matrix we checked earlier indicated that query cost analysis isn’t supported out of the box by Graphene. This means that a developer who wants to set up a cost analyzer will need to customize their setup.
In a real life situation, you may encounter GraphQL servers where this has been overlooked.
DoS: Field duplication attack
A simple way to try out a DoS attack against a GraphQL server is to simply duplicate fields in a query:
However, unless you select a resource intensive query, you will need to duplicate the chosen field a very large number of times to have any effect.
A faster option is to use a Python exploit that automates a large scale field duplication attack. You can download the exploit here:
Now run the exploit against the target DVGA app:
python3 exploit_threaded_field_dup.py http://192.168.1.26:5013/graphql
This makes the app unresponsive. You can interrup the script with
CTRL-C to get your shell back. And you will also need to restart DVGA on your target VM.
Note that if a GraphQL server implements query cost analysis, as explained above, this attack will not be as easy to carry out. Also, some servers may implement a field de-duplication middleware, that will clean out duplicate fields from queries.
DoS: Aliases based attack
GraphQL doesn’t like dealing with identical response keys and will generally complain if a query includes a given field name twice and you pass an argument with a different value for each.
This is why using aliases will allow the GraphQL server to return a response without having identical response keys for both fields to deal with.
Not sure what this means? Never mind… For the moment, just try sending a query with a number of instances of the
systemUpdate field, using aliases:
This make the app unrespensive. So our DoS attack is successful.
For clarity’s sake, let’s just mention that the example above is similar to the solution given on the Solutions page of DVGA. However, since
systemUpdate doesn’t produce identical responses key conflicts, the following will also work:
The difference is that using aliases will sometimes be more efficient, especially if a field de-duplication middleware is implemented. Note that a specific query middleware is needed on the GraphQL server to detect the use of aliases.
Having said all the above, my favourite way of performing an alias based attack is to use a python one-liner to generate the attack:
python3 -c 'for i in range(0, 1000): print("q"+str(i)+":"+"systemUpdate")'
This generates a list of 1000 aliases:
Paste the list into a query in Altair and your DVGA instance is a good as dead, ready for a restart.
DoS: Circular fragment
Let’s cause an infinite loop by using two fragments that cross reference each other.
Here we are creating two fragments on the
PasteObject object style called
End, which call each other:
This crashes DVGA instantly with an error reported in the query response.
Now as much as I like this attack’s effectiveness, know that the GraphQL specs indicate that fragments should not be allowed to form any cycles. So on a real target that uses a properly designed GraphQL engine, this attack should not work.
Keep it in mind anyway and give it a try whenever you test real targets. You never know, you may get lucky.
Information disclosure: GraphQL introspection
Introspection queries are a great way to explore the schema and hunt for some unintentional disclosure of sensitive data that we can use to our advantage.
The solution provided in the Solutions page for this task is a very basic introspection query that doesn’t offer much info:
Here is a slightly more detailed one that lists the names of all available queries, mutations and subscriptions in the schema:
However, you will get a better view by using the GraphQL Voyager representation we generated during our recon stage. The menu at the bottom of the interface lets you toggle between queries, mutations and subscriptions and also lets you focus on specific object types:
Now the most effective way of getting familiar with the schema and understanding the different fields available is to use Postman’s GraphQL client (you need Postman v.10.10 or above). In Postman, select New then GraphQL Request to access the GraphQL client.
The schema explorer (on the left side) lists all the available fields for each operation type (query, mutation, or subscription) and indicates the available or required arguments and the corresponding values with their scalar types. All you need to do is click your way through and Postman builds the queries for you.
Information disclosure: GraphQL interface
The Solutions page tells us DVGA offers a GraphiQL IDE. So let’s try to find it (the default GraphiQL endpoint is
/graphiql, but let’s assume the IDE maybe uses an unconventional endpoint).
Let’s first create a list of possible GraphQL endpoints. Then let’s use this word list to fuzz the URL:
We get two hits: one for the endpoint we already know (
/graphql), one for the IDE (
Let’s check our new endpoint:
Sure enough, we found the IDE.
Information disclosure: GraphQL field suggestions
GraphQL supports field suggestions. This means you can send a query with a deliberately incorrect or incomplete field name and get a response with suggestions of valid fields with similar names. The interesting aspect is that this works whether introspection is enabled or not. So field suggestions help shedding some light on an unavailable schema.
We also get field info as we type our queries:
Information disclosure: Server side request forgery (SSRF)
Let’s look for some operations with arguments that would let us pass a URL. On the DVGA main page, click on Import Paste. The Import a Paste page has a field that accepts URLs. There is even a suggested URL to a Pastebin page.
Type in this URL and intercept the request in Burp Suite.
You can see this request uses the
ImportPaste mutation, which accepts four arguments:
scheme. The values for these arguments make up the URL that is used to fetch the paste from Pastebin.
The body of the imported paste is displayed in the response in the
result field. You can also check the Private Pastes page on DVGA to see the paste has indeed been imported.
We can use this to access maybe other URLs on DVGA’s local network.
Let’s switch back to Postman’s GraphQL client and see what we can do with this mutation.
Here, we are trying to import a paste from this URL:
This brings back an empty result (port 8080 is actually not open on DVGA). But we do get a response regardless.
Now we can try to probe for a service that does exist, on an open port. To do this, we need to simulate such a service by opening a netcat listener in the DVGA container by running the following Docker command in the target system’s terminal:
sudo docker exec -it dvga nc -lvp 7773
listening on [::]:7773 ...
Next, go back to your attacker machine and send the following mutation from Postman:
This time, the request hangs and we don’t see a response.
Switch back to the target system and you should see this in the terminal:
connect to [::ffff:127.0.0.1]:7773 from localhost:56738 ([::ffff:127.0.0.1]:56738)
GET /ssrf HTTP/1.1
So this shows DVGA did make a request to the port we set our listener to (7773), so our SSRF attack was successful. The request sent from Postman hangs because our listener is not responding and Postman is waiting for a response.
Also, if you try to access the same port using netcat from your attacker system, you will see that the port is inaccessible. This is why we are using a SSRF attack to access it through DVGA itself.
Injection: SQL injection
To test for injection vulnerabilities, we first need to locate fields with arguments that accept string inputs and could be used as injection points.
In Altair’s documentation section (on the right), let’s look at the list of fields in the query section of the schema.
The first field listed is
pastes and has a
filter argument that accepts strings. So let’s first send a basic query.
This lists all the public pastes in the database. Now let’s try sending a query that filters for the content of one of the pastes.
This returns only the corresponding paste. Now let’s test for SQL injection by adding a ‘ to the string.
We get an error message. This confirms the app is vulnerable to SQL injection. We can also see the database engine is sqlite3.
Let’s now use Sqlmap to exploit this vulnerability and see if we can obtain some more useful info.
First, let’s proxy the request into Burp Suite. Then we need to change the value of the
filter argument to
test* and we can then export the request in a file by right-clicking and selecting
Copy to File. Let’s save it as
Now switch to the terminal and run:
sqlmap -r request.txt -dbms=sqlite -tables
We get a list of the different tables in the database.
Now run the following:
sqlmap -r request.txt -dbms=sqlite -dump
Now we get a dump of all the tables in the database. Scroll back towards the top section of the output and you will see a table with the username and password of registered users including admin. Let’s keep this in mind for later.
Code execution: OS command injection #1
As seen earlier, we know we can use the
importPaste mutation for SSRF, let’s see if we can also leverage it for a command injection.
In Postman, let’s go back to the original Pastbin request we sent earlier and add a command to the value of the
In the response, we can see that the content of the
/etc/passwd file is displayed. So our command injection is successful.
Code execution: OS command injection #2
The problem statement tells us the
systemDiagnostics query acts as a restricted shell, that allows us to pass certain UNIX commands. But it’s password protected.
Let’s use the admin credentials we obtained with our SQL injection attack and try passing a UNIX command in the
We can see the result of our command is returned, so we can indeed pass commands to the server using
Injection: stored XSS
The problem statement suggests we may use the
importPaste mutations for a XSS attack.
Let’s use the
createPaste mutation to create a new paste and place our XSS payload into the
We get a response that shows our paste has been accepted. Now go back to DVGA’s interface and visit the Public Pastes page. This lists all public pastes in the database, including the one we just created.
Injection: Log injection / spoofing
Operations names are names we, as users, can give to an operation we send to the GraphQL server as part of a query.
DVGA has an audit log that keeps track of all queries sent, with the name of the user, the name of the operation and the content of the query.
Let’s send a mutation and try to conceal it by giving it a
getPastes operation name.
Now switch back to the DVGA interface and visit the audit log (choose Audit in the user menu at the top right).
At the top of the list, you can see the
createPaste operation we just did is listed as a
getPastes operation in the GraphQL Operation column. So an analyst combing through the log a bit too quickly could likely miss it.
Injection: HTML injection
Let’s attempt a XSS attack by uploading a script into the database from a text file.
Start by saving the following as a text file (call it XSS.txt):
<p>This is an HTML injection uploaded from a file.</p>
<script>alert("You have just been XSSed")</script>
Now go back to the browser and, on the DVGA main page, click Import Paste and load the file we just created (you can also proxy the request through Burp Suite if you want to see what the query looks like).
Now display the Private Pastes page.
You can see the script we upload with the file is executed. So our attack is successful.
Authorisation bypass: GraphQL interface protection bypass
You probably noticed that the GraphiQL interface we discovered earlier is unusable. As soon as we access the endpoint, the interface displays a 400 error indicating that access is rejected. Even when sending a valid query, we always get the same response.
So the queries we send to
/graphql through Altair are accepted, but the same queries sent to
/graphiql are rejected.
Let’s pull out our browser’s developer tools and see what’s going on.
Go to the Network tab and select the POST request to
graphiql at the bottom of the list. In the Cookies tab on the right, you will see a cookie listed with a
"graphiql:disable" value (you may need to reload the page first).
Let’s change that. Go to the Storage tab, click on the Cookies section and select the cookie for our target. Select the value of the
env cookie and change it to
Now go back to the browser’s main window and reload the page (make sure the URL is still
http://192.168.1.26:5013/graphiql). Now the IDE is functional and we can send our queries.
You will also notice that the schema is accessible in the Documentation Explorer section on the right. We even get field info as we type our queries.
Authorization bypass: GraphQL query deny list bypass
This task requires DVGA’s Expert mode. On DVGA’s main page, you can use the menu with the three little cubes (top right of the interface) to turn on the Expert mode.
The problem statement tells us DVGA has a deny-list mechanism that rejects queries using the
systemHealth field. As you can see below, a standard
systemHealth query is denied:
Now try the same query but give it a
getPastes operation name.
This time our query is accepted and we get a response. This means we can fool the deny-list mechanism by giving innocent looking operation names to our queries.
Don’t forget to switch back to Beginner mode.
Miscellaneous: Arbitrary file write / path traversal
Let’s go back to the HTML injection scenario above and load the XSS.txt file using the Upload a Paste page. But this time, proxy the request into Burp Suite and send it to Repeater (right-clic then Send to Repeater).
Looking at the request, we can see that the query places the content of the text file into the
content variable and the name of the file into the
This is because DVGA saves the file somewhere in the server’s file system.
Let’s try a path traversal technique to save the file somewhere else in the server’s file system. To do this, replace the value of the
filename field (
XSS.txt in the present case) by:
Then send the request.
Now switch to your target machine and check the
/tmp directory. You should find the XSS.txt file. This means we can use this mutation to load a malicious file where we want in the server’s file system (note: this did NOT work on my system – currently investigating and will update when clarified).
Miscellaneous: GraphQL weak password protection
The problem statement tells us we need a username and password to access the
systemDiagnosis query and suggests a brute force attack to obtain the password.
The solution provided is a script that runs four passwords against the
admin username. However, none of the four actually match. Also, this seems like an unrealistic scenario as a brute force attempt will usually require cycling through a much larger wordlist.
In the present case, we already have valid credentials obtained by exploiting a SQL injection so we don’t need to look any further.
Information disclosure: stack trace errors
The problem statement tells us the GraphiQL interface throws stack trace errors and debugging messages when the user sends erroneous queries.
Let’s try it for ourselves. Open the GraphiQL IDE interface we found earlier (if you switched between Beginner and Expert modes for the GraphQL query deny list bypass, check your browser’s developer tool to make sure the
env cookie is still set to
grapiql:enable otherwise the IDE won’t be usable).
Send an invalid query and check the response.
The response includes an
extensions field with a
stack sub-field in the
errors array, with ample detail on the server’s file structure. Interestingly, this field is not displayed when sending the same query using Altair. So when you are testing an app and happen to have both interfaces available, be sure to test both.
Authorization bypass: GraphQL JWT token forge
Here, we want to forge a JWT token to use with the
me query operation to obtain admin credentials.
In Postman, let’s first create a new user, with the
Next, let’s log in using these credentials, with the
This returns an access token:
Paste the token into https://jwt.io. Notice the
identity field in the payload section, with the name of the user we just created.
Now change the value of the
identity field to
Copy the header and payload sections of the JWT (the red and purple sections). Don’t forget to include the black period at the end of the payload section. You can leave out the signature (the blue section) as we are hoping the app will not check the validity of the token’s signature.
In Altair, write a query using the
me operation and paste the forged token as the value of the
As you can see, the token is accepted and the app returns the username and password of the admin user. So the app has indeed failed to check that our JWT has a valid signature.
Congratulations for getting all the way down this post! 😉