GraphQL

Вивчайте хакінг AWS від нуля до героя з htARTE (HackTricks AWS Red Team Expert)!

Інші способи підтримки HackTricks:

Вступ

GraphQL виділяється як ефективна альтернатива REST API, пропонуючи спрощений підхід для запитування даних з бекенду. У відміну від REST, який часто потребує численних запитів до різних кінцевих точок для збору даних, GraphQL дозволяє отримати всю необхідну інформацію через один запит. Це спрощення значно корисне для розробників, зменшуючи складність їх процесів отримання даних.

GraphQL та Безпека

З появою нових технологій, включаючи GraphQL, також виникають нові вразливості безпеки. Важливо зауважити, що GraphQL не включає механізми аутентифікації за замовчуванням. Це відповідальність розробників реалізувати такі заходи безпеки. Без належної аутентифікації, кінцеві точки GraphQL можуть розкривати чутливу інформацію незаутентифікованим користувачам, створюючи значний ризик для безпеки.

Атаки на перебір каталогів та GraphQL

Для ідентифікації відкритих екземплярів GraphQL рекомендується включення конкретних шляхів у атаки на перебір каталогів. Ці шляхи включають:

  • /graphql

  • /graphiql

  • /graphql.php

  • /graphql/console

  • /api

  • /api/graphql

  • /graphql/api

  • /graphql/graphql

Ідентифікація відкритих екземплярів GraphQL дозволяє перевірити підтримувані запити. Це важливо для розуміння даних, до яких можна отримати доступ через кінцеву точку. Система інтроспекції GraphQL сприяє цьому, деталізуючи запити, які підтримує схема. Для отримання додаткової інформації звертайтеся до документації GraphQL щодо інтроспекції: GraphQL: Мова запитів для API.

Відбиток

Інструмент graphw00f може виявити, який двигун GraphQL використовується на сервері, а потім надрукувати корисну інформацію для аудитора з питань безпеки.

Універсальні запити

Щоб перевірити, чи URL є службою GraphQL, можна відправити універсальний запит, query{__typename}. Якщо відповідь містить {"data": {"__typename": "Query"}}, це підтверджує, що URL містить кінцеву точку GraphQL. Цей метод ґрунтується на полі __typename GraphQL, яке вказує тип запитаного об'єкта.

query{__typename}

Базова Енумерація

GraphQL зазвичай підтримує GET, POST (x-www-form-urlencoded) та POST(json). Хоча з міркувань безпеки рекомендується дозволяти лише json, щоб запобігти атакам CSRF.

Інтроспекція

Для використання інтроспекції для виявлення інформації схеми, запитайте поле __schema. Це поле доступне на кореневому типі всіх запитів.

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

З цим запитом ви знайдете назву всіх використовуваних типів:

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

За допомогою цього запиту ви можете витягти всі типи, їх поля та аргументи (і тип аргументів). Це буде дуже корисно, щоб знати, як виконувати запити до бази даних.

Помилки

Цікаво знати, чи будуть показані помилки, оскільки вони додадуть корисну інформацію.

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

Перелік схеми бази даних через інтроспекцію

Якщо інтроспекція увімкнена, але вищезазначений запит не виконується, спробуйте видалити директиви onOperation, onFragment та onField зі структури запиту.

#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 інтроспекційний запит:

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

Останній рядок коду - це запит graphql, який виведе всю мета-інформацію з graphql (назви об'єктів, параметри, типи...)

Якщо ввімкнуто інтроспекцію, ви можете використовувати GraphQL Voyager, щоб переглянути у GUI всі опції.

Запити

Тепер, коли ми знаємо, який вид інформації зберігається в базі даних, давайте спробуємо витягти деякі значення.

У інтроспекції ви можете знайти, який об'єкт ви можете запитувати безпосередньо (тому що ви не можете запитувати об'єкт лише тому, що він існує). На наступному зображенні ви можете побачити, що "queryType" називається "Query", і одним з полів об'єкта "Query" є "flags", який також є типом об'єкта. Тому ви можете запитувати об'єкт прапорця.

Зверніть увагу, що тип запиту "flags" - "Flags", і цей об'єкт визначено так:

Ви можете побачити, що об'єкти "Flags" складаються з імені та значення. Тоді ви можете отримати всі імена та значення прапорців за допомогою запиту:

query={flags{name, value}}

Зверніть увагу, що у випадку, якщо об'єкт для запиту є примітивним типом таким як рядок, як у наступному прикладі

Ви можете просто запитати його за допомогою:

query={hiddenFlags}

У іншому прикладі, де було 2 об'єкти всередині об'єкта "Query": "user" та "users". Якщо цим об'єктам не потрібно жодного аргументу для пошуку, можна отримати всю інформацію з них, просто запитавши дані, які вам потрібні. У цьому прикладі з Інтернету ви можете витягти збережені імена користувачів та паролі:

Однак у цьому прикладі, якщо ви спробуєте це зробити, ви отримаєте цю помилку:

Здається, що він буде шукати за допомогою "uid" аргументу типу Int. В будь-якому випадку, ми вже знали, що в розділі Базова Енумерація було запропоновано запит, який показував нам всю необхідну інформацію: query={__schema{types{name,fields{name, args{name,description,type{name, kind, ofType{name, kind}}}}}}}

Якщо ви прочитаєте надане зображення, коли я виконав цей запит, ви побачите, що "user" мав аргумент "uid" типу Int.

Таким чином, виконавши деякий легкий uid брутфорс, я виявив, що в uid=1 було отримано ім'я користувача та пароль: query={user(uid:1){user,password}}

Зверніть увагу, що я виявив, що я можу запитувати параметри "user" та "password", оскільки, якщо я спробую знайти щось, що не існує (query={user(uid:1){noExists}}), я отримаю цю помилку:

І під час фази енумерації я виявив, що об'єкт "dbuser" мав поля "user" та "password.

Хитрість з витісканням рядка запиту (завдяки @BinaryShadow_)

Якщо ви можете шукати за типом рядка, наприклад: query={theusers(description: ""){username,password}} і ви шукаєте порожній рядок, він витісне всі дані. (Зауважте, що цей приклад не пов'язаний з прикладом уроків, для цього прикладу припустимо, що ви можете шукати за допомогою "theusers" за полем типу "description").

Пошук

У цій конфігурації база даних містить осіб та фільми. Особи ідентифікуються за їхнім електронним листом та ім'ям; фільми - за їхнім ім'ям та рейтингом. Особи можуть бути друзями один одного і також мати фільми, що вказує на взаємозв'язки в базі даних.

Ви можете шукати осіб за ім'ям та отримувати їхні електронні листи:

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

Ви можете шукати осіб за ім'ям та отримувати їх підписані фільми:

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

Зверніть увагу на те, як вказано отримати name subscribedMovies особи.

Ви також можете шукати кілька об'єктів одночасно. У цьому випадку виконується пошук 2 фільмів:

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

Чи навіть відносини кількох різних об'єктів за допомогою псевдонімів:

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

Мутації

Мутації використовуються для внесення змін на серверному боці.

У інтроспекції ви можете знайти оголошені мутації. На наступному зображенні "MutationType" називається "Mutation", а об'єкт "Mutation" містить назви мутацій (наприклад, "addPerson" у цьому випадку):

У цьому налаштуванні база даних містить осіб та фільми. Особи ідентифікуються за їхнім електронним листом та ім'ям; фільми - за їхнім ім'ям та рейтингом. Особи можуть бути друзями один одного і також мати фільми, що вказує на відносини в базі даних.

Мутація для створення нових фільмів у базі даних може виглядати наступним чином (у цьому прикладі мутація називається addMovie):

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

Зверніть увагу, як у запиті вказані як значення, так і тип даних.

Крім того, база даних підтримує операцію мутації, під назвою addPerson, яка дозволяє створювати осіб разом з їх асоціаціями з існуючими друзями та фільмами. Важливо зауважити, що друзі та фільми повинні існувати в базі даних перед тим, як їх зв'язати з новоствореною особою.

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

Перевантаження директив

Як пояснено в одному з вразливостей, описаних у цьому звіті, перевантаження директив передбачає виклик директиви навіть мільйони разів, щоб змусити сервер витрачати операції до тих пір, поки можливо DoS.

Пакетна атака методом брут-форсу в одному запиті API

Цю інформацію було взято з https://lab.wallarm.com/graphql-batching-attack/. Аутентифікація через GraphQL API з одночасним відправленням багатьох запитів з різними обліковими даними для перевірки. Це класична атака брут-форсу, але зараз можливо відправляти більше однієї пари логін/пароль за один HTTP-запит через функцію пакетної обробки GraphQL. Цей підхід змусить зовнішні програми моніторингу швидкості вважати, що все гаразд і немає жодного бота, який намагається вгадати паролі.

Нижче ви можете побачити найпростішу демонстрацію запиту аутентифікації додатка з 3 різними парами електронної пошти/паролів одночасно. Очевидно, можливо відправити тисячі за один запит таким же чином:

Як ми можемо побачити на знімку відповіді, перший і третій запити повернули null і відобразили відповідну інформацію в розділі error. Друга мутація мала правильні аутентифікаційні дані, і відповідь містить правильний токен сеансу аутентифікації.

GraphQL без інтроспекції

Дедалі більше кінцеві точки graphql вимикають інтроспекцію. Однак помилки, які викидає graphql, коли отримує неочікуваний запит, достатньо для інструментів, таких як clairvoyance, щоб відтворити більшу частину схеми.

Більше того, розширення Burp Suite GraphQuail спостерігає за запитами GraphQL API, які проходять через Burp і створює внутрішню схему GraphQL з кожним новим запитом, який він бачить. Воно також може викрити схему для GraphiQL та Voyager. Розширення повертає фальшиву відповідь, коли отримує запит на інтроспекцію. В результаті GraphQuail показує всі запити, аргументи та поля, доступні для використання в API. Для отримання додаткової інформації перевірте це.

Чудовий словник для виявлення сутностей GraphQL можна знайти тут.

Обхід захисту від інтроспекції GraphQL

Обхід захисту від інтроспекції GraphQL

Для обходу обмежень на інтроспекційні запити в API вставка спеціального символу після ключового слова __schema доводиться ефективною. Цей метод використовує загальні помилки розробників у регулярних виразах, які спрямовані на блокування інтроспекції, фокусуючись на ключовому слові __schema. Додавання символів, таких як пробіли, нові рядки та коми, які GraphQL ігнорує, але можуть не бути враховані в регулярних виразах, дозволяє обійти обмеження. Наприклад, запит інтроспекції з новим рядком після __schema може обійти такі захисти:

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

Якщо невдача, розгляньте альтернативні методи запитів, такі як GET-запити або POST з x-www-form-urlencoded, оскільки обмеження можуть застосовуватися лише до POST-запитів.

Виявлення викритих структур GraphQL

Коли інтроспекція вимкнена, вивчення вихідного коду веб-сайту для попередньо завантажених запитів у бібліотеках JavaScript є корисною стратегією. Ці запити можна знайти за допомогою вкладки Sources у засобах розробника, що надає відомості про схему API та розкриває потенційно викриті чутливі запити. Команди для пошуку у засобах розробника:

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

CSRF в GraphQL

Якщо ви не знаєте, що таке CSRF, прочитайте наступну сторінку:

pageCSRF (Cross Site Request Forgery)

Тут ви зможете знайти кілька кінцевих точок GraphQL, налаштованих без токенів CSRF.

Зверніть увагу, що запити GraphQL зазвичай відправляються через запити POST з використанням Content-Type application/json.

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

Проте більшість кінцевих точок GraphQL також підтримують form-urlencoded POST-запити:

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

Отже, оскільки запити CSRF, подібні до попередніх, відправляються без попередніх запитів, можливо внести зміни в GraphQL, зловживаючи CSRF.

Проте слід зауважити, що нове значення куки за замовчуванням для прапорця samesite в Chrome - Lax. Це означає, що куки будуть відправлятися лише з веб-сайту третьої сторони у запитах GET.

Також слід зауважити, що зазвичай можливо відправити запит query також як GET запит, і токен CSRF може не перевірятися у запиті GET.

Також, зловживаючи атакою XS-Search, можливо витягти вміст з кінцевої точки GraphQL, зловживаючи обліковими даними користувача.

Для отримання додаткової інформації перегляньте оригінальний пост тут.

Авторизація в GraphQL

Багато функцій GraphQL, визначених на кінцевій точці, можуть перевіряти лише аутентифікацію запитувача, але не авторизацію.

Зміна вхідних змінних запиту може призвести до витоку чутливих даних про обліковий запис витік.

Мутація може навіть призвести до захоплення облікового запису спробою змінити дані іншого облікового запису.

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

Обхід авторизації в GraphQL

Ланцюжкові запити можуть обійти слабку систему автентифікації.

У наведеному нижче прикладі можна побачити, що операція - "forgotPassword", і вона повинна виконувати лише запит forgotPassword, пов'язаний з нею. Це можна обійти, додавши запит в кінець, у цьому випадку ми додаємо "register" та змінну користувача для реєстрації в системі як нового користувача.

Обхід обмежень швидкості за допомогою псевдонімів в GraphQL

У GraphQL псевдоніми - це потужна функція, яка дозволяє явно називати властивості при здійсненні запиту до API. Ця можливість особливо корисна для отримання кількох екземплярів одного типу об'єкта в одному запиті. Псевдоніми можна використовувати для подолання обмеження, яке не дозволяє об'єктам GraphQL мати кілька властивостей з однаковою назвою.

Для детального розуміння псевдонімів в GraphQL рекомендується скористатися наступним ресурсом: Псевдоніми.

Хоча основна мета псевдонімів - зменшення необхідності в численних викликах API, було виявлено непередбачений випадок використання, де псевдоніми можуть бути використані для виконання атак перебору на кінцеву точку GraphQL. Це можливо, оскільки деякі кінцеві точки захищені обмежувачами швидкості, призначеними для запобігання атакам перебору шляхом обмеження кількості HTTP-запитів. Однак ці обмежувачі швидкості можуть не враховувати кількість операцій у кожному запиті. Оскільки псевдоніми дозволяють включати кілька запитів у один HTTP-запит, вони можуть обійти такі обмеження швидкості.

Розгляньте наведений нижче приклад, який показує, як запити з псевдонімами можуть бути використані для перевірки валідності кодів знижок магазину. Цей метод може обійти обмеження швидкості, оскільки він об'єднує кілька запитів в один HTTP-запит, що потенційно дозволяє перевіряти одночасно численні коди знижок.

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

Інструменти

Сканери вразливостей

  • https://github.com/gsmith257-cyber/GraphCrawler: Набір інструментів, який може бути використаний для отримання схем та пошуку чутливих даних, тестування авторизації, перебору схем та пошуку шляхів до певного типу.

  • https://blog.doyensec.com/2020/03/26/graphql-scanner.html: Може бути використаний як самостійний інструмент або розширення для Burp.

  • https://github.com/swisskyrepo/GraphQLmap: Може бути використаний як клієнт командного рядка для автоматизації атак.

  • https://gitlab.com/dee-see/graphql-path-enum: Інструмент, який перераховує різні способи досягнення певного типу в схемі GraphQL.

  • https://github.com/doyensec/inql: Розширення для Burp для продвинутого тестування GraphQL. Компонент Scanner є основою InQL v5.0, де ви можете проаналізувати кінцеву точку GraphQL або локальний файл інтроспекції схеми. Він автоматично генерує всі можливі запити та мутації, організовуючи їх у структурований вигляд для вашого аналізу. Компонент Attacker дозволяє вам запускати пакетні атаки GraphQL, що може бути корисним для обходу погано реалізованих обмежень швидкості.

Клієнти

Автоматичні тести

Посилання

Вивчайте хакінг AWS від нуля до героя з htARTE (HackTricks AWS Red Team Expert)!

Інші способи підтримки HackTricks:

Last updated