GraphQL

Support HackTricks

Introdução

GraphQL é destacado como uma alternativa eficiente à REST API, oferecendo uma abordagem simplificada para consultar dados do backend. Em contraste com REST, que muitas vezes exige várias solicitações em diferentes endpoints para reunir dados, o GraphQL permite a recuperação de todas as informações necessárias por meio de uma única solicitação. Essa simplificação beneficia significativamente os desenvolvedores ao diminuir a complexidade de seus processos de recuperação de dados.

GraphQL e Segurança

Com o advento de novas tecnologias, incluindo o GraphQL, novas vulnerabilidades de segurança também surgem. Um ponto chave a ser observado é que o GraphQL não inclui mecanismos de autenticação por padrão. É responsabilidade dos desenvolvedores implementar tais medidas de segurança. Sem a autenticação adequada, os endpoints do GraphQL podem expor informações sensíveis a usuários não autenticados, representando um risco significativo à segurança.

Ataques de Força Bruta em Diretórios e GraphQL

Para identificar instâncias expostas do GraphQL, recomenda-se a inclusão de caminhos específicos em ataques de força bruta em diretórios. Esses caminhos são:

  • /graphql

  • /graphiql

  • /graphql.php

  • /graphql/console

  • /api

  • /api/graphql

  • /graphql/api

  • /graphql/graphql

Identificar instâncias abertas do GraphQL permite a análise das consultas suportadas. Isso é crucial para entender os dados acessíveis através do endpoint. O sistema de introspecção do GraphQL facilita isso ao detalhar as consultas que um esquema suporta. Para mais informações sobre isso, consulte a documentação do GraphQL sobre introspecção: GraphQL: A query language for APIs.

Impressão Digital

A ferramenta graphw00f é capaz de detectar qual motor GraphQL está sendo usado em um servidor e, em seguida, imprime algumas informações úteis para o auditor de segurança.

Consultas Universais

Para verificar se uma URL é um serviço GraphQL, uma consulta universal, query{__typename}, pode ser enviada. Se a resposta incluir {"data": {"__typename": "Query"}}, isso confirma que a URL hospeda um endpoint GraphQL. Este método depende do campo __typename do GraphQL, que revela o tipo do objeto consultado.

query{__typename}

Enumeração Básica

Graphql geralmente suporta GET, POST (x-www-form-urlencoded) e POST(json). Embora, por questões de segurança, seja recomendado permitir apenas json para prevenir ataques CSRF.

Introspecção

Para usar a introspecção para descobrir informações do esquema, consulte o campo __schema. Este campo está disponível no tipo raiz de todas as consultas.

query={__schema{types{name,fields{name}}}}

Com esta consulta, você encontrará o nome de todos os tipos que estão sendo usados:

query={__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name, kind}}}}}}}

Com esta consulta, você pode extrair todos os tipos, seus campos e seus argumentos (e o tipo dos argumentos). Isso será muito útil para saber como consultar o banco de dados.

Erros

É interessante saber se os erros serão exibidos, pois eles contribuirão com informações úteis.

?query={__schema}
?query={}
?query={thisdefinitelydoesnotexist}

Enumerar Esquema de Banco de Dados via Introspecção

Se a introspecção estiver habilitada, mas a consulta acima não for executada, tente remover as diretivas onOperation, onFragment e onField da estrutura da consulta.

#Full introspection query

query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
subscriptionType {
name
}
types {
...FullType
}
directives {
name
description
args {
...InputValue
}
onOperation  #Often needs to be deleted to run query
onFragment   #Often needs to be deleted to run query
onField      #Often needs to be deleted to run query
}
}
}

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
}
}
}
}

Consulta de introspecção inline:

/?query=fragment%20FullType%20on%20Type%20{+%20%20kind+%20%20name+%20%20description+%20%20fields%20{+%20%20%20%20name+%20%20%20%20description+%20%20%20%20args%20{+%20%20%20%20%20%20...InputValue+%20%20%20%20}+%20%20%20%20type%20{+%20%20%20%20%20%20...TypeRef+%20%20%20%20}+%20%20}+%20%20inputFields%20{+%20%20%20%20...InputValue+%20%20}+%20%20interfaces%20{+%20%20%20%20...TypeRef+%20%20}+%20%20enumValues%20{+%20%20%20%20name+%20%20%20%20description+%20%20}+%20%20possibleTypes%20{+%20%20%20%20...TypeRef+%20%20}+}++fragment%20InputValue%20on%20InputValue%20{+%20%20name+%20%20description+%20%20type%20{+%20%20%20%20...TypeRef+%20%20}+%20%20defaultValue+}++fragment%20TypeRef%20on%20Type%20{+%20%20kind+%20%20name+%20%20ofType%20{+%20%20%20%20kind+%20%20%20%20name+%20%20%20%20ofType%20{+%20%20%20%20%20%20kind+%20%20%20%20%20%20name+%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20ofType%20{+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20kind+%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name+%20%20%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20%20%20}+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}++query%20IntrospectionQuery%20{+%20%20schema%20{+%20%20%20%20queryType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20mutationType%20{+%20%20%20%20%20%20name+%20%20%20%20}+%20%20%20%20types%20{+%20%20%20%20%20%20...FullType+%20%20%20%20}+%20%20%20%20directives%20{+%20%20%20%20%20%20name+%20%20%20%20%20%20description+%20%20%20%20%20%20locations+%20%20%20%20%20%20args%20{+%20%20%20%20%20%20%20%20...InputValue+%20%20%20%20%20%20}+%20%20%20%20}+%20%20}+}

A última linha de código é uma consulta graphql que irá despejar todas as metainformações do graphql (nomes de objetos, parâmetros, tipos...)

Se a introspecção estiver habilitada, você pode usar GraphQL Voyager para visualizar em uma GUI todas as opções.

Consultando

Agora que sabemos que tipo de informação está salva dentro do banco de dados, vamos tentar extrair alguns valores.

Na introspecção, você pode encontrar qual objeto você pode consultar diretamente (porque você não pode consultar um objeto apenas porque ele existe). Na imagem a seguir, você pode ver que o "queryType" é chamado de "Query" e que um dos campos do objeto "Query" é "flags", que também é um tipo de objeto. Portanto, você pode consultar o objeto flag.

Note que o tipo da consulta "flags" é "Flags", e este objeto é definido como abaixo:

Você pode ver que os objetos "Flags" são compostos por name e value. Então você pode obter todos os nomes e valores das flags com a consulta:

query={flags{name, value}}

Note que, caso o objeto a ser consultado seja um tipo primitivo como string, como no exemplo a seguir

Você pode simplesmente consultá-lo com:

query={hiddenFlags}

Em outro exemplo onde havia 2 objetos dentro do objeto do tipo "Query": "user" e "users". Se esses objetos não precisarem de nenhum argumento para pesquisar, poderia recuperar todas as informações deles apenas pedindo os dados que você deseja. Neste exemplo da Internet, você poderia extrair os nomes de usuário e senhas salvos:

No entanto, neste exemplo, se você tentar fazer isso, receberá este erro:

Parece que, de alguma forma, ele irá pesquisar usando o argumento "uid" do tipo Int. De qualquer forma, já sabíamos disso, na seção Basic Enumeration foi proposta uma consulta que mostrava todas as informações necessárias: query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

Se você ler a imagem fornecida quando executei essa consulta, verá que "user" tinha o arg "uid" do tipo Int.

Assim, realizando um leve uid bruteforce, descobri que em uid=1 um nome de usuário e uma senha foram recuperados: query={user(uid:1){user,password}}

Note que eu descobri que poderia pedir os parâmetros "user" e "password" porque se eu tentar procurar algo que não existe (query={user(uid:1){noExists}}) recebo este erro:

E durante a fase de enumeração, descobri que o objeto "dbuser" tinha como campos "user" e "password.

Truque de despejo de string de consulta (graças ao @BinaryShadow_)

Se você pode pesquisar por um tipo de string, como: query={theusers(description: ""){username,password}} e você pesquisar por uma string vazia, isso irá despejar todos os dados. (Note que este exemplo não está relacionado com o exemplo dos tutoriais, para este exemplo suponha que você pode pesquisar usando "theusers" por um campo String chamado "description").

Pesquisa

Nesta configuração, um banco de dados contém pessoas e filmes. Pessoas são identificadas por seu email e nome; filmes por seu nome e avaliação. Pessoas podem ser amigas umas das outras e também ter filmes, indicando relacionamentos dentro do banco de dados.

Você pode pesquisar pessoas pelo nome e obter seus emails:

{
searchPerson(name: "John Doe") {
email
}
}

Você pode buscar pessoas pelo nome e obter seus filmes assinados:

{
searchPerson(name: "John Doe") {
email
subscribedMovies {
edges {
node {
name
}
}
}
}
}

Note como é indicado recuperar o name dos subscribedMovies da pessoa.

Você também pode pesquisar vários objetos ao mesmo tempo. Neste caso, uma pesquisa de 2 filmes é feita:

{
searchPerson(subscribedMovies: [{name: "Inception"}, {name: "Rocky"}]) {
name
}
}r

Ou até mesmo relações de vários objetos diferentes usando aliases:

{
johnsMovieList: searchPerson(name: "John Doe") {
subscribedMovies {
edges {
node {
name
}
}
}
}
davidsMovieList: searchPerson(name: "David Smith") {
subscribedMovies {
edges {
node {
name
}
}
}
}
}

Mutations

As mutações são usadas para fazer alterações no lado do servidor.

Na introspecção, você pode encontrar as mutações declaradas. Na imagem a seguir, o "MutationType" é chamado de "Mutation" e o objeto "Mutation" contém os nomes das mutações (como "addPerson" neste caso):

Nesta configuração, um banco de dados contém pessoas e filmes. Pessoas são identificadas por seu email e nome; filmes por seu nome e avaliação. Pessoas podem ser amigas umas das outras e também ter filmes, indicando relacionamentos dentro do banco de dados.

Uma mutação para criar novos filmes dentro do banco de dados pode ser como a seguinte (neste exemplo, a mutação é chamada de addMovie):

mutation {
addMovie(name: "Jumanji: The Next Level", rating: "6.8/10", releaseYear: 2019) {
movies {
name
rating
}
}
}

Observe como tanto os valores quanto o tipo de dados são indicados na consulta.

Além disso, o banco de dados suporta uma operação de mutação, chamada addPerson, que permite a criação de pessoas juntamente com suas associações a amigos e filmes existentes. É crucial notar que os amigos e filmes devem pré-existir no banco de dados antes de serem vinculados à nova pessoa criada.

mutation {
addPerson(name: "James Yoe", email: "jy@example.com", friends: [{name: "John Doe"}, {email: "jd@example.com"}], subscribedMovies: [{name: "Rocky"}, {name: "Interstellar"}, {name: "Harry Potter and the Sorcerer's Stone"}]) {
person {
name
email
friends {
edges {
node {
name
email
}
}
}
subscribedMovies {
edges {
node {
name
rating
releaseYear
}
}
}
}
}
}

Sobrecarga de Diretivas

Como explicado em uma das vulnerabilidades descritas neste relatório, uma sobrecarga de diretivas implica chamar uma diretiva até milhões de vezes para fazer o servidor desperdiçar operações até que seja possível realizar um DoS.

Agrupamento de força bruta em 1 solicitação de API

Esta informação foi retirada de https://lab.wallarm.com/graphql-batching-attack/. Autenticação através da API GraphQL com envio simultâneo de muitas consultas com diferentes credenciais para verificá-las. É um ataque clássico de força bruta, mas agora é possível enviar mais de um par login/senha por solicitação HTTP devido ao recurso de agrupamento do GraphQL. Essa abordagem enganaria aplicativos externos de monitoramento de taxa, fazendo-os pensar que está tudo bem e que não há um bot de força bruta tentando adivinhar senhas.

Abaixo você pode encontrar a demonstração mais simples de uma solicitação de autenticação de aplicativo, com 3 pares de email/senha diferentes por vez. Obviamente, é possível enviar milhares em uma única solicitação da mesma forma:

Como podemos ver na captura de tela da resposta, a primeira e a terceira solicitações retornaram null e refletiram as informações correspondentes na seção error. A segunda mutação teve os dados de autenticação corretos e a resposta contém o token de sessão de autenticação correto.

GraphQL Sem Introspecção

Cada vez mais endpoints graphql estão desativando a introspecção. No entanto, os erros que o graphql gera quando uma solicitação inesperada é recebida são suficientes para ferramentas como clairvoyance recriarem a maior parte do esquema.

Além disso, a extensão Burp Suite GraphQuail observa solicitações da API GraphQL passando pelo Burp e constrói um esquema interno de GraphQL com cada nova consulta que vê. Ela também pode expor o esquema para GraphiQL e Voyager. A extensão retorna uma resposta falsa quando recebe uma consulta de introspecção. Como resultado, o GraphQuail mostra todas as consultas, argumentos e campos disponíveis para uso dentro da API. Para mais informações verifique isso.

Uma boa lista de palavras para descobrir entidades GraphQL pode ser encontrada aqui.

Contornando defesas de introspecção do GraphQL

Para contornar restrições em consultas de introspecção em APIs, inserir um caractere especial após a palavra-chave __schema prova ser eficaz. Este método explora descuidos comuns de desenvolvedores em padrões regex que visam bloquear a introspecção, concentrando-se na palavra-chave __schema. Ao adicionar caracteres como espaços, novas linhas e vírgulas, que o GraphQL ignora, mas que podem não ser considerados no regex, as restrições podem ser contornadas. Por exemplo, uma consulta de introspecção com uma nova linha após __schema pode contornar tais defesas:

# Example with newline to bypass
{
"query": "query{__schema
{queryType{name}}}"
}

Se não tiver sucesso, considere métodos de solicitação alternativos, como solicitações GET ou POST com x-www-form-urlencoded, uma vez que as restrições podem se aplicar apenas às solicitações POST.

Tente WebSockets

Como mencionado em esta palestra, verifique se pode ser possível conectar-se ao graphQL via WebSockets, pois isso pode permitir que você contorne um potencial WAF e faça a comunicação websocket vazar o esquema do graphQL:

ws = new WebSocket('wss://target/graphql', 'graphql-ws');
ws.onopen = function start(event) {
var GQL_CALL = {
extensions: {},
query: `
{
__schema {
_types {
name
}
}
}`
}

var graphqlMsg = {
type: 'GQL.START',
id: '1',
payload: GQL_CALL,
};
ws.send(JSON.stringify(graphqlMsg));
}

Descobrindo Estruturas GraphQL Expostas

Quando a introspecção está desativada, examinar o código-fonte do site em busca de consultas pré-carregadas em bibliotecas JavaScript é uma estratégia útil. Essas consultas podem ser encontradas usando a aba Sources nas ferramentas de desenvolvedor, fornecendo insights sobre o esquema da API e revelando potencialmente consultas sensíveis expostas. Os comandos para pesquisar dentro das ferramentas de desenvolvedor são:

Inspect/Sources/"Search all files"
file:* mutation
file:* query

CSRF em GraphQL

Se você não sabe o que é CSRF, leia a página a seguir:

CSRF (Cross Site Request Forgery)

Por aí, você poderá encontrar vários endpoints GraphQL configurados sem tokens CSRF.

Observe que as requisições GraphQL geralmente são enviadas via requisições POST usando o Content-Type application/json.

{"operationName":null,"variables":{},"query":"{\n  user {\n    firstName\n    __typename\n  }\n}\n"}

No entanto, a maioria dos endpoints GraphQL também suporta form-urlencoded POST requests:

query=%7B%0A++user+%7B%0A++++firstName%0A++++__typename%0A++%7D%0A%7D%0A

Portanto, como as solicitações CSRF, como as anteriores, são enviadas sem solicitações de pré-vôo, é possível realizar alterações no GraphQL abusando de um CSRF.

No entanto, note que o novo valor padrão do cookie da flag samesite do Chrome é Lax. Isso significa que o cookie só será enviado de um site de terceiros em solicitações GET.

Note que geralmente é possível enviar a solicitação de consulta também como uma solicitação GET e o token CSRF pode não estar sendo validado em uma solicitação GET.

Além disso, abusando de um XS-Search ataque pode ser possível exfiltrar conteúdo do endpoint GraphQL abusando das credenciais do usuário.

Para mais informações verifique o post original aqui.

Sequestro de WebSocket entre sites no GraphQL

Semelhante às vulnerabilidades CRSF abusando do GraphQL, também é possível realizar um sequestro de WebSocket entre sites para abusar de uma autenticação com GraphQL com cookies desprotegidos e fazer um usuário realizar ações inesperadas no GraphQL.

Para mais informações, verifique:

WebSocket Attacks

Autorização no GraphQL

Muitas funções GraphQL definidas no endpoint podem apenas verificar a autenticação do solicitante, mas não a autorização.

Modificar variáveis de entrada da consulta pode levar a detalhes sensíveis da conta vazados.

A mutação pode até levar a uma tomada de conta ao tentar modificar dados de outra conta.

{
"operationName":"updateProfile",
"variables":{"username":INJECT,"data":INJECT},
"query":"mutation updateProfile($username: String!,...){updateProfile(username: $username,...){...}}"
}

Bypass authorization in GraphQL

Chaining queries juntos pode contornar um sistema de autenticação fraco.

No exemplo abaixo, você pode ver que a operação é "forgotPassword" e que deve executar apenas a consulta forgotPassword associada a ela. Isso pode ser contornado adicionando uma consulta ao final, neste caso, adicionamos "register" e uma variável de usuário para o sistema registrar como um novo usuário.

Bypassing Rate Limits Using Aliases in GraphQL

Em GraphQL, aliases são um recurso poderoso que permite a nomeação de propriedades explicitamente ao fazer uma solicitação de API. Essa capacidade é particularmente útil para recuperar múltiplas instâncias do mesmo tipo de objeto em uma única solicitação. Aliases podem ser empregados para superar a limitação que impede objetos GraphQL de ter várias propriedades com o mesmo nome.

Para uma compreensão detalhada dos aliases do GraphQL, o seguinte recurso é recomendado: Aliases.

Embora o propósito principal dos aliases seja reduzir a necessidade de numerosas chamadas de API, um caso de uso não intencional foi identificado onde aliases podem ser aproveitados para executar ataques de força bruta em um endpoint GraphQL. Isso é possível porque alguns endpoints são protegidos por limitadores de taxa projetados para frustrar ataques de força bruta, restringindo o número de solicitações HTTP. No entanto, esses limitadores de taxa podem não levar em conta o número de operações dentro de cada solicitação. Dado que os aliases permitem a inclusão de várias consultas em uma única solicitação HTTP, eles podem contornar tais medidas de limitação de taxa.

Considere o exemplo fornecido abaixo, que ilustra como consultas com alias podem ser usadas para verificar a validade de códigos de desconto de loja. Este método poderia contornar a limitação de taxa, uma vez que compila várias consultas em uma única solicitação HTTP, potencialmente permitindo a verificação de numerosos códigos de desconto simultaneamente.

# Example of a request utilizing aliased queries to check for valid discount codes
query isValidDiscount($code: Int) {
isvalidDiscount(code:$code){
valid
}
isValidDiscount2:isValidDiscount(code:$code){
valid
}
isValidDiscount3:isValidDiscount(code:$code){
valid
}
}

Ferramentas

Scanners de vulnerabilidade

Clientes

Testes Automáticos

Referências

Support HackTricks

Last updated