В ходе пентеста веб-приложений специалист по тестированию на проникновение достаточно часто сталкивается с необходимостью тестировать API. Как правило это REST API, про тестирование которого написано уже много. Однако все чаще и чаще встречается API на основе GraphQL.
Информации об этой технологии и вероятных атаках на нее в сети тоже достаточно. Но пентестеру, слабо знакомому с технологией, приходится небольшими частями собирать информацию во множестве разных источниках, чтобы сложилось целостное представление об объекте тестирования, о методологии и методиках тестирования. Я, так же столкнувшись с такой проблемой, скомпилировал полученную информацию в одном месте и решил ей поделиться с читателями Хабра.
Что такое GraphQL.
GraphQL – это одновременно и язык запросов для API, и среда выполнения этих запросов. И фронтенд и бэкенд. На клиентской стороне формируется запрос, сервер его обрабатывает, делает запрос к базе данных (или другому источнику данных), принимает из базы ответ и возвращает ответ клиенту.
GraphQL не зависит от используемого языка программирования. Есть множество его реализаций для разных языков, фреймворков и CMS. Он не зависит от используемых источников данных, а лишь предоставляет интерфейс для работы с данными. Источником данных может быть база данных, удаленные API, локальный кэш. Источников данных может быть несколько.
Первое существенное отличие от REST API в том, что в GraphQL есть только одна конечная точка (endpoint), через которую отправляются все запросы, как правило методом POST.
Второе существенное отличие – в GraphQL мы запрашиваем только те данные, которые нам нужны.
Система типов.
Для описания данных GraphQL имеет свою систему типов. Базовым типом является тип Object. Этот тип (например, это может быть users) имеет поля разных типов, например поле Age вероятнее всего будет типа Int, а поле Name типа String. Кроме того, тип Object может в себе содержать поле типа Object, которое само будет содержать вложенные поля других типов. Очевидно, что поле типа Int или String не может содержать вложенных полей. Такие типы называются скалярными.
Пример типа Object с именем users, который содержит три скалярных поля – id, age и username
{
"data":{
"users":[
{
"id":"1",
"age":33
"username":"Admin"
}
]
}
}
Также в GraphQL есть два особых типа – Query и Mutation (на самом деле есть еще тип Subscription, но в этой статье я его не рассматривал). Их особенность состоит в том, что они являются точками входа в систему. С помощью этих типов мы можем извлекать данные и манипулировать ими.
Тип Query.
Этот тип можно рассматривать как аналог GET в HTTP.
В самом простом случае мы хотим запросить одно или два поля одного объекта. Для этого нужно выполнить запрос типа Query, указать нужный тип объекта и нужное поле объекта, как на рисунке ниже. Так же необходимо указать значение id в аргументах запроса либо title, чтобы отфильтровать возвращаемые объекты по этим полям:
Структура ответа, как видим, полностью отражает структуру запроса.
Тип Mutation.
Данный тип предназначен для создания, изменения и удаления данных. В примере выше мы запросили два поля из публикации (paste) по её id. Давайте изменим содержание этой публикации:
Теперь если мы снова запросим этот объект и посмотрим в поле content, то увидим, что содержимое изменилось:
Схема.
Для разработчиков приложений, наверное, удобно и полезно знать, какие поля для каких типов они могут запросить? Все это описывается в схеме. Когда в GraphQL выполняется запрос, прежде чем он будет обработан, он проверяется на соответствие схеме и только потом выполняется.
Базовым компонентом схемы является тип Object и его поля. Любая схема имеет тип Query и может иметь или не иметь тип Mutation.
И так, когда мы пытаемся выполнить какой-то запрос, он сначала проверяется на соответствие схеме, то есть валидируется. Если мы, обращаясь к какому-то типу, запросим у него несуществующее поле, запрос не пройдет валидацию и не выполнится. Если запрос должен вернуть поля кроме скалярных (как сказано выше, скалярные типы не могут иметь своих полей), то мы также должны указать, какие вложенные поля мы хотим получить из поля типа объекта. В противном случае запрос будет не валидным. Аналогично, если поле является скалярным, то не имеет смысла запрашивать у него какие-нибудь поля.
После того, как запрос валидирован, он выполняется на стороне сервера. Запрос выполняется рекурсивно по мере вложенности. Если поле возвращает только скалярное значение (такие как строка или число), то выполнение запроса прекращается. Если поле возвращает тип Object, тогда запрашиваются данные из вложенных полей. Так будет продолжаться до тех пор, пока не встретится какое-то скалярное значение. В конечном итоге запросы в GraphQL всегда завершаются скалярными значениями (так называемые листья запроса).
По мере выполнения запроса и разрешения каждого поля результирующие значения помещаются в карту “ключ-значение”. В качестве ключа - имя поля, в качестве значения – значение этого поля. Все вместе это создает структуру, которая отражает исходный запрос. Затем данная структура, обычно в формате JSON, отправляется клиенту, который делал запрос.
Так может выглядить часть схемы
Тестирование.
Теперь, когда мы имеем базовое представление о том, что такое GraphQL, настало время проверить его на прочность. Важно добавить, что возможности GraphQL значительно шире, чем описано во вводной части.
Для демонстрации атак я развернул у себя на виртуальном сервере заведомо уязвимое веб-приложение Damn Vulnerable GraphQL Application и одну из лабораторий skf-labs. Также я постарался найти живые примеры к некоторым проводимым атакам – это отчеты на HackerOne.com и эксплойты на Exploit-db.com.
Ищем конечную точку (endpoint) и определяем движок GraphQL.
Тут все просто. Обычно конечная точка расположена по путям /graphql, /graphql.php или что-то похожее. Для поиска конечной точки можно использовать словарь из Seclists. Также можно настроить перехватывающий прокси, поиграть с приложением и посмотреть, куда уходят запросы.
Когда нашли конечную точку, необходимо выяснить, какая реализация используется для работы GraphQL, так как для некоторых реализаций могут быть известные уязвимости.
В определении используемого ПО поможет утилита graphw00f:
В примере выше используется библиотека Graphene, язык программирования - Python.
Мы знаем эндпоинт и движок, но пока не знаем, какие запросы мы можем отправить на сервер. Ранее мы говорили о схеме, в которой описаны все доступные запросы, типы, поля. Функция интроспекции позволяет запрашивать схему конкретного API, что в свою очередь позволяет обнаружить доступные типы запросов, объектов и их поля.
Для отправки запросов я буду использовать InQL – расширения для Burp Suite (также есть консольная утилита InQL).
Для получения схемы используется специальный тип __schema.
Получить все доступные типы данных из схемы:
Получить схему для запросов типа Query:
Аналогично можно получить схему для запросов типа Mutation, или получить всю схему целиком и разом, отправив такой запрос:
Запрос на получение всей схемы:
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
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
query IntrospectionQuery {
__schema {
queryType {
name
}
mutationType {
name
}
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
Расширение InQL для Burp умеет получать схему автоматически:
Прямо из вкладки InQL Scanner можно отправить запрос во вкладку Repeater и начинать с ним работать.
Стоит добавить, что иногда из браузера доступен графический интерфейс для отправки запросов GraphiQL.
Интерфейс GraphiQL:
Если по какой-то причине разработчик веб-приложения оставил нам возможность пользоваться QraphiQL, то можно воспользоваться поиском или всплывающими подсказками в ходе редактирования запроса.
Примеры:
Иногда функция интроспекции выключена и схема нам не доступна. В этом случае полезно изучить все запросы, которые отправляются при взаимодействии пользователя с веб-интерфейсом приложения, а также воспользоваться фаззером для поиска валидных типов и полей в схеме.
Раскрытие информации.
Как при тестировании любого другого веб-приложения, тут действуют те же правила и принципы. Полезная для пентестера информация может вывалиться в ответе сервера, если вызвать ошибку в серверной части. Проще всего это сделать, подставив лишний символ в каком-нибудь месте запроса. В примере ниже приложение само подсказало, где допущена ошибка и правильное название поля:
Пример:
Вряд ли это можно назвать уязвимостью, но в полезности информации для атакующего, которому схема недоступна, сомневаться не приходится.
Интересный отчет на HackerOne по раскрытию информации в GraphQL.
DoS.
В силу своих некоторых особенностей GraphQl подвержен DoS-атакам.
В GraphQL реализована пакетная обработка запросов. Несколько запросов собираются в один пакет, а затем весь этот пакет запросов отправляется в базу данных. Отправив несколько сотен или тысяч запросов одним пакетом, можно нарушить работу приложения. В англоязычном сегменте интернета эта атака называется Batching Attack.
На гитхабе нашелся небольшой скрипт batchql, который как раз подойдет для демонстрации атаки. В уязвимом приложении выбрал запрос, который показался мне наиболее подходящим.
Запрос, который я буду использовать в Batching Attack:
На виртуальном сервере с GraphQL я запустил мониторинг ресурсов, чтобы можно было наблюдать результат. И запустил скрипт batchql. В то место, где в запросе #VARIABLE#, подставляются слова из списка words.txt:
Затем смотрю в монитор ресурсов на атакуемом сервере.
Как видно на скриншоте, нагрузка на процессор сразу возросла до 100%:
У пакетной обработки запросов есть еще один побочный эффект – мы можем перебирать учетные данные, отправляя в одном запросе тысячи разных вариантов. Таким образом можно обойти ограничения со стороны WAF на количество неудачных попыток аутентификации, ведь для файрволла это была одна попытка. Также можно попытаться обойти 2FA – если секретный код двухфакторной аутентификации состоит из 4-х цифр, как это часто бывает, то все 10 тысяч вариантов кода можно отправить одним запросом.
Другой вариант положить приложение – Deep Recursion Query Attack. Когда объект А ссылается на объект Б, а объект Б ссылается на объект А, мы можем построить такой циклический запрос, который вызовет отказ в обслуживании.
В демонстрационном уязвимом приложении есть подходящие для этой атаки объекты. На рисунке ниже наглядно показано, что owner ссылается на pastes, а pastes в свою очередь ссылается на owner:
Теперь составим вредоносный запрос.
Исходный запрос я взял в схеме из InQL:
Этот запрос вернет все публикации, которые соответствуют условиям в аргументах. Аргумент filter оставлю пустым, аргумент limit оставлю, как на картинке под спойлером.
Обратите внимание, что у типа pastes есть поле owner, которое тоже имеет поля. Если у вложенного поля owner запросить его pastes, а затем у этих pastes снова запросить owner, то получится запрос:
Вредоносный запрос:
query {
pastes(filter:"", public:true, limit:1334) {
owner {
id
pastes {
burn
userAgent
title
ownerId
content
ipAddr
public
id
owner {
name
pastes {
burn
userAgent
title
ownerId
content
ipAddr
public
id
owner {
name
pastes {
burn
userAgent
title
ownerId
content
ipAddr
public
id
owner {
name
pastes {
burn
userAgent
title
ownerId
content
ipAddr
public
id
}
}
}
}
}
}
}
}
burn
userAgent
title
ownerId
content
ipAddr
public
id
}
}
Не такая уж и большая глубина вложенности, однако после отправки этого запроса сервер не отвечал около 10 минут.
Результат атаки:
Рассмотрим еще один способ вызвать отказ в обслуживании - Aliases based Attack. В GraphQL есть возможность выполнить несколько запросов одновременно без необходимости использования пакетной обработки. Нужно выбрать ресурсозатратный запрос и продублировать его несколько раз, назначив каждой копии запроса собственный псевдоним:
Несколько интересных отчетов с HackerOne по DoS-атакам на GraphQL: один, два, три.
Приведенные выше атаки типичны для GraphQL, поэтому лично мне они были наиболее интересны при изучении вопроса. Ниже приведу примеры атак, которые могут встречаться в любых веб-приложениях, в том числе с API на основе GraphQL.
Server Side Request Forgery.
В схеме есть такой интересный запрос:
Типичное место для SSRF. На удаленной машине поднимаю http-сервер и в Burp выполняю запрос:
SSRF успешно отработал:
Отчет на HackerOne. В комментариях автор пишет об обнаруженном SSRF.
OS Command Injection.
В этом же запросе можно провести другую атаку - внедрение команд ОС.
На стороне сервера вводимые пользователем данные помещаются в cURL. Если мы попытаемся объединить две команды через символ “;”, то получится выполнить нужную нам команду bash:
SQL-injection.
Если GraphQL берет данные из SQL-базы, то почему бы не присутствовать SQL-инъекциям? В демонстрационном приложении используется база SQLite. Попробую узнать версию:
Отчет на HackerOne по SQL-инъекции в GraphQL и выполнение команд ОС через SQL-инъекцию на Exploit-db.
Stored XSS.
Внедрение XSS ничем не отличается от подобных атак в любых других веб-приложениях:
Отчеты с HackerOne по XSS в GraphQL: один, два + HTML-инъекция в GraphQL.
IDOR.
Допустим, что мы имеем законный аккаунт John Doe на сайте. Когда мы пройдем аутентификацию и перейдем в настройки нашего аккаунта, то увидим некоторую информацию, принадлежащую только John Doe:
Для получения этой информации был отправлен следующий запрос GraphQL:
Что будет, если в аргументе запроса (user: 1) изменить идентификатор на какой-нибудь другой:
В результате выполнения этого запроса получим данные пользователя Jim Carry:
Отчет на HackerOne по IDOR в GraphQL.
File Write / Path Traversal.
В некоторых случаях также возможна запись или чтение произвольных файлов на сервере и обход каталогов. Попробуем записать произвольный файл за пределами корневой директории веб-приложения:
Теперь прочитаем файл, используя ранее рассмотренную инъекцию команд ОС:
Заключение.
Как вы могли заметить, GraphQL подвержен различным атакам, как характерным для этой технологии, так и характерным для всех веб-приложений. Рассмотренные атаки далеко не покрывают весь спектр возможного воздействия на приложения. GraphQL также подвержен таким атакам, как CSRF, Improper Access Control (один, два), Local File Read.
Для отработки навыков тестирования GraphQL рекомендую заведомо уязвимые приложения из списка ниже:
Материалы, использованные при подготовке статьи:
https://cheatsheetseries.owasp.org/cheatsheets/GraphQL_Cheat_Sheet.html
https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/GraphQL%20Injection
https://book.hacktricks.xyz/network-services-pentesting/pentesting-web/graphql
https://infosecwriteups.com/pwning-your-assignments-stored-xss-via-graphql-endpoint-6dd36c8a19d5
https://medium.com/@localh0t/discovering-graphql-endpoints-and-sqli-vulnerabilities-5d39f26cea2e
Комментарии (3)
Sipaha
12.07.2022 16:42+2Рисунок 9. Один и тот же запрос с разными псевдонимами.
Я думал это недоработка реализации GraphQL API в java, но видимо проблема куда шире. У нас по сути ReadOnly древовидный запрос атрибутов, из которого можно выкинуть все псевдонимы перед вычислением (объединить {aa: abc{def}} и bb: abc{hij} сделав abc{def,hij}) и только получив все нужные данные вернуть их клиенту со всеми псевдонимами. Странно, что этого нет по умолчанию во всех реализациях GraphQL API.
sky2high0
13.07.2022 23:42А что из списка кроме DoS специфично именно для API на GraphQL? Любую другую атаку можно повторить на тот же Swagger, другие фреймворки для API и обычный «простой» API.
Luchnik22
Спасибо за статью, для себя сделал пару заметок. Также стоит сделать оговорку, что это уязвимости не GraphQL, как такового, а проблемы его реализаций в конкретных фреймворках и либах.
Например, SQL инъекция лечится на другом уровне, а не в GraphQL и том же REST. Или, например, защититься от DoS в Apollo Server можно одним флажком и whitelist запросов (хотя есть простые решения для ограничения "глубоких" и рекурсивных запросов)