Почти два года назад я наткнулся на довольно значительную уязвимость в сети сайтов StackExchange. Я говорю «наткнулся» потому, что я не пытался взломать сайт. Обстоятельства приоткрыли мне дверь. Сама уязвимость является довольно интересной, и содержит урок для всех, кто создает и занимается поддержкой сайтов или серверной инфраструктуры. Итак, вот история о том, как я взломал StackOverflow…
Исходные данные
В то время я работал в небольшой компании, где имелся фаервол с совершенно драконовскими ограничениями. Он резал все заголовки запросов и ответов, не соответствующие спецификации HTTP/1.1 (и даже резал валидные HTTP/1.1 заголовки). Это играло злую шутку с сайтами, которые полагаются на такие вещи, как X-Requested-With
. Поэтому, для своих «внешних похождений», я настроил прокси.
У меня было несколько серверов в то время, так что я просто поставил на одном из них Squid. Так как я кое-что соображал, я разрешил подключения к прокси только с 127.0.0.1
. Я поднял SSH туннель к серверу и указал браузеру localhost в качестве прокси. Браузер подключался к туннелю, который подключался к Squid'у на сервере. Все было отлично. Кроме того, что весь мой трафик шифровался, я получил возможность нормально пользоваться сайтами.
Для тех из вас, кто хочет мне сказать, что я поступил плохо, я хочу обратить ваше внимание на то, что у меня была возможность сделать это. И не просто возможность, мне открыто было сказано использовать прокси, так как мы должны были работать с некоторыми сайтами, которые не работали через наш фаервол. Так что я не делал ничего «неправильного».
Атака
В то время я часто бывал в чате StackOverflow. Тогда он еще только появился, и в нем была парочка багов. В один прекрасный день сайт начал показывать мне стек вызовов. Я не придал этому значения, так как часто встречался с этим по всему интернету. Вообще-то, почти каждый раз, когда я получал сообщение об ошибке на сайте, написанном на ASP.NET, я видел стек вызовов.
И тут я заметил новый пункт меню в чате. Этот новый пункт меню назывался «Admin». Чисто из любопытства, я нажал на ссылку, полагая, что мне будет отказано в доступе. То, что произошло дальше, удивило меня. Я получил полный доступ ко всему. У меня была консоль разработчика, где я мог видеть кто что делает. У меня был интерфейс для работы с базой данных, где я мог напрямую запрашивать что угодно из любой базы данных. Я получил полные админские права.
Что произошло дальше
Следующее, что я сделал — сразу сообщил об этом модератору. Через несколько минут я был в приватном чате с модератором, а также двумя разработчиками. Причина была найдена приблизительно в течении 10 минут. Еще через 10 минут проблема была решена поверхностно. На полное исправление ушла пара часов. Они отлично сработали. У меня до сих пор лежит лог того чата, и я хочу сказать, что эти разработчики заслуживают всяческих похвал. Они ответили быстро и профессионально. И решили проблему в в кратчайшие сроки.
Уязвимость
Если вы сообразительны, то, скорее всего, догадались, что произошло. Так как я выходил через прокси, к моим запросам добавлялся заголовок X-Forwarded-For
. Значением этого заголовка был IP с которого запрашивался прокси-сервер. Но, так как я подключался к прокси через SSH туннель, IP был локальный. Так что Squid добавил X-Forwarded-For: 127.0.0.1
…
Но самое интересное было то, что показывал ASP в логах. Когда они посмотрели дампы моих запросов, там красовалась запись REMOTE_ADDR: 127.0.0.1
! В их приложении все проверки заголовков были реализованы правильно. Но IIS был неправильно настроен и он перезаписывал REMOTE_ADDR
значением X-Forwarded-For
, если тот присутствовал. И благодаря такой вот ошибке конфигурации я смог получить доступ к админке, при помощи моего прокси-сервера.
Выводы
Никогда не полагайтесь на X-Forwarded-For
, как мы видим — это не безопасно. Всегда используйте REMOTE_ADDR
. Здесь стоит задуматься, нужна ли вам вообще защита, основанная на IP. Или, по крайней мере, не стоит на нее полностью полагаться, а использовать лишь как дополнительную меру.
Интересно отметить, что разработчики действительно использовали надлежащую проверку заголовков. Суть в том, что вы никогда не должны слепо доверять вашей инфраструктуре. Эта дыра появилась из-за разницы конфигурации между сервером и приложением. Такие мелочи случаются каждый день. Приложение предполагает одно, а сервер — другое. Проблема в том, что такое доверие может полностью подорвать безопасность. В этом случае, разработчики доверяли значению REMOTE_ADDR
(вполне обосновано), но сервер был неправильно настроен. Конечно, есть ситуации, когда вы должны доверять серверу или другим компонентам, но надо помнить о том, что слепое доверие — это не очень хорошая вещь. Подумайте об этом, и постарайтесь застраховаться от подобных случаев.
Команда StackOverflow великолепно решила этот вопрос. Быстро, отзывчиво и разумно. Они попросили меня о помощи (которую я с удовольствием предоставил), и оба вели себя профессионально и уважительно. Они проделали фантастическую работу. Мы все должны извлечь урок. Реагировать на сообщения о уязвимостях серьезно. Решать вопрос профессионально и быстро.
Что касается PHP
Самое интересное здесь то, что приложения, написанные на PHP, могут иметь такую же уязвимость. Смотрим Symfony2 Request class. Выглядит отлично. Пока вы не заметите, что он использует статическую переменную, чтобы определить, следует ли доверять прокси. Это означает, что если любая часть вашего приложения захочет получить загловки от прокси (например, для записи в лог), все ваше приложение после этого будет получать загловоки именно от прокси. Чтобы увидеть, подвержен ли ваш код подобной уязвимости, поищите в нем вызов $request->trustProxy()
. Также обратите внимание, что обратного механизма нет. Вы не можете «перестать доверять» прокси. Я думаю, это большой архитектурный просчет…
Zend Framework 2 поступает аналогично. Он имеет класс, который ведет себя похожим образом (в плане получения IP). Интересно, что в Zend Framework 1 способ получения IP-адреса был адекватным. Вызывающий код должен явно указывать что он хочет получить, а по-умолчанию должен быть безопасный вариант.
Заключение
Эта проблема стала результатом сочетания двух мелких просчетов. По отдельности их очень легко упустить из виду. Но если они сойдутся определенным образом, вы получите очень серьезную угрозу безопасности. И самый большой урок в том, что вы действительно не можете доверять чему-либо за пределами вашего приложения. Если вы можете обойти это (например, не доверяя заголовкам и переменным, вроде REMOTE_ADDR
), то вы можете сделать ваше приложение более безопасным. Но, прежде всего, думайте о коде, который вы пишете и о системах, которые вы строите. И поддерживайте их.
Автор: truezemez