hacktricks/network-services-pentesting/pentesting-web/graphql.md
2024-02-10 15:36:32 +00:00

28 KiB

GraphQL

Lernen Sie AWS-Hacking von Grund auf mit htARTE (HackTricks AWS Red Team Expert)!

Andere Möglichkeiten, HackTricks zu unterstützen:

Einführung

GraphQL wird als effiziente Alternative zu REST-APIs hervorgehoben und bietet einen vereinfachten Ansatz zum Abfragen von Daten vom Backend. Im Gegensatz zu REST, bei dem oft mehrere Anfragen an verschiedene Endpunkte erforderlich sind, um Daten zu sammeln, ermöglicht GraphQL das Abrufen aller erforderlichen Informationen über eine einzige Anfrage. Diese Vereinfachung kommt den Entwicklern erheblich zugute, da sie die Komplexität ihrer Datenabrufprozesse verringert.

GraphQL und Sicherheit

Mit der Einführung neuer Technologien, einschließlich GraphQL, treten auch neue Sicherheitslücken auf. Ein wichtiger Punkt ist, dass GraphQL standardmäßig keine Authentifizierungsmechanismen enthält. Es liegt in der Verantwortung der Entwickler, solche Sicherheitsmaßnahmen zu implementieren. Ohne ordnungsgemäße Authentifizierung können GraphQL-Endpunkte sensible Informationen für nicht authentifizierte Benutzer freigeben und somit ein erhebliches Sicherheitsrisiko darstellen.

Directory-Brute-Force-Angriffe und GraphQL

Um freigegebene GraphQL-Instanzen zu identifizieren, wird empfohlen, bestimmte Pfade in Directory-Brute-Force-Angriffen einzuschließen. Diese Pfade sind:

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

Die Identifizierung offener GraphQL-Instanzen ermöglicht die Untersuchung unterstützter Abfragen. Dies ist entscheidend, um die über den Endpunkt zugänglichen Daten zu verstehen. Das Introspection-System von GraphQL erleichtert dies, indem es die Abfragen auflistet, die ein Schema unterstützt. Weitere Informationen hierzu finden Sie in der GraphQL-Dokumentation zur Introspection: GraphQL: Eine Abfragesprache für APIs.

Fingerprint

Das Tool graphw00f kann erkennen, welche GraphQL-Engine auf einem Server verwendet wird, und liefert dann einige hilfreiche Informationen für den Sicherheitsauditor.

Universelle Abfragen

Um zu überprüfen, ob eine URL einen GraphQL-Dienst darstellt, kann eine universelle Abfrage, query{__typename}, gesendet werden. Wenn die Antwort {"data": {"__typename": "Query"}} enthält, bestätigt dies, dass die URL einen GraphQL-Endpunkt hostet. Diese Methode basiert auf dem __typename-Feld von GraphQL, das den Typ des abgefragten Objekts angibt.

query{__typename}

Grundlegende Enumeration

Graphql unterstützt normalerweise GET, POST (x-www-form-urlencoded) und POST(json). Es wird jedoch empfohlen, nur json zuzulassen, um CSRF-Angriffe zu verhindern.

Introspektion

Um die Schema-Informationen mit Hilfe der Introspektion zu entdecken, fragen Sie das Feld __schema ab. Dieses Feld ist auf dem Wurzeltyp aller Abfragen verfügbar.

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

Mit dieser Abfrage finden Sie den Namen aller verwendeten Typen:

{% code overflow="wrap" %}

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

{% endcode %}

Mit dieser Abfrage können Sie alle Typen, ihre Felder und ihre Argumente (und den Typ der Argumente) extrahieren. Dies ist sehr nützlich, um zu wissen, wie die Datenbank abgefragt werden kann.

Fehler

Es ist interessant zu wissen, ob die Fehler angezeigt werden, da sie nützliche Informationen liefern können.

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

Datenbankschema über Introspektion ermitteln

{% hint style="info" %} Wenn die Introspektion aktiviert ist, aber die obige Abfrage nicht ausgeführt wird, versuchen Sie, die Direktiven onOperation, onFragment und onField aus der Abfragestruktur zu entfernen. {% 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
}
}
}
}

Inline-Introspektionsabfrage:

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

Die letzte Codezeile ist eine GraphQL-Abfrage, die alle Metainformationen aus dem GraphQL (Objektnamen, Parameter, Typen...) ausgibt.

Wenn die Introspektion aktiviert ist, können Sie GraphQL Voyager verwenden, um in einer GUI alle Optionen anzuzeigen.

Abfragen

Nun, da wir wissen, welche Art von Informationen in der Datenbank gespeichert sind, versuchen wir, einige Werte abzurufen.

In der Introspektion können Sie herausfinden, welches Objekt Sie direkt abfragen können (weil Sie ein Objekt nicht einfach abfragen können, nur weil es existiert). Im folgenden Bild sehen Sie, dass der "queryType" "Query" genannt wird und dass eines der Felder des "Query"-Objekts "flags" ist, das auch ein Objekttyp ist. Daher können Sie das Flag-Objekt abfragen.

Beachten Sie, dass der Typ der Abfrage "flags" "Flags" ist und dieses Objekt wie folgt definiert ist:

Sie können sehen, dass die "Flags"-Objekte aus Name und Wert bestehen. Sie können also alle Namen und Werte der Flags mit der Abfrage erhalten:

query={flags{name, value}}

Beachten Sie, dass im Falle des zu abfragenden Objekts ein primitiver Typ wie String ist, wie im folgenden Beispiel:

Sie können es einfach abfragen mit:

query={hiddenFlags}

In einem anderen Beispiel, in dem es 2 Objekte innerhalb des "Query"-Typobjekts gab: "user" und "users". Wenn diese Objekte keine Argumente zur Suche benötigen, können Sie alle Informationen von ihnen abrufen, indem Sie einfach nach den gewünschten Daten fragen. In diesem Beispiel aus dem Internet könnten Sie die gespeicherten Benutzernamen und Passwörter extrahieren:

Jedoch erhalten Sie in diesem Beispiel bei dem Versuch, dies zu tun, folgenden Fehler:

Es scheint, dass auf irgendeine Weise das "uid"-Argument vom Typ Int verwendet wird. Wie auch immer, wir wussten bereits in dem Abschnitt Grundlegende Enumeration, dass eine Abfrage vorgeschlagen wurde, die uns alle benötigten Informationen zeigt: query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

Wenn Sie das bereitgestellte Bild lesen, sehen Sie, dass "user" das arg "uid" vom Typ Int hatte.

Daher habe ich durch eine leichte uid-Brute-Force herausgefunden, dass bei uid=1 ein Benutzername und ein Passwort abgerufen wurden:
query={user(uid:1){user,password}}

Beachten Sie, dass ich herausgefunden habe, dass ich nach den Parametern "user" und "password" fragen konnte, weil ich bei dem Versuch, nach etwas zu suchen, das nicht existiert (query={user(uid:1){noExists}}), diesen Fehler erhalte:

Und während der Enumeration-Phase habe ich herausgefunden, dass das Objekt "dbuser" die Felder "user" und "password" hatte.

Trick zum Dumpen von Abfragezeichenfolgen (danke an @BinaryShadow_)

Wenn Sie nach einem Zeichenfolgentyp suchen können, wie z.B. query={theusers(description: ""){username,password}} und Sie nach einer leeren Zeichenfolge suchen, werden alle Daten abgerufen. (Beachten Sie, dass dieses Beispiel nicht mit dem Beispiel der Tutorials zusammenhängt. Für dieses Beispiel nehmen Sie an, dass Sie mit "theusers" nach einem String-Feld namens "description" suchen können).

Suche

In dieser Konfiguration enthält eine Datenbank Personen und Filme. Personen werden anhand ihrer E-Mail und ihres Namens identifiziert; Filme anhand ihres Namens und ihrer Bewertung. Personen können miteinander befreundet sein und auch Filme haben, was Beziehungen in der Datenbank anzeigt.

Sie können Personen nach dem Namen suchen und ihre E-Mails erhalten:

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

Sie können Personen nach ihrem Namen suchen und ihre abonnierten Filme erhalten:

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

Beachten Sie, wie angegeben wird, den name der subscribedMovies der Person abzurufen.

Sie können auch mehrere Objekte gleichzeitig suchen. In diesem Fall wird eine Suche nach 2 Filmen durchgeführt:

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

Oder sogar Beziehungen mehrerer verschiedener Objekte unter Verwendung von Aliassen:

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

Mutationen

Mutationen werden verwendet, um Änderungen auf der Serverseite vorzunehmen.

In der Introspektion können Sie die deklarierten Mutationen finden. Im folgenden Bild wird "MutationType" als "Mutation" bezeichnet und das Objekt "Mutation" enthält die Namen der Mutationen (wie "addPerson" in diesem Fall):

In dieser Konfiguration enthält eine Datenbank Personen und Filme. Personen werden anhand ihrer E-Mail und ihres Namens identifiziert, Filme anhand ihres Namens und ihrer Bewertung. Personen können miteinander befreundet sein und auch Filme haben, was Beziehungen innerhalb der Datenbank anzeigt.

Eine Mutation zum Erstellen neuer Filme in der Datenbank könnte wie folgt aussehen (in diesem Beispiel wird die Mutation addMovie genannt):

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

Beachten Sie, wie sowohl die Werte als auch der Datentyp in der Abfrage angegeben sind.

Darüber hinaus unterstützt die Datenbank eine Mutation-Operation namens addPerson, mit der Personen erstellt werden können, zusammen mit ihren Verbindungen zu vorhandenen Freunden und Filmen. Es ist wichtig zu beachten, dass die Freunde und Filme bereits in der Datenbank vorhanden sein müssen, bevor sie mit der neu erstellten Person verknüpft werden können.

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

Stapelbruteforce in 1 API-Anfrage

Diese Informationen wurden von https://lab.wallarm.com/graphql-batching-attack/ übernommen.
Authentifizierung über die GraphQL-API durch gleichzeitiges Senden vieler Abfragen mit unterschiedlichen Anmeldeinformationen, um sie zu überprüfen. Es handelt sich um einen klassischen Brute-Force-Angriff, bei dem jetzt aufgrund der GraphQL-Batch-Funktion mehr als ein Anmelde-/Passwortpaar pro HTTP-Anfrage gesendet werden kann. Mit diesem Ansatz kann externe Rate-Monitoring-Anwendungen getäuscht werden, indem sie denken, dass alles in Ordnung ist und es keinen Brute-Force-Bot gibt, der Passwörter errät.

Unten finden Sie die einfachste Demonstration einer Anforderung zur Anwendungsberechtigung mit 3 verschiedenen E-Mail-/Passwortpaaren gleichzeitig. Natürlich ist es auf die gleiche Weise möglich, Tausende in einer einzigen Anfrage zu senden:

Wie wir aus dem Antwort-Screenshot sehen können, haben die ersten und dritten Anfragen null zurückgegeben und die entsprechenden Informationen im error-Abschnitt reflektiert. Die zweite Mutation hatte die richtigen Authentifizierungsdaten und die Antwort enthält das richtige Authentifizierungssitzungstoken.

GraphQL ohne Introspektion

Immer mehr GraphQL-Endpunkte deaktivieren die Introspektion. Die Fehler, die GraphQL wirft, wenn eine unerwartete Anfrage empfangen wird, reichen jedoch aus, damit Tools wie clairvoyance den Großteil des Schemas rekonstruieren können.

Darüber hinaus beobachtet die Burp Suite-Erweiterung GraphQuail die durch Burp gehenden GraphQL-API-Anfragen und erstellt ein internes GraphQL-Schema mit jeder neuen Abfrage, die sie sieht. Es kann auch das Schema für GraphiQL und Voyager freigeben. Die Erweiterung gibt eine gefälschte Antwort zurück, wenn sie eine Introspektionsabfrage erhält. Dadurch zeigt GraphQuail alle Abfragen, Argumente und Felder an, die in der API verwendet werden können. Weitere Informationen finden Sie hier.

Eine gute Wortliste, um GraphQL-Entitäten zu entdecken, finden Sie hier.

Umgehung von GraphQL-Introspektionsabwehrmaßnahmen

Umgehung von GraphQL-Introspektionsabwehrmaßnahmen

Um Einschränkungen bei Introspektionsabfragen in APIs zu umgehen, ist es wirksam, nach dem Schlüsselwort __schema ein spezielles Zeichen einzufügen. Diese Methode nutzt häufige Entwicklerfehler in Regex-Mustern aus, die darauf abzielen, die Introspektion durch Fokussierung auf das Schlüsselwort __schema zu blockieren. Durch das Hinzufügen von Zeichen wie Leerzeichen, Zeilenumbrüche und Kommas, die von GraphQL ignoriert werden, aber möglicherweise nicht in Regex berücksichtigt werden, können Einschränkungen umgangen werden. Zum Beispiel kann eine Introspektionsabfrage mit einem Zeilenumbruch nach __schema solche Abwehrmaßnahmen umgehen:

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

Wenn dies nicht erfolgreich ist, sollten alternative Anfragemethoden wie GET-Anfragen oder POST mit x-www-form-urlencoded in Betracht gezogen werden, da Einschränkungen möglicherweise nur für POST-Anfragen gelten.

Aufdecken von freigelegten GraphQL-Strukturen

Wenn die Introspektion deaktiviert ist, ist es eine nützliche Strategie, den Quellcode der Website nach vorab geladenen Abfragen in JavaScript-Bibliotheken zu untersuchen. Diese Abfragen können mithilfe des Sources-Tabs in den Entwicklertools gefunden werden und geben Einblicke in das Schema der API und enthüllen potenziell sensible freigelegte Abfragen. Die Befehle zum Suchen in den Entwicklertools lauten:

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

CSRF in GraphQL

Wenn Sie nicht wissen, was CSRF ist, lesen Sie die folgende Seite:

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

Dort finden Sie mehrere GraphQL-Endpunkte, die ohne CSRF-Token konfiguriert sind.

Beachten Sie, dass GraphQL-Anfragen normalerweise über POST-Anfragen mit dem Content-Type application/json gesendet werden.

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

Jedoch unterstützen die meisten GraphQL-Endpunkte auch form-urlencoded POST-Anfragen:

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

Daher ist es möglich, CSRF-Anfragen wie die vorherigen ohne Preflight-Anfragen zu senden, um Änderungen in GraphQL durch Missbrauch eines CSRF durchzuführen.

Beachten Sie jedoch, dass der neue Standardwert für das samesite-Flag von Chrome Lax ist. Dies bedeutet, dass das Cookie nur von einer Drittanbieter-Website in GET-Anfragen gesendet wird.

Beachten Sie auch, dass es in der Regel möglich ist, die Abfrageanfrage auch als GET-Anfrage zu senden und das CSRF-Token möglicherweise nicht in einer GET-Anfrage validiert wird.

Es ist auch möglich, durch Ausnutzung eines XS-Search-Angriffs Inhalte aus dem GraphQL-Endpunkt unter Ausnutzung der Anmeldeinformationen des Benutzers zu exfiltrieren.

Weitere Informationen finden Sie im Originalbeitrag hier.

Autorisierung in GraphQL

Viele in der Endpunkt definierte GraphQL-Funktionen überprüfen möglicherweise nur die Authentifizierung des Anfragenden, jedoch nicht die Autorisierung.

Die Änderung der Eingabevariablen der Abfrage könnte zum Leck sensibler Kontodetails führen.

Mutationen könnten sogar zu Account-Übernahmen führen, wenn versucht wird, andere Kontodaten zu ändern.

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

Umgehung der Autorisierung in GraphQL

Das Verketten von Abfragen kann ein schwaches Authentifizierungssystem umgehen.

Im folgenden Beispiel sehen Sie, dass die Operation "forgotPassword" ist und dass nur die damit verbundene forgotPassword-Abfrage ausgeführt werden sollte. Dies kann umgangen werden, indem am Ende eine zusätzliche Abfrage hinzugefügt wird. In diesem Fall fügen wir "register" hinzu und eine Benutzervariable, um das System als neuen Benutzer zu registrieren.

Umgehung von Rate Limits mit Aliases in GraphQL

In GraphQL sind Aliases ein leistungsstarkes Feature, das es ermöglicht, Eigenschaften explizit zu benennen, wenn eine API-Anfrage gestellt wird. Diese Funktion ist besonders nützlich, um mehrere Instanzen desselben Objekttyps in einer einzigen Anfrage abzurufen. Aliases können verwendet werden, um die Einschränkung zu umgehen, dass GraphQL-Objekte nicht mehrere Eigenschaften mit demselben Namen haben können.

Für ein detailliertes Verständnis von GraphQL-Aliases wird die folgende Ressource empfohlen: Aliases.

Obwohl der Hauptzweck von Aliases darin besteht, die Notwendigkeit für zahlreiche API-Aufrufe zu reduzieren, wurde ein unbeabsichtigter Anwendungsfall identifiziert, bei dem Aliases genutzt werden können, um Brute-Force-Angriffe auf einen GraphQL-Endpunkt auszuführen. Dies ist möglich, da einige Endpunkte durch Rate-Limiter geschützt sind, die Brute-Force-Angriffe durch die Begrenzung der Anzahl der HTTP-Anfragen verhindern sollen. Diese Rate-Limiter berücksichtigen jedoch möglicherweise nicht die Anzahl der Operationen in jeder Anfrage. Da Aliases das Hinzufügen mehrerer Abfragen in einer einzigen HTTP-Anfrage ermöglichen, können sie solche Rate-Limiting-Maßnahmen umgehen.

Betrachten Sie das untenstehende Beispiel, das veranschaulicht, wie aliased Abfragen verwendet werden können, um die Gültigkeit von Rabattcodes für einen Online-Shop zu überprüfen. Diese Methode könnte das Rate Limiting umgehen, da sie mehrere Abfragen in einer HTTP-Anfrage zusammenfasst und somit die Überprüfung zahlreicher Rabattcodes gleichzeitig ermöglicht.

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

Werkzeuge

Schwachstellen-Scanner

Clients

Automatische Tests

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

Referenzen

Lernen Sie AWS-Hacking von Grund auf mit htARTE (HackTricks AWS Red Team Expert)!

Andere Möglichkeiten, HackTricks zu unterstützen: