В ходе пентеста веб-приложений специалист по тестированию на проникновение достаточно часто сталкивается с необходимостью тестировать 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, чтобы отфильтровать возвращаемые объекты по этим полям:

Рисунок 1. Запрос публикации с id равным 12.
Рисунок 1. Запрос публикации с id равным 12.
Рисунок 2. Ответ на предыдущий запрос.
Рисунок 2. Ответ на предыдущий запрос.

Структура ответа, как видим, полностью отражает структуру запроса.

Тип Mutation.

Данный тип предназначен для создания, изменения и удаления данных. В примере выше мы запросили два поля из публикации (paste) по её id.  Давайте изменим содержание этой публикации:

Рисунок 3. Тип Mutation.
Рисунок 3. Тип Mutation.

Теперь если мы снова запросим этот объект и посмотрим в поле content, то увидим, что содержимое изменилось:

Рисунок 4. Изменения в поле content.
Рисунок 4. Изменения в поле 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:

Рисунок 5. Пример работы утилиты graphw00f
Рисунок 5. Пример работы утилиты graphw00f

В примере выше используется библиотека Graphene, язык программирования - Python.

Мы знаем эндпоинт и движок, но пока не знаем, какие запросы мы можем отправить на сервер. Ранее мы говорили о схеме, в которой описаны все доступные запросы, типы, поля. Функция интроспекции позволяет запрашивать схему конкретного API, что в свою очередь позволяет обнаружить доступные типы запросов, объектов и их поля.

Для отправки запросов я буду использовать InQL – расширения для Burp Suite (также есть консольная утилита InQL).

Для получения схемы используется специальный тип __schema.

Получить все доступные типы данных из схемы:
Запрос на получение доступных типов.
Запрос на получение доступных типов.
Часть доступных типов в схеме.
Часть доступных типов в схеме.

Получить схему для запросов типа Query:
Запрос схемы для типа Query.
Запрос схемы для типа Query.
Типы, которые доступны с типом Query.
Типы, которые доступны с типом 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 умеет получать схему автоматически:

Рисунок 6. Выбрав любой элемент в дереве, сразу видим структуру запроса.
Рисунок 6. Выбрав любой элемент в дереве, сразу видим структуру запроса.

Прямо из вкладки InQL Scanner можно отправить запрос во вкладку Repeater и начинать с ним работать.

Стоит добавить, что иногда из браузера доступен графический интерфейс для отправки запросов GraphiQL.

Интерфейс GraphiQL:
Интерфейс GraphiQL.
Интерфейс GraphiQL.

Если по какой-то причине разработчик веб-приложения оставил нам возможность пользоваться QraphiQL, то можно воспользоваться поиском или всплывающими подсказками в ходе редактирования запроса.

Примеры:
Всплывающие подсказки.
Всплывающие подсказки.
Поиск в схеме по ключевым словам.
Поиск в схеме по ключевым словам.

Иногда функция интроспекции выключена и схема нам не доступна. В этом случае полезно изучить все запросы, которые отправляются при взаимодействии пользователя с веб-интерфейсом приложения, а также воспользоваться фаззером для поиска валидных типов и полей в схеме.

Раскрытие информации.

Как при тестировании любого другого веб-приложения, тут действуют те же правила и принципы. Полезная для пентестера информация может вывалиться в ответе сервера, если вызвать ошибку в серверной части. Проще всего это сделать, подставив лишний символ в каком-нибудь месте запроса. В примере ниже приложение само подсказало, где допущена ошибка и правильное название поля:

Пример:
Вместо username отправили usernames.
Вместо username отправили usernames.
Действуя по аналогии, можно постепенно составить валидный запрос.
Действуя по аналогии, можно постепенно составить валидный запрос.

Вряд ли это можно назвать уязвимостью, но в полезности информации для атакующего, которому схема недоступна, сомневаться не приходится.

Интересный отчет на HackerOne по раскрытию информации в GraphQL.

DoS.

В силу своих некоторых особенностей GraphQl подвержен DoS-атакам.

В GraphQL реализована пакетная обработка запросов. Несколько запросов собираются в один пакет, а затем весь этот пакет запросов отправляется в базу данных. Отправив несколько сотен или тысяч запросов одним пакетом, можно нарушить работу приложения. В англоязычном сегменте интернета эта атака называется Batching Attack.

На гитхабе нашелся небольшой скрипт batchql, который как раз подойдет для демонстрации атаки. В уязвимом приложении выбрал запрос, который показался мне наиболее подходящим.

Запрос, который я буду использовать в Batching Attack:
В этом запросе выполняется поиск по ключевому слову (аргумент keyword).
В этом запросе выполняется поиск по ключевому слову (аргумент keyword).

На виртуальном сервере с GraphQL я запустил мониторинг ресурсов, чтобы можно было наблюдать результат. И запустил скрипт batchql. В то место, где в запросе #VARIABLE#, подставляются слова из списка words.txt:

Рисунок 7. Начал атаку.
Рисунок 7. Начал атаку.

Затем смотрю в монитор ресурсов на атакуемом сервере.

Как видно на скриншоте, нагрузка на процессор сразу возросла до 100%:
Результат атаки.
Результат атаки.

У пакетной обработки запросов есть еще один побочный эффект – мы можем перебирать учетные данные, отправляя в одном запросе тысячи разных вариантов. Таким образом можно обойти ограничения со стороны WAF на количество неудачных попыток аутентификации, ведь для файрволла это была одна попытка. Также можно попытаться обойти 2FA – если секретный код двухфакторной аутентификации состоит из 4-х цифр, как это часто бывает, то все 10 тысяч вариантов кода можно отправить одним запросом.

Другой вариант положить приложение – Deep Recursion Query Attack. Когда объект А ссылается на объект Б, а объект Б ссылается на объект А, мы можем построить такой циклический запрос, который вызовет отказ в обслуживании.

В демонстрационном уязвимом приложении есть подходящие для этой атаки объекты. На рисунке ниже наглядно показано, что owner ссылается на pastes, а pastes в свою очередь ссылается на owner:

Рисунок 8. Визуализация схемы в https://ivangoncharov.github.io/graphql-voyager/
Рисунок 8. Визуализация схемы в https://ivangoncharov.github.io/graphql-voyager/

Теперь составим вредоносный запрос.

Исходный запрос я взял в схеме из 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 есть возможность выполнить несколько запросов одновременно без необходимости использования пакетной обработки. Нужно выбрать ресурсозатратный запрос и продублировать его несколько раз, назначив каждой копии запроса собственный псевдоним:

Рисунок 9. Один и тот же запрос с разными псевдонимами.
Рисунок 9. Один и тот же запрос с разными псевдонимами.
Рисунок 10. Пока выполнялся запрос, сервер не отвечал на другие запросы.
Рисунок 10. Пока выполнялся запрос, сервер не отвечал на другие запросы.

Несколько интересных отчетов с HackerOne по DoS-атакам на GraphQL: один, два, три.

Приведенные выше атаки типичны для GraphQL, поэтому лично мне они были наиболее интересны при изучении вопроса. Ниже приведу примеры атак, которые могут встречаться в любых веб-приложениях, в том числе с API на основе GraphQL.

Server Side Request Forgery.

В схеме есть такой интересный запрос:

Рисунок 11. Привлекают внимание аргументы host и port.
Рисунок 11. Привлекают внимание аргументы host и port.

Типичное место для SSRF. На удаленной машине поднимаю http-сервер и в Burp выполняю запрос:

Рисунок 12. Пробую выполнить запрос на удаленный сервер.
Рисунок 12. Пробую выполнить запрос на удаленный сервер.

SSRF успешно отработал:

Рисунок 13. Результат выполнения запроса.
Рисунок 13. Результат выполнения запроса.

Отчет на HackerOne. В комментариях автор пишет об обнаруженном SSRF.

OS Command Injection.

В этом же запросе можно провести другую атаку - внедрение команд ОС.

На стороне сервера вводимые пользователем данные помещаются в cURL. Если мы попытаемся объединить две команды через символ “;”, то получится выполнить нужную нам команду bash:

Рисунок 14. Пробую внедрить команду id в аргумент path.
Рисунок 14. Пробую внедрить команду id в аргумент path.
Рисунок 15. Успешное внедрение команды.
Рисунок 15. Успешное внедрение команды.

SQL-injection.

Если GraphQL берет данные из SQL-базы, то почему бы не присутствовать SQL-инъекциям? В демонстрационном приложении используется база SQLite. Попробую узнать версию:

Рисунок 16.  Уязвимое место – аргумент filter.
Рисунок 16. Уязвимое место – аргумент filter.
Рисунок 17. Выполнение внедренной команды SQL.
Рисунок 17. Выполнение внедренной команды SQL.

Отчет на HackerOne по SQL-инъекции в GraphQL и выполнение команд ОС через SQL-инъекцию на Exploit-db.

Stored XSS.

Внедрение XSS ничем не отличается от подобных атак в любых других веб-приложениях:

Рисунок 18. При создании публикации JavaScript внедряется в ее заголовок.
Рисунок 18. При создании публикации JavaScript внедряется в ее заголовок.
Рисунок 19. XSS.
Рисунок 19. XSS.

Отчеты с HackerOne по XSS в GraphQL: один, два + HTML-инъекция в GraphQL.

IDOR.

Допустим, что мы имеем законный аккаунт John Doe на сайте. Когда мы пройдем аутентификацию и перейдем в настройки нашего аккаунта, то увидим некоторую информацию, принадлежащую только John Doe:

Рисунок 20. Конфиденциальная информация пользователя.
Рисунок 20. Конфиденциальная информация пользователя.

Для получения этой информации был отправлен следующий запрос GraphQL:

Рисунок 21. В запросе присутствует аргумент user со значением 1.
Рисунок 21. В запросе присутствует аргумент user со значением 1.

Что будет, если в аргументе запроса (user: 1) изменить идентификатор на какой-нибудь другой:

Рисунок 22. Изменили идентификатор на 2.
Рисунок 22. Изменили идентификатор на 2.

В результате выполнения этого запроса получим данные пользователя Jim Carry:

Рисунок 23. Данные другого пользователя.
Рисунок 23. Данные другого пользователя.

Отчет на HackerOne по IDOR в GraphQL.

File Write / Path Traversal.

В некоторых случаях также возможна запись или чтение произвольных файлов на сервере и обход каталогов. Попробуем записать произвольный файл за пределами корневой директории веб-приложения:

Рисунок 24. Аргумент filename уязвим к обходу каталогов.
Рисунок 24. Аргумент filename уязвим к обходу каталогов.

Теперь прочитаем файл, используя ранее рассмотренную инъекцию команд ОС:

Рисунок 25. Читаем ранее созданный файл.
Рисунок 25. Читаем ранее созданный файл.
Рисунок 26. Как видим, атака прошла успешно.
Рисунок 26. Как видим, атака прошла успешно.

Заключение.

Как вы могли заметить, GraphQL подвержен различным атакам, как характерным для этой технологии, так и характерным для всех веб-приложений. Рассмотренные атаки далеко не покрывают весь спектр возможного воздействия на приложения. GraphQL также подвержен таким атакам, как CSRF, Improper Access Control (один, два), Local File Read.

Для отработки навыков тестирования GraphQL рекомендую заведомо уязвимые приложения из списка ниже:

Материалы, использованные при подготовке статьи:

Комментарии (3)


  1. Luchnik22
    12.07.2022 16:03
    +4

    Спасибо за статью, для себя сделал пару заметок. Также стоит сделать оговорку, что это уязвимости не GraphQL, как такового, а проблемы его реализаций в конкретных фреймворках и либах.

    Например, SQL инъекция лечится на другом уровне, а не в GraphQL и том же REST. Или, например, защититься от DoS в Apollo Server можно одним флажком и whitelist запросов (хотя есть простые решения для ограничения "глубоких" и рекурсивных запросов)


  1. Sipaha
    12.07.2022 16:42
    +2

    Рисунок 9. Один и тот же запрос с разными псевдонимами.

    Я думал это недоработка реализации GraphQL API в java, но видимо проблема куда шире. У нас по сути ReadOnly древовидный запрос атрибутов, из которого можно выкинуть все псевдонимы перед вычислением (объединить {aa: abc{def}} и bb: abc{hij} сделав abc{def,hij}) и только получив все нужные данные вернуть их клиенту со всеми псевдонимами. Странно, что этого нет по умолчанию во всех реализациях GraphQL API.


  1. sky2high0
    13.07.2022 23:42

    А что из списка кроме DoS специфично именно для API на GraphQL? Любую другую атаку можно повторить на тот же Swagger, другие фреймворки для API и обычный «простой» API.