Об одной малоизвестной уязвимости в веб сайтах

от автора

Первое правило безопасности при разработке Веб приложений гласит: —

Не доверять данным пришедшим от клиента.

Почти все это правило хорошо знают и соблюдают. Мы пропускаем через валидаторы данные форм, кукисы, даже URI.
Но недавно я с удивлением обнаружил, что есть одна переменная, приходящая от клиента, которую почти никто не фильтрует.
Речь пойдет о компрометации веб приложения через подмену значения HTTP_HOST и SERVER_NAME.

Если выполнить поиск по Гитхабу, по ключевому слову «HTTP_HOST», то можно найти порядка 43 страниц репозитариев, в которых используется $_SERVER['HTTP_HOST']. При беглом просмотре я обнаружил достаточно много случаев, когда данные пришедшие в этой переменной остались без фильтрации. Чаще всего проверяют только существование $_SERVER['HTTP_HOST'], но не проводят валидацию.

Опишу ситуацию для связки Nginx+php (php-fpm либо fcgi-spawn), для других веб-серверов или другого языка программирования ситуация будет отличаться в деталях, но общие принципы сохраняются.
Поведение Apache описано тут: shiflett.org/blog/2006/mar/server-name-versus-http-host

Способы компрометации

Для иллюстрации, будем использовать telnet
Заголовки, которые отправляет браузер серверу выглядят примерно так:

GET / HTTP/1.1 Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Charset:windows-1251,utf-8;q=0.7,*;q=0.3 Accept-Language:ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4 Cache-Control:max-age=0 Connection:keep-alive Host:site.dev Referer:http://site.dev/index.htm User-Agent:TelnetTest

Если из них убрать строку (передача запроса без HTTP_HOST)

Host:site.dev,

то сервер вернет

400 Bad Request

Другой способ отправки заголовков:

GET http://site.dev/ HTTP/1.1 Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Charset:windows-1251,utf-8;q=0.7,*;q=0.3 Accept-Language:ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4 Cache-Control:max-age=0 Connection:keep-alive Host:site.dev Referer:http://site.dev/index.htm User-Agent:TelnetTest

Результат будет точно такой же, как и при отправке первого заголовка

GET / HTTP/1.1

Но вот, если первым заголовком передать не

GET http://site.dev/ HTTP/1.1

а

GET http://site.dev/

То все последующие заголовки будут отброшены, Nginx отработает секцию server определенную для

    server_name site.dev; 

Но HTTP_HOST и SERVER_NAME не будут определены.
Передать пустой HTTP_HOST не получится:

Host:

Но получится передать

Host:_

или

Host:""

Теперь самое интересное.
Подключаемся к телнету

$ telnet site.dev 80 Trying 127.0.0.1... Connected to site.dev. Escape character is '^]'. 

Оправляем

GET http://site.dev/phpinfo.php HTTP/1.1 Accept:text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Charset:windows-1251,utf-8;q=0.7,*;q=0.3 Accept-Language:ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4 Cache-Control:max-age=0 Connection:keep-alive Host:~%#$^&*()<>?@\!."'{}[]=+| Referer:http://site.dev/index.htm User-Agent:TelnetTest 

И смотрим:

_SERVER["SERVER_NAME"]: ~%#$^&*()<>?@\!."'{}[]=+| _SERVER["HTTP_HOST"]: ~%#$^&*()<>?@\!."'{}[]=+| 

Ответ сервера:

HTTP/1.1 200 OK Server: nginx/1.0.10 Date: Wed, 23 Jan 2013 10:31:14 GMT Content-Type: text/html Transfer-Encoding: chunked Connection: keep-alive 

Если в заголовке Host: будет присутствовать '/', то сервер вернет 400 Bad Request.
Т.е. такой заголовок Host:../../ не пройдет, такой Host:http://evil.site тоже

Уязвимости

Получение доступа к приватным данным
SQL-инъекции

Способы защиты

Самый простой и доступный способ защиты (нашел тут: stackoverflow.com/questions/1459739/php-serverhttp-host-vs-serverserver-name-am-i-understanding-the-ma).

$allowed_hosts = array('foo.example.com', 'bar.example.com'); if (!isset($_SERVER['HTTP_HOST']) || !in_array($_SERVER['HTTP_HOST'], $allowed_hosts)) {     header($_SERVER['SERVER_PROTOCOL'].' 400 Bad Request');     exit; } 

Самый эффективный способ защиты — явно определить HTTP_HOST на стороне веб сервера.
Что бы понять, как переопределять HTTP_HOST добавим в конфиг Nginx’а такие строки:

fastcgi_param	HTTP_HOST1		$http_host; fastcgi_param	HTTP_HOST2		$host; fastcgi_param	HTTP_HOST3		$server_name; 

Допустим у нас определены две секции server:

server { 	listen  80; 	server_name  site1.dev; 	... } server { 	listen  80; 	server_name  site2.dev site3.dev; 	... } 

Сделаем такой запрос

$ telnet site1.dev 80 Trying 127.0.0.1... Connected to site.dev. Escape character is '^]'. 
GET http://site3.dev/phpinfo.php HTTP/1.1 Host:~%#$^&*()<>?@\!."'{}[]=+| User-Agent:TelnetTest 

На выходе получим

_SERVER["HTTP_HOST1"]: ~%#$^&*()<>?@\!."'{}[]=+| _SERVER["HTTP_HOST2"]: site3.dev _SERVER["HTTP_HOST3"]: site2.dev 

Все логично. И наиболее корректной будет запись:

fastcgi_param	HTTP_HOST	$host; 

Если сделать запрос вида

$ telnet site3.dev 80

GET /phpinfo.php HTTP/1.1 Host:~%#$^&*()<>?@\!."'{}[]=+| User-Agent:TelnetTest 

то отработает секция

server {     listen      80 default_server;     server_name "";     return      444; } 

которая легко отсеет такой запрос.

ссылка на оригинал статьи http://habrahabr.ru/post/166855/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *