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

29 KiB

GraphQL

Apprenez le piratage AWS de zéro à héros avec htARTE (HackTricks AWS Red Team Expert)!

Autres moyens de soutenir HackTricks :

Introduction

GraphQL se présente comme une alternative à l'API REST. Les API REST nécessitent que le client envoie plusieurs requêtes à différents points de terminaison de l'API pour interroger les données de la base de données backend. Avec GraphQL, vous n'avez besoin d'envoyer qu'une seule requête pour interroger le backend. C'est beaucoup plus simple car vous n'avez pas à envoyer plusieurs requêtes à l'API, une seule requête peut être utilisée pour rassembler toutes les informations nécessaires.

GraphQL

Avec l'émergence de nouvelles technologies, de nouvelles vulnérabilités apparaissent également. Par défaut, GraphQL n'implémente pas l'authentification, cela est laissé à l'implémentation du développeur. Cela signifie que par défaut, GraphQL permet à quiconque de l'interroger, toute information sensible sera disponible pour les attaquants non authentifiés.

Lorsque vous effectuez vos attaques de force brute sur les répertoires, assurez-vous d'ajouter les chemins suivants pour vérifier les instances GraphQL.

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

Une fois que vous trouvez une instance GraphQL ouverte, vous devez savoir quelles requêtes elle prend en charge. Cela peut être fait en utilisant le système d'introspection, plus de détails peuvent être trouvés ici : GraphQL : Un langage de requête pour les API.
Il est souvent utile de demander à un schéma GraphQL des informations sur les requêtes qu'il prend en charge. GraphQL nous permet de le faire…

Empreinte

L'outil graphw00f est capable de détecter quel moteur GraphQL est utilisé sur un serveur, puis imprime des informations utiles pour l'auditeur de sécurité.

Requêtes universelles

Si vous envoyez query{__typename} à n'importe quel point de terminaison GraphQL, il inclura la chaîne {"data": {"__typename": "query"}} quelque part dans sa réponse. C'est ce qu'on appelle une requête universelle, et c'est un outil utile pour sonder si une URL correspond à un service GraphQL.

La requête fonctionne parce que chaque point de terminaison GraphQL a un champ réservé appelé __typename qui renvoie le type de l'objet interrogé sous forme de chaîne.

Énumération de base

GraphQL prend généralement en charge GET, POST (x-www-form-urlencoded) et POST(json). Cependant, pour des raisons de sécurité, il est recommandé de n'autoriser que le json pour prévenir les attaques CSRF.

Introspection

Pour utiliser l'introspection afin de découvrir des informations sur le schéma, interrogez le champ __schema. Ce champ est disponible sur le type racine de toutes les requêtes.

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

{% endcode %}

Avec cette requête, vous trouverez le nom de tous les types utilisés :

query={__schema{types{name,fields{name,args{name,description,type{name,kind,ofType{name, kind}}}}}}}
Avec cette requête, vous pouvez extraire tous les types, leurs champs et leurs arguments (ainsi que le type des arguments). Cela sera très utile pour savoir comment interroger la base de données.

![](<../../.gitbook/assets/image (207) (3).png>)

**Erreurs**

Il est intéressant de savoir si les **erreurs** vont être **affichées** car elles apporteront des **informations** utiles.
?query={__schema}
?query={}
?query={thisdefinitelydoesnotexist}

Énumération du schéma de base de données via l'introspection

{% hint style="info" %} Si l'introspection est activée mais que la requête ci-dessus ne s'exécute pas, essayez de retirer les directives onOperation, onFragment et onField de la structure de la requête. {% 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
}
}
}
}

Requête d'introspection en ligne :

/?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}+}

La dernière ligne de code est une requête graphql qui extraira toutes les métadonnées de graphql (noms d'objets, paramètres, types...)

Si l'introspection est activée, vous pouvez utiliser GraphQL Voyager pour visualiser dans une interface graphique toutes les options.

Interrogation

Maintenant que nous savons quel type d'informations est enregistré dans la base de données, essayons d'extraire certaines valeurs.

Dans l'introspection, vous pouvez trouver quel objet vous pouvez interroger directement (car vous ne pouvez pas interroger un objet simplement parce qu'il existe). Dans l'image suivante, vous pouvez voir que le "queryType" est appelé "Query" et que l'un des champs de l'objet "Query" est "flags", qui est également un type d'objet. Par conséquent, vous pouvez interroger l'objet flag.

Notez que le type de la requête "flags" est "Flags", et cet objet est défini comme ci-dessous :

Vous pouvez voir que les objets "Flags" sont composés de name et value. Ensuite, vous pouvez obtenir tous les noms et valeurs des flags avec la requête :

query={flags{name, value}}

Notez que si l'objet à interroger est un type primitif comme une chaîne de caractères comme dans l'exemple suivant

Vous pouvez simplement l'interroger avec :

query={hiddenFlags}

Dans un autre exemple où il y avait 2 objets à l'intérieur de l'objet "Query": "user" et "users".
Si ces objets ne nécessitent aucun argument pour la recherche, vous pourriez récupérer toutes les informations les concernant en demandant simplement les données que vous voulez. Dans cet exemple d'Internet, vous pourriez extraire les noms d'utilisateur et mots de passe enregistrés :

Cependant, dans cet exemple, si vous essayez de faire cela, vous obtenez cette erreur :

Il semble que la recherche se fasse en utilisant l'argument "uid" de type Int.
Quoi qu'il en soit, nous savions déjà cela, dans la section Énumération de base une requête avait été proposée qui nous montrait toutes les informations nécessaires : query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

Si vous lisez l'image fournie lorsque j'exécute cette requête, vous verrez que "user" avait l'arg "uid" de type Int.

Ainsi, en effectuant un léger bruteforce sur uid, j'ai trouvé que pour uid=1, un nom d'utilisateur et un mot de passe étaient récupérés :
query={user(uid:1){user,password}}

Notez que j'ai découvert que je pouvais demander les paramètres "user" et "password" car si j'essaie de chercher quelque chose qui n'existe pas (query={user(uid:1){noExists}}), je reçois cette erreur :

Et pendant la phase d'énumération, j'ai découvert que l'objet "dbuser" avait comme champs "user" et "password".

Astuce pour le vidage de chaîne de requête (merci à @BinaryShadow_)

Si vous pouvez rechercher par un type de chaîne, comme : query={theusers(description: ""){username,password}} et que vous cherchez une chaîne vide, cela videra toutes les données. (Notez que cet exemple n'est pas lié à l'exemple des tutoriels, pour cet exemple supposez que vous pouvez rechercher en utilisant "theusers" par un champ de type String appelé "description").

GraphQL est une technologie relativement nouvelle qui commence à gagner en popularité parmi les startups et les grandes entreprises. Outre l'absence d'authentification par défaut, les points de terminaison GraphQL peuvent être vulnérables à d'autres bugs tels que l'IDOR.

Recherche

Pour cet exemple, imaginez une base de données avec des personnes identifiées par l'email et le nom et des films identifiés par le nom et la note. Une personne peut être amie avec d'autres personnes et une personne peut posséder des films.

Vous pouvez rechercher des personnes par le nom et obtenir leurs emails :

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

Vous pouvez rechercher des personnes par nom et obtenir leurs films abonnés :

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

Notez comment il est indiqué de récupérer le name des subscribedMovies de la personne.

Vous pouvez également rechercher plusieurs objets en même temps. Dans ce cas, une recherche de 2 films est effectuée :

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

Ou même les relations de plusieurs objets différents en utilisant des alias :

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

Mutations

Les mutations sont utilisées pour effectuer des changements côté serveur.

Dans l'introspection, vous pouvez trouver les mutations déclarées. Dans l'image suivante, le "MutationType" est appelé "Mutation" et l'objet "Mutation" contient les noms des mutations (comme "addPerson" dans ce cas) :

Pour cet exemple, imaginez une base de données avec des personnes identifiées par l'email et le nom, et des films identifiés par le nom et la note. Une personne peut être amie avec d'autres personnes et une personne peut posséder des films.

Une mutation pour créer de nouveaux films dans la base de données peut ressembler à la suivante (dans cet exemple, la mutation est appelée addMovie) :

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

Remarquez comment les valeurs et le type de données sont indiqués dans la requête.

Il peut également y avoir une mutation pour créer des personnes (appelée addPerson dans cet exemple) avec des amis et des films (notez que les amis et les films doivent exister avant de créer une personne qui leur est liée) :

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çage brutal par lot en 1 requête API

Cette information a été prise de https://lab.wallarm.com/graphql-batching-attack/.
Authentification via l'API GraphQL avec l'envoi simultané de nombreuses requêtes avec différentes informations d'identification pour les vérifier. C'est une attaque de force brute classique, mais il est maintenant possible d'envoyer plus d'une paire identifiant/mot de passe par requête HTTP grâce à la fonctionnalité de lot de GraphQL. Cette approche pourrait tromper les applications de surveillance de taux externes en leur faisant croire que tout va bien et qu'il n'y a pas de bot de force brute essayant de deviner des mots de passe.

Ci-dessous, vous pouvez trouver la démonstration la plus simple d'une requête d'authentification d'application, avec 3 paires email/mot de passe différentes à la fois. De toute évidence, il est possible d'envoyer des milliers dans une seule requête de la même manière :

Comme nous pouvons le voir sur la capture d'écran de la réponse, les première et troisième requêtes ont renvoyé null et reflété les informations correspondantes dans la section error. La deuxième mutation avait les données d'authentification correctes et la réponse contient le jeton de session d'authentification correct.

GraphQL Sans Introspection

De plus en plus de points de terminaison graphql désactivent l'introspection. Cependant, les erreurs que graphql génère lorsqu'une requête inattendue est reçue sont suffisantes pour des outils comme clairvoyance pour recréer la majeure partie du schéma.

De plus, l'extension Burp Suite GraphQuail observe les requêtes de l'API GraphQL passant par Burp et construit un schéma GraphQL interne avec chaque nouvelle requête qu'elle voit. Elle peut également exposer le schéma pour GraphiQL et Voyager. L'extension renvoie une fausse réponse lorsqu'elle reçoit une requête d'introspection. En conséquence, GraphQuail montre toutes les requêtes, arguments et champs disponibles pour une utilisation au sein de l'API. Pour plus d'informations vérifiez ceci.

Une bonne liste de mots pour découvrir les entités GraphQL peut être trouvée ici.

Contournement des défenses d'introspection GraphQL

Si vous ne parvenez pas à exécuter des requêtes d'introspection pour l'API que vous testez, essayez d'insérer un caractère spécial après le mot-clé __schema.

Lorsque les développeurs désactivent l'introspection, ils pourraient utiliser une expression régulière pour exclure le mot-clé __schema dans les requêtes. Vous devriez essayer des caractères comme les espaces, les nouvelles lignes et les virgules, car ils sont ignorés par GraphQL mais pas par des expressions régulières défectueuses.

Ainsi, si le développeur a seulement exclu __schema{, alors la requête d'introspection ci-dessous ne serait pas exclue.

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

Si cela ne fonctionne pas, essayez d'exécuter la sonde sur une méthode de requête alternative, car l'introspection peut être désactivée uniquement pour les requêtes POST. Essayez une requête GET, ou une requête POST avec un content-type de x-www-form-urlencoded.

Structures GraphQL divulguées

Si l'introspection est désactivée, essayez de regarder le code source du site web. Les requêtes sont souvent préchargées dans le navigateur sous forme de bibliothèques javascript. Ces requêtes préécrites peuvent révéler des informations précieuses sur le schéma et l'utilisation de chaque objet et fonction. L'onglet Sources des outils de développement peut rechercher dans tous les fichiers pour énumérer où les requêtes sont sauvegardées. Parfois, même les requêtes protégées par l'administrateur sont déjà exposées.

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

CSRF dans GraphQL

Si vous ne savez pas ce qu'est un CSRF, lisez la page suivante :

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

Vous trouverez de nombreux points de terminaison GraphQL configurés sans jetons CSRF.

Notez que les requêtes GraphQL sont généralement envoyées via des requêtes POST utilisant le Content-Type application/json.

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

Cependant, la plupart des points de terminaison GraphQL prennent également en charge les requêtes POST form-urlencoded :

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

Par conséquent, comme les requêtes CSRF telles que les précédentes sont envoyées sans requêtes de pré-vol, il est possible d'effectuer des changements dans le GraphQL en abusant d'un CSRF.

Cependant, notez que la nouvelle valeur par défaut du cookie pour le drapeau samesite de Chrome est Lax. Cela signifie que le cookie ne sera envoyé que dans les requêtes GET provenant d'un site tiers.

Notez qu'il est généralement possible d'envoyer la requête query également en tant que requête GET et que le jeton CSRF pourrait ne pas être validé dans une requête GET.

De plus, en abusant d'une attaque XS-Search, il pourrait être possible d'exfiltrer du contenu de l'endpoint GraphQL en abusant des identifiants de l'utilisateur.

Pour plus d'informations, consultez le post original ici.

Autorisation dans GraphQL

De nombreuses fonctions GraphQL définies sur l'endpoint peuvent uniquement vérifier l'authentification du demandeur mais pas l'autorisation.

La modification des variables d'entrée de la requête pourrait conduire à la fuite de détails de compte sensibles leak.

La mutation pourrait même conduire à la prise de contrôle d'un compte en essayant de modifier les données d'autres comptes.

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

Contournement de l'autorisation dans GraphQL

Enchaîner les requêtes peut contourner un système d'authentification faible.

Dans l'exemple ci-dessous, vous pouvez voir que l'opération est "forgotPassword" et qu'elle ne devrait exécuter que la requête forgotPassword qui lui est associée. Cela peut être contourné en ajoutant une requête à la fin, dans ce cas nous ajoutons "register" et une variable utilisateur pour que le système enregistre un nouvel utilisateur.

Contournement de la limite de taux en utilisant des alias

Normalement, les objets GraphQL ne peuvent pas contenir plusieurs propriétés portant le même nom. Les alias vous permettent de contourner cette restriction en nommant explicitement les propriétés que vous souhaitez que l'API retourne. Vous pouvez utiliser des alias pour retourner plusieurs instances du même type d'objet dans une seule requête.

Pour plus d'informations sur les alias GraphQL, voir Aliases.

Bien que les alias soient destinés à limiter le nombre d'appels API que vous devez effectuer, ils peuvent également être utilisés pour forcer brutalement un point de terminaison GraphQL.

De nombreux points de terminaison auront une sorte de limiteur de taux en place pour prévenir les attaques par force brute. Certains limiteurs de taux fonctionnent sur la base du nombre de requêtes HTTP reçues plutôt que sur le nombre d'opérations effectuées sur le point de terminaison. Parce que les alias vous permettent effectivement d'envoyer plusieurs requêtes dans un seul message HTTP, ils peuvent contourner cette restriction.

L'exemple simplifié ci-dessous montre une série de requêtes aliassées vérifiant si les codes de réduction de magasin sont valides. Cette opération pourrait potentiellement contourner la limitation de taux car il s'agit d'une seule requête HTTP, même si elle pourrait potentiellement être utilisée pour vérifier un grand nombre de codes de réduction à la fois.

#Request with aliased queries
query isValidDiscount($code: Int) {
isvalidDiscount(code:$code){
valid
}
isValidDiscount2:isValidDiscount(code:$code){
valid
}
isValidDiscount3:isValidDiscount(code:$code){
valid
}
}

Outils

Scanners de vulnérabilités

Clients

Tests automatiques

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

Références

Apprenez le hacking AWS de zéro à héros avec htARTE (HackTricks AWS Red Team Expert)!

Autres moyens de soutenir HackTricks :