CSS Injection - Атака с использованием только CSS (Часть 1)
Last updated
Last updated
В предыдущих частях мы рассматривали различные атаки, такие как Prototype Pollution и DOM-Clobbering, которые манипулируют выполнением JavaScript для получения неожиданных результатов. Иными словами, JavaScript в конечном итоге отвечает за импакт, причиненный этими атаками.
Теперь давайте рассмотрим несколько методов атаки, которые могут иметь эффект без использования JavaScript. Первым, о котором мы поговорим, будет инъекция CSS.
Если у вас есть опыт работы с фронт-эндом, вы, возможно, уже знаете, что CSS - это мощный инструмент. Например, вы можете создать:
1. 2. 3.
Да, вы правильно прочитали. Эти примеры созданы с использованием чистого CSS и HTML, без единой строки JavaScript. CSS действительно волшебен.
Но как CSS может быть использован в качестве средства атаки?
Как следует из названия, инъекция CSS означает возможность вставить любой синтаксис CSS или, более конкретно, использовать тег <style> на веб-странице.
На мой взгляд, существует два распространенных сценария. Первый - когда веб-сайт фильтрует многие теги, но пропускает тег <style>, считая его безопасным. Например, DOMPurify, который часто используется для санитизации, по умолчанию фильтрует различные опасные теги, оставляя только безопасные, такие как <h1> или <p>. Однако <style> включен в список безопасных тегов по умолчанию. Поэтому, если не установлены конкретные параметры, <style> не попадет под фильтр, что позволяет злоумышленникам внедрять CSS.
Второй сценарий - когда можно вводить HTML, но выполнение JavaScript предотвращается благодаря политике Content Security Policy (CSP). В этом случае, поскольку JavaScript невозможно выполнить, злоумышленники прибегают к использованию CSS для осуществления опасных действий.
Так что можно добиться с помощью инъекции CSS? Разве CSS не используется просто для стилизации веб-страниц? Может ли изменение цвета фона страницы считаться атакой?
Хотя CSS преимущественно используется для стилизации веб-страниц, он может быть использован с двумя функциями для кражи данных.
В CSS есть несколько селекторов, которые могут целиться на элементы с атрибутами, которые соответствуют определенным условиям. Например, input[value^=a] выбирает элементы, значение которых начинается на "a".
Некоторые похожие селекторы включают:
1. input[value^=a] (prefix) выбирает элементы, значение которых начинается на "a". 2. input[value$=a] (suffix) выбирает элементы, значение которых заканчивается на "a". 3. input[value*=a] (contains) выбирает элементы, значение которых содержит "a".
Вторая особенность - возможность отправлять запросы с использованием CSS, например, загружая фоновое изображение с сервера, что по сути является отправкой запроса.
Допустим, у нас есть следующее содержимое на веб-странице: <input name="secret" value="abc123">. Если я могу внедрить CSS, я могу написать следующее:
Что произойдет?
Следовательно, когда сервер получит этот запрос, он узнает, что атрибут "value" ввода начинается с буквы "a", успешно крадя первый символ.
Вот почему с помощью CSS можно красть данные. Объединив селекторы атрибутов с возможностью загрузки изображений, сервер может определить значение атрибута определенного элемента на веб-странице.
Теперь, когда мы подтвердили, что CSS может красть значения атрибутов, давайте обратимся к двум вопросам:
1. Что можно украсть? 2. Вы продемонстрировали только кражу первого символа, как можно украсть второй символ?
Начнем с первого вопроса. Что можно украсть? Обычно это конфиденциальные данные, верно?
Самой распространенной целью является токен CSRF. Если вы не знакомы с CSRF, я обсужу это в будущей статье.
Вкратце, если токен CSRF украден, это может привести к атакам CSRF. Просто считайте этот токен важным. Обычно токен CSRF хранится в скрытом поле ввода, типа так:
Как мы можем украсть данные внутри?
Для скрытых полей ввода наш предыдущий метод не сработает:
Поскольку тип ввода скрыт, этот элемент не будет отображаться на экране. Поскольку он не отображается, браузеру не нужно загружать фоновое изображение, поэтому сервер не получит никаких запросов. Это ограничение очень строгое, и даже использование display:block !important; не может его переопределить.
Что же нам делать? Не волнуйтесь, у нас есть еще один вариант селектора, вот так:
В конце дополнительно указан + input. Этот плюс - это другой селектор, означающий "выбрать элемент, который идет после". Таким образом, вместе селектор означает "я хочу выбрать ввод с именем 'csrf-token' и значением, начинающимся с 'a', который идет после ввода с именем 'username'". Другими словами, <input name="username">.
Таким образом, фоновое изображение действительно загружается другим элементом, который не имеет type=hidden, поэтому изображение будет загружено нормально.
Но что, если после него нет других элементов? Например, как здесь:
С :has, мы, по сути, непобедимы, потому что можем указать, какой родительский элемент меняет фон. Таким образом, мы можем выбирать, что нам угодно.
Помимо помещения данных в скрытые поля ввода, некоторые веб-сайты также помещают данные в теги <meta>, например, <meta name="csrf-token" content="abc123">. Мета-теги также являются невидимыми элементами. Как мы можем их украсть?
Во-первых, как упоминалось в конце предыдущего абзаца, has - это абсолютный способ кражи. Мы можем сделать это так:
Но кроме того, есть другие способы кражи.
Хотя теги <meta> также невидимы, в отличие от скрытых полей ввода, мы можем сделать этот элемент видимым с помощью CSS:
Но этого недостаточно. Вы заметите, что запрос все еще не отправляется. Это потому, что <meta> находится под <head>, а у <head> есть свойство display:none по умолчанию. Поэтому нам также нужно специально настроить <head>, чтобы сделать <meta> "видимым":
Так, написав это, браузер отправит запрос. Однако на экране ничего не отобразится, потому что, в конце концов, content — это атрибут, а не текстовый узел HTML, поэтому он не будет отображаться на экране. Но сам элемент meta на самом деле видим, поэтому запрос отправляется:
Если вы действительно хотите отобразить содержимое на экране, это можно сделать с помощью псевдо-элементов с attr:
Тогда вы увидите содержимое, находящееся внутри мета-тега, отображаемое на экране.
Наконец, давайте рассмотрим практический пример.
Токен CSRF в HackMD размещен в двух местах, одно из них - скрытый ввод, а другое - мета-тег, с таким содержимым:
И HackMD на самом деле поддерживает использование <style>, этот тег не будет отфильтрован, поэтому вы можете написать любой стиль. Связанный CSP выглядит так:
Как вы можете видеть, разрешен unsafe-inline, поэтому вы можете вставить любой CSS.
После подтверждения того, что можно вставить CSS, вы можете начать подготовку к краже данных. Помните неотвеченный вопрос ранее, "Как украсть символы после первого?" Позвольте ответить на него с примером HackMD.
Во-первых, токены CSRF обычно изменяются при обновлении страницы, поэтому вы не можете обновлять страницу. К счастью, HackMD поддерживает обновления в реальном времени. Когда содержимое меняется, оно немедленно отражается на экранах других клиентов. Таким образом, возможно "обновление стилей без обновления страницы". Процесс следующий:
1. Подготовьте style для кражи первого символа и вставьте его в HackMD. 2. Жертва открывает страницу. 3. Сервер получает запрос на первый символ. 4. Сервер обновляет содержимое HackMD и заменяет его на нагрузку, чтобы украсть второй символ. 5. Страница жертвы обновляется в реальном времени и загружает новый стиль. 6. Сервер получает запрос на второй символ. 7. Повторите этот процесс, пока не украдены все символы.
Простая схема потока следующая:
Код следующий:
Вы можете запустить его с помощью Node.js. После запуска откройте соответствующий документ в браузере, и вы сможете увидеть прогресс утечки в терминале.
Однако, даже если вам удастся украсть токен CSRF HackMD, вы все равно не сможете выполнить атаку CSRF, потому что HackMD на сервере проверяет другие заголовки HTTP-запросов, такие как origin или referer, чтобы убедиться, что запрос исходит из корректного источника.
В мире кибербезопасности творчество и воображение имеют большое значение. Иногда сочетание нескольких небольших уязвимостей может увеличить их степень серьезности. В данном случае хочу поделиться примером задачи CTF, которая сочетает инъекцию CSS с другой уязвимостью, которую я нахожу довольно интересной.
Целью атаки является блог, написанный на React, и целью является успешное кража данных со страницы /home. Вы можете добавлять статьи, а содержимое статей отображается с помощью следующего метода:
Как уже упоминалось ранее, современные фреймворки для front-end автоматически кодируют вывод, поэтому нет нужды беспокоиться о проблемах с XSS. Однако, dangerouslySetInnerHTML в React означает: "Это не страшно, просто установи innerHTML напрямую", поэтому вы можете вставить любой HTML здесь. Но проблема в правилах CSP: script-src 'self'; object-src 'none'; base-uri 'none';.
Эти правила очень строгие. script может быть загружен только из того же источника, в то время как для других элементов, таких как стиль, нет ограничений. Очевидно, что мы можем использовать инъекцию CSS для кражи данных со страницы.
Однако есть еще одна проблема. URL статей - /posts/:id, а данные, которые мы хотим украсть, находятся на странице /home. CSS не может влиять на другие страницы. Даже если мы можем использовать iframe для встраивания страницы /home, мы не можем внедрить стиль на эту страницу.
Что мы можем сделать в этом случае?
На этом этапе мне пришла в голову идея: использовать элемент iframe с srcdoc, с помощью этого мы можем создать новую страницу, где снова отобразим React App:
Однако консоль показывает ошибку, связанную с react-router:
react-router - это библиотека, используемая для маршрутизации на стороне клиента. Базовое использование выглядит примерно так, указывая, какой компонент соответствует какому пути:
Это в конечном итоге определяется window.location.pathname, и ключевым моментом является то, что этот window происходит от document.defaultView, вкратце, document.defaultView.location.pathname.
Что это значит? Это означает, что мы можем переопределить его с помощью DOM clobbering!
Ранее мы упоминали, что мы не можем переопределять существующие свойства окна, поэтому мы не можем переопределить window.location. Однако для document все иначе; мы можем переопределить document.
Если мы разместим <iframe name=defaultView src="/home"> на странице, тогда document.defaultView будет contentWindow этого iframe, а src здесь - это /home, которая имеет тот же источник. Следовательно, мы можем получить доступ к document.defaultView.location.pathname и получить pathname страницы /home, отображая содержимое главной страницы внутри iframe.
Таким образом, мы можем сочетать это с инъекцией CSS, которую мы обнаружили ранее. Пример показан ниже:
Интерфейс будет выглядеть так:
Мы повторно отобразили React-приложение в srcdoc iframe, и через DOM clobbering это приложение React отобразило другую страницу. Используя инъекцию CSS, мы можем украсть данные и достичь нашей цели.
В этой части мы рассмотрели принцип использования CSS для кражи данных, который сводится к использованию "селектора атрибутов" в сочетании с функциональностью "загрузки изображений". Мы также продемонстрировали, как красть данные из скрытых полей ввода и мета-тегов, используя HackMD в качестве практического примера.
Однако еще остаются некоторые неурегулированные вопросы, такие как:
1. В HackMD можно загрузить новые стили без обновления страницы благодаря его синхронизации контента в реальном времени. А как насчет других сайтов? Как мы можем украсть символы за первым? 2. Если мы можем украсть только один символ за один раз, не займет ли это много времени? Это возможно на практике? 3. Есть ли способ украсть что-то кроме атрибутов? Например, текстовое содержимое на странице или даже код JavaScript? 4. Какие есть механизмы защиты от этой техники атаки?
На эти вопросы будет дан ответ в следующем посте.
Так как правило успешно целится на соответствующий элемент, фон ввода будет состоять из изображения с сервера, заставляя браузер отправить запрос на .
В этом случае, в прошлом это было невозможно, потому что CSS не имел селектора для выбора "предыдущих элементов". Но теперь всё иначе, потому что у нас есть . Этот селектор может выбирать "элементы ниже, которые соответствуют конкретным условиям", например:
Это означает, что я хочу выбрать форму "ниже (ввод, который соответствует этому условию)". Таким образом, форма будет загружать фон, а не скрытый ввод. Этот селектор :has достаточно новый и официально поддерживается, начиная с Chrome 105, выпущенного в конце августа 2022 года. В настоящее время только стабильная версия Firefox еще не поддерживает его. Дополнительные сведения см. на:
DOMException: Failed to execute 'replaceState' on 'History': A history state object with URL 'about:srcdoc' cannot be created in a document with origin '' and URL 'about:srcdoc'.
Вы когда-нибудь задумывались, как он определяет текущий путь? Если вы посмотрите на код , вы увидите следующий раздел:
Это задание из corCTF 2022 modernblog и создано @strellic. Для получения дополнительной информации вы можете обратиться к подробному объяснению: