Главная » Статьи » Защита и взлом |
При использовании метода 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 | |
Разное [0] |
Интернет [2] |
Защита и взлом [23] |
Халява [2] |
Софт [8] |
Железо [8] |
Медицина и здоровье [11] |
Спорт [10] |
Юмор [16] |
Искусство [7] |
Игры [1] |
Мобильные телефоны [10] |
Вебмастеринг [15] |
Непознанное [2] |
Наука и оброзование [9] |
Авто/мото [11] |
Секс [4] |
Справочная информация [17] |