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

28 KiB

GraphQL

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥

Introdução

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

GraphQL

À medida que surgem novas tecnologias, surgirão novas vulnerabilidades. 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 pessoa o consulte, qualquer informação sensível estará disponível para atacantes não autenticados.

Ao realizar seus ataques de força bruta de diretório, certifique-se de adicionar os seguintes caminhos para verificar as instâncias do GraphQL.

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

Uma vez que você encontre uma instância aberta do 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 perguntar a um esquema GraphQL informações sobre quais consultas ele suporta. O GraphQL nos permite fazer isso...

Fingerprint

A ferramenta graphw00f é capaz de detectar qual mecanismo 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 do GraphQL, ele incluirá a string {"data": {"__typename": "query"}} em algum lugar de 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 do GraphQL possui um campo reservado chamado __typename que retorna o tipo do objeto consultado como uma string.

Enumeração básica

O GraphQL geralmente suporta GET, POST (x-www-form-urlencoded) e POST(json). Embora, por motivos de segurança, seja recomendado permitir apenas json para evitar 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 sendo usados:

{% code overflow="wrap" %}

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

{% endcode %}

Com essa 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 do Banco de Dados via Introspecção

{% hint style="info" %} Se a introspecção estiver habilitada, 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 em linha:

{
  __schema {
    types {
      name
      kind
      description
      fields {
        name
        description
        args {
          name
          description
          type {
            name
            kind
          }
        }
      }
    }
  }
}
/?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á extrair 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 interface gráfica todas as opções.

Consultando

Agora que sabemos que tipo de informação está armazenada no 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.

Observe que o tipo da consulta "flags" é "Flags", e esse objeto é definido da seguinte forma:

Você pode ver que os objetos "Flags" são compostos por nome e valor. 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 ser consultado seja um tipo primitivo como string, como no exemplo a seguir

Você pode consultá-lo apenas com:

query={hiddenFlags}

Em outro exemplo, onde havia 2 objetos dentro do objeto "Query": "user" e "users".
Se esses objetos não precisam de nenhum argumento para pesquisar, é possível recuperar todas as informações deles apenas solicitando os dados desejados. Neste exemplo da Internet, você pode 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 fará a pesquisa usando o argumento "uid" do tipo Int.
De qualquer forma, já sabíamos disso, na seção Enumeração Básica 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 eu executo essa consulta, verá que "user" tinha o arg "uid" do tipo Int.

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

Observe que descobri que poderia solicitar 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 (obrigado a @BinaryShadow_)

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

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

Pesquisando

Para este exemplo, imagine um banco de dados com pessoas identificadas pelo e-mail e pelo nome e filmes identificados pelo nome e pela 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 e-mails:

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

Você pode pesquisar pessoas pelo nome e obter os filmes inscritos por elas:

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

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

Você também pode pesquisar vários objetos ao mesmo tempo. Neste caso, é feita uma pesquisa por 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

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):

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 mutação para criar novos filmes no 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 os valores e o tipo de dados são indicados na consulta.

Também pode haver uma mutação para criar pessoas (chamada addPerson neste exemplo) com amigos e arquivos (observe 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
}
}
}
}
}
}

Agrupando ataques de força bruta em 1 solicitação de API

Essa informação foi retirada de https://lab.wallarm.com/graphql-batching-attack/.
Autenticação por meio da API GraphQL com o envio simultâneo de várias consultas com credenciais diferentes para verificá-las. É um ataque clássico de força bruta, mas agora é possível enviar mais de um par de 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 robô de força bruta tentando adivinhar senhas.

Abaixo você pode encontrar a demonstração mais simples de uma solicitação de autenticação do aplicativo, com 3 pares de email/senha diferentes de uma 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 as informações correspondentes na seção de error. A segunda mutação teve a autenticação correta e a resposta possui o token de sessão de autenticação correto.

GraphQL sem Introspecção

Cada vez mais, os pontos de extremidade do GraphQL estão desabilitando a introspecção. No entanto, os erros que o GraphQL gera quando uma solicitação inesperada é recebida são suficientes para que ferramentas como clairvoyance possam recriar a maior parte do esquema.

Além disso, a extensão do Burp Suite GraphQuail observa as solicitações da API GraphQL que passam pelo Burp e constrói um esquema interno do GraphQL com cada nova consulta que ele vê. Ele 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 na API. Para mais informações, verifique isso.

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

Bypassing defesas de introspecção do GraphQL

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

Quando os desenvolvedores desabilitam a introspecção, eles podem usar uma expressão regular para excluir a palavra-chave __schema nas consultas. Você deve tentar caracteres como espaços, quebras de linha e vírgulas, pois eles são ignorados pelo GraphQL, mas não pela expressão regular com falhas.

Portanto, se o desenvolvedor tiver excluído apenas __schema{, a consulta de introspecção abaixo não será 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 tipo de conteúdo x-www-form-urlencoded.

Estruturas GraphQL Vazadas

Se a introspecção estiver desativada, tente analisar 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 poderosas sobre o esquema e o uso de cada objeto e função. A guia Sources das ferramentas de desenvolvedor pode pesquisar todos os arquivos para enumerar onde as consultas estão salvas. Às vezes, até mesmo 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á você vai encontrar vários pontos de extremidade GraphQL configurados sem tokens CSRF.

Observe que as solicitações GraphQL geralmente são enviadas por meio de solicitaçõ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 pontos de extremidade do GraphQL também suporta solicitações POST no formato form-urlencoded:

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 no Chrome é Lax. Isso significa que o cookie só será enviado por um site de terceiros em solicitações GET.

Observe também 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, abusando de um ataque XS-Search, pode ser possível extrair conteúdo do ponto de extremidade GraphQL abusando das credenciais do usuário.

Para mais informações, verifique a postagem original aqui.

Autorização no GraphQL

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

Modificar as variáveis de entrada da consulta pode levar à exposição de detalhes sensíveis da conta vazada.

A mutação pode até levar à assunção de conta tentando modificar dados de outras contas.

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

Bypassar autorização no GraphQL

Encadeando consultas é possível contornar um sistema de autenticação fraco.

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

Bypass de limite de taxa usando aliases

Normalmente, objetos GraphQL não podem conter várias propriedades com o mesmo nome. Os aliases permitem contornar essa restrição nomeando explicitamente as propriedades que você 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 obter mais informações sobre aliases no GraphQL, consulte 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çar bruta em um ponto de extremidade GraphQL.

Muitos pontos de extremidade terão algum tipo de limitador de taxa para evitar 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 ponto de extremidade. Como os aliases permitem efetivamente enviar várias 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 os códigos de desconto da loja são válidos. Essa operação pode potencialmente contornar o limite de taxa, pois é uma única solicitação HTTP, mesmo que possa ser usada para verificar uma grande quantidade 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

☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥