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

    Первое правило безопасности при разработке Веб приложений гласит: —
    Не доверять данным пришедшим от клиента.
    Почти все это правило хорошо знают и соблюдают. Мы пропускаем через валидаторы данные форм, кукисы, даже 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;
    }
    

    которая легко отсеет такой запрос.
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 51
    • +6
      Здесь есть ещё один неприятный момент.
      Встречался в Битриксе, но всплыть может ещё где-нибудь.
      Условия:
      — Веб-сервер отвечает на любой хост одним сайтом
      — Относительные URL на сайте приводятся к полным URL с включением хоста
      — Результатирующая страница кешируется
      — Есть ещё какие-нибудь домены, ссылающиеся на наш IP

      Кто-то делает запрос на левый домен, кеш истек, а страница кешируется с адресами с этим левым доменом. В итоге может появиться множество «зеркал» сайта с разными хостами.
      Более того, пользователь зашедший с правильного домена и попавший на кеш с левым доменом, уйдет на этот левый домен.
      • +11
        Я думал, что эти переменные нельзя переопределить. Теперь буду знать, спасибо!

        P.S. Слава Богу нигде не использую :)
        • +23
          Их не просто так назвали переменными.
          • +11
            А зачем парня заминусовали? Человек не постеснялся сказать, что чего-то не знал — так это же хорошо. Такие вещи нужно наоборот поощрять.
            Никто опытным не рождается. Я как раз для таких людей и писал заметку.
          • 0
            Решение проблемы для Nginx есть, а с Apache как быть?
            • 0
              А с апачем это не прокатит, по-моему.
              По крайней мере сейчас попробовал на локалхосте — Apache 2.2.22, не прокатывает.
              Если запрос
              GET http://www.site.lo/hosttest.php
              Host: xxx
              То SERVER_NAME правильный, а HTTP_HOST'а вообще нет.
              • 0
                Все логично. Если первая строка без указания протокола, то последующие заголовки не будут учтены. Соответственно, HTTP_HOST будет пустой.
                Попробуйте так:
                GET http://www.site.lo/hosttest.php HTTP/1.1
                Host: xxx
                • 0
                  В самой заметке есть же ссылка по Apache. В частности, должно быть UseCanonicalName Off.
                  • 0
                    А, сорри, я почему-то понял пример так, что надо как раз без протокола отправлять запрос.
                    Да, с указанием протокола действительно HTTP_HOST меняется… Но SERVER_NAME в апаче, правда, остаётся правильный, а всякие **внобитриксы, кстати, используют SERVER_NAME, а не HTTP_HOST, насколько я помню.
                    Блин. А вот MediaWiki, между прочим, уязвима, в том числе её последние версии. Она сначала проверяет HTTP_HOST, и только потом SERVER_NAME (ну т.е., уязвима, если в конфиге гвоздями имя сервера не забить).
                    Похоже зашлю в геррит патчик, чтобы она хотя бы на SERVER_NAME сначала смотрела — он явно покорректней будет…
                    • 0
                      лучше пусть на host смотрит :) А то, как показано выше, NgX за server_name считает тот хост, что первым указан в этой директиве, а не тот, куда пришёл запрос :(
              • 0
                Довольно интересно.
                Я так понимаю что если у меня в nginx перечислены все возможные server_name и default_server ведет на страничку 404 то все нормально?
                • +3
                  Не совсем. В статье есть пример с таким вот запросом через telnet
                  GET http://site3.dev/phpinfo.php HTTP/1.1
                  Если имя сервера передано в первой строке запроса, то в дальше оно может быть переопределено. Host:~%#$^&*()<>?@\!."'{}[]=+|
                  Можно ограничиться защитой только на стороне Nginx, но для этого стоит явно определить переменную HTTP_HOST: fastcgi_param HTTP_HOST $host;
                • +6
                  Если сервер отвечает на любой хост своим сайтом, то это кроме всего прочего уязвимость — DNS Rebinding.
                  Если хост не сматчился — нужно отдавать страницу, что-то типа «This host is not found on our server», пример.
                  • +12
                    В общем-то как мне кажется это не уязвимость, а типичный мисконфигурейшин.
                    Ну дефолтный виртхост должен всегда отдавать или что-то типа 403 или редиректа…
                    • +1
                      Любой мисконфигурейшин может стать уязвимостью и многие (не все) уязвимости есть следствие мисконфигурейшина.
                      В HTTP_HOST кроме подмены самого хоста можно передать непредусмотренную последовательность символов.
                    • +21
                      Я так и не понял, где уязвимость дальше чем «нам отдадут не тот виртуальный хост».
                      • 0
                        Автор, сдается мне, немного не в ту сторону ушел.
                        Уязвимость можно реализовать подменой, например, HTTP_REFERER или HTTP_USER_AGENT, при условии что они где-то на стороне сервера вообще как-то обрабатываются.
                        • 0
                          К сожалению, именно HTTP_HOST часто оставляют без валидации. Хотя все эти переменные начинаются с HTTP_, что как бы намекает. А вот с SERVER_NAME менее очевидно.
                        • +10
                          Автор просто не раскрыл тему до конца. Точнее как-то двумя строчками ограничился в разделе «уязвимости».
                          В общем, суть в том, что если разработчик просто подставляет данные, например, из переменной $_SERVER['HTTP_HOST'], без какой-либо фильтрации при работе с базой — то мы можем провести sqli. Похожее задание было на phdays quals, краткий врайтап по ссылке.

                          Предложенный способ защиты на php является костылём и говорит о том, что программист не до конца понимает работу HTTP и работу виртуальных хостов. Как уже верно подметили, нужно просто четко и явно матчить HOST, а все, что не наше — отдавать 404, 403 или редирект.
                          • +13
                            В общем, суть в том, что если разработчик просто подставляет данные, например, из переменной $_SERVER['HTTP_HOST'], без какой-либо фильтрации при работе с базой — то мы можем провести sqli.

                            Да сколько же раз говорить, что при работе с базой все данные нужно трактовать одинаково (кроме тех, которые мы порождаем сами внутри security boundary)? И все эти защиты на уровне конфигов веб-сервера к этому никакого отношения не имеют.
                            • 0
                              Да я абсолютно согласен :)
                              Но не сказал бы, что не имеют. Порой на плечи админов ложится лишняя нагрузка, из-за программистов, которые не уделяют нужное внимание security (проблема многих проектов). Как, порой, и наоборот, когда в директории кладут пустые index.html, если по каким-то причинам не получается отключить индексирование (переопределение в .htaccess запрещено) или вообще используется nginx и до админа не достучаться.
                              • +5
                                Порой на плечи админов ложится лишняя нагрузка, из-за программистов, которые не уделяют нужное внимание security (проблема многих проектов).

                                Лучше бейте программистов. Это полезнее.

                                Костыли в безопасности — опасная вещь.
                              • 0
                                вот вот. должна быть единая идеология — архитектура по управлению данными, внезависимости откуда ты их взял. тут помоеж т ORM
                                • +2
                                  … и ORM тут тоже не при чем совершенно. То, о чем вы говорите — задача DAL в произвольном виде, вне зависимости от используемого шаблона.
                              • 0
                                Совершенно верно. Добавлю, что я не ставил себе цели описать все возможные векторы атак, а лишь пытался дать предостережение тем программистам и системным администраторам, которые не придавали этим переменным должного внимания.
                                Как показывает практика, это не такая уж и редкость.
                            • +9
                              Все, что начинается с HTTP_* можно «подделать». Но это не подделка, это просто данные полученные от клиента.
                              • +1
                                ничего не понял :(
                                • +4
                                  >Получение доступа к приватным данным
                                  SQL-инъекции

                                  таки где демо? чем эта переменная круче REFERRER например который тоже могут теоретически не фильтровать? 0day?
                                  • 0
                                    Фильтровать нужно только если используется. Ничем не круче REFERRER. Я к сожалению, сейчас не найду, но на том же Гитхабе видел подключение конфигурационных файлов по полученному имени хоста. Потенциально это дыра.
                                    То есть с одной стороны программисты не фильтруют, т.к. это должно быть сконфигурировано админами, а админы оставляют на откуп программистам. Возьмите любой шаред хостинг и почти наверняка получится повторить приведенные в посте примеры.
                                    • 0
                                      Может поделитесь способом подменять REFERRER? Есть просто проблема, флеш баннеры в упор не хотят передавать REFERRER.
                                      • 0
                                        с помощью искусственного запроса, можно и реферрер подделать и IP, и прокси
                                        • +1
                                          подделать и IP

                                          ну с 'HTTP_X_FORWARDED_FOR' понятно все, а вот как REMOTE_ADDR искусственным запросом подделать очень даже интересно послушать
                                          • 0
                                            REMOTE_ADDR http заголовками не подделаешь, но бывают совсем отмороженные админы, которые настраивают сервер так, что при отправке заголовка X-Forwarded-For: fail_IP воспринимают fail_IP как настоящий. Сам такое видел.
                                            В наказание таким админам и их клиентам как раз и появился вот этот github.com/marson/XForwardedForHeader аддон.
                                        • 0
                                          Еще есть случаи когда в закрытой части сайта делают редирект на страницу входа,
                                          а в теле все равно страница, например админки,
                                    • +2
                                      Не доверять данным пришедшим от клиента


                                      Оно звучит не совсем так. Правильно:

                                      Не доверять данным, пересекающим границы доверия во всех направлениях


                                      Нет объективных причин, не доверяя данным, пришедшим на сервер от клиента, доверять при этом данным, которые клиент получил от сервера в ответ.
                                      • НЛО прилетело и опубликовало эту надпись здесь
                                        • +3
                                          … только что, вы прослушали краткое руководство по защите от фишинга в Google Mail ;)
                                      • 0
                                        Я так и не понял, как это может быть эксплуатировано. И при чем тут SQL-инъекции.
                                        Злоумышленник только ссылки себе на неправильный хост в генерируемых страничках получит — вот и все.
                                        • 0
                                          Если скрипт должен собирать статистику в БД, а программист не ожидает подвоха, то вполне возможно. Понятно, что данные перед записью в БД должны фильтроваться в любом случае.
                                          Еще в самом первом комментарии прекрасный пример XSS — нарочно и не придумаешь.
                                          • –1
                                            Я считаю, что вы не правы. Прочитайте статью о SQL-инъекциях: habrahabr.ru/post/148701/
                                            Такую ошибку могут допустить только новички, которые работают с СУБД без дополнительных уровней абстракции. Даже если человек не предусмотрел, что в данном поле могут быть опасные данные, абстракция все равно не допустит SQL-инъекции.
                                            • 0
                                              Ну а пример «Получение доступа к приватным данным»?
                                        • +1
                                          А в чем суть нолика в слове HOST -> H0ST?
                                          • 0
                                            Может опечатка? А где вы увидели нолик?
                                            • 0
                                              Почти везде. В некоторых шрифтах особенно заметно:



                                              Причем, в коде так же. Я думал, может какое-то хитрое вуду на этом замешано :)
                                              • 0
                                                Английская O находится аккурат под 0, пару раз сам так случайно опечатывался
                                                Наверное единственная опечатка без потери смысла/орфографии :)
                                                • 0
                                                  Если бы человек опечатывался, то был бы не 0, а ")", потому что это слово набирается с шифтом.
                                                  В данном случае 0 набрано сознательно. Быть может для защиты от скрипт-кидди, но это явно слишком просто.
                                                  • 0
                                                    Прямо целая теория заговора на моих глазах родилась :)
                                                    Где вы там все же нули видите, я не пойму?
                                                    Нет там никаких нулей у меня!
                                                    • +2
                                                      Действительно нет. Но вы же не будете разрушать нашу красивую, загадочную теорию заговора? Впишите нули в текст! :)
                                          • 0
                                            пардон, а чем HTTP_HOST безопаснее любых других суперглобалов? экранировать все в любом случае!
                                            • +1
                                              Данный баг был например в CMS е107. Там в шаблон включалась переменная основаная на HTTP_HOST, для вставки картинок на сайте. Далее производилась замена псевдопеременных на данные используя preg_replace с модификатором /e
                                              Как итог, выполнение произвольного кода, который мог присутствовать в HTTP_HOST ввиде {phpinfo();}, и решением была замена preg_replace на preg_replace_callback, но использование HTTP_HOST так никто и не переписал.

                                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.