Обход защитных мер - Mutation XSS
Last updated
Last updated
Когда мы ранее говорили о санитизации, я напомнил всем не пытаться реализовать её самостоятельно, а использовать существующие библиотеки. Это связано с тем, что при реализации фильтраций существует много подводных камней.
Но есть ли у этих библиотек проблемы? Это возможно, и на самом деле это уже происходило. Одна из распространенных атак на санитайзеры называется мутационный XSS, также известный как mXSS.
Прежде чем понять mXSS, давайте посмотрим, как обычно работают санитайзеры.
Исходя из нашего предыдущего опыта, входными данными для санитайзера является строка, содержащая HTML, и выходными данными также является строка, содержащая HTML. Вот пример того, как это используется:
const inputHtml = '<h1>hello</h1>'const safeHtml = sanitizer.sanitize(inputHtml)document.body.innerHTML = safeHtml
Итак, как работает санитайзер внутри? На самом деле его внутренняя работа очень похожа на санитайзер, который мы реализовали с использованием BeautifulSoup:
1. Преобразовать inputHtml в DOM-дерево. 2. Удалить недопустимые узлы и атрибуты на основе файла конфигурации. 3. Преобразовать DOM-дерево обратно в строку. 4. Вернуть строку.
Этот процесс, кажется, не вызывает проблем, но дьявол кроется в деталях. Что, если "HTML, который кажется безопасным, на самом деле таковым не является"? Подождите, разве мы уже не санитизировали его? Как он может быть небезопасен? Давайте сначала рассмотрим пример.
Браузер - это внимательное программное обеспечение, которое, чтобы справиться с различными ситуациями и соответствуя спецификациям, может не отображать HTML именно так, как вы его видите. Например, рассмотрим следующий пример:
Поместить <h1> внутрь <table> кажется нормальным, но если вы откроете эту веб-страницу, заметите:
Структура HTML изменилась!
Она становится:
<h1>, который должен был быть внутри <table>, "выпрыгивает" из него. Это происходит, потому что браузер, исходя из спецификации HTML, определяет, что <h1> не должен быть внутри <table>, поэтому он любезно убирает его. Исходя из истории развития веба, для браузеров нормально пытаться исправить недействительный HTML. В конце концов, это лучше, чем выбрасывание ошибки или отображение пустой страницы.
Это поведение "HTML-строки изменяются браузером при рендеринге" называется мутацией. И XSS, достигнутый за счет использования этого поведения, естественно, называется мутационным XSS.
Рассмотрим еще один пример:
Результат рендеринга:
Браузер считает, что <p> не должен быть внутри <svg>, поэтому он перемещает <p> из <svg> и также исправляет HTML, добавляя </p>.
А как насчет этого еще более странного примера? На этот раз, вместо <p>, это </p>:
Результат:
Браузер автоматически исправляет </p>, добавляя перед ним <p>, но тег все равно остается внутри <svg>.
(Примечание: Поведение браузера Chrome было исправлено, и теперь будет <svg></svg><p></p>hello. Поэтому, в настоящее время, мы не можем воспроизвести эту ситуацию, но продолжим.)
Теперь, происходит что-то интересное. Если мы берем <svg><p></p>hello</svg> и передаем его innerHTML, каков будет результат?
Результат:
Не только <p>, но даже следующий "hello" выпрыгивает. Все, что было первоначально внутри <svg>, теперь находится вне его.
Итак, как эта серия изменений помогает нам обойти санитайзер? Для этого требуется сочетание с процессом санитайзера, упомянутым ранее.
Допустим, наш inputHtml выглядит так: <svg></p>hello</svg>. Первый шаг санитайзера - преобразовать его в DOM-дерево. Исходя из предыдущего эксперимента, это становится:
Он выглядит абсолютно нормально, ничего не нужно фильтровать. Следующий шаг - преобразовать DOM-дерево обратно в строку, что дает: <svg><p></p>hello</svg>.
Далее, команда фронтенд-разработчиков получает safeHtml и выполняет document.body.innerHTML = safeHtml. Получающийся HTML выглядит следующим образом:
Для санитайзера <p> и "hello" находятся внутри SVG, но финальный результат другой. Они размещены снаружи. Таким образом, через этую мутацию мы можем заставить любой элемент выпрыгнуть из <svg>.
Вы может спросить: "И что? В чем польза этого?" Вот где дело становится интересным
Тег <style> - это волшебный тег, потому что все, что находится внутри этого тега, интерпретируется как текст. Например:
Интерпретируется как:
Черный текст соответственно представляет собой текст .
Но вот интересная часть. Если мы добавим внешний <svg>, то браузер интерпретирует его по-другому, и все меняется. Текущий исходный HTML-код:
В результате интерпретируется:
Тег <a> внутри <style> становится настоящим HTML-элементом, а не просто текстом.
Еще интереснее то, что вы можете построить следующий HTML:
И это будет отображаться как:
Здесь мы просто добавили идентификатор <a> со значением </style><img src=x onerror=alert(1)>. Хотя он содержит </style>, он не закрывает предыдущий <style>. Вместо этого, он становится частью атрибута id. То же самое касается тега <img>. Это не новый тег, а часть содержимого атрибута.
Однако, если мы удалим <svg> и изменить на:
Поскольку <a> больше не является элементом, а просто текстом, у него нет атрибутов. Таким образом, </style> здесь закроет предыдущий <style>, в результате чего получится:
Тег <img> внутри идентификатора <a> изначально был лишь частью содержимого атрибута, но теперь, из-за предшествующего </style>, он представлен как собственный HTML элемент.
Исходя из приведенных выше экспериментов, можно сделать вывод, что наличие <svg> в <style> важно, поскольку это влияет на интерпретацию браузером.
Мы упомянули в начале об изменении поведения браузера, которое позволяет нам "вытащить все элементы из <svg>". Мы также упоминали, что "наличие <svg> для <style> важно". Комбинируя эти два понятия, мы можем получить mXSS.
19 сентября 2019 года DOMPurify выпустила версию 2.0.1, чтобы исправить уязвимость mXSS, которая обошла проверки с помощью мутаций. Проблемный в данном случае код был:
После преобразования этого в DOM-древо, структура становится:
Браузер делает здесь несколько вещей:
1. Преобразует </p> в <p></p> 2. Автоматически закрывает теги <svg>, <style>, и <a>
Затем DOMPurify проверяет на основе этого DOM-дерева. Поскольку <svg>, <p>, <style>, и <a> — все это разрешенные теги, и id является разрешенным атрибутом, все в порядке. Поэтому она возвращает сериализованный результат
Затем пользовательская программа передает эту строку в innerHTML, и происходят вышеупомянутые мутации. Все теги выбрасываются из <svg>, в результате чего получается:
Поскольку <style> также выбрасывается, элемент <a> больше не существует и становится простым текстом. В результате, </style> преждевременно закрывается, что приводит к тому, что скрытый <img> становится настоящим HTML-элементом внутри содержимого атрибута. Это в конечном итоге приводит к XSS.
Конечный результат заключался в том, что в спецификацию было добавлено новое правило, и Chromium исправил эту уязвимость на основе нового правила.
Так что потом похожие уязвимости больше не встречались, и все жили счастливо и долго… или все же нет?
Нет, позже было обнаружено, что у DOMPurify был более сложный метод обхода, но после его исправления он стал ещё сильнее и проблемы в основном не возникали.
Человек, который обнаружил эту проблему, был Michał Bentkowski, старший эксперт по безопасности в области разработки веб-безопасности. Он сообщал о различных больших и маленьких проблемах и имеет глубокое понимание парсинга HTML и различных механизмов. Позже мы увидим некоторые из классических уязвимостей, которые он сообщал.
Если вы хотите углубиться в эту проблему, вы можете обратиться к статьям, которые он написал ранее. Мои знания о mXSS происходят от него:
Когда я впервые столкнулся с mXSS, я был запутан и не полностью его понял. Для написания этой статьи я снова прошёл через контекст и попробовал сам, и тогда мне показалось, что я понял, что происходит. Понимание его концепции не сложно, но вникнуть во все детали потребует немного больше времени. Более того, обнаруженные уязвимости уже были исправлены, поэтому их невозможно воспроизвести в текущих браузерах, что немного проблематично.
Но в целом, я думаю, что mXSS - это более продвинутая тема внутри XSS. Она включает в себя спецификацию HTML, парсинг браузера и работу санитайзеров. Это нормально, что на её понимание требуется немного больше времени.
Чтобы исправить эту проблему, DOMPurify добавил в код, чтобы предотвратить подверженность mXSS.
В то же время, эта проблема была также сообщена в Chromium, потому что она была связана с ошибкой парсера, которая вызывала эту странную мутацию:. В результате, в ходе обсуждения разработчики обнаружили, что это поведение вполне соответствует спецификации, что означает, что это была ошибка в спецификации HTML!
Таким образом, эта проблема стала вопросом исправления самой спецификации, и они открыли вопрос в репозитории спецификации:
1. 2. 3.