Вступление
Всем привет! На связи Владимир Исабеков, руководитель группы статического тестирования безопасности приложений в Swordfish Security. В предыдущей статье мы рассказывали о Stateful REST API-фаззинге с применением инструмента RESTler. Сегодня мы поговорим о продвинутых возможностях RESTler-а и покажем, как настроить фаззер на примере более сложного приложения. Этот материал мы подготовили вместе с Артемом Мурадяном @TOKYOBOY0701, инженером по безопасности.
Выбираем приложение для фаззинга
В качестве подопытного возьмем приложение Vampi – это заранее уязвимый API, созданный с помощью Flask и включающий в себя уязвимости из OWASP Top 10 API. После регистрации в Vampi пользователь может авторизоваться в системе и опубликовать книгу, используя токен, полученный при входе. Для размещения необходимо ввести название и секрет книги. Только ее владельцу разрешено просматривать этот секрет.
Проводим фаззинг
Итак, мы определились с приложением и готовы перейти непосредственно к фаззингу. Рассмотрим его основные этапы и покажем возможности инструмента RESTler.
1. Компиляция грамматики
Начнем с компиляции грамматики из спецификации API. Для этого скомпилируем спецификацию Vampi:
C:\restler_bin\restler\Restler.exe compile --api_spec C:\Vampi\openapi3.yml
Подробнее о том, как компилируется грамматика, можно узнать в нашей первой статье.
2. Настройка аутентификации
Так как в приложении присутствует аутентификация, нам необходимо, чтобы фаззер мог ее проходить. Существует несколько способов сделать это:
Способ 1. Текстовый файл
Первый и самый простой способ – это чтение текстового файла с токеном, который будет применяться для аутентификации. Чтобы фаззер использовал файл с токеном, нужно указать его в специальном конфигурационном файле Engine_settings (при последующем запуске фаззера путь к файлу указывается в параметре --settings):
"authentication": {
"token": {
"location": "/путь/до/токена.txt"
}
}
Способ 2. Скрипт
Это программа, которая возвращает токен. Скрипт можно указать в том же конфигурационном файле Engine_settings:
"authentication": {
"token": {
"token_refresh_cmd": "python acquire_token.py",
"token_refresh_interval": 200
}
}
Еще его можно разместить в параметрах командной строки при запуске фаззера
--token_refresh_command "python C:\restler-fuzzer-main\acquire_token.py" --token_refresh_interval 200
Способ 3. Модуль
Это подключаемый модуль, который реализует аутентификацию. RESTler импортирует его и вызывает функцию для получения токенов. Как и в предыдущих способах, модуль указывается в конфигурационном файле Engine_settings:
"authentication": {
"token": {
"module": {
"file": "/путь/до/acquire_token.py",
"function": "acquire_token_data", #(функция по умолчанию «acquire_token»)
"data": {
"client_id": "client_id"
}
},
"token_refresh_interval": 200
}
}
В отличие от скрипта, в модуль можно передать дополнительные параметры, необходимые для аутентификации. Делается это для того, чтобы в будущем не было нужды изменять сам скрипт. Вне зависимости от способа, формат токена одинаковый: в первой строке метаданные, которые записываются в журнал, в следующих – заголовок и сам токен:
{u'app1': {<метаданные для отладки >}, u'app2':{}}
ApiTokenTag: 9A
ApiTokenTag: ZQ
Для выбранного приложения мы решили реализовать второй способ, так как в нем не требуется менять пользователей во время работы фаззера, но при этом нужно постоянно обновлять токен. Использовались две учетные записи – обычного пользователя и админа (для покрытия фаззингом админских ручек). Наш скрипт для аутентификации выглядит так:
import requests
import json
import os
from dotenv import load_dotenv
def acquire_token(username, password):
url = "http://127.0.0.1:5000/users/v1/login"
payload = json.dumps({
"username": username,
"password": password
})
headers = {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
response = requests.request("POST", url, headers=headers, data=payload)
res = response.json()
# print(res)
stf = f'Authorization: Bearer {res["auth_token"]}'
return stf
metadata = "{u'first_token': {u'username':u'admin'}, u'second_token': {u'username':'name1'}}"
load_dotenv()
lgn1 = os.getenv("login1")
pswd1 = os.getenv("password1")
lgn2 = os.getenv("login2")
pswd2 = os.getenv("password2")
print(metadata)
print(acquire_token(lgn1,pass1))
print(acquire_token(lgn2,pswd2))
После успешной компиляции нужно проверить, сколько API-методов покрывается тестами RESTler-a.
3. Тестовое покрытие
Запускаем фаззер в режиме Test (выполнение всех запросов в спецификации OpenAPI хотя бы один раз и получение в ответ кода состояния «200»). Затем выполняем команду:
C:\restler_bin\restler\restler.exe test --grammar_file C:\Vampi\Compile\grammar.py --dictionary_file C:\Vampi\Compile\dict.json --settings C:\restler-fuzzer-main\Compile\engine_settings.json --token_refresh_interval 2 --token_refresh_command "python C:\restler-fuzzer-main\acquire_token.py" --no_ssl
Видим результаты:
От трех интерфейсов RESTler не смог получить код состояния «200». От одного из них инструмент получил код состояния «500». По умолчанию фаззер считает все ответы с кодом «500» багами и сохраняет их в файл main_driver_500_{}.json. Мы разберем результаты как раз из ответа Internal server error.
4. Анализ логов
В логах (..\Test\RestlerResults\experiment{}\bug_buckets\main_driver_500_1.json) мы можем увидеть, какой эндпоинт вызвал ошибку (/books/v1) и traceback приложения. Также благодаря им можно выяснить причину проблемы. Ошибка произошла из-за того, что сначала был вызван метод удаления пользователя, а после – метод, который выводил книги всех юзеров (/books/v1).
Интересно, что этот баг не значится в списке уязвимостей от разработчика Vampi и сохраняется даже при переводе приложения в «неуязвимый режим», при котором остальные уязвимости не срабатывают.
5. Повышаем покрытие API
Теперь нужно повысить покрытие нашего API тестами фаззера. Чтобы разобраться в проблемах, необходимо изучить файл C:\Vampi\Test\coverage_failures_to_investigate.txt, в котором
находятся все неудачные запросы.
Запрос Get /users/v1/{username} пытается получить пользователя, которого нет в базе, а Get /books/v1/{book_title} – книгу, которая там тоже отсутствует. Эту проблему легко исправить, изменив примеры соответствующих запросов в спецификации API.
Третий же запрос — Get /books/v1. Поскольку уязвимость на данном эндпоинте уже была найдена, можно удалить его из спецификации, чтобы не получать лишние срабатывания. Также мы приняли решение убрать метод /createdb, так как он приводит базу данных приложения в исходное состояние, а это может помешать правильной работе фаззера при формировании цепочек вызовов (может, например, удалиться какой-нибудь объект, созданный ранее в ходе цепочки вызовов API). После этих действий получаем полное покрытие оставшихся API-методов:
6. Фаззинг
Теперь, когда корректно сформирована грамматика и все API методы покрыты тестами, можно приступить к фаззингу. Запускаем фаззер в режиме fuzz командой
C:\restler_bin\restler\restler.exe fuzz --grammar_file C:\Vampi\Compile\grammar.py --dictionary_file C:\Vampi\Compile\dict.json --settings C:\restler-fuzzer-main\Compile\engine_settings.json --token_refresh_interval 2 --token_refresh_command "python C:\restler-fuzzer-main\acquire_token.py" --no_ssl
Через несколько секунд работы фаззера приложение зависает, в логах срабатывает чекер InvalidValueChecker_timeout:
Как мы видим, при запросе к эндпоинту /users/v1/{username}/email, в параметр почты которого помещен большой массив данных, произошло зависание сервиса. Это случилось из-за того, что регулярное выражение для обработки почты написано неверно. Появилась возможность атаки RegexDOS, при которой приложение тратит очень много времени на обработку зловредной входящей строки с большим количеством альтернативных путей для регулярного выражения. Это может привести к отказу в обслуживании для других пользователей. Данную уязвимость можно классифицировать как API4:2023 Unrestricted Resource Consumption.
Чтобы продолжить фаззинг, мы изменили грамматику (grammar.py) таким образом, чтобы поле почты больше не изменялось в процессе.
В новой итерации фаззинга появилось несколько ошибок. Сработал чекер InvalidValueChecker. Он вставляет в параметры запроса различные недопустимые значения. В этом случае можно использовать кастомный словарь недопустимых значений или генератор. Подробнее о настройках чекеров можно почитать тут.
При запросе к эндпоинту /users/v1/{username}, в котором в параметр имени пользователя была подставлена сгенерированная строка, произошла ошибка сервера. Раскрытие этой информации можно классифицировать как API8:2023 - Security Misconfiguration. Как мы видим, в traceback присутствует сам запрос в базу данных, что говорит о возможном наличии SQL-инъекции.
Еще одна внутренняя ошибка сервера была получена при запросе к /books/v1.
Здесь была попытка создания книги, которая уже есть в базе. Это привело к ошибке и появлению traceback.
Фаззер может отлавливать ответы не только с кодом «500», но и с любыми другими. Для этого нужно указать в файле Engine_settings параметр custom_bug_codes со списком кодов состояния, которые будут помечены как ошибки, либо custom_non_bug_codes с перечнем кодов, которые не будут помечены как ошибки (а все остальные будут). В данном случае мы изменили конфигурационный файл так, чтобы RESTler отлавливал коды «200» для эндпоинта /users/v1/{username}/password. Также добавили параметр include_requests, что позволяет отфильтровать грамматику и посылать запросы только к указанным методам.
Таким образом можно проверить возможность несанкционированной смены пароля другими пользователями. В ходе последующего фаззинга в файле с багами main_driver_20x_1.json был обнаружен успешный запрос на смену пароля пользователю name2 (скрипт авторизации был изменен, чтобы фаззер в данном случае мог авторизоваться только от лица обычного пользователя name1)
7. Результаты
В ходе фаззинга были найдены 4 подтвержденных бага, 3 из которых приводят к уязвимостям. Мы также просканировали приложение с помощью DAST сканера OWASP ZAP. Результаты можно увидеть в таблице ниже.
Уязвимости |
OWASP API top 10 |
Эндпоинты |
RESTler |
ZAP |
---|---|---|---|---|
SQL Injection |
API8:2019 Injection |
/users/v1/{username} |
+ |
+ |
Unauthorized Password Change |
API2:2023 Broken Authentication |
/users/v1/{username}/password |
+ |
|
Broken Object Level Authorization |
API1:2023 Broken Object Level Authorization |
/books/v1/{book_title} |
||
Mass Assignment |
API3:2023 Broken Object Property Level Authorization |
/users/v1/register |
||
Excessive Data Exposure |
API3:2023 Broken Object Property Level Authorization |
/users/v1/_debug; /books/v1 |
+ |
|
User and Password Enumeration |
API2:2023 Broken Authentication |
/users/v1/login |
||
RegexDOS (Denial of Service) |
API4:2023 Unrestricted Resource Consumption |
/users/v1/{username}/email |
+ |
|
Lack of Resources & Rate Limiting |
API2:2023 Broken Authentication |
/users/v1/login |
Заключение
Сегодня мы продемонстрировали возможности и эффективность фаззера RESTler. Этот инструмент находит баги, которые могут приводить к серьезным уязвимостям API, например Excessive Data Exposure, Injection, Unrestricted Resource Consumption и Broken User Authentication. В будущих статьях цикла мы расскажем вам о различных режимах фаззинга, об аннотациях, а также о проблемах, возникающих при подготовке и проведении фаззинг-тестирования API, и способах их решения. Оставайтесь с нами!