blin.ws

Приветствую Вас Гость | Регистрация | Вход

Интересное

Главная » Статьи » Защита и взлом

Digest vs Basic
При использовании метода Digest, как уже было сказано, пароль не передается, и его невозможно отснифить, однако есть и другая сторона проблемы. Для того, чтобы проверить пароль, сервер должен вычислить ответ и сравнить его с ответом клиента, следовательно, на сервере должен храниться пароль или зависящие от него данные, необходимые для прохождения аутентификации. Отсюда следует, что человек, получивший права на чтение аккаунтов (например, с помощью SQL-injection), сможет получить доступ к страницам, защищенным методом Digest. При использовании метода Basic возможно хранение хешей вместо паролей, что не дает поднять права, прочитав эти хеши (ниже мы увидим, что в Digest тоже могут храниться хеши, но такие, что их знания достаточно для вычисления ответа). Таким образом, перед нами дилемма: либо наш пароль отснифят, либо получат через web-уязвимость, которую кто-нибудь обязательно отыщет, потому что кто ищет, тот всегда найдет. Есть метод аутентификации без обоих этих недостатков - метод аутентификации на основе открытого ключа: для проверки нужен открытый ключ, а для прохождения проверки - секретный, однако в HTTP 1.1 такой метод не предусмотрен. RFC 2069 рекомендует использовать SSL, если защита так важна. Защищается только передача пароля, а контент не шифруется, так что нет смысла защищать этим методом ресурсы, откуда пользователь получает секретную информацию. Для них необходим SSL. А имеет смысл защищать, например, форум или заливку контента на сайт. Итак, если хостинг не поддерживает SSL, а аутентификация должна быть безопасной, то будем использовать Digest. В Apache предусмотрен модуль mod_digest. Для его использования в конфиге (или в .htaccess) пишем:

AuthType Digest
AuthUserFile <файл>
AuthName <название защищаемой области>
Require valid_user

Файлы пользователей создаются утилитой htdigest. Про mod_digest одно время появлялись сообщения, что он уязвим, так что, возможно, там еще какие-нибудь проблемы обнаружатся. Кроме того, когда я попытался его использовать у себя дома, получил ошибку 500 Server Internal Error. Кроме того, если добавление аккаунтов должно происходить автоматически, и их должно быть много, они должны храниться не в конфиге Апача, а в MySQL. Решение - использовать PHP. В PHP нет встроенной поддержки этого метода, поэтому его придется реализовать. Для этого необходимо изучить этот метод подробно. Сразу замечу, что приведенная в этой статье реализация работает только на Apache, так как полный доступ к заголовкам запроса (функция apache_request_headers) работает только в Apache, а на других серверах может отсутствовать. Нам же просто необходимо прочитать заголовок Authorization.

Описание метода

Полностью описание метода можно прочитать в RFC 2069, а если вкратце, то метод работает так. Когда сервер получает запрос, относящийся к защищенной области, он выдает ошибку 401 Authorization Required и заголовок с запросом аутентификации такого вида:

WWW-Authenticate: Digest realm="secure area", nonce="123456123456"

realm - это название защищенной области, а nonce - одноразовое значение. Есть еще необязательные параметры, которые мы обсуждать не будем. Клиент повторяет запрос, добавив к нему заголовок такого вида:

Authorization: Digest realm="secure area", username="123", uri="/index.php", nonce="123456123456", response="1234567890abcdef1234567890abcdef"

Параметр uri должен совпадать с URI в запросе, а response - это ответ, который вычисляется так:

response = H(H(A1) + ":" + nonce + ":" + H(A2))
H - хеш-функция, по умолчанию MD5
A1 = логин + ":" + realm + ":" + пароль
A2 = метод запроса + ":" + URI
метод запроса - это GET, POST и тд.

Как видим, A1 не зависит ни от запроса, ни от одноразового значения, поэтому на сервере может храниться не пароль, а H(A1). Именно так это реализовано в mod_digest в Apache. Однако этих же данных достаточно и клиенту. Злоумышленник, получив этот хеш, может вычислить ответ по приведенным выше формулам и сформировать HTTP-запрос, например, с помощью программы AccessDriver и ее инструмента HTTP Debugger. Подробнее этот процесс будет показан ниже. Сервер должен проверить, является ли одноразовое значение тем, которое было ранее выдано клиенту и не устарело ли оно. Если ответ соответствует параметру nonce, но значение этого параметра не актуально, выдается описанный выше ответ с кодом 401 с той лишь разницей, что в заголовок WWW-Authenticate добавляется параметр stale=true, указывающий, что в доступе отказано лишь по этой причине, и следует повторить попытку, не запрашивая у пользователя новый пароль. Это, имхо, неудобно, поскольку если такая ситуация возникнет при запросе POST или PUT с большим блоком данных, то клиенту придется передать все данные дважды. Во избежание этого стандартом предусмотрен заголовок Authentication-Info, в котором сервер может при ответе на успешный запрос сообщить клиенту следующее одноразовое значение. Синтаксис такой же, как у WWW-Authenticate, кроме того что nonce заменяется на nextnonce. Однако, судя по результатам моих экспериментов, Opera игнорирует этот заголовок. Другое решение: в соответствии с RFC 2068 (HTTP/1.1), сервер может ответить раньше, чем завершится запрос, чтобы клиент прервал ненужную передачу данных, но на Apache+PHP это не реализуется, поскольку скрипт начинает выполняться только после того, как Apache полностью получит и пропарсит запрос.

Хранение данных между запросами

В реализации метода challenge/response на PHP есть тонкий момент. Одноразовый параметр формируется и выдается клиенту в одном ответе, а проверяется уже в другом сеансе работы скрипта. То есть его необходимо сохранить от одного вызова скрипта до другого, и для этого придется использовать файлы или БД. В моем примере используются файлы с именами, соответствующими одноразовым значениям, а в самих файлах записаны IP-адреса клиентов, которым они выданы. В примере не реализован сбор мусора: надо периодически удалять старые файлы.

Разбор кода

Этот скрипт проверяет только пароль, и работает независимо от
логина. В зависимости от успешности проверки выдаются простые ответы.


$realm = 'secure area'; // Название защищаемой области
$pass = 'pass'; // Пароль
$fileprefix = './'; // Путь для файлов-меток, обозначающих валидность nonce

/* Сконструируем одноразовый параметр так, как рекомендуется в RFC2069, хотя можно и по-другому. Параметр, по рекомендации, должен зависеть от адреса клиента, текущего времени и секретной строки. */
$nonce = md5($_SERVER['REMOTE_ADDR'] . ':' . time() . ':MyCooolPrivateKey');

// Получаем заголовки
$headers = apache_request_headers();

// Флаг, который мы установим в TRUE при успешной проверке
$auth_success = FALSE;
$stale = "";

// Если нет заголовка Authorization, то нечего и проверять
if (isset($headers['Authorization']))
{
$authorization = $headers['Authorization'];

/* Пропарсим заголовок с помощью регулярного выражения. Заголовок содержит слово "Digest" и список пареметров вида param="value" или param=value через запятую. Это регулярное выражение соответствует одному такому параметру. */
preg_match_all('/(,|\s|^)(\w+)=("([^"]*)"|([\w\d]*))(,|$)/',
$authorization, $matches, PREG_SET_ORDER);

/* Теперь сформируем для удобства дальнейшей обработки массив, где ключи - названия параметров, а значения элементов массива - значения параметров. */
$auth_params = Array();
for ($i = 0; $i < count($matches); $i++)
{
$match = $matches[$i];

/* Название всегда во второй группе скобок, в значениев зависимости от того, в кавычках оно или нет, может быть в 4-й или 5-й группе. Для групп скобок, попавших в нереализованную ветвь, в массиве пустая строка, поэтому можно просто сложить значения. */
$auth_params[$match[2]] = $match[4] . $match[5];
}

/* Вычислим ответ, который соответствует логину, введенному пользователем, нашему паролю и одноразовому параметру, переданному пользователем. */
$a1 = $auth_params['username'] . ':' . $auth_params['realm'] . ':' . $pass;
$a2 = $_SERVER['REQUEST_METHOD'] . ':' . $_SERVER['REQUEST_URI'];
$resp = md5(md5($a1) . ':' . $auth_params['nonce'] . ':' . md5($a2));

// Проверяем ответ.
if ($resp == $auth_params['response'])
{
// Проверяем актуальность одноразового параметра
$fn = $fileprefix . $auth_params['nonce'];
if (@file_get_contents($fn) == $_SERVER['REMOTE_ADDR'])
{
unlink($fn); // Больше этот параметр неактуален
$auth_success = TRUE; // Аутентификация пройдена
} else
{
// Одноразовый параметр неактуален
$stale = ", stale=true";
}
}
}


if ($auth_success)
{
print("
");

print("Successfully authenticated\n");
var_dump($auth_params);

print("
");

} else
{
file_put_contents($fileprefix . $nonce, $_SERVER['REMOTE_ADDR']);

$proto = $_SERVER['SERVER_PROTOCOL'];
Header("$proto 401 Not Authorized");
Header("WWW-Authenticate: Digest realm=\"$realm\", nonce=\"$nonce\"$stale");

print("
");
print("You must authenticate with Digest method");
print("
");
}

?>


Прохождение Digest Auth при известном H(A1)

Покажу на примере, как проходить проверку, если пароль неизвестен, но известен H(A1). Для этого, как уже было сказано, понадобится AccessDriver. Расчеты хешей я буду делать вызывая из командной строки PHP CLI. Защищенная страница пусть находится по адресу http://mrblack.local/auth1.php, а хеш H(A1) равен "a8fb5b2d780a7bf0782207a51a013f04".

Открываем AccessDriver->Tools->HTTP Debugger и вбиваем адрес "http://mrblack.local/auth1.php". Жмем "Connect". Получаем:

HTTP Header[0] = HTTP/1.1 401 Authorization Required
HTTP Header[1] = Date: Mon, 04 Jul 2005 08:09:17 GMT
HTTP Header[2] = Server: Apache/1.3.31 (Win32) PHP/5.0.2
HTTP Header[3] = X-Powered-By: PHP/5.0.2
HTTP Header[4] = WWW-Authenticate: Digest realm="secure area", nonce="5925bea78552224abda11bfe318a8a03"
HTTP Header[5] = Connection: close
HTTP Header[6] = Content-Type: text/html

Открываем консоль, переходим в папку с PHP и вбиваем такую команду:

php -r "print md5('a8fb5b2d780a7bf0782207a51a013f04: 5925bea78552224abda11bfe318a8a03: '.md5('GET:http://mrblack.local/auth1.php'));"

Получаем искомый Digest-ответ: c6d0af0db239d75c 3f59640a4896d096
Теперь в AccessDriver ставим галочку "Header Data", копируем в появившееся поле заголовки, которые были посланы в прошлом запросе, и дописываем к ним Authorization. Вот что получается:

GET http://mrblack.local/auth1.php HTTP/1.1
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/x-shockwave-flash, */*
Accept-Language: en-us,en;q=0.5
User-Agent: Mozilla compatible
Host: mrblack.local
Pragma: no-cache
Authorization: Digest username="mrblack", realm="secure area", nonce="5925bea78552224ab da11bfe318a8a03", uri="http://mrblack.local/auth1.php", response="c6d0af0db239d75c3f59 640a4896d096"

Жмем "Connect". Получаем результат:

HTTP Header[0] = HTTP/1.1 200 OK
HTTP Header[1] = Date: Mon, 04 Jul 2005 08:12:11 GMT
HTTP Header[2] = Server: Apache/1.3.31 (Win32) PHP/5.0.2
HTTP Header[3] = X-Powered-By: PHP/5.0.2
HTTP Header[4] = Connection: close
HTTP Header[5] = Content-Type: text/html

Авторизация пройдена, получен положительный ответ.


Категория: Защита и взлом | (24 Ноября 2007)
Просмотров: 511
Всего комментариев: 0
Добавлять комментарии могут только зарегистрированные пользователи.
[ Регистрация | Вход ]
| | | Карта сайта