29 KiB
GraphQL
Apprenez le piratage AWS de zéro à héros avec htARTE (HackTricks AWS Red Team Expert)!
Autres moyens de soutenir HackTricks :
- Si vous souhaitez voir votre entreprise annoncée dans HackTricks ou télécharger HackTricks en PDF, consultez les PLANS D'ABONNEMENT!
- Obtenez le merchandising officiel PEASS & HackTricks
- Découvrez La Famille PEASS, notre collection d'NFTs exclusifs
- Rejoignez le 💬 groupe Discord ou le groupe telegram ou suivez moi sur Twitter 🐦 @carlospolopm.
- Partagez vos astuces de piratage en soumettant des PR aux dépôts github HackTricks et HackTricks Cloud.
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
- https://github.com/gsmith257-cyber/GraphCrawler : Boîte à outils pouvant être utilisée pour récupérer des schémas et rechercher des données sensibles, tester l'autorisation, forcer brutalement des schémas et trouver des chemins vers un type donné.
- https://blog.doyensec.com/2020/03/26/graphql-scanner.html : Peut être utilisé seul ou comme extension Burp.
- https://github.com/swisskyrepo/GraphQLmap : Peut également être utilisé comme client CLI pour automatiser les attaques.
- https://gitlab.com/dee-see/graphql-path-enum : Outil qui liste les différentes manières d'atteindre un type donné dans un schéma GraphQL.
- https://github.com/doyensec/inql : Extension Burp pour des tests GraphQL avancés. Le Scanner est le cœur d'InQL v5.0, où vous pouvez analyser un point de terminaison GraphQL ou un fichier de schéma d'introspection local. Il génère automatiquement toutes les requêtes et mutations possibles, les organisant dans une vue structurée pour votre analyse. Le composant Attacker vous permet d'exécuter des attaques GraphQL en lot, ce qui peut être utile pour contourner des limites de taux mal implémentées.
Clients
- https://github.com/graphql/graphiql : Client GUI
- https://altair.sirmuel.design/ : Client GUI
Tests automatiques
{% embed url="https://graphql-dashboard.herokuapp.com/" %}
- Vidéo expliquant AutoGraphQL : https://www.youtube.com/watch?v=JJmufWfVvyU
Références
- https://jondow.eu/practical-graphql-attack-vectors/
- https://medium.com/@the.bilal.rizwan/graphql-common-vulnerabilities-how-to-exploit-them-464f9fdce696
- https://medium.com/@apkash8/graphql-vs-rest-api-model-common-security-test-cases-for-graphql-endpoints-5b723b1468b4
- http://ghostlulz.com/api-hacking-graphql/
- https://github.com/swisskyrepo/PayloadsAllTheThings/blob/master/GraphQL%20Injection/README.md
- https://medium.com/@the.bilal.rizwan/graphql-common-vulnerabilities-how-to-exploit-them-464f9fdce696
- https://portswigger.net/web-security/graphql
Apprenez le hacking AWS de zéro à héros avec htARTE (HackTricks AWS Red Team Expert)!
Autres moyens de soutenir HackTricks :
- Si vous souhaitez voir votre entreprise annoncée dans HackTricks ou télécharger HackTricks en PDF, consultez les PLANS D'ABONNEMENT!
- Obtenez le merchandising officiel PEASS & HackTricks
- Découvrez La Famille PEASS, notre collection d'NFTs exclusifs
- Rejoignez le 💬 groupe Discord ou le groupe Telegram ou suivez moi sur Twitter 🐦 @carlospolopm.
- Partagez vos astuces de hacking en soumettant des PR aux dépôts github HackTricks et HackTricks Cloud.