• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья RCE на серверах VK

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,178
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
В этой статье я расскажу о том, как я связал в цепочку несколько проблем безопасности с целью удаленного выполнения кода (RCE) на серверах компании VK. Я описал свои шаги в подробностях, чтобы показать, как исследователь мыслит во время обнаружения необычных уязвимостей.

Pentest Award​

Для просмотра ссылки Войди или Зарегистрируйся — премии для специалистов по тестированию на проникновение, которую учредила компания Awillix. Мы публикуем лучшие работы из каждой номинации. Эта статья — победитель в номинации «Пробив». Читай также Для просмотра ссылки Войди или Зарегистрируйся.

Не буду скрывать, я — фанат программы баг‑баунти Для просмотра ссылки Войди или Зарегистрируйся. Иногда холдинг приобретает новые компании, и программа расширяется, что дает баг‑хантерам неплохой шанс собрать «низко висящие фрукты» — уязвимости, которые могут быть найдены без существенных затрат времени и усилий.


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

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

Моя первая встреча с Seedr состоялась в октябре 2021 года. Я начал тестировать и сразу же обнаружил несколько банальных XSS-уязвимостей, но решил не сообщать о них, так как шанс получить дубликат был слишком высок.

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

Находка, которая привлекла мое внимание​

Я вернулся к тестированию Seedr во время декабрьского отпуска в другой стране, где с собой у меня был лишь рюкзак и ноутбук. После некоторого времени пребывания в таких условиях у меня просыпается «баг‑баунти‑голод» и появляется желание найти что‑нибудь интересное. Для разогрева я обычно возвращаюсь к уже знакомым сервисам и стараюсь посмотреть на них свежим взглядом.

На этот раз я уделил больше внимания разведке Seedr, а именно поиску и перечислению поддоменов, сканированию портов, перебору веб‑директорий и так далее. К счастью, я нашел более заманчивые вещи: GitLab, Grafana, несколько хостов API, cron-файлы в веб‑директории, трассировки стека и многое другое. Чем больше точек входа находишь, тем выше шанс отыскать что‑то интересное. Хотя ни одна из находок не оказалась стоящей того, чтобы о ней сообщить, кое‑что все же привлекло мое внимание.

В исходном HTML-коде страницы Для просмотра ссылки Войди или Зарегистрируйся я заметил следующий комментарий:

[URL unfurl="true"]https://player.seedr.ru/video?vid=cpapXGq50UY&post_id=57b6ceef64225d5b0f8b456c&config=https%3A%2F%2Fseedr.com%2Fconfig%2F57975d1b64225d607e8b456e.json&hosting=youtube[/URL]
Для просмотра ссылки Войди или Зарегистрируйся
Готов поспорить, что более опытный читатель уже захотел изменить GET-параметр config на свой хост для получения входящего HTTP-соединения, что я и сделал. Но после нескольких попыток не получил ни одного отстука и продолжил экспериментировать с другими параметрами.

Я открыл в браузере вот такую ссылку:

Для просмотра ссылки Войди или Зарегистрируйся 57b6ceef64225d5b0f8b456c&config=https%3A%2F%2Fseedr.com%2Fconfig%2F57975d1b64225d607e8b456e.json&hosting=youtube
Заглянув в код, я заметил, что метатеги заполнены по разметке Open Graph и содержат информацию о видео: название, описание, превью и так далее.

Для просмотра ссылки Войди или Зарегистрируйся
После нескольких тестовых запросов я понял, что GET-параметры post_id и config не оказывают существенного влияния на ответ, поэтому упростил URL до Для просмотра ссылки Войди или Зарегистрируйся.

Предположив, что плеер, скорее всего, поддерживает не только YouTube, я изменил GET-параметр hosting на coub и vimeo.

Для просмотра ссылки Войди или ЗарегистрируйсяДля просмотра ссылки Войди или Зарегистрируйся
Итак, похоже, в зависимости от значения GET-параметра hosting сервер с помощью PHP-функции file_get_contents() выполняет HTTP-запрос к YouTube, Vimeo или Coub API, загружает метаданные о видео (GET-параметр vid), обрабатывает их и возвращает HTML-страницу плеера с видео и заполненными по разметке Open Graph метатегами.

GET-параметр vid является точкой инъекции, так как он позволяет контролировать последнюю часть пути в функции file_get_contents() с помощью символов обхода пути (/../) и других полезных символов (?, #, @ и так далее).

Что еще интересно, в случае с Vimeo сервер делает запрос к Для просмотра ссылки Войди или Зарегистрируйся. И оказывается, что при использовании расширения .php в пути Vimeo возвращает не JSON, а сериализованные данные!

Для просмотра ссылки Войди или Зарегистрируйся
Я предположил, что после функции file_get_contents() сервер десериализует ответ от Vimeo с помощью функции unserialize().

Для просмотра ссылки Войди или Зарегистрируйся
Ого, неужели у нас здесь небезопасная десериализация? Безопасная, пока ответ контролирует Vimeo.


Возможные сценарии​

В тот момент у себя в голове я уже видел три возможных сценария атаки:

  1. Фаззинг функции file_get_contents() с целью добиться слепой SSRF, то есть выполнить HTTP-запрос на подконтрольный мне ресурс и в теории добиться небезопасной десериализации.
  2. Найти контролируемый ответ на vimeo.com и добиться небезопасной десериализации.
  3. Найти открытый редирект на vimeo.com → SSRF → небезопасная десериализация.
После нескольких часов различных модификаций GET-параметра vid и локального фаззинга функции file_get_contents() я не нашел ничего полезного и параллельно решил поделиться всей имеющейся информацией об этой находке с несколькими надежными товарищами.

Итак, первый сценарий не сработал, поэтому я перешел к следующему — контролируемому ответу на vimeo.com.

Эндпоинт с контролируемым ответом должен отвечать следующим требованиям:

  • код ответа HTTP — 200 OK;
  • доступен для неавторизованного пользователя;
  • контролируемая строка должна находиться в начале тела ответа (PHP успешно десериализует {VALID_SER_STRING}TRASH);
  • контролируемая строка должна поддерживать символы { }, "", необходимые для хранения сериализованных объектов.
Ниже представлены некоторые из моих попыток найти требуемое поведение на vimeo.com.

Первая: injection is not a valid method.

Недостатки: код ответа HTTP 404 Not Found, не поддерживаются символы {}, "".

Для просмотра ссылки Войди или Зарегистрируйся
Вторая: injection is not a valid format.

Недостатки: код ответа HTTP 404 Not Found, не поддерживаются символы {}, "".

Для просмотра ссылки Войди или Зарегистрируйся
Третья: JavaScript callback.

Недостатки: /**/ в начале строки, не поддерживаются символы {}, "".

Для просмотра ссылки Войди или Зарегистрируйся
Четвертая: экспорт чата прямой трансляции.

Недостатки: дата и имя в начале строки, требуется аутентификация.

Для просмотра ссылки Войди или Зарегистрируйся
К сожалению, второй сценарий также не сработал, поэтому моей последней надеждой оставалось найти открытый редирект на vimeo.com. Ранее я уже встречал опубликованный Для просмотра ссылки Войди или Зарегистрируйся на HackerOne от 2015 года с открытым редиректом на vimeo.com, поэтому предположил, что есть небольшой шанс найти еще один. На самом деле я одновременно искал открытый редирект еще во время проверки второго сценария, но снова ничего не нашел.

Открытый редирект​

Все это время, пока я раскручивал уязвимость, я думал о статье Harsh Jaiswal Для просмотра ссылки Войди или Зарегистрируйся. Я отчетливо помнил, что для успешной эксплуатации использовалось несколько открытых редиректов на vimeo.com. Уязвимость была найдена еще в 2019 году, поэтому я ожидал, что описываемые в статье открытые редиректы уже исправлены. Но поскольку, вероятно, это был мой единственный шанс, я начал копать в этом направлении.

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

Для просмотра ссылки Войди или Зарегистрируйся
Я редко прошу кого‑то о помощи, не считая нескольких друзей, но, поскольку я был в тупике, Harsh был моей последней надеждой.

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

Для просмотра ссылки Войди или Зарегистрируйся
Итак, теперь у меня есть работающий открытый редирект на vimeo.com, осталось только его применить.

Для просмотра ссылки Войди или ЗарегистрируйсяДля просмотра ссылки Войди или Зарегистрируйся
Отлично, я наконец‑то словил HTTP-запрос на свой хост. Прежде чем перейти к десериализации, я решил немного поиграть с SSRF. Результаты смотри на скриншотах.

Для просмотра ссылки Войди или ЗарегистрируйсяДля просмотра ссылки Войди или ЗарегистрируйсяДля просмотра ссылки Войди или Зарегистрируйся
Из‑за того, что возвращаемое значение из функции file_get_contents() передается сразу в функцию unserialize(), у меня не получилась полная SSRF, чтобы читать успешные ответы от внутренних сервисов. Но по крайней мере у меня уже была полуслепая SSRF с возможностью выполнять сканирование портов.

Для просмотра ссылки Войди или Зарегистрируйся
Как только я понял, что использовал почти весь потенциал этой SSRF, я переключился на эксплуатацию функции unserialize().

Небезопасная десериализация​

Вкратце объясню, что необходимо для успешной эксплуатации небезопасной десериализации в PHP:
  • контролируемые входные данные;
  • класс с магическим методом (__wakeup(), __destroy(), __toString() и так далее);
  • в магическом методе определена полезная функциональность, которой можно злоупотребить (например, манипуляция с файловой системой или выполнение запросов к базе данных);
  • класс загружен.
Как видишь, на тот момент выполнялось только одно требование из четырех. О серверном коде на хосте я знал слишком мало, поэтому единственный способ эксплуатации — это вслепую попробовать все известные цепочки гаджетов. Для этого я использовал инструмент PHPGGC, который по сути является набором полезных нагрузок для эксплуатации функции unserialize() вместе с инструментом для их генерации. В то время он содержал почти 90 доступных нагрузок. Большая часть из них предназначена для различных CMS и фреймворков, таких как WordPress, ThinkPHP, TYPO3, Magento, Laravel, которые в моем случае были совершенно бесполезны. Поэтому я сделал ставку на такие широко используемые библиотеки, как Doctrine, Guzzle, Monolog и Swift Mailer.

С помощью PHPGGC я предварительно сгенерировал все возможные нагрузки, разместил их на контролируемом сервере и начал перебор. Однако во всех случаях я получал одну и ту же ошибку: Для просмотра ссылки Войди или Зарегистрируйся.

Для просмотра ссылки Войди или Зарегистрируйся
В тот момент я уже смирился с тем, что уязвимый PHP-скрипт, скорее всего, примитивен и не подгружает никаких дополнительных классов, которые я бы мог использовать. Печально, но я хотя бы попытался. Так часто бывает, когда раскручиваешь крутую уязвимость, но сталкиваешься с чем‑то, что полностью блокирует дальнейшее продвижение.

После обобщения всех результатов я отправился на HackerOne и составил отчет под названием «[player.seedr.ru] Semi-blind SSRF», не забыв пригласить Harsh Jaiswal в качестве соавтора за предоставленный открытый редирект на vimeo.com.

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

Kohana​

Несколько дней спустя мой взгляд случайно зацепился за какую‑то информацию про уязвимость use after free в функции unserialize(). Версия PHP на player.seedr.ru оказалась устаревшей, и я сразу начал изучать эту тему. Я ознакомился с отчетами Taoguang Chen, который сообщил команде PHP о нескольких десятках проблем с функцией unserialize(). Хотя уязвимости, связанные с памятью, все еще темный лес для меня, я все же постарался сгенерировать несколько нагрузок. После продолжительных локальных тестов я вернулся на player.seedr.ru, разместил нагрузку на контролируемом сервере, отправил запрос, и...

Для просмотра ссылки Войди или Зарегистрируйся
Серьезно? На устройстве не осталось места? Я только начал. Но подожди, это не похоже на стандартное сообщение о закончившемся месте на устройстве:

ErrorException [ 2 ]: file_put_contents(/var/www/seedr.backend.v2/application/logs/2021/12/20.php): failed to open stream: No space left on device \~ SYSPATH/classes/kohana/log/file.php [ 81 ]
Скорее всего, эта ошибка возникла потому, что мои сканеры отправили слишком много запросов, когда в предыдущие дни я искал скрытые веб‑директории и файлы.
Кастомный класс для логирования? Видимо, этот «примитивный» PHP-скрипт все же что‑то подгружает. Интересно. Kohana? Я уже встречал это слово во время тестирования Seedr. Но где?
Благодаря Burp Suite Professional я быстро нашел первое упоминание о Kohana в истории прокси, открыл нужную ссылку и увидел подробную страницу ошибки.

Для просмотра ссылки Войди или ЗарегистрируйсяДля просмотра ссылки Войди или Зарегистрируйся
Здесь я сделаю небольшое отступление, чтобы рассказать о Seedr и о том, откуда взялся v2.nativeroll.tv. Однако стоит отметить, что вся информация, которую я буду предоставлять, — это мои личные предположения, а они могут оказаться неточными.

Seedr и Nativeroll — платформы для видеорекламы. У Seedr устаревший дизайн, поэтому я предположил, что она была создана задолго до Nativeroll. Обе платформы были куплены на тот момент еще Mail.Ru Group, вероятно, каким‑то образом объединены и размещены на HackerOne в одном скоупе. Таким образом, v2.nativeroll.tv/api/, api.seedr.ru, api-stage.seedr.ru, player.seedr.ru имели общую кодовую базу. Надеюсь, теперь стало немного понятнее.

Хорошо, давай теперь вернемся к красивой странице с ошибкой. Environment, Included files, Loaded extensions — выглядит сочно. Вот что я увидел после нажатия на ссылку Included files.

Для просмотра ссылки Войди или Зарегистрируйся
Почти 90 файлов, по сути — различные классы, подгруженные с помощью чего‑то вроде autoload.php. Является ли Kohana чем‑то вроде CMS или фреймворка? Да, это так. После небольшого поиска я нашел на GitHub Для просмотра ссылки Войди или Зарегистрируйся, который выглядит заброшенным.

Для просмотра ссылки Войди или Зарегистрируйся
Поскольку v2.nativeroll.ru и api.seed.ru имеют общую кодовую базу, я успешно вызвал Error exception на api.seedr.ru таким же способом (Для просмотра ссылки Войди или Зарегистрируйся<svg>) и получил тот же результат.

Чтобы вызвать Error exception именно на api.seedr.ru/video (эндпоинт, который я атаковал), я взял ответ с Для просмотра ссылки Войди или Зарегистрируйся и изменил тип значения атрибута description со строки на массив.

Для просмотра ссылки Войди или ЗарегистрируйсяДля просмотра ссылки Войди или Зарегистрируйся
Во время выполнения скрипта функция htmlspecialchars() ожидала строку, но получила массив, что вызвало Error exception с частичным раскрытием PHP-шаблона и трассировкой стека.

Для просмотра ссылки Войди или ЗарегистрируйсяДля просмотра ссылки Войди или Зарегистрируйся
Как я и думал, там присутствовал скрипт автозагрузки Composer. Среди подгруженных файлов я выделил несколько потенциально полезных при десериализации:

  • Guzzle (/var/www/sentry/vendor/guzzlehttp/...)
  • Swift Mailer (MODPATH/email/vendor/swiftmailer/...)
  • Symfony (/var/www/sentry/vendor/symfony/...)
  • Mustache (MODPATH/kostache/vendor/mustache/...)
  • Sentry (/var/www/sentry/vendor/sentry/...)
Я знал, что в PHPGGC есть несколько цепочек гаджетов для Guzzle, Swift Mailer и Symfony. После того как я сгенерировал и протестировал нагрузки на api-stage.seedr.ru, появились новые ошибки. Например, попытка с нагрузкой для Guzzle вернула ошибку «FnStream never should be unserialized». Это указывало на то, что скрипт использовал уже Для просмотра ссылки Войди или Зарегистрируйся.

Для просмотра ссылки Войди или Зарегистрируйся
Swift Mailer и Symfony не сработали вообще, а анализ кода Mustache и Sentry на GitHub также не принес никаких плодов, так что сторонние библиотеки меня не выручили. Пришло время погрузиться в Kohana.

Поиск магических методов, таких как __wakeup(), __destruct, __toString(), в репозитории Kohana оказался безрезультатным.

Для просмотра ссылки Войди или Зарегистрируйся
Но в этом репозитории есть каталог system, который на самом деле является отдельным репозиторием Kohana Core.

Для просмотра ссылки Войди или ЗарегистрируйсяДля просмотра ссылки Войди или Зарегистрируйся
Попробуем поискать магические методы уже в этом репозитории. Для __destruct(), __wakeup() результатов почти нет, но результаты для __toString() обнадеживают.

Для просмотра ссылки Войди или Зарегистрируйся
Я бегло просмотрел результаты, и файл Для просмотра ссылки Войди или Зарегистрируйся, а также его функция Для просмотра ссылки Войди или Зарегистрируйся сразу же привлекли мое внимание.

Должен сказать, что в прошлом у меня был небольшой опыт бэкенд‑разработки. Я написал несколько проектов на Laravel и уже был знаком с паттерном MVC (model — view — controller). Для рендеринга шаблонов и представлений в Laravel используется движок Blade. Поскольку такие движки обычно загружают шаблоны, я предположил, что смогу как‑то передать в функцию свой собственный файл или контент.

Давай внимательно рассмотрим функцию render():

Код:
public function render($file = NULL)
{
    if ($file !== NULL)
    {
        $this->set_filename($file);
    }

    if (empty($this->_file))
    {
        throw new View_Exception('You must set the file to use within your view before rendering');
    }

    // Combine local and global data and capture the output
    return View::capture($this->_file, $this->_data);
}

Функция render() принимает один аргумент под названием $file, а затем вызывает функцию capture().

protected static function capture($kohana_view_filename, array $kohana_view_data)
Код:
{
    // Import the view variables to local namespace
    extract($kohana_view_data, EXTR_SKIP);

    if (View::$_global_data)
    {
        // Import the global view variables to local namespace
        extract(View::$_global_data, EXTR_SKIP | EXTR_REFS);
    }

    // Capture the view output
    ob_start();

    try
    {
        // Load the view within the current scope
        include $kohana_view_filename;
    }
    catch (Exception $e)
    {
        // Delete the output buffer
        ob_end_clean();

        // Re-throw the exception
        throw $e;
    }

    // Get the captured output and close the buffer
    return ob_get_clean();
}

Как сказано в комментарии, функция capture() объединяет локальные и глобальные переменные и фиксирует вывод. Она принимает два аргумента: $kohana_view_filename и $kohana_view_data. Ты, вероятно, уже заметил функцию, которой потенциально можно злоупотребить при десериализации:

Код:
{
 // Load the view within the current scope
 include $kohana_view_filename;
}
Функция include()! Это уже попахивает LFI и RCE. Но есть ли у нас контроль над $kohana_view_filename?

Оказывается, да! Мы можем передать его в качестве аргумента в функцию __construct() во время создания объекта View:

public function __construct($file = NULL, array $data = NULL)
{
    if ($file !== NULL)
    {
        $this->set_filename($file);
    }

    if ($data !== NULL)
    {
        // Add the values to the current data
        $this->_data = $data + $this->_data;
    }
}

В тот момент у меня выполнялись все условия для успешной эксплуатации небезопасной десериализации:
  • я контролировал входные данные;
  • у меня был магический метод __toString() класса View с полезной функцией include();
  • класс View был загружен.
Бинго!

Всё вместе​

Через некоторое время я создал гаджет и цепочку для PHPGGC локально, которые позже были добавлены в основной репозиторий:

Код:
<?php

namespace GadgetChain\Kohana;

class FR1 extends \PHPGGC\GadgetChain\FileRead
{
    public static $version = '3.*';
    public static $vector = '__toString';
    public static $author = 'byq';
    public static $information = 'include()';

    public function generate(array $parameters)
    {
        return new \View($parameters['remote_path']);
    }
}
<?php

class View
{
    protected $_file;

    public function __construct($_file) {
        $this->_file = $_file;
    }
}

Затем я просто запустил PHPGGC и получил следующий сериализованный объект.

Для просмотра ссылки Войди или Зарегистрируйся
Я разместил нагрузку на контролируемом сервере и отправил запрос. Результат показан на следующем скриншоте.

Для просмотра ссылки Войди или Зарегистрируйся
По крайней мере это было что‑то новенькое. Но на что я надеялся? Ведь использовался метод __toString(), а не методы __wakeup() или __destruct(), которые срабатывают в момент создания и уничтожения объекта соответственно. В Для просмотра ссылки Войди или Зарегистрируйся сказано следующее.

Для просмотра ссылки Войди или Зарегистрируйся
Получается, мне как‑то необходимо вывести объект View. На самом деле несложно было понять, что я должен передать свой объект View в качестве значения атрибута title или description — трюк, который я проделал ранее с массивом, чтобы вызвать Error exception. Вот как выглядела моя нагрузка.

Для просмотра ссылки Войди или Зарегистрируйся
Я снова обновил нагрузку на контролируемом сервере, отправил запрос и наконец получил ответ.

Для просмотра ссылки Войди или Зарегистрируйся
Я получил содержимое файла /etc/passwd внутри метатега og:description. Круто, локальное чтение файлов намного лучше, чем полуслепая SSRF, но это все еще не RCE.


Логи​

Уязвимость LFI — настолько редкая находка в современных веб‑приложениях, что мне пришлось вспоминать, где можно разместить нагрузку, чтобы загрузить ее с помощью функции include() и получить RCE. Наиболее распространены такие техники:

  • загрузка файлов (в моем случае в приложении не было этой функциональности);
  • логи (Apache, Nginx, Mail, SSH...);
  • /proc/*/fd, /proc/self/environ;
  • файл PHP-сессии.
Для просмотра ссылки Войди или ЗарегистрируйсяДля просмотра ссылки Войди или ЗарегистрируйсяДля просмотра ссылки Войди или ЗарегистрируйсяДля просмотра ссылки Войди или Зарегистрируйся
Как ты уже понял, я перепробовал почти всё, но ничего не сработало. Пришло время сделать несколько шагов назад, а именно к ошибке, связанной с отсутствием места на устройстве.

Для просмотра ссылки Войди или Зарегистрируйся
Из этой ошибки я смог извлечь путь к какому‑то логу /application/logs/2021/12/20.php. После попытки открыть Для просмотра ссылки Войди или Зарегистрируйся в браузере я получил ошибку «No direct script access». Почти в каждом PHP-файле фреймворка Kohana есть такая строчка в начале.

Для просмотра ссылки Войди или Зарегистрируйся
Похоже, что я не могу получить доступ к логам с расширением .php непосредственно из браузера. К моему удивлению, попробовав открыть на stage-хосте с адресом Для просмотра ссылки Войди или Зарегистрируйся, я получил код ответа HTTP 404. Не знаю, что меня подтолкнуло, но я изменил расширение .php на .log, и...

Для просмотра ссылки Войди или Зарегистрируйся
Да, я получил огромный лог‑файл, после чего мой Burp Suite даже немного подвис. Должен отметить, что такой трюк не сработал на production-хосте api.seedr.ru. Думаю, что разработчики Seedr специально что‑то поменяли на stage-хосте, чтобы упростить доступ к логам. Но, как обычно, это привело к проблеме безопасности.

В очередной раз передо мной открылась новая дверь. Ты еще помнишь, как я вызвал Error exception в первый раз (Для просмотра ссылки Войди или Зарегистрируйся<svg>)? Вот запись об этом в логе.

Для просмотра ссылки Войди или Зарегистрируйся
После краткого анализа логов я «отравил» его такой записью.

Для просмотра ссылки Войди или Зарегистрируйся
С помощью PHPGGC я сгенерировал новый сериализованный объект View с файлом /var/www/t1.seedr.backend/application/logs/2021/12/20.log, разместил его на контролируемом сервере, отправил запрос и получил следующую ошибку.

Для просмотра ссылки Войди или Зарегистрируйся
Видимо, из‑за того, что файл журнала был слишком большим (более 200 000 строк), какая‑то функция ломалась на одном из символов *?, выбрасывала исключение и останавливала выполнение скрипта. На самом деле я просто опечатался, предлагаю тебе самостоятельно найти ошибку в моей нагрузке. Из Для просмотра ссылки Войди или Зарегистрируйся я узнал следующее.

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

Принимая утренний душ, я вспомнил еще одну потрясающую статью от Charlese Fol: Для просмотра ссылки Войди или Зарегистрируйся. В ней автор использует технику с особенностью множественного декодирования Base64, которая игнорирует не-Base64-символы. Изначально я прочитал об этом в Для просмотра ссылки Войди или Зарегистрируйся.

Моя идея заключалась в том, чтобы отравить лог PHP-нагрузкой, закодированной в Base64 несколько раз, а затем раскодировать его с помощью нескольких PHP-фильтров convert.base64-decode внутри функции include(), чтобы обойти ошибку с символом ?. Но, поскольку у меня была бессонная ночь, мой мозг работал плохо, и я совсем забыл, что в случае с Laravel исследователь злоупотреблял цепочкой функций file_get_contents() и file_put_contents() с одинаковыми аргументами внутри, что позволило ему переписать лог. Я также забыл и о следующем ограничении.

Для просмотра ссылки Войди или Зарегистрируйся
Из‑за предсказуемого пути (/application/logs/2021/12/20.log) я скачал несколько логов за предыдущие дни и планировал отравить лог за 21 декабря в начале суток, пока он не стал слишком большим.

Я добавил новую информацию в отчет на HackerOne, и у меня в наличии оставался целый день до 21 декабря. Не теряя времени, я попытался проэксплуатировать уязвимость на api.seedr.ru, так как все последние тесты я проводил на api-stage.seedr.ru. Еще раз с помощью PHPGGC я сгенерировал объект View с файлом /etc/passwd, разместил его на контролируемом сервере и не увидел в ответе содержимого файла /etc/passwd. Я повторил те же шаги на api-stage.seedr.ru, но там по‑прежнему все работало как надо. Упс, неужели уязвим только stage-хост?


Нулевой байт​

Здесь я должен признаться, что, когда я генерировал сериализованный объект с помощью PHPGGC, я немного изменял его.

Для просмотра ссылки Войди или Зарегистрируйся
Действительно ли строка *_file состоит из восьми символов? Нет, только из шести. Именно это я исправлял каждый раз, и все отрабатывало без ошибок на api-stage.seedr.ru. Позже в трассировке стека я заметил следующее.

Для просмотра ссылки Войди или Зарегистрируйся
Значение защищенного атрибута _file равно NULL, но по какой‑то причине у объекта View еще есть публичный атрибут *_file с моей нагрузкой. Возможно, знатоки PHP уже поняли причину такого поведения, но мне пришлось потратить некоторое время на решение этой проблемы.

Как ты мог заметить по скриншотам, для хранения нагрузки я использовал сервис Для просмотра ссылки Войди или Зарегистрируйся — быстрое и простое решение для приема входящих HTTP-соединений и размещения нагрузки. К сожалению, на сей раз это сыграло со мной злую шутку. Дело в том, что Для просмотра ссылки Войди или Зарегистрируйся. Вот почему *_file состоит из восьми символов.

Для просмотра ссылки Войди или Зарегистрируйся
Поскольку я просто копировал нагрузку на webhook.site, он не сохранял эти нулевые символы и передавал в функцию unserialize() публичный атрибут _file. Чтобы решить проблему, я просто разместил сериализованную строку с нулевыми байтами на своем сервере. Теперь vimeo.com перенаправлял запросы на мой сервер, где при помощи функции echo() я отдавал нагрузку с нулевыми символами. После того как мне удалось загрузить содержимое файла /etc/passwd на api.seedr.ru, я снова вернулся к анализу загруженных логов.


Последнее «отравление»​

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

Для просмотра ссылки Войди или Зарегистрируйся
Этот тип записи хорош тем, что он сохранял нагрузку только один раз и не повторял ее, как в предыдущей попытке. Я приметил также возможную точку инъекции: заголовок user-agent. Но проблема заключалась в том, что я не знал, как именно сгенерировать такую запись в логе, к какому эндпоинту мне следует обратиться.

Я грепнул лог со своим IP и обнаружил, что в сегодняшнем журнале запись с моим IP уже присутствовала. Это означало, что я уже точно обращался к нужному эндпоинту. К тому времени в моей истории Burp Proxy насчитывалось больше 40 тысяч записей, поэтому найти нужный эндпоинт оказалось не так‑то просто. Сравнив время записи с активностью, которой я был занят в то время, я понял, что запись, вероятно, была сгенерирована во время сканирования с помощью dirsearch. Я запустил его повторно, и через некоторое время эндпоинт, который генерировал такую запись, был найден — api-stage.seedr.ru/inc.

На локальном окружении я спрятал новую нагрузку в тестовый лог, загрузил его через функцию include() и получил вывод команды bash. Оставалось только дождаться 21 декабря и свежего лога, потому что логи за 20 декабря для api.seedr.ru и api-stage.seedr.ru были отравлены моими неудачными нагрузками.

На следующий день я «отравил» лог с помощью следующего запроса.

Для просмотра ссылки Войди или Зарегистрируйся
Сгенерировал нагрузку, разместил на сервере, отправил запрос...

Для просмотра ссылки Войди или Зарегистрируйся
Да, я забыл поменять $argv[1] на $_GET[1] после локальных тестов... В ожидании следующего дня вспомнил, что сегодня у меня есть еще одна попытка на api-stage.seedr.ru.

Для просмотра ссылки Войди или Зарегистрируйся
Атака завершилась полным успехом!

Вот схема, где показана вся цепочка атаки.

Для просмотра ссылки Войди или Зарегистрируйся

info​

Выражаю благодарность Для просмотра ссылки Войди или Зарегистрируйся за то, что поделился открытым перенаправлением; Для просмотра ссылки Войди или Зарегистрируйся и моего личного эксперта по PHP — за то, что они были моими резиновыми уточками.

Ссылки по теме​


Выводы​

Работа над уязвимостью велась в декабре 2021 года, но, оказывается, уже тогда существовал альтернативный способ эскалировать LFI до RCE, используя только обертки PHP. Таким образом «отравление» логов не потребовалось бы. Техника получила широкую известность не так давно, в октябре 2022 года. Почитать про нее подробнее можно на Для просмотра ссылки Войди или Зарегистрируйся и на Для просмотра ссылки Войди или Зарегистрируйся.

Кстати, в узких кругах о гаджете с Kohana тоже известно уже давно. В августе 2022 года я случайно обнаружил Для просмотра ссылки Войди или Зарегистрируйся от 2015 года за авторством Для просмотра ссылки Войди или Зарегистрируйся. Как это бывает в любом открытии, есть исследователи, которые преодолели этот путь до тебя, но от этого само исследование ничуть не становится менее увлекательным.
 
Activity
So far there's no one here
Сверху Снизу