Как за неделю превратить Open redirect в RCE

от автора

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

Введение

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

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

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

Моя первая встреча с Seedr состоялась в октябре 2021 года. В течение нескольких минут после начала тестирования я обнаружил несколько банальных RCE в .api/nr/report/{id}/download

  • SSRF + RCE через fastCGI в POST /api/nr/video

  • XSS Stored on https://seedr.ru

  • OS command injection on seedr.ru

  • Подумав, что мой поезд ушел, я решил не тратить много времени на Seedr и продолжил прокрастинировать.

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

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

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

    В исходном HTML-коде страницы https://api-stage.seedr.ru/player я заметил следующий комментарий:

    https://player.seedr.ru/video?vid=cpapXGq50UY&post_id=57b6ceef64225d5b0f8b456c&config=https%3A%2F%2Fseedr.com%2Fconfig%2F57975d1b64225d607e8b456e.json&hosting=youtube

    Готов поспорить, что более опытный читатель уже захотел изменить GET-параметр config на свой хост для получения входящего HTTP-соединения, что я и сделал. Но после нескольких попыток не получил ни одного отстука и продолжил экспериментировать с другими параметрами.

    Когда я открыл ссылку https://player.seedr.ru/video?vid=cpapXGq50UY&post_id=57b6ceef64225d5b0f8b456c&config=https%3A%2F%2Fseedr.com%2Fconfig%2F57975d1b64225d607e8b456e.json&hosting=youtube в браузере, я заметил, что метатеги заполнены по разметке Open Graph и содержат информацию о видео: название, описание, превью и т.д.

    После нескольких тестовых запросов я понял, что GET-параметры post_id и config не оказывают существенного влияния на ответ, поэтому давайте упростим URL до https://player.seedr.ru/video?vid=cpapXGq50UY&hosting=youtube.

    Предположив, что плеер скорее всего поддерживает не только 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, как вы могли заметить на предыдущем скриншоте, сервер делает запрос к http://vimeo.com/api/v2/video/VID.php. И оказывается, что при использовании расширения .php в пути, Vimeo возвращает не JSON, а сериализованные данные!

    Я предположил, что после функции file_get_contents() сервер десериализует ответ от Vimeo с помощью функции unserialize():

    «Ого, неужели у нас здесь небезопасная десериализация?»

    Безопасная, пока ответ контролирует Vimeo.

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

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

    1. Фаззинг функции file_get_contents() с целью добиться слепой , необходимые для хранения сериализованных объектов.

    2. Ниже представлены некоторые из моих попыток найти требуемое поведение на vimeo.com:

      1. injection is not a valid method. 

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

      1. injection is not a valid format.

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

      1. JavaScript callback.

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

      1. Экспорт чата прямой трансляции:

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

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

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

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

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

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

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

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

      Отлично, я наконец-то словил HTTP-запрос на свой хост. Прежде чем перейти к десериализации, я решил немного поиграть с SSRF:

      • https://127.0.0.1

      • https://127.0.0.1:22

      • http://127.0.0.1:25

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

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

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

      Вкратце объясню, что необходимо для успешной эксплуатации небезопасной десериализации в PHP:

      • Контролируемые входные данные;

      • Класс с магическим методом (__wakeup(), __destroy(), __toString() и т.д.);

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

      • Класс загружен.

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

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

      The error occurs because inside serialized string there is a reference to a class that hasn’t been included yet — so the PHP autoloading mechanism is triggered to load that class, and this fails for some reason. © Sven

      В тот момент я уже смирился с тем, что уязвимый 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 таким же способом (https://api.seedr.ru/<svg>) и получил тот же результат.

      Чтобы вызвать Error exception именно на api.seedr.ru/video (эндпоинт, который я атаковал), я взял ответ с http://vimeo.com/api/v2/video/123456.php и изменил тип значения атрибута 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() обнадеживают:

      Я бегло просмотрел результаты, и файл classes/Kohana/View.php и его функция render() сразу же привлекли мое внимание.

      Должен сказать, что в прошлом у меня был небольшой опыт бэкенд разработки. Я написал несколько проектов на 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() объединяет локальные и глобальные переменные и фиксирует вывод.

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

      try { // Load the view within the current scope include $kohana_view_filename; }

      include()! Это уже попахивает 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(), которые срабатывают в момент создания и уничтожения объекта соответственно. В документации PHP сказано:

      Получается, мне как-то необходимо вывести объект 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. После попытки открыть https://api.seedr.ru/application/logs/2021/12/20.php в браузере, я получил ошибку No direct script access. Почти в каждом PHP-файле фреймворка Kohana есть такая строчка в начале:

      Похоже, что я не могу получить доступ к логам с расширением .php непосредственно из браузера. К моему удивлению, попробовав открыть на stage хосте http://api-stage.seedr.ru/application/logs/2021/12/20.php, я получил код овета HTTP 404. Не знаю, что меня подтолкнуло, но я изменил расширение .php на .log, и …

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

      В очередной раз передо мной открылась новая дверь. Вы все ещё помните, как я вызвал Error exception в первый раз (https://api.seedr.ru/<svg>)? Вот запись об этом в логе:

      После краткого анализа логов я «отравил» его такой записью:

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

      Видимо, из-за того, что файл журнала был слишком большим (>200000 строк), какая-то функция ломалась на одном из символов «, выбрасывала исключение и останавливала выполнение скрипта. На самом деле я просто опечатался, предлагаю  вам найти ошибку в моей нагрузке. Из документации PHP я узнал, что:

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

      Во время утреннего душа я вспомнил ещё одну потрясающую статью от Charlese Fol Laravel <= v8.4.2 debug mode: Remote code execution (CVE-2021-3129). В ней автор использует технику с особенностью множественного декодирования base64, которая игнорирует не base64 символы. Изначально я прочитал об этом в блоге тайваньского исследователя безопасности Orange Tsai. Моя идея заключалась в том, чтобы отравить лог 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 состоит из 8 символов? Нет, только из 6. Именно это я исправлял каждый раз, и всё отрабатывало без ошибок на api-stage.seedr.ru. Позже в трассировке стека я заметил следующее:

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

      Как вы могли заметить по скриншотам, для хранения нагрузки я использовал сервис https://webhook.site/ — быстрое и простое решение для приема входящих HTTP-соединений и размещения нагрузки. К сожалению, в тот раз это сыграло со мной злую шутку. Дело в том, что для хранения защищенного значения в сериализованной строке PHP использует нулевые символы (\0) вокруг символа «*». Вот почему *_file состоит из 8 символов:

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

      Последнее отправление

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

      Этот тип записи хорош тем, что он записывал нагрузку только один раз и не повторял ее, как в предыдущей попытке. Я также приметил возможную точку инъекции: заголовок user-agent. Но проблема заключалась в том, что я не знал, как именно сгенерировать такую запись в логе, к какому эндпоинту мне следует обратиться. Я грепнул лог со своим IP и обнаружил, что в сегодняшнем логе запись с моим IP уже присутствовала, что означало, что я уже точно обращался к нужному эндпоинту. К тому времени в моей истории Burp Proxy насчитывалось более 40000 записей, поэтому найти нужный эндпоинт оказалось не так-то просто. Сравнив время записи с моим IP и активностью, которой был занят в то время, я понял, что запись, вероятно, была сгенерирована во время сканирования с помощью 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:

      $$$
      $$$

      TL;DR

      https://imgur.com/DrNEGRH
      https://imgur.com/DrNEGRH

      Благодарность:

      • @rootxharsh за то, что поделился открытым перенаправлением;

      • @act1on3 и моего личного эксперта по PHP за то, что они были моими резиновыми уточками.

      Полезные ссылки:

      https://twitter.com/rootxharsh 

      https://infosecwriteups.com/vimeo-ssrf-with-code-execution-potential-68c774ba7c1e

      https://github.com/ambionics/phpggc

      https://hackerone.com/ryat

      https://twitter.com/cfreal_

      https://www.ambionics.io/blog/laravel-debug-rce

      https://twitter.com/orange_8361 

      http://blog.orange.tw/2018/10/

      P.S.

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

      https://gist.github.com/loknop/b27422d355ea1fd0d90d6dbc1e278d4d

      https://www.synacktiv.com/publications/php-filters-chain-what-is-it-and-how-to-use-it.html

      Кстати, в узких кругах о гаджете с Kohana тоже судя по всему известно уже давно. В августе 2022 случайно обнаружил видео доклада Paul Axe от 2015 года:

      https://www.youtube.com/watch?v=PWjkz8xTI8g&t=314s


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


    Комментарии

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *