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

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

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

  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:

  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()! Это уже попахивает 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

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

Комментарии (13)


  1. FanatPHP
    29.12.2022 13:07
    +3

    Фантастика! Это тянет на статью года.
    Именно подходом, декларированным в самом начале — отслеживание хода мысли, подробное описание использованных приёмов, а не как обычно "вот мы находим дырку… profit!"
    Спасибо огромное за терпение, и при раскручивании этой уязвимости, и — главное — при написании статьи!


    Как раз сейчас перечитывал "Хакеров" Стивена Леви, и в статье прямо чувствуется дух тех времен зарождения хакерства, как искусства.


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


    1. ByQwert Автор
      29.12.2022 13:12

      Спасибо за отзыв!
      Первая версия статьи как раз была на английском, ищется по ключевым словам на Medium.


      1. FanatPHP
        29.12.2022 13:21

        Спасибо, нашел. Не хотите его запостить в /r/PHP на Реддите? А то у меня самого руки чешутся :) Но когда автор размещает, то это всегда лучше — и фидбек из первых рук, и вообще…
        Там просто ссылку даешь, и все.


        1. ByQwert Автор
          29.12.2022 13:44

          Реддитом не пользуюсь, можете запостить, я не против)


          1. FanatPHP
            29.12.2022 14:02

            Хорошо. Хочу только уточнить один момент. Я правильно понимаю, что https://api-stage.seedr.ru/player был открыт для доступа (сейчас он у меня не открывается) и при этом вываливал на экран отладочную информацию? То есть на проде этого всего не было, но благодаря открытому стейджу и удалось получить фидбек по ошибкам?


            1. ByQwert Автор
              29.12.2022 19:50
              +2

              Сидр закрыли в феврале, поэтому да, хосты могут не работать.

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

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


  1. FanatPHP
    29.12.2022 14:39
    +4

    Решил составить TL;DR:
    Надеюсь, все правильно понял


    • встроенный плеер запрашивает информацию о видео со сторонних сервисов
    • Вимео отдает инфу в формате РНР serialize(!)
    • Также в вимео есть открытый редирект (определенный параметр в URL заставляет любой скрипт вместо отдачи контента сделать редирект на указанный адрес)
    • соответственно, скрипту на Seedr пожно подсунуть свой сериализованный объект.
    • API Seedr выдает наружу системные ошибки РНР()
    • из текста ошибки стало ясно, что на сервере используется Кохана
    • стейдж сервер доступен без пароля, и при этом при ошибках вываливает дебаг панель. Хотя это, наверное, непринципиальный момент, но сильно упростил дальнейшие ходы
    • с помощью стандартной уязвимости unserialize можно создать экземпляр класса View и получить контролируемый инклюд файла
    • теперь остается найти этот файл. самый очевидный киндидат — это логи
    • находим файл, в который пишется переданный на сайт запрос. И в этом запросе размещается РНР код
    • затем этот файл с логом иклюдится через класс View.


    1. ByQwert Автор
      29.12.2022 19:54
      +1

      В целом верно, спасибо.


  1. Looser_friendly
    29.12.2022 21:22
    +1

    Большое спасибо за статью! Серьёзный труд был вложен как в работу, так и в написание)

    Можете поделиться, какой опыт в пентесте и багбаунти, давно ли этим занимаетесь? И про опыт разработки, если не сложно.

    Мне до подобного ещё далеко, но хоть яснее наметить себе путь для развития) Спасибо!


    1. ByQwert Автор
      29.12.2022 21:43
      +3

      Спасибо.


      5 лет в безопасности, учитывая год самообучения до начала работы по текущему профилю. В баг-баунти года 3-4, но с большими перерывами. До этого был небольшой опыт технической поддержки, обслуживания биллинга и немного разработки на PHP, JS, Python.

      Ели заинтересованы в безопасности веб-приложений, то для начала могу порекомендовать бесплатные лабы от PortSwigger. Успехов!


      1. Looser_friendly
        29.12.2022 21:54

        Благодарю за содержательный ответ!


  1. Protos
    29.12.2022 22:25

    Мужик!


  1. BugM
    30.12.2022 03:32
    +1

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