Пользователь
0,0
рейтинг
11 февраля 2013 в 10:56

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

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

которая легко отсеет такой запрос.
rotor @rotor
карма
31,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (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
        Ну а пример «Получение доступа к приватным данным»?
        • 0
          habrahabr.ru/post/166855/#comment_5855369 — короче что то подключается по имени хоста. Наиболее вероятное использование в парковочных движках.
  • +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 так никто и не переписал.

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