Содержание статьи
Эффективность DAST
Динамическое сканирование (DAST) — один из важных этапов анализа защищенности приложения. Будь то пентест или процессы application security, в любом случае мы запускаем сканер уязвимостей, который отправляет к приложению множество запросов и пытается выявить проблемы безопасности.
Однако DAST не лишен недостатков.
Во‑первых, настройки сканеров по умолчанию не всегда позволяют пройти валидацию, так как не учитываются особенности конкретных запросов к API, например, что в одном из параметров должен быть UUID.
Во‑вторых, сканер по умолчанию ничего не знает про аутентификацию. Если его запросы не содержат bearer-токен или cookie, то мы просто получим ошибку, не дойдя до проблемы.
В‑третьих, это отчет, в котором может быть отмечено множество незначительных недостатков безопасности, что мешает заметить реально важные уязвимости и уделить им время.
В этой статье я покажу, как улучшить DAST, настроив динамическое сканирование под конкретное приложение с помощью аутентификации и фаззинга OpenAPI, на примере сканера уязвимостей Nuclei. Также напишем три кастомных шаблона для нахождения уязвимостей:
- Разглашение информации.
- Error based SQL-инъекции.
- Broken Object Level Authorization.
В качестве подопытного возьмем VAmPI — специальное уязвимое приложение для тестирования защищенности API. У него есть поддержка OpenAPI и показательные примеры небезопасной реализации. Чтобы его установить, достаточно клонировать репозиторий и запустить docker
:
git clone https://github.com/erev0s/VAmPI
cd VAmPI
docker compose up
Также необходимо инициализировать базу данных, отправив GET-запрос:
curl http://127.0.0.1:5002/createdb
{ "message": "Database populated." }
Для начала запустим Nuclei с настройками по умолчанию (достаточно передать цель для сканирования):
nuclei -u http://127.0.0.1:5002
При таком запуске Nuclei использует встроенные шаблоны. В результате видим ложноположительные результаты и информационные замечания, которые могут быть полезны только на этапе сбора информации при пентесте.
Наша цель — найти реальные проблемы, поэтому мы напишем кастомные шаблоны для сканирования известного REST API на уязвимости, наличие которых мы можем предполагать из понимания реализации. В этом нам помогут возможности, которые появились в Nuclei версии 3.2.
Фаззинг с Nuclei
Если у приложения много эндпоинтов HTTP, каждый из которых может обрабатывать множество параметров, проверка становится утомительной.
Автоматизировать анализ защищенности крупных API помогает фаззинг — техника, которая последовательно применяет набор проверок ко всем возможным вариациям запросов к приложению.
Мы можем передать реальные запросы из дампа трафика других инструментов, логов или спецификации OpenAPI в Nuclei. Остановимся на OpenAPI, так как ее поддержка есть у многих современных веб‑приложений и в отличие от сбора трафика такой метод не требует активных действий.
Передать OpenAPI в Nuclei можно следующим образом:
nuclei -l openapi.json -im openapi -t templates
Пример шаблона для фаззинга:
http: # filter checks if the template should be executed on a given request - pre-condition: - type: dsl dsl: - method == POST - len(body) > 0 condition: and # payloads that will be used in fuzzing payloads: injection: # Variable name for payload - "'" - "\"" - ";" # fuzzing rules fuzzing: - part: body # This rule will be applied to the Body type: postfix # postfix type of rule (i.e., payload will be added at the end of exiting value) mode: single # single mode (i.e., existing values will be replaced one at a time) fuzz: # format of payload to be injected - '{{injection}}' # here, we are directly using the value of the injection variable
В шаблоне выше секция pre-condition
фильтрует и запускает проверку только POST-запросов с непустым телом.
- pre-condition: - type: dsl dsl: - method == POST - len(body) > 0 condition: and
Вот какие части HTTP-запроса может фаззить Nuclei:
-
part:
— параметры запроса;query -
part:
— путь;path -
part:
— заголовок;header -
part:
— cookie;cookie -
part:
— тело запроса.body
Это далеко не все настройки фаззинга Nuclei, но для наших целей достаточно.
Сканирование с аутентификацией
Если запросы сканера не проходят дальше авторизации приложения, то от него может быть мало пользы, так как многие уязвимости скрываются гораздо глубже — в слоях с бизнес‑логикой и в репозитории.
Для сканирования с аутентификацией необходимо создать специальный файл и передать его Nuclei при сканировании:
nuclei -u http://127.0.0.1:5002 -secret-file secrets.yaml
Поддерживаются следующие виды аутентификации:
- Basic Auth;
- API Key;
- Bearer Token;
- Custom Header;
- Cookie.
Пример файла аутентификации:
# static secretsstatic: # 1. Basic Auth based auth - type: basicauth domains: - scanme.sh username: test password: test # 2. API Key (via query parameters) based auth - type: query domains: - example.com params: - key: token value: 1a2b3c4d5e6f7g8h9i0j # 3. Bearer Token based auth - type: bearertoken domains-regex: - .*scanme.sh - .*pdtm.sh token: test # 4. Custom Header based auth - type: header domains: - api.projectdiscovery.io - cve.projectdiscovery.io - chaos.projectdiscovery.io headers: - key: x-pdcp-key value: <api-key-here> # 5. Cookie based auth - type: cookie domains: - scanme.sh cookies: - key: PHPSESSID value: 1a2b3c4d5e6f7g8h9i0j # raw: "PHPSESSID=1a2b3c4d5e6f7g8h9i0j" (an alternative way to specify cookie value)
Подготавливаем OpenAPI
OpenAPI — это не самая строгая спецификация, а ее обработка в Nuclei не идеальна (по крайней мере текущая реализация), поэтому иногда необходимо редактирование.
На нашем стенде с VAmPI по пути /
располагается Swagger UI.
Скачаем OpenAPI:
wget http://127.0.0.1:5002/openapi.json
Если попробовать запустить Nuclei со скачанной спецификацией, будет ошибка.
Ошибка требует, чтобы в OpenAPI был явно задан URL тестируемого приложения. Давай это исправим.
Если снова попытаться запустить сканирование, получим еще одну ошибку.
На этот раз Nuclei не нравится пустой массив bearerAuth
в секции security
, хотя именно так задан пример bearer-аутентификации на сайте самой OpenAPI.
Нас это не остановит: мы просто везде уберем секцию security
. Вручную это делать не советую, есть замечательная утилита jq
, с помощью которой мы справимся с этим одной командой:
jq 'del(.. | .security?)' openapi.json > openapi_done.json
Также, если нам хочется работать с фаззингом параметров в path, c текущей реализацией Nuclei у нас это сделать нормально не получится, потому что он работает с path
как с полной строкой, а не как с отдельными параметрами.
info
В процессе исследования я отправил репорт разработчикам Nuclei и предложил доработать этот момент. Репорт приняли, и он даже нашел одобрение со стороны CTO разработчика.
А пока что к моменту, когда запросы поступают на фаззинг, в path
вместо параметров в фигурных скобках уже подставлены значения из OpenAPI example
для этого параметра. Чтобы обойти эту проблему, мы используем небольшой хак: поменяем значения в example
на name
, чтобы в URL при фаззинге были имена параметров.
Естественно, руками мы это делать тоже не будем. Снова воспользуемся jq
:
jq 'walk(if type == "object" and has("in") and .in == "path" then .schema.example = .name else . end)' openapi.json > openapi_done.json
И чтобы не выполнять все эти три действия каждый раз, соберем одну команду jq, которая преобразует всё сразу:
- удаляет все свойства
security
на любом уровне вложенности; - устанавливает свойство
servers
на первом уровне вложенности в значение[{
;"url": "http:// 127. 0. 0. 1: 5002"} ] - для всех параметров
path
из спецификации OpenAPI меняет значение свойстваexample
наname
самого параметра.
jq ' del(.. | .security?) | .servers = [{"url": "http://127.0.0.1:5002"}] | walk( if type == "object" and has("in") and .in == "path" then .schema.example = .name else . end )' openapi.json > openapi_done.json
Подготовив OpenAPI, мы можем приступить к написанию кастомных шаблонов Nuclei для нашего приложения.
Пишем кастомные шаблоны Nuclei
Начнем с аутентификации. Регистрируем двух пользователей:
curl -X 'POST' \\ 'http://127.0.0.1:5002/users/v1/register' \\ -H 'accept: application/json' \\ -H 'Content-Type: application/json' \\ -d '{ "email": "alice@mail.com", "password": "alice", "username": "alice"}'{"message": "Successfully registered. Login to receive an auth token.", "status": "success"}curl -X 'POST' \\ 'http://127.0.0.1:5002/users/v1/register' \\ -H 'accept: application/json' \\ -H 'Content-Type: application/json' \\ -d '{ "email": "bob@mail.com", "password": "bob", "username": "bob"}'{"message": "Successfully registered. Login to receive an auth token.", "status": "success"}
Логинимся от имени одного из них и получаем auth_token
:
curl -X 'POST' \\ 'http://127.0.0.1:5002/users/v1/login' \\ -H 'accept: application/json' \\ -H 'Content-Type: application/json' \\ -d '{ "password": "bob", "username": "bob"}'{"auth_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjI5Njk4MzgsImlhdCI6MTcyMjk2OTc3OCwic3ViIjoiYWxpY2UifQ.BkZug8W447WMpKWlbQv_GlgTePh8ROoIC0LlLwd_pJk", "message": "Successfully logged in.", "status": "success"}
Составляем незамысловатый файл для аутентификации:
secrets.yaml
static: - type: bearertoken domains: - 127.0.0.1:5002 token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjI5NDI2MjAsImlhdCI6MTcyMjk0MjU2MCwic3ViIjoiYm9iIn0.gXjM40hPihDDrKFhOIuZCRjGsxWJamWjATGAynEoW6w
Наконец‑то можно приступить к шаблонам для уязвимостей.
Раскрытие информации
Напишем шаблон, который проверяет все конечные точки API на раскрытие чувствительной информации.
В нашем учебном примере уязвимость заключается в раскрытии паролей пользователей. Чтобы обнаружить пароли в ответах, мы добавим матчер с регулярным выражением.
Также нужно задать условие, определяющее, какие эндпоинты подлежат проверке. Нам нужны все, кроме /
, который отвечает за инициализацию базы и может поломать нам стенд.
Чтобы сделать запрос на все подходящие эндпоинты, мы укажем part
фаззинга как path
, а в качестве значения, которое будем фаззить, используем пустую строку.
templates/disclosure.yaml
id: disclosureinfo: name: disclosure author: test severity: highhttp: - pre-condition: - type: dsl negative: true dsl: - 'contains(path,"createdb")' matchers: - type: regex regex: - '"password":\\s*".+"' stop-at-first-match: true fuzzing: - part: path mode: single type: postfix fuzz: - ""
SQL-инъекции
Следующим определим шаблон, который отвечает за поиск Error Based SQL-инъекций. Подвергаем проверке фаззингом все параметры path
, query
и body
всех эндпоинтов API (кроме /
). В качестве матчера используем регулярные выражения, которым могут соответствовать сообщения об ошибках SQL.
templates/sqli.yaml
id: sqliinfo: name: sqli author: test severity: criticalhttp: - pre-condition: - type: dsl negative: true dsl: - 'contains(path,"createdb")' matchers: - type: regex regex: - "SELECT" - "SQL" condition: or stop-at-first-match: true payloads: injection: - "'" - "\"" - "`" fuzzing: - part: path type: postfix mode: single fuzz: - "{{injection}}" - part: query type: postfix mode: single fuzz: - "{{injection}}" - part: body type: postfix mode: single fuzz: - "{{injection}}"
Уязвимость Broken Object Level Authorization
Проверим, правильно ли реализована авторизация. Может ли один пользователь получить неправомерный доступ к данным, принадлежащим другому? Так как в выбранном тестовом стенде такая уязвимость встречается в path-параметрах, здесь мы снова сталкиваемся с ограничением Nuclei, о котором я упоминал выше. В случае с query
или body
таких проблем нет. Но так как мы это уже предусмотрели, можем реализовать проверку и для path-параметров тоже.
Подготовим стенд, создав книгу от пользователя alice
:
curl --location 'http://127.0.0.1:5002/books/v1' \\--header 'Content-Type: application/json' \\--header 'Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE3MjI5ODMwNTgsImlhdCI6MTcyMjk4Mjk5OCwic3ViIjoiYm9iIn0.xDWXPKyfSEGtoIbQHI7Njd72byquLvg9f9RcRtU8ybM' \\--data '{"book_title": "alice_book","secret": "ALICE_SECRET"}'{"message": "Book has been added.",
"status": "success"}
Теперь напишем шаблон, который проверяет все path-параметры с названием book_title
на Broken Object Level Authorization, подставляя название книги alice
. Проверка при этом будет проводиться с аутентификацией от имени bob
.
Особенность проверки path
в том, что название параметра должно задаваться в свойстве values
, а также необходимо использовать type:
, чтобы заменить только исследуемый параметр, а не весь path
.
В качестве матчера задаем поиск заранее известного значения, которое может быть доступно только пользователю alice
.
templates/bola.yaml
id: bolainfo: name: bola author: test severity: highhttp: - matchers: - type: word words: - "ALICE_SECRET" fuzzing: - part: path type: replace-regex replace-regex: "book_title" mode: single values: - "book_title" fuzz: - "alice_book"
Сканируем стенд
Запустим Nuclei с подготовленной спецификацией OpenAPI, нашими кастомными шаблонами фаззинга и аутентификацией на стенде. В файле аутентификации должен быть токен bob
.
nuclei -l openapi.json -im openapi -t templates -secret-file secrets.yaml
Видим, что каждый шаблон нашел уязвимости в API, на которые он был рассчитан.
Nuclei в CI/CD
На этом возможности Nuclei не заканчиваются, современная разработка — это непрерывный процесс, а значит, и проверки должны быть непрерывными!
- Nuclei можно внедрить в пайплайн CI/CD GitLab или GitHub.
- Он позволяет создавать отчеты и отправлять их в системы управления уязвимостей.
Интеграция безопасности в CI/CD повышает защищенность разрабатываемых приложений благодаря систематической проверке каждой сборки или даже коммита. Кроме того, автоматизация позволяет сократить количество рутинной работы и сосредоточиться на интересных и нетривиальных задачах.
Выводы
С помощью кастомных шаблонов для фаззинга и настроенной аутентификации мы быстро нашли реальные и опасные уязвимости.
Основное достоинство описанного подхода в том, что мы проверяем API тестируемого приложения таргетированно с учетом необходимого формата запросов и не спотыкаемся о валидацию и авторизацию.
Важный плюс в том, что мы можем заново использовать один раз написанные шаблоны для новых версий текущего приложения, а также применять для других приложений со спецификацией OpenAPI.