GraphQL

Impara l'hacking AWS da zero a ero con htARTE (Esperto Red Team AWS di HackTricks)!

Altri modi per supportare HackTricks:

Introduzione

GraphQL è evidenziato come un'efficiente alternativa alle API REST, offrendo un approccio semplificato per interrogare i dati dal backend. A differenza di REST, che spesso richiede numerose richieste su endpoint diversi per raccogliere dati, GraphQL consente di recuperare tutte le informazioni necessarie tramite una singola richiesta. Questa ottimizzazione beneficia significativamente gli sviluppatori riducendo l'ingiunzione dei loro processi di recupero dati.

GraphQL e Sicurezza

Con l'avvento di nuove tecnologie, tra cui 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 un'adeguata autenticazione, i punti finali GraphQL possono esporre informazioni sensibili agli utenti non autenticati, creando un rischio significativo per la sicurezza.

Attacchi di Forza Bruta alle Directory e GraphQL

Per identificare istanze GraphQL esposte, è consigliabile includere specifici percorsi negli attacchi di forza bruta alle directory. Questi percorsi sono:

  • /graphql

  • /graphiql

  • /graphql.php

  • /graphql/console

  • /api

  • /api/graphql

  • /graphql/api

  • /graphql/graphql

Identificare istanze GraphQL aperte consente di esaminare le query supportate. Questo è cruciale per comprendere i dati accessibili attraverso il punto finale. Il sistema di introspezione di GraphQL facilita ciò dettagliando le query supportate da uno schema. Per ulteriori informazioni a riguardo, fare riferimento alla documentazione di GraphQL sull'introspezione: GraphQL: Un linguaggio di interrogazione per le API.

Fingerprint

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

Query universali

Per verificare 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 punto finale GraphQL. Questo metodo si basa sul campo __typename di GraphQL, che rivela il tipo dell'oggetto interrogato.

query{__typename}

Enumerazione di Base

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

Introspezione

Per utilizzare l'introspezione per scoprire informazioni sullo schema, interrogare 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 in uso:

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

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 l'Introspezione

Se l'introspezione è abilitata ma la query sopra non viene eseguita, prova a rimuovere le direttive onOperation, onFragment e onField dalla struttura della query.

#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 in linea:

/?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 scaricherà tutte le meta-informazioni dal graphql (nomi degli oggetti, parametri, tipi...)

Se l'introspezione è abilitata, è possibile utilizzare GraphQL Voyager per visualizzare in una GUI tutte le opzioni.

Querying

Ora che sappiamo che tipo di informazioni sono salvate all'interno del database, proviamo a estrarre alcuni valori.

Nell'introspezione è possibile trovare quali oggetti è possibile interrogare direttamente (perché non è possibile 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 è possibile 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 nome e valore. Quindi puoi ottenere tutti i nomi e i valori delle bandiere con la query:

query={flags{name, value}}

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

Puoi semplicemente interrogarlo con:

query={hiddenFlags}

In un altro esempio in cui c'erano 2 oggetti all'interno dell'oggetto tipo "Query": "user" e "users". Se questi oggetti non hanno bisogno di alcun argomento per essere cercati, è possibile recuperare tutte le informazioni da essi chiedendo semplicemente i dati desiderati. In questo esempio preso da Internet è possibile estrarre i nomi utente e le password salvate:

Tuttavia, in questo esempio se si prova a farlo si ottiene questo errore:

Sembra che in qualche modo verrà effettuata la ricerca utilizzando l'argomento "uid" di tipo Int. Comunque, sapevamo già che, nella sezione Enumerazione di Base è 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'argomento "uid" di tipo Int.

Quindi, facendo un leggero uid bruteforce ho scoperto che con uid=1 è stata recuperata una username e una password: query={user(uid:1){user,password}}

Nota che ho scoperto di poter chiedere i parametri "user" e "password" perché se cerco 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 per il dump della stringa di query (grazie a @BinaryShadow_)

Se puoi cercare per tipo stringa, come: query={theusers(description: ""){username,password}} e cerchi una stringa vuota verranno scaricati tutti i dati. (Nota che questo esempio non è correlato con l'esempio dei tutorial, per questo esempio supponi di poter cercare usando "theusers" per un campo String chiamato "description").

Ricerca

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

È possibile cercare le 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 sia indicato il recupero del name dei subscribedMovies della persona.

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

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

Oppure 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'ispezione è possibile trovare le mutazioni dichiarate. Nell'immagine seguente il "MutationType" è chiamato "Mutation" e l'oggetto "Mutation" contiene i nomi delle mutazioni (come ad esempio "addPerson" in questo caso):

In questa configurazione, un database contiene persone e film. Le persone sono identificate dal loro indirizzo email e nome; i film dal loro nome e valutazione. Le persone possono essere amici 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 siano 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 poterli collegare 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
}
}
}
}
}
}

Sovraccarico della direttiva

Come spiegato in uno dei vuln descritti in questo report, un sovraccarico della direttiva implica la chiamata di una direttiva anche milioni di volte per far perdere al server operazioni fino a quando è possibile effettuare un attacco DoS.

Forza bruta batching in 1 richiesta API

Questa informazione è stata presa da https://lab.wallarm.com/graphql-batching-attack/. Autenticazione tramite API GraphQL con invio simultaneo di molte query con credenziali diverse per verificarla. Si tratta di un attacco di forza bruta classico, ma ora è possibile inviare più di una coppia login/password per richiesta HTTP a causa della funzionalità di batching di GraphQL. Questo approccio ingannerebbe le applicazioni esterne di monitoraggio del tasso a pensare che tutto vada bene e che non ci sia un bot di forza bruta che cerca di indovinare le password.

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

Come possiamo vedere dallo screenshot della risposta, la prima e la terza richiesta hanno restituito null e hanno riflettuto 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ù punti di accesso graphql stanno disabilitando l'introspezione. Tuttavia, gli errori che graphql restituisce quando riceve 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 qui.

Un bel elenco di parole per scoprire entità GraphQL può essere trovato qui.

Eludere le difese di introspezione GraphQL

Eludere le Difese di Introspezione GraphQL

Per eludere le restrizioni sulle query di introspezione nelle API, inserire un carattere speciale dopo la parola chiave __schema risulta efficace. Questo metodo sfrutta le comuni sviste degli sviluppatori nei pattern regex che mirano a bloccare l'introspezione concentrandosi sulla parola chiave __schema. Aggiungendo caratteri come spazi, nuove righe e virgole, che GraphQL ignora ma potrebbero non essere considerati nei regex, le restrizioni possono essere aggirate. Ad esempio, una query di introspezione con una nuova riga dopo __schema potrebbe eludere tali difese:

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

Se non avete successo, considerate metodi di richiesta alternativi, come richieste GET o POST con x-www-form-urlencoded, poiché le restrizioni potrebbero essere applicate solo alle richieste POST.

Scoperta delle Strutture GraphQL Esposte

Quando l'introspezione è disabilitata, esaminare il codice sorgente del sito web per le query pre-caricate nelle librerie JavaScript è una strategia utile. Queste query possono essere trovate utilizzando la scheda Sources negli strumenti per sviluppatori, fornendo approfondimenti 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 seguente pagina:

Qui fuori sarai in grado di trovare diversi endpoint GraphQL configurati senza token CSRF.

Nota che le richieste GraphQL di solito vengono 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 dei punti finali GraphQL supporta anche le richieste POST form-urlencoded:

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 di preflight, è possibile effettuare modifiche nel GraphQL abusando di una CSRF.

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

Tieni presente che di solito è possibile inviare la richiesta query anche come una richiesta GET e il token CSRF potrebbe non essere convalidato in una richiesta GET.

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

Per ulteriori informazioni controlla il post originale qui.

Autorizzazione in GraphQL

Molte funzioni GraphQL definite sul punto finale potrebbero controllare solo l'autenticazione del richiedente ma non l'autorizzazione.

La modifica delle variabili di input della query potrebbe portare a dettagli dell'account sensibili leaked.

La mutazione potrebbe addirittura portare al furto dell'account cercando di modificare altri dati dell'account.

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

Bypass dell'autorizzazione in GraphQL

Concatenare le query insieme può bypassare un sistema di autenticazione debole.

Nell'esempio seguente puoi vedere che l'operazione è "forgotPassword" e che dovrebbe eseguire solo la query forgotPassword associata ad essa. Questo può essere bypassato aggiungendo una query alla fine, in questo caso aggiungiamo "register" e una variabile utente per far registrare il sistema come nuovo utente.

Bypass dei limiti di velocità utilizzando Alias in GraphQL

In GraphQL, gli alias sono una funzionalità potente che consente di denominare esplicitamente le proprietà durante 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 il limite che impedisce agli oggetti GraphQL di avere più proprietà con lo stesso nome.

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

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 brute force su un endpoint GraphQL. Questo è possibile perché alcuni endpoint sono protetti da limitatori di velocità progettati per contrastare gli attacchi brute force limitando il numero di richieste HTTP. Tuttavia, questi limitatori di velocità potrebbero non considerare il numero di operazioni all'interno di ciascuna richiesta. Dato che gli alias consentono l'inclusione di più query in una singola richiesta HTTP, possono aggirare 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 i limiti di velocità poiché combina diverse query in una singola richiesta HTTP, consentendo potenzialmente la verifica di numerosi codici sconto contemporaneamente.

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

Strumenti

Scanner di vulnerabilità

Client

Test automatici

Riferimenti

Impara l'hacking di AWS da zero a eroe con htARTE (HackTricks AWS Red Team Expert)!

Altri modi per supportare HackTricks:

Last updated