Обнаружением данной уязвимости я обязан Ragnar’у, собственно по его просьбе сейчас и пишу этот пост с целью объяснения сути самой баги. Однажды он постучал мне в асю и попросил поковыряться в web-админке DDOS-бота Illusion – видимо хотел угнать чей-то ботнет =) Предложение я принял и через некоторое время нашел уязвимость, а именно SQL-инъекцию. Так как основной функционал админки был недоступен для неавторизованных пользователей (а пароль хранился прямо в исходнике =\), то свои надежды я возложил на интерфейс для ботов, что и принесло мне успех. Итак, рассмотрим, какие значения параметров скрипт ожидает от ботов и в чем заключается уязвимость.
<?php
/* this function will be used by bots */
if ($act == "online")
{
if (isset( $_GET["nickname"] ))
$nickname = base64_decode( $_GET["nickname"] );
else exit();
if (isset( $_GET["s4"] )) $s4 = $_GET["s4"]; else $s4 = 0;
if (isset( $_GET["s5"] )) $s5 = $_GET["s5"]; else $s5 = 0;
if (isset( $_POST["msg_out"] )) $msg_out = base64_decode( $_POST["msg_out"] );
else $msg_out = "";
# if (isset( $_GET["msg_out"] )) $msg_out = $_GET["msg_out"];
else $msg_out = "";
die( db_bot_online( $nickname, $msg_out, $s4, $s5 ) );
}
?>
Переменная act передается GET-запросом и если ее содержимое равно online, это означает, что к нам пожаловал бот. Далее формируются все необходимые значения, которые передаются в функцию db_bot_online, ее листинг смотрим ниже.
<?php
/* add/update DB record about bot */
function db_bot_online( $nickname, $mo, $socks4_port, $socks5_port )
{
GLOBAL $mysql_host, $mysql_user, $mysql_password, $mysql_dbname, $mysql_bots_table,
$HTTP_ENV_VARS;
if (!@mysql_connect( $mysql_host, $mysql_user, $mysql_password ))
return "400";
mysql_select_db( $mysql_dbname );
$time = time();
$ip = $_SERVER["REMOTE_ADDR"];
$msg_out = str_replace( "\", "\\", $mo );
$msg_out = htmlspecialchars( $msg_out );
$r = mysql_query( "SELECT time, status, ip, msg_in FROM $mysql_bots_table
WHERE ip=\"$ip\"" );
$msg_in = get_new_cmd();
if ($arr = mysql_fetch_array( $r ))
{
$msg_in = $arr["msg_in"];
$status = $arr["status"];
$t = $arr["time"];
$fetched = 1;
}
else
$fetched = 0;
if ($msg_out == "")
if ($fetched)
{mysql_query( "UPDATE $mysql_bots_table SET time=$time,
nickname=\"$nickname\",socks4=$socks4_port,socks5=$socks5_port WHERE ip=\"$ip\"" );
}
else
{
if ($msg_in) $st = 0; else $st = 1;
mysql_query( "INSERT INTO $mysql_bots_table VALUES($time, \"$ip\",
\"$nickname\", \"$msg_in\", \"\", $st, $socks4_port, $socks5_port)" );
$status = $st;
}
else {
/**/
}
/**/
}
?>
Из всех входящих переменных фильтруется лишь msg_out, более того в запросах UPDATE и INSERT переменные st, socks4_port, socks5_port не обрамлены в кавычки. Это значит, что не придется париться с magic_quotes_gpc. С другой стороны, UNION провести мы не можем, так как в единственном SELECT –запросе подставляется значение из REMOTE_ADDR, т.е. наш ip, который изменить на произвольное значение нельзя. Остается лишь UPDATE и INSERT, поэтому придется получать инфу из БД на основании временных задержек, которые реализуются с помощью MySQL-функции BENCHMARK. Вкратце суть данного приема заключается в следующем: если условие истинно, то n-ое количество раз выполняется наше выражение, что приводит к задержке ответа, если же условие ложно, то ответ будет получен без задержки. Однако есть и ограничение: данная техника актуальна только для версии MySQL выше 4.1, так как именно с этой версии в MySQL появилась поддержка подзапросов.
index.php?act=online&nickname=emxpbmE%3D &s4=1&s5= (IF(1=1,BENCHMARK(3000000,MD5(31337)),1)) – есть задержка
index.php?act=online&nickname=emxpbmE%3D&s4=1&s5= (IF(2=1,BENCHMARK(3000000,MD5(31337)),1)) – нет задержки
Подставляя вместо MD5(31337) полезные для нас запросы, можно получить интересные сведения из БД.
Подробнее об этом методе можно прочитать здесь:
Посимвольный перебор в базах данных на примере MySQL
Итак, написав небольшой скрипт, автоматизирующий действия по сбору данных, мы с Ragnar’ом попытались узнать версию мускула. Удача была на нашей стороне, так как на сервере крутился пятый MySQL и мы могли узнать названия таблиц. Как я уже упоминал, пароль хранился прямо в исходнике поэтому достать его было невозможно ввиду отсутствия file_priv у текущего пользователя MySQL. Среди обнаруженных таблиц ничего интересного также не оказалось, поэтому получить доступ к админке так и не удалось =\ В конце концов, пришлось лишь довольствоваться наличием баги.

Leave a Reply