В этой статье я расскажу вам о том, как ровно год назад я связал в цепочку несколько проблем безопасности для достижения Удаленного выполнения кода (RCE) на нескольких серверах компании VK. Я постарался описать свои шаги в подробностях, так как мне самому, как постоянному читателю отчетов по баг-баунти, всегда хочется понять, как исследователь мыслит во время обнаружения необычных уязвимостей. Надеюсь, для вас эта статья будет интересна.
Введение
Не буду скрывать, я являюсь фанатом баг-баунти программы VK на HackerOne. Иногда холдинг приобретает новые компании, и программа пополняется новыми активами, что дает баг-хантерам неплохой шанс собрать "низко висящие фрукты" - уязвимости, которые могут быть найдены без существенных затрат времени и усилий.
По своему опыту могу сказать, что получить доступ к чему-то, что до вас никто не пытался взломать, может быть очень выгодно. На площадке HackerOne есть возможность подписаться на интересующую программу и получать обновления о любых изменениях в правилах, чем я и воспользовался, чтобы быть одним из первых, кто начнет тестировать недавно добавленный сервис.
В течение 2021 года я не очень активно хантил и особо не следил за обновлениями в избранной программе, именно по этой причине я пропустил уведомление о том, что платформа Seedr, которая помогает быстро распространять видео в интернете, сейчас уже приостановленная, была добавлена в скоуп.
Моя первая встреча с Seedr состоялась в октябре 2021 года. В течение нескольких минут после начала тестирования я обнаружил несколько банальных XSS уязвимостей, но решил не сообщать о них, так как шанс получить дубликат был слишком высок.
В настоящее время вы можете просмотреть несколько раскрытых отчетов других баг-хантеров и заметить, насколько нетипичные для современных приложений уязвимости они обнаружили в Seedr:
Подумав, что мой поезд ушел, я решил не тратить много времени на 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.
Возможные сценарии
В тот момент у себя в голове я уже видел три возможных сценария атаки:
Фаззинг функции
file_get_contents()
с целью добиться слепой SSRF, т.е. выполнить HTTP-запрос на подконтрольный мне ресурс, и в теории добиться небезопасной десериализации;Найти контролируемый ответ на vimeo.com -> добиться небезопасной десериализации;
Найти открытый редирект на 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 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()
! Это уже попахивает 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()
, которые срабатывают в момент создания и уничтожения объекта соответственно. В документации 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
Благодарность:
@rootxharsh за то, что поделился открытым перенаправлением;
@act1on3 и моего личного эксперта по PHP за то, что они были моими резиновыми уточками.
Полезные ссылки:
https://twitter.com/rootxharsh
https://infosecwriteups.com/vimeo-ssrf-with-code-execution-potential-68c774ba7c1e
https://github.com/ambionics/phpggc
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 года:
Комментарии (13)
FanatPHP
29.12.2022 14:39+4Решил составить TL;DR:
Надеюсь, все правильно понял- встроенный плеер запрашивает информацию о видео со сторонних сервисов
- Вимео отдает инфу в формате РНР serialize(!)
- Также в вимео есть открытый редирект (определенный параметр в URL заставляет любой скрипт вместо отдачи контента сделать редирект на указанный адрес)
- соответственно, скрипту на Seedr пожно подсунуть свой сериализованный объект.
- API Seedr выдает наружу системные ошибки РНР()
- из текста ошибки стало ясно, что на сервере используется Кохана
- стейдж сервер доступен без пароля, и при этом при ошибках вываливает дебаг панель. Хотя это, наверное, непринципиальный момент, но сильно упростил дальнейшие ходы
- с помощью стандартной уязвимости unserialize можно создать экземпляр класса View и получить контролируемый инклюд файла
- теперь остается найти этот файл. самый очевидный киндидат — это логи
- находим файл, в который пишется переданный на сайт запрос. И в этом запросе размещается РНР код
- затем этот файл с логом иклюдится через класс View.
Looser_friendly
29.12.2022 21:22+1Большое спасибо за статью! Серьёзный труд был вложен как в работу, так и в написание)
Можете поделиться, какой опыт в пентесте и багбаунти, давно ли этим занимаетесь? И про опыт разработки, если не сложно.
Мне до подобного ещё далеко, но хоть яснее наметить себе путь для развития) Спасибо!
ByQwert Автор
29.12.2022 21:43+3Спасибо.
5 лет в безопасности, учитывая год самообучения до начала работы по текущему профилю. В баг-баунти года 3-4, но с большими перерывами. До этого был небольшой опыт технической поддержки, обслуживания биллинга и немного разработки на PHP, JS, Python.Ели заинтересованы в безопасности веб-приложений, то для начала могу порекомендовать бесплатные лабы от PortSwigger. Успехов!
BugM
30.12.2022 03:32+1Классно. Прямо хорошо показано что такое современные уязвимости, откуда они берутся и как их находят и эксплуатируют. На настоящем живом примере.
FanatPHP
Фантастика! Это тянет на статью года.
Именно подходом, декларированным в самом начале — отслеживание хода мысли, подробное описание использованных приёмов, а не как обычно "вот мы находим дырку… profit!"
Спасибо огромное за терпение, и при раскручивании этой уязвимости, и — главное — при написании статьи!
Как раз сейчас перечитывал "Хакеров" Стивена Леви, и в статье прямо чувствуется дух тех времен зарождения хакерства, как искусства.
Если есть англоязычный вариант, то его надо обязательно на Реддит. Если нету, то я готов помочь с переводом.
ByQwert Автор
Спасибо за отзыв!
Первая версия статьи как раз была на английском, ищется по ключевым словам на Medium.
FanatPHP
Спасибо, нашел. Не хотите его запостить в /r/PHP на Реддите? А то у меня самого руки чешутся :) Но когда автор размещает, то это всегда лучше — и фидбек из первых рук, и вообще…
Там просто ссылку даешь, и все.
ByQwert Автор
Реддитом не пользуюсь, можете запостить, я не против)
FanatPHP
Хорошо. Хочу только уточнить один момент. Я правильно понимаю, что https://api-stage.seedr.ru/player был открыт для доступа (сейчас он у меня не открывается) и при этом вываливал на экран отладочную информацию? То есть на проде этого всего не было, но благодаря открытому стейджу и удалось получить фидбек по ошибкам?
ByQwert Автор
Сидр закрыли в феврале, поэтому да, хосты могут не работать.
Сразу отладочную информацию ни прод, ни стейдж не вываливал, но если стригерить ошибку, подробный дебаг был включен и там, и там.
Путь к логам был получен как раз с прода, но открыть их удалось только на стейдже из-за особенности с расширением. Точно не помню, но, возможно, где-то в исходниках на Гитхабе как раз и увидел в некоторых местах расширение .log. Теоретически и путь к логам, можно было понять по коду, без ошибки про свободное место.