По традиции 9 мая займёмся задачками на информационную безопасность web-серверов. Сегодня задачка с портала root-me.org, называется "PHP - register globals". За решение задачки дают 25 баллов, ближе к среднему уровню.
Нам дают две подсказки:
- Уязвимость связана с register_globals.
- А ещё разработчики часто забывают почистить за собой файлы бэкапа.
Задачки с файлами бэкапов мы уже решали:
Многие текстовые редакторы в процессе редактирования делают резервные копии файлов, сохраняя их с другим расширением.
А потом снаружи появляется возможность доступа к файлам бэкапа текстовых редакторов.
register_globals=On — настройка PHP, которая автоматически регистрирует переменные EGPCS (Environment, GET, POST, Cookie, Server) в качестве глобальных переменных.
Опасная штука, не рекомендуется включать. Начиная с версии PHP 4.2.0 значением по умолчанию для этой директивы является off. Удалена в PHP 5.4.0.
Ссылки
Решение
Переходим на страницу задания:
http://challenge01.root-me.org/web-serveur/ch17/
Мы видим форму авторизации. Есть поле пароля и кнопка "connect".
Посмотрим код страницы. Ничего интересного, форма отправляет POST запрос на текущую страницу и передаёт переменную password. Вероятно, если передать правильный пароль, то чудо произойдёт.
По умолчанию основная страница index.php. Проверим это, допишем в URL. Действительно, основной исполняемый файл index.php.
Давайте проверим, не забыл ли разработчик отключить бэкап файлов в своём любимом текстовом редакторе. Проверим, не забыл ли разработчик удалить за собой файл index.php.bak. Забыл-таки.
Скачивается файл index.php.bak.
Посмотрим что внутри.
<?php
function auth($password, $hidden_password){
$res=0;
if (isset($password) && $password!=""){
if ( $password == $hidden_password ){
$res=1;
}
}
$_SESSION["logged"]=$res;
return $res;
}
function display($res){
$aff= '
<html>
<head>
</head>
<body>
<h1>Authentication v 0.05</h1>
<form action="" method="POST">
Password <br/>
<input type="password" name="password" /><br/><br/>
<br/><br/>
<input type="submit" value="connect" /><br/><br/>
</form>
<h3>'.htmlentities($res).'</h3>
</body>
</html>';
return $aff;
}
session_start();
if ( ! isset($_SESSION["logged"]) )
$_SESSION["logged"]=0;
$aff="";
include("config.inc.php");
if (isset($_POST["password"]))
$password = $_POST["password"];
if (!ini_get('register_globals')) {
$superglobals = array($_SERVER, $_ENV,$_FILES, $_COOKIE, $_POST, $_GET);
if (isset($_SESSION)) {
array_unshift($superglobals, $_SESSION);
}
foreach ($superglobals as $superglobal) {
extract($superglobal, 0 );
}
}
if (( isset ($password) && $password!="" && auth($password,$hidden_password)==1) || (is_array($_SESSION) && $_SESSION["logged"]==1 ) ){
$aff=display("well done, you can validate with the password : $hidden_password");
} else {
$aff=display("try again");
}
echo $aff;
?>
Обратим внимание на последний участок кода.
Если переменная $password (а мы её передаём из формы) равна переменной $hidden_password, то нам выведут на экран значение $hidden_password. Если переменная $_SESSION["logged"] равна единице, то мы тоже получим то что нужно.
Если в настройках PHP стоит register_globals=On, то мы смело можем передать в URL вместе с переменной password ещё и hidden_password. Можем передать любые значения, главное, чтобы они были равны.
- password = lab
- hidden_password = lab
Ну вот, мы выполнили условие функции и получили значение переменной hidden_password, вот только мы его переопределили и видим тот текст, который передали — "lab". Но ничего страшного, смотрим в эту часть кода:
Если пароли равны, то взводится сессия logged. Убираем все параметры из URL.
Поскольку у нас сессия logged=1, то видим значение скрытой переменной hidden_password. Вот и флаг. Валидируем:
И получаем 25 баллов. ? Это ПОБЕДА, друзья!
Безопасность
Если у вас старая версия PHP, отключайте, по возможности, настройку register_globals. Это можно сделать в php.ini:
register_globals=Off
Большинство хостинг провайдеров вам это не позволят, потому придется воспользоваться файлом .htaccess:
php_flag register_globals off
Бонус
Настройка register_globals=On автоматически регистрирует и переменные сессии. Мы просто могли передать в URL параметр logged=1 и получить тот же результат: