Эффективный поиск XSS-уязвимостей
Про XSS-уязвимости известно давным-давно — казалось бы, нужен ли миру ещё один материал о них? Но когда Иван Румак, занимающийся тестированием безопасности, поделился методологией их поиска на нашей конференции Heisenbug, реакция зрителей оказалась очень положительной.
И спустя два года у этого доклада по-прежнему растут просмотры и лайки, это один из самых востребованных материалов Heisenbug. Поэтому теперь мы решили, что многим будет полезна текстовая версия, и сделали ее для Хабра.
Под катом — и текст, и видео. Далее повествование идет от лица Ивана.
Обо мне
Я занимаюсь тестированием безопасности. По сути, занимаюсь всеми вопросами, связанными с безопасностью сайтов. Параллельно участвую в разных Bug Bounty, занимаю 110 место на платформе HackerOne, нахожу баги в Mail.ru, Яндексе, Google, Yahoo! и других крупных компаниях. Обучаю, консультирую, рассказываю про безопасность в вебе и не только.
История доклада
Когда я начал интересоваться безопасностью, то был тестировщиком и проверял функциональные баги, а не те, что связаны с безопасностью. Но я увлекся безопасностью и однажды стал самым прошаренным тестировщиком в этой сфере. Ко мне начали приходить другие тестировщики и разработчики. Я понял, что тестировщики тоже хотят научиться искать уязвимости, им это интересно, но при этом они не знают, что конкретно нужно делать.
Что такое XSS? Как искать? Как понимать, есть XSS или нет? Сейчас разберемся.
План
Что такое XSS-уязвимости
Методология поиска XSS (которой пользуюсь сам и с помощью которой нашел более 60 XSS в Bug Bounty за последний год)
Какую проверочную строку (пейлоад) использовать для поиска XSS-уязвимостей
Кейсы из разных Bug Bounty-программ (какие XSS были, как их можно найти, и баги, которые по методологии поиска похожи на поиск XSS)
Зачем искать уязвимости?
Вам — полезный навык, который никогда не будет лишним. Компании, где вы работаете — дополнительная безопасность. Win-win!
Что такое XSS
XSS (Cross-Site Scripting) — возможность выполнения произвольного JavaScript-кода в браузере жертвы в контексте вашего сайта.
Вспомним, как вызывается JavaScript из HTML:
— всё, что внутри, будет срендерено браузером как JavaScript.
test
— можно использовать обработчики событий, то есть атрибут, например, onerror
. Браузер попробует подгрузить картинку по источнику x. Если картинка не прогрузится, он выполнит то, что указано в обработчике событий.
click to trigger javascript
— если гиперссылка ведет не на схему HTTP/HTTPS, а начинается со схемы JavaScript, то при нажатии на ссылку всё, что после схемы JavaScript, будет срендерено как JavaScript.
— то же самое, что и с гиперссылкой, только ничего не надо кликать, сработает при прогрузке.
XSS — одна из самых распространенных уязвимостей в вебе. К XSS уязвимо более 95% веб-приложений. Чтобы найти баг, не обязательно обладать специальными навыками, проходить курсы или получать высшее образование.
И действительно, несмотря на то, что XSS — распространенная уязвимость, она остается одной из самых серьезных клиентских уязвимостей.
Причины возникновения XSS
Во-первых, XSS возникает при генерации HTML-страницы, когда разработчику нужно поместить туда указанные пользователем данные (ФИО, организация). Если разработчик записал данные в БД, затем тянет ее в HTML-шаблон, то это stored (сохраненный) XSS.
Разработчику могут понадобиться параметры из URL или тела запроса. Такой тип XSS называется reflected.
Причин XSS куча, потому что есть динамические изменения страницы с помощью JS, есть события, которые постоянно происходят на клиентской стороне с JS.
Но в этом докладе я расскажу про самые распространенные типы — stored XSS и reflected XSS.
Возьмем пример — обычная страница ВКонтакте. О чем подумает человек, который хочет найти XSS-уязвимости?
Во-первых, он обратит внимание на то, что есть поля, можно куда-то зарегистрироваться и что-то ввести.
Попробуем ввести туда честные данные, но при этом добавим к ним . Он нужен для вызова JavaScript между открывающим и закрывающим тегами.
Что произойдет в этом случае?
Мы, как пользователь, который хочет зарегистрироваться во ВКонтакте, заливаем ему наши проверочные строки. Дальше разработчик сохраняет их в базу данных, и с этими данными ему надо работать. Нужно показывать их пользователю на его странице, в личных сообщениях и много где еще. Дальше данные попадают пользователю в браузер, когда они возвращаются ему обратно.
Допустим, разработчик не подумал, что в качестве имени пользователя могут быть не только честные данные, а еще и HTML-теги, которые встраиваются в оригинальный HTML-шаблон. Браузеру пофиг, он рендерит все, что ему сказал разработчик, поэтому рендерится и строка.
Оно могло бы выстрелить где-нибудь здесь:
Конечно, во ВКонтакте такой уязвимости нет. Но, так как эта страница является публично доступной, любой в интернете может на нее зайти, то это была бы довольно серьезная уязвимость.
Но вообще мы, как тестировщики, которые ищут XSS-уязвимости, чаще всего делаем это блэкбоксом. Мы не знаем, что происходит на сервере, какая база данных там используется, делает ли разработчик что-то с этими данными. Всё, что у нас есть, — это поле, куда мы можем что-то ввести, и какие-то страницы, куда это потом возвращается.
Методология поиска XSS, которую я сейчас вам покажу, основана как раз на том, что мы не знаем, какие процессы происходят на сервере.
XSS-методология
Помещаем пейлоад (проверочную строку, призванную выявлять уязвимости) во все поля и параметры.
Смотрим в DOM на предмет санитизации.
Рано или поздно спецсимволы не перекодируются, или выполнится функция alert.
Раскручиваем дальше или репортим, как есть.
Еще один пример — страница поиска. В поле поиска попробуем ввести «qweqwe».
Поищем это в DOM:
F12 → Ctrl +F → «qweqwe»
Мы видим, что строка «qweqwe» попала из поля для поисков в параметр query. И она попала в страницу 17 раз. То есть у нас есть 17 потенциальных мест, где разработчик может не подумать о выводе этой строки пользователю в браузер, и может возникнуть XSS-уязвимость.
Конечно, «qweqwe» недостаточно, чтобы выявить XSS-уязвимость, мы добавим туда спецсимволы:
Input: qweqwe ' » > <
Посмотрим, что выведется в DOM:
Спецсимволы превратились в закодированный кусок символов. Это уже сигнализирует, что есть санитизация, возможно, неосознанная.
Но в девятом месте, где наша строка встраивается в DOM, спецсимволы на первый взгляд не перекодировались, то есть они отображаются здесь как есть.
Но если мы попробуем отредактировать это как HTML, то увидим, что двойная кавычка превратилась в "
.
Это называется HTML entities. Особенность использования браузером этой кодировки заключается в том, что браузер рисует соответствующий символ на странице, но HTML-теги, состоящие из этих символов, не рендерятся браузером как код:
‘ — '
" — "
> — >
< — <
& — &
Это выглядит вот так:
Слева у нас HTML-код, который должен отрендерить браузер, но он просто показывает его как строку.
Санитизация — преобразование определенных символов пользовательской строки в соответствующие HTML entities или другую кодировку.
Другими словами, у нас есть набор потенциально опасных символов, которые мы хотим санитизировать. Мы хотим их превратить в HTML entities, чтобы они не встраивались в наш изначальный шаблон, который мы хотим исполнять на пользователя, и нельзя было протолкнуть чужой JS.
Вернемся к этому примеру. Двойная кавычка заинкодилась в ", получается, санитизация есть.
А если бы не было? Мы попробуем ввести » » test, и поищем по строке «qweqwe»:
Input: qweqwe ' » test
F12 → Ctrl +F → «qweqwe»
Что мы увидим?
Мы увидим, что test начал подсвечиваться коричневым. Браузеры помогают нам: они подсвечивают атрибуты, значения атрибутов и названия тегов разными цветами. Атрибуты всегда коричневые, имена тегов — розовые, значения параметров — синие.
Если бы вся строка была синяя, мы могли бы сразу понять, что она попала внутрь значения атрибута, и можно было бы сделать вывод, что XSS нет.
Но здесь предположим, что она есть, и у нас записался атрибут test. И если мы вместо этого атрибута используем обработчик событий, например:
Input: qweqwe ' » onfocus='alert ()' autofocus
Получаем reflected XSS:
Это сложно, поэтому я предложу решение в виде универсального пейлоада — это строка, которая должна выявлять XSS в разных контекстах и которая не требует дополнительного раскручивания в таких местах.
XSS — Level 0
Начнем с самой простой строки, на которую натыкались все, кто когда-то интересовался тестированием безопасности:
Посмотрим на примере языка PHP, когда я как разработчик хочу выводить пользователю HTML-код и подтягиваю туда значение параметра, в нашем случае — name:
Привет, !
Функция echo () в PHP не делает санитизацию, она выводит всё как есть. То есть это типичная reflected XSS-уязвимость. И если мы поместим в параметр name на этой странице наш текущий пейлоад, он срендерится браузером, потому что никакой санитизации нет. Он встраивается как есть, браузер не отличает пользовательскую строку от оригинальной и рендерит.
/page.php?name=
Привет, !
То же самое, если разработчик не берет параметр из URL, а берет из базы данных данные, которые когда-то вводил пользователь:
Привет, !
Привет, Вася!
Вот пример посложнее:
Что, если мне надо брать значение параметра и отображать его внутри значения атрибута? Как меня могут хакнуть в этом случае?
Если мы поместим туда наш , разумеется, это не сработает. Даже если нет санитизации и вставка небезопасна, то
просто не выявит эту XSS-уязвимость, потому что нужно закрыть атрибут.
Мы закроем не только атрибут, но еще и тег , куда мы попали, и встроим свой
, который отрендерится браузером:
/page.php?name=">
И раз мы знаем, что есть кейсы, когда мы можем попасть внутрь значения атрибута, почему бы нам сразу не добавить ">
в пейлоад?
XSS — Level 1
А если мне надо подставить пользовательское значение внутрь
? Подставим туда наш текущий пейлоад:
Привет,">
Это не сработает, потому что тег
нужен браузеру, чтобы отображать имя текущей вкладки. Браузер думает, что раз это всего лишь название вкладки, ему незачем рендерить значение этого тега, и он просто будет рендерить всё как текст.
Здесь нужно добавить перед нашим вредоносным
.
/page.php?name=">
Привет,">
Таким образом мы закроем оригинальный
.
Раз мы знаем, что разработчик может подтянуть наши значения еще и внутрь тега
, почему бы сразу не добавить его в пейлоад и не вставлять этот пейлоад везде? Так мы попадаем сразу в несколько ситуаций.
Вроде звучит здорово, но если мы попали внутрь тега :
Разработчик написал JavaScript, внедрил его у себя на страницу, но какую-то переменную берет из пользовательского значения. Поместим туда наш текущий пейлоад:
/page.php?name=">
";
Всё не отработает до момента, когда мы закрыли .
Здесь все тоже достаточно тривиально, просто закрываем тег разработчика :
/page.php?name=">
";
Я не буду дальше мучить вас каждым таким тегом.
На самом деле, надо закрывать еще и , и
,
и , значения которых рендерятся браузером как строка.
XSS — Level 2
Помните, мы попадали внутрь значения атрибута, закрывали его, открывали , чтобы выполнить функцию
alert ()
, и мы использовали там двойную кавычку:
А мог ли разработчик обособлять все одинарными кавычками? Браузер это принимает, это вполне нормальное поведение. И если бы мы поместили туда наш текущий пейлоад, разумеется, он бы не сработал: он бы не обнаружил эту XSS-уязвимость, потому что мы закрываем двойную кавычку.
/page.php?name=">
Здесь тоже все просто: достаточно добавить одинарную кавычку перед двойной, и мы закроем и этот кейс.
', '>', $_GET["name"]); ?>
А вот если разработчик подумал: «Я допускаю, что пользователь может использовать кавычки, но главное, что он не закрывает мой и не открывает теги вроде
. Тогда я буду просто энкодить закрывающую угловую скобку, чтобы он не смог закрыть мой
».
Но если мы действительно попробуем закрыть и открыть
, то ничего не сработает:
/page.php?name='>
Здесь достаточно использовать обработчики событий:
/page.php?name='%20autofocus%20onfocus='alert();
Возможность писать свои атрибуты уже дает нам гарантированную XSS-уязвимость.
Когда мы попадали внутрь , то закрывали его, открывали свой
и делали что-то внутри. А могли бы мы продолжить писать JS внутри JS, который написал разработчик? То есть продолжить его код, стараясь не вызвать синтаксическую ошибку. Да, могли бы:
/page.php?name=";+alert();//
Есть еще случай, когда разработчику надо подставлять параметры внутрь гиперссылки:
Цель — редиректнуть пользователя туда, откуда он пришел. Например, пользователь пришел в приложение. Оно редиректит его на аутентификационный поддомен, и когда он аутентифицировался, этот поддомен должен редиректнуть его обратно в приложение.
Можно вызывать JS в гиперссылках, в том числе при редиректах, если использовать схему JS:
/page.php?returnUrl=javascript:alert()
При нажатии на «Вернуться» сработает функция alert()
.
Если поставить перед javascript пробел, то это тоже сработает:
/page.php?returnUrl=%20javascript:alert()
Сработает не только %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()
при нажатии:
Мы вызываем single line comment, комментим всё до переноса строки, переносим строку и выполняем наш alert()
. Можно сконструировать такой пейлоад.
А это сработает, если запретить слово javascript в URL?
Как нам уже известно про HTML entity, браузер в некоторых местах использует эту кодировку неоднозначно. Если слово javascript: заэнкожено в HTML entity, при нажатии на эту ссылку браузер всё равно поймет, что HTML entity — это схема JavaScript, её тоже можно использовать.
Вернуться
javascript: = javascript:
Таким образом мы обойдем защиту, если бы она была.
Единственный правильный способ здесь — это требовать, чтобы ссылка начиналась на http (s) или была относительной.
XSS — Level 3
Наш пейлоад довольно большой и попадает в несколько кейсов:
'">
Мне не нравится, что мы вроде делаем крутой пейлоад, а используем — самую нубскую вещь, которую можно найти в начале поиска XSS-уязвимостей.
Я предлагаю использовать iframe с обработчиком событий:
'">
— тег для отображения страницы внутри страницы. Допустим, вы находитесь на каком-нибудь сайте и, если сайт хочет подгрузить в себя еще один сайт, то разработчик использует этот тег. Также там есть атрибут
src
— адрес до сайта, который он хочет показать у себя. И независимо от того, прогрузился путь или нет, onload
будет работать всегда.
Плюсы iframe:
Легко заметить, если пейлоад встраивается в страницу, но на onload работают санитайзеры.
Есть волшебный аттрибут
srcdoc
:
Разработчики используют iframe на всяких форумах, дают возможность пользователю помещать его на сайт, но если они не подумали про этот атрибут, то возможно выполнение произвольного JS.
Здесь в качестве значения атрибута srcdoc
используется просто HTML entity:
- Не раскрутить XSS — есть почти гарантированный open redirect.
Если у вас не получилось раскрутить XSS, например, разработчик решил, что хочет разрешить встраивать пользовательский iframe, но без обработчиков событий и без srcdoc, то у нас всё равно есть уязвимость другого типа — open redirect, пейлоад которого выглядит так:
У нас есть iframe, его src указан на другую страницу в интернете, подконтрольную злоумышленнику. Содержимое этой страницы довольно простое — всего лишь скрипт, который задает top.window.location на другую страницу.
И если браузер срендерит это на каком-то сайте, произойдет редирект на https://evil.com.
У браузера есть иерархия окон, есть окно верхнего уровня и промежуточные окна. И iframe, который подгружается внутри сайта, является промежуточным окном, но при этом он может влиять на окно верхнего уровня. Он может переписать его top.window.location, и возникает уязвимость open redirect.
Лечится это атрибутом sandbox, но никто об этом не задумывается. Если есть разрешение устраивать пользовательский iframe, то таким пейлоадом можно редиректить других пользователей куда угодно.
XSS — Level 1337
Перейдем на суперхакерский уровень XSS-уязвимостей:
- Пробелы между атрибутами в теге могут замениться слэшем. Тег необязательно закрывать!
- Браузеры закрывают теги за разработчиков.
Таким образом, исходя из этих двух фактов, мы можем прийти от такого пейлоада:
'">
К такому:
'">
Поменяли все пробелы на слэш и убрали закрывающую скобку у iframe. Есть кейс, когда пейлоад попадает внутрь комментария ()
, нужно сразу закрыть его.
'">
Покажу еще один пример из 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/>{{7×7}}