XSS Advanced Level pt2
Когда речь заходит о прохождении лабораторных и личном развитии, то я всегда отдаю предпочтения сложным челленджам, подобным тому о котором сегодня пойдет речь. Прежде чем у меня получилось решить лабораторную, мне потребовалось подробнее изучить часть документации mozilla, подробнее изучить работу DOM-модели, вспомнить о свойствах и методах в HTML — это по даёт возможность мозгу взглянуть на привычные вещи под другим углом и узнать что-то новое.
В статье будут показаны принципы работы postMessage, onMessage, window.open, window.opener и многое другое.
Увидев название лабораторной, связанное с Self-XSS и Race Condition, я сначала не совсем понял, как это может работать совместно, но если при решении задачи действовать поэтапно, разбираясь в каждом нюансе, решение уже не кажется чем-то сложным, поэтому начнём с самого начала.
Self-XSS
При входе в приложение перед нами появляется панель аутентификации, и в ней есть уязвимое поле — username:
Первая уязвимость, которую мы встречаем — это Self-XSS.
Полезная нагрузка для XSS выглядит так:
Наша цель — захватить secret администратора.
Чтобы понять, как вывести этот элемент, заходим в инспектор и копируем CSS-селектор нашего secret:
Изменим полезную нагрузку для вывода secret:
Теперь необходимо доработать полезную нагрузку, чтобы она отправляла секрет на наш сервер, а данные кодировались и передавались без потерь.
Проверяем и получаем ответ на свой сервер:
Self-XSS работает как нужно, но возникает вопрос: каким образом мы можем внедрить полезную нагрузку, если она находится в имени пользователя?
Ведь, если username будет отличным от имени администратора, вход будет выполнен в совершенно другой аккаунт, и мы не украдём секрет.
Race Condition DOM Model
Для решения возникшей проблемы постараемся разобраться, что происходит при входе в профиль пользователя.
Включаем intercept и следим за тем, как рендерится DOM-модель.
1. При нажатии кнопки login происходит POST-запрос на роут /login и передача username и secret в JSON:
2. После входа в аккаунт происходит GET-запрос на /profile, который открывает наш профиль:
3. И дальше самое интересное: как можно заметить, username до сих пор не отобразился, а secret уже отрисован на странице. Следующий запрос который идёт на роут /api/me, который как раз таки и возвращает наш username по cookie:
Такое поведение происходит из-за того, что DOM-модель рендерится сверху вниз, и скрипт, который отвечает за «отрисовку» username, выполняется в самом конце:
Сеансовые cookie содержат внутри себя наш username и secret, но, к сожалению, мы не может их подделать или манипулировать ими из-за флага HttpOnly. Следовательно, вектор с cookie придётся отбросить..
В итоге получается, что нам необходимо создать эксплойт, который сначала запросит /profile с secret'ом администратора, а затем в отдельном окне осуществит запрос на роут /login с полем username, содержащим полезную нагрузку. Если это будет происходить достаточно быстро, то сеансовые cookie будут затёрты, и запрос на /api/me подтянет username, который содержит полезную нагрузку.
CSRF
Для начала реализуем рабочую CSRF, которая будет передавать необходимые нам данные в JSON. В лабораторной не проверяется Content-Type, поэтому можно передать JSON с помощью form.
Сгенерированный PoC в Burp не срабатывает, так как в конце body добавляется знак =
Обратившись к Hacktricks, я нашел способ передать в запросе JSON:
history.pushState('', '', '/'); document.forms[0].submit();
Получаем успешную подделку запроса:
Для успешной атаки нам необходимо несколько условностей:
1. Первым делом должна открыться страница /profile, где будет отрендерен secret администратора
2. Вторым по порядку должен быть осуществлен запрос на /login с полезной нагрузкой для XSS
3. После этого должен быть выполнен запроc на /api/me с другим сеансом, который вернёт на страницу полезную нагрузку из другого аккаунта
Прежде чем у меня получилось реализовать данную атаку, мне пришлось вспомнить немного теории.
window.opener
Существует такая уязвимость как ReverseTabNabbing
ReverseTabNabbing — это вид атаки, который используется для перехвата браузера пользователя после того, как он кликает по внешней ссылке на сайте. Эта уязвимость основана на использовании свойства target="_ blank" при создании HTML-ссылок, которое открывает новую вкладку или окно браузера.
Когда пользователь кликает на такую ссылку, атакующий сайт может исполнять JavaScript-код, который изменяет адрес (URL) предыдущей страницы (станицы, откуда была открыта ссылка) на фишинговый сайт посредством объекта window.opener. Если пользователь затем вернется на предыдущую вкладку, он увидит поддельную страницу вместо ожидаемой, что может привести к различным мошенническим схемам (например, к краже паролей).
Для защиты от ReverseTabNabbing при создании ссылок с атрибутомtarget="_ blank"
нужно также добавлять атрибутrel="noopener noreferrer"
, который предотвратит перехват предыдущей вкладки через JavaScript..
Посмотрим, как это выглядит на практике. Cоздаем три страницы:
Victim Site Controlled by wr3dmast3r
window.opener.location = "http://127.0.0.1:8000/malicious_redir.html";
wr3dmast3r attacked you
После создания страниц поднимем простой HTTP-сервер (например, с помощью модуля Python: pythom3 -m http.server), а затем откроем vulnerable.html. После щелчка по ссылке обращаем внимание, как изменится исходный URL веб-страницы (URL страницы, откуда был осуществлён переход по вредоносной ссылке).
Открытая в начале страница сменилась на подконтрольную нам. Таким образом на простом примере мы рассмотрели реализацию данной атаки.
Self-XSS + CSRF + ReverseTabNabbing + Race Condition
Теперь необходимо собрать все вводные и воспользоваться изученной информацией для решения лабораторной.
PoC PoC const csrf = window.open("/csrf.html") window.onmessage = () => { window.location = "https://url/profile" }
Это будет нашей страницей, куда мы перенаправим пользователя.
Рядом создадим ещё один HTML-документ:
PoC CSRF window.opener.postMessage({}, "*") form.submit();
Одинарные кавычки внутри полезной нагрузки для Self-XSS необходимо заменить на ' для соблюдения корректного синтаксиса.
Постараюсь подробнее остановиться на некоторых деталях нашего эксплойта:
Начнём с postMessage:
window.opener.postMessage({}, "*")
Эта строка кода используется для отправки сообщения родительскому окну (опенеру), которое открыло текущее окно или вкладку, через механизм оконного сообщения HTML5. Метод postMessage позволяет безопасно осуществлять междоменное общение между окнами; в данном случае отправляется пустой объект {}, и в качестве второго параметра указывается «*», что позволяет принимать это сообщение любому origin.
Однако использование звёздочки («*») в реальных приложениях потенциально небезопасно, так как это позволяет любому сайту получить сообщение; по возможности следует указывать конкретный origin, которому вы доверяете.
onMessage:
window.onmessage = () => { window.location = "https://url/profile" }
Данный участок кода назначает обработчик события для onmessage на текущее окно (или вкладку). Событие onmessage возникает, когда текущее окно получает сообщение через механизм postMessage. В данном случае функция обработчика немедленно перенаправляет браузер на новый URL, указанный в строке «https://url/profile». Это означает, что как только сообщение будет получено, браузер переходит на страницу профиля по заданному адресу.
Как это работает совместно:
Когда форма отправляется (form.submit()), происходит отправка данных формы на сервер. Перед этим, window.opener.postMessage({}, "*") отправляет сигнал в родительское окно о том, что форма была отправлена. Родительское окно в своих скриптах слушает onmessage, и выполняет заложенного в него действие.
Пробуем атаковать себя.
Создаем учетную запись с любым username и любым secret:
Закрываем страницу и переходим по ссылке на наш эксплойт, точнее на index.html.
У нас открылось дополнительное окно, которое свидетельствует об успешном входе в аккаунт и корректной отработке CSRF:
index.html в это время превратился в ссылку на /profile:
Наш сервер получил запрос, и это и есть созданный нами secret:
Важно уточнить, что для успешного осуществления атаки критически важен порядок действий: сначала должен быть открыт профиль, и только после этого должна произойти отправка формы.
window.opener.postMessage({}, "*") form.submit();
Дело остаётся за малым: размещаем эксплойт на нашем сервере и отправляем ссылку администратору.
Очень приятно было представить решение на лабораторную которую решило такое маленькое количество человек :)