Не все cookie одинаково полезны
В этой статье я хотел бы рассказать о том, как можно объединить небольшие недочеты в обработке cookie-значений в цепочку, и произвести за счет этого атаку на пользователей популярных веб-приложений.
История началась больше года назад, когда я испытывал программу DOMinator для поиска DOM-Based XSS на сайтах Bug Bounty программ. Одним из первых предупреждений, которое я получил, была уязвимость Cookie Injection в JavaScript сценарии Google Analytics.
При обращении к сайту с Google Analytics, сценарий обрабатывает значение HTTP заголовка Referer и извлекает из него host и путь к сценарию, для отслеживания откуда пришел пользователь. В дальнейшем эти данные попадают в cookie параметр __utmz
.
Выглядит это следующим образом: __utmz=123.123.11.2.utmcsr=[HOST]|utmccn=(referral)|utmcmd=referral|utmcct=[PATH]
Изменяя путь к сценарию на своем сайте, с которого пользователь переходит на сайт с Google Analytics, можно влиять на конец значения cookie __utmz
и попытаться изменить атрибуты cookie, так как путь никак не обрабатывается перед попаданием в значение. Однако, атрибуты будут перезаписаны последующими значениями, которые подставляет Google Analytics.
http://blackfan.ru/x/injection;injection=injection?r=http://site.com/
Результат: document.cookie=__utmz=blah...|utmcct=/x/injection;injection=injection; path=/; domain=.site.com
На данный момент это было сложно назвать уязвимостью, но тем не менее поведение сценария довольно интересное, к тому же он невероятно распространен.
Bug Bounty и Cookie
Небольшое отступление. Уязвимости, связанные с cookie, в рамках программ вознаграждения за уязвимости, примечательны тем, что у вас может получится два основных сценария:
- Найдена возможность перезаписи и создания произвольных cookie параметров.
Ответ: Это всего лишь cookie, что это тебе даст? - Найдена XSS через cookie параметр.
Ответ: Ты ведь не можешь создать произвольный cookie параметр, значит, это не уязвимость!
И только если оба варианта оказались в рамках одной Bug Bounty программы, то вам, так уж и быть, заплатят.
Необходимость каждый раз доказывать угрозу от проблем, связанных с сookie, зачастую отталкивает людей от их поиска. Так как за потраченное на это время можно найти еще десяток уязвимостей, которые будут приняты без вопросов. В основном проверяются только самые базовые вещи, типа фиксации сессии и правильности установленных атрибутов secure, httpOnly…
Cookie
Взглянем на структуру сookie заголовков:
Set-Cookie: par=val; path=/; httpOnly; secure;
Cookie: par=val; par2="val2"; par3=val3;
Структура довольно сложная и с ней работают при каждом запросе браузер, JavaScript сценарии и веб-сервер. Каждый при этом обрабатывает по-своему, и одна и та же строка может дать разные результаты.
Анализируя обработку cookie необходимо задаться следующими вопросами:
- Нужны ли пробелы после;
- Какие символы можно использовать вместо ;
- Какое значение будет результирующее в случае одинаковых ключей
- Важен ли регистр у ключей
- Сколько может быть атрибутов у параметра
- Какое значение будет результирующее в случае одинаковых атрибутов
- Как правильно кодировать спецсимволы
Особенности обработки Cookie
Первая и наиболее известная особенность — Safari позволяет объявлять несколько параметров через один заголовок Set-Cookie.
Set-Cookie: param1=value1; path=/, param2=value2; httpOnly;
Возвращаясь к проблеме Google Analytics, проверим данную особенность в установке cookie через JavaScript. Получаем первый вариант эксплуатации:
http://blackfan.ru/x/injection;,injection_cookie=injection;?r=http://site.com/
Результатdocument.cookie=__utmz=blah...|utmcct=/r/injection;,injection_cookie=injection; path=/; domain=.site.com
Safari создаст два cookie параметра __utmz
и injection_cookie
.
То есть, для пользователя Safari на любом сайте с Google Analytics можно создать произвольный cookie параметр. Осталось только придумать зачем…
CSRF
Защиту от CSRF можно условно разделить на 3 типа:
- Различные токены для каждого действия. Хранятся на сервере.
- Один сессионный токен на все действия. Хранится на сервере в сессии пользователя.
- Один сессионный токен на все действия. Хранится в cookie параметре.
Третий вариант основывается на том, что значение токена в cookie пользователя недоступно для злоумышленника. Для прохождения проверки достаточно просто послать одинаковое значение токена в cookie и post параметрах. То есть, простая перезапись значения через Google Analytics тут подойдет идеально.
Особенности обработки Cookie #2
Что если развернуть атаку на 180 градусов? Не обязательно, чтобы в браузере действительно был cookie параметр с CSRF токеном, значение которого мы знаем. Достаточно, чтобы так считал веб-сервер.
В этом поможет еще одна обнаруженная особенность обработки сookie.
RFC2109
Note: For backward compatibility, the separator in the Cookie header
is semi-colon (;) everywhere. A server should also accept comma (,)
as the separator between cookie-values for future compatibility.
Многие веб-серверы поддерживают перечисление сookie не только через точку с запятой, но и через запятую.
Cookie: par=val; par2="val2"; par3=val3;
Cookie: par=val, par2="val2", par3=val3,
Более того, в некоторых случаях пробел не обязателен.
Cookie: par=val;par2="val2";par3=val3;
Cookie: par=val,par2="val2",par3=val3,
В свою очередь, для большинства браузеров такие символы как пробел и запятая являются вполне нормальными. И если установить:
Set-Cookie: par=val, csrftoken=val2;
document.cookie="par=val, csrftoken=val2;";
Для браузера это будет одно значение, но для некоторых серверных реализаций — два сookie параметра. Однако, в случае эксплуатации данных особенностей для обхода CSRF защиты, необходимо помнить, что мы не перезаписываем старый токен, а добавляем еще один, то есть важен порядок обработки сookie.
Cookie: csrftoken=realvalue; par=val, csrftoken=fakevalue;
Итого получается уже две цепочки эксплуатации уязвимости:
Safari -> WebApp (GA & Double Submit Cookies)
Любой браузер -> WebApp (GA & Double Submit Cookies & Запятая, как разделитель cookie)
Эксплуатация
После подготовки неплохой базы, необходима проверка в реальных условиях. Практически сразу находится идеальный вариант mobile.twitter.com
.
На нем реализована CSRF защита, основанная на cookie, сервер поддерживает перечисление cookie через запятую без пробела, но… На нем нет Google Analytics. Зато он есть на translate.twitter.com
! Самое время проверить обработку атрибутов cookie значений, а если точнее, проверить возможность отбрасывания добавляемых в конце значений path и domain в случае, если инъекция происходит в значение cookie.
Оказалось, что Google Chrome в случае большого количества атрибутов cookie в какой-то момент просто прекращает их разбирать и не доходит до последних валидных значений.
То есть, в данном случае:
Set-Cookie: test=test; domain=.google.com; domain=.google.com; domain=.google.com; domain=.google.com; [...]; domain=blah.blah.blah.google.com;
Cookie будет установлена на .google.com
, а не на blah.blah.blah.google.com
.
Таким образом, добавляется еще одна цепочка эксплуатации:
Chrome -> WebApp 1 (Double Submit Cookies & Запятая, как разделитель cookie) & WebApp 2 (GA)
Формируем PoC:
Tweet "PoC"
Описание:
- Пользователь авторизован на
twitter.com
- Мобильная версия
mobile.twitter.com
подхватывает сессию основного сайта даже если пользователь не заходил на нее - Предполагаем, что пользователь не был на
translate.twitter.com
и у него не установлена cookie__utmz
на нем - Передаем Referer с путем, в котором содержится инъекция в cookie, на сайт
translate.twitter.com
- Google Analytics создает сookie
__utmz=blah...|,m5_csrf_tkn=x,
- Из-за большого количества атрибутов Chrome перезаписывает domain на
.twitter.com
- Ждем окончание обработки запроса и посылаем еще один запрос на создание твита «PoC», используя токен «x»
- В соответствии с порядком отправленных cookie,
mobile.twitter.com
берет наше значение токена «x» и убеждается, что значение в post запросе и в cookie одинаковые - У пользователя появляется твит PoC
Особенности обработки Cookie #3
Следующей целью стал instagram.com
, а точнее все сайты на Django. CSRF защита в Django также основана на cookie. Для успешного прохождения проверки достаточно послать одинаковые значения в cookie csrftoken
и post параметре csrfmiddlewaretoken
, либо в HTTP заголовке X-CSRFToken
. Однако, есть дополнительная проверка, которая в дальнейшем может помешать. В случае, если сайт работает по HTTPS, Django проверяет заголовок Referer и, в случае несовпадения, блокирует запрос, даже если в нем указан правильный токен. Post запросы без Referer также блокируются.
В ходе изучения обработки сookie в Django были обнаружены следующие особенности:
- В качестве разделителя не обязательно использовать точку с запятой, достаточно любого пробельного символа между параметрами
Cookie: test=test test2=test2
- Если в значении сookie есть символы
[ \ ]
, то первая часть сookie отбрасываетсяCookie: test=test]test2=test2
.
В результате создается только сookietest2
.
Оказалось, что данная проблема даже не в Django, а в Python. Библиотека Cookie обрабатывает значения в соответствии с RFC2109. И действительно оказывается, что использование символов [ \ ]
не предусмотрено, если значение не обрамлено двойными кавычками. Для браузеров же использование этих символов — вполне нормальное явление.
PoC для instagram практически идентичен предыдущему:
Для пробрасывания cookie __utmz
используется blog.instagram.com
, а для создания токена конструкция /r/]csrftoken=x,;domain=.instagram.com;
.
Особенности обработки Cookie #4
После общения с Google, Twitter, Facebook, Django и Python я вновь решил попробовать обойти их исправления.
Произошли следующие изменения условий:
- Google Analytics начал отбрасывать часть после точки с запятой и принудительно заменять пробел на %20
- Python исправил некорректную обработку
[ \ ]
Однако, все еще оставалась возможность перечисления cookie через пробельный символ в Django, чему сильно мешала замена используемая Google.
Проверка пробельных символов показала следующее:
- Internet Explorer заменяет символы \x09 \x0b \x0c на _
- Chrome не устанавливает cookie, если оно содержит символы \x09 \x0b \x0c
- FireFox считает данные символы нормальными
В результате чего получается следующий вариант эксплуатации для FireFox
https://instagram.com/?utm_source=1&utm_medium=2&utm_campaign=3&utm_term=4&utm_content=5%09csrftoken%3dx
Особенности обработки Cookie #5
Также я обнаружил еще один интересный вариант в некоторых серверных реализациях, но пока не нашел применения в реальных условиях. При использовании спецсимволов в cookie, значение обрамляют в двойные кавычки. Данное поведение можно использовать следующим образом:
Set-Cookie: test="test
Set-Cookie: foo=bar
Set-Cookie: test2="
Для браузеров двойные кавычки не являются каким-то особым символом и в результате получится такой заголовок:
Cookie: test="test; foo=bar; test2="
Но некоторые веб-серверы могут обработать данные значения, как один параметр test, в результате чего параметр foo=bar
не будет создан. При совпадении невероятного количества условий, данная особенность также может быть использована.
Результаты
Варианты эксплуатации
Safari -> WebApp (GA & Double Submit Cookies)
Любой браузер -> WebApp (GA & Double Submit Cookies & Запятая, как разделитель cookie)
Chrome -> WebApp 1 (Double Submit Cookies & Запятая, как разделитель cookie) & WebApp 2 (GA)
FireFox -> WebApp (GA & Double Submit Cookies & Пробельные символы, как разделитель cookie)
Фиксы
- В Google Analytics добавили принудительную замену пробела на %20 в значении cookie (сомнительное улучшение)
- В Google Analytics исправили возможность изменить атрибуты cookie путем отбрасывания всего, что идет после символа »;»
- Google Chrome НЕ будет исправлять перезапись с помощью большого количества атрибутов, так как возможности устанавливать произвольные атрибуты не должно быть у клиента изначально
- Python исправил проблему с символами
[ \ ]
(https://hg.python.org/cpython/rev/270f61ec1157) - Twitter изменили тип CSRF защиты на сайте
mobile.twitter.com
При общении с Google я столкнулся с тотальным непониманием сути уязвимости. От меня требовали примера на сайтах Google, которого у меня не было, и их совершенно не интересовало, что Google Analytics может нести угрозу на других сайтах. И лишь спустя десяток писем мой отчет попал к Krzysztof«у (судя по всему это был @kkotowicz), который разобрался что к чему и донес информацию до соответствующих разработчиков.