CSS Injection - Атака с использованием только CSS (Часть 2)
Last updated
Last updated
В предыдущем посте мы узнали об основных принципах кражи данных с помощью CSS и продемонстрировали это на практическом примере с использованием HackMD, успешно похитив токен CSRF. В этой части мы подробно рассмотрим некоторые детали инъекции CSS и ответим на следующие вопросы:
Поскольку HackMD может загружать новые стили без обновления страницы, как мы можем украсть символы за первым на других сайтах?
Если мы можем украсть только один символ за раз, не займет ли это много времени? Является ли это осуществимым на практике?
Возможно ли украсть что-то кроме атрибутов? Например, текстовое содержимое на странице или даже код JavaScript?
Какие механизмы защиты существуют против этой техники атаки?
В предыдущей части мы упоминали, что данные, которые мы хотим украсть, могут измениться при обновлении (например, токен CSRF), поэтому нам нужно загружать новые стили без обновления страницы.
Причина, по которой мы смогли сделать это в предыдущем посте, заключается в том, что сам HackMD является сервисом, который обеспечивает обновления в реальном времени. Но что насчет обычных веб-страниц? Как нам динамически загружать новые стили без использования JavaScript?
По этому вопросу Pepe Vila дал ответ в своей презентации показанной в 2019 году: .
В CSS вы можете использовать @import
для импорта внешних стилей, аналогично import
в JavaScript.
Вы можете использовать эту функцию для создания цикла для импорта стилей, как показано в приведенном ниже примере кода:
Затем сервер отвечает следующим стилем:
Вот ключ: хотя мы импортируем 8 сразу, сервер будет "висеть" на следующих 7 запросах и не предоставит ответ. Только первый URL https://myserver.com/payload?len=1
вернет ответ, который содержит ранее упомянутый payload для кражи данных:
Когда браузер получает ответ, он загружает приведенный выше сниппет CSS. После загрузки элементы, которые соответствуют условиям, отправляют запросы на backend. Допустим, первый символ - 'd'. Тогда на этом этапе сервер отвечает содержимым https://myserver.com/payload?len=2
:
Этот процесс продолжается, повторяя эти шаги, что позволяет нам отправить все символы на сервер. Это связано с тем, что import
будет сначала загружать уже загруженные ресурсы, а затем ждать те, которые еще не были загружены.
Одна важная деталь, которую стоит здесь отметить, - вы заметите, что мы загружаем стили с домена myserver.com
, в то время как домен изображения на заднем плане - b.myserver.com
. Это потому, что браузеры обычно имеют ограничения на количество запросов, которые они могут одновременно загружать с одного домена. Поэтому, если вы используете только myserver.com
, вы обнаружите, что запросы на изображения на заднем плане не проходят, поскольку они блокируются импортом CSS.
Поэтому необходимо настроить два домена, чтобы избежать этой ситуации.
Этот подход также хорошо работает в Chrome, поэтому, применяя его, вы можете одновременно поддерживать оба браузера.
В общем, используя функцию @import
в CSS, мы можем достичь "динамической загрузки новых стилей без перезагрузки страницы" и, таким образом, украсть каждый символ по одному.
Если мы хотим выполнить этот тип атаки в реальном мире, нам может потребоваться улучшить эффективность. Возьмем в качестве примера HackMD, токен CSRF состоит из 36 символов, поэтому нам нужно отправить 36 запросов, что довольно много.
На самом деле мы можем украсть по два символа за раз, потому что, как упоминалось в предыдущем разделе, помимо префиксного селектора, есть также суффиксный селектор. Поэтому мы можем сделать это так:
Помимо кражи префикса, мы можем также украсть суффикс, удвоив таким образом эффективность. Важно отметить, что CSS для префикса и суффикса использует разные свойства, одно использует background
, а другое - border-background
. Это потому, что если мы используем то же самое свойство, содержимое будет перезаписано другими, в результате будет отправлен только один запрос.
Если в содержимом нет много возможных символов, например, 16 символов, мы можем непосредственно украсть по два префикса и два суффикса за один раз. Общее количество правил CSS будет 16*16*2
= 512, что должно быть в пределах приемлемого диапазона и ускорит процесс в два раза.
Помимо этих методов, мы также можем улучшить ситуацию на стороне сервера. Например, использование HTTP/2 или даже HTTP/3 может потенциально ускорить загрузку запросов и повысить эффективность.
Кроме кражи свойств, есть ли способ украсть что-то еще? Например, другой текст на странице или даже код внутри скриптов?
Основываясь на принципах, обсуждаемых в предыдущем разделе, это невозможно. Способность к краже свойств обусловлена "селектором атрибутов", который позволяет нам выбирать конкретные элементы. Однако в CSS нет селектора, который мог бы выбрать "содержимое" само по себе.
Поэтому нам нужно более глубокое понимание CSS и стилей на веб-странице, чтобы выполнить эту на первый взгляд невозможную задачу.
Юникод для &
- это U+0026
, поэтому только символ &
будет отображаться в другом шрифте, а остальные будут использовать тот же шрифт.
Разработчики могли уже использовать эту технику, например, для отображения английского и китайского с использованием разных шрифтов. Эта техника также может быть использована для кражи текста на странице, вот так:
Если вы проверите вкладку сеть, вы увидите, что было отправлено 4 запроса:
С помощью этой техники мы можем определить, что на странице есть четыре символа: 13ac.
Однако эта техника имеет свои ограничения:
Мы не знаем порядок символов.
Мы не знаем, есть ли повторяющиеся символы.
Но мысль о том, как украсть символы с точки зрения "загрузки шрифтов", предоставила многим людям новый способ мышления и привела к разработке различных других методов.
Цель этой техники - решить проблему, с которой мы столкнулись в предыдущей технике: "не зная порядок символов". Это сочетание многих деталей и оно включает в себя несколько шагов, так что здесь стоит быть внимательным.
Во-первых, мы на самом деле можем украсть символы с использованием встроенных шрифтов без загрузки внешних шрифтов. Как мы можем это сделать? Нам нужно найти два набора встроенных шрифтов с разной высотой.
Например, существует шрифт по имени "Comic Sans MS", который имеет большую высоту, чем другой шрифт под названием "Courier New."
Скажем, стандартная высота шрифта составляет 30px, а Comic Sans MS - 45px. Теперь, если мы установим высоту контейнера текста на 40px и загрузим шрифт, вот так:
Мы увидим разницу на экране:
Ясно, что символ A имеет высоту, большую, чем у других символов. Согласно нашим настройкам CSS, если высота содержимого превышает высоту контейнера, появится полоса прокрутки. Хотя это может быть не видно на снимке экрана выше, у ABC ниже есть полоса прокрутки, тогда как у DBC выше ее нет.
Более того, мы можем установить внешний фон для полосы прокрутки:
Это означает, что если появится полоса прокрутки, наш сервер получит запрос. Если полоса прокрутки не появится, запроса не будет.
Кроме того, когда я применяю шрифт fa
к div, если символ A появится на экране, будет полоса прокрутки, и сервер получит запрос. Если символ A не появится на экране, ничего не произойдет.
Таким образом, если мы многократно загружаем разные шрифты, сервер может знать, какие символы отображаются на экране, что аналогично тому, что мы достигли с помощью unicode-range
.
Так как же мы решаем проблему порядка?
На экране будет виден только символ "C". Это происходит потому, что мы устанавливаем размер шрифта всех символов на 0, используя font-size: 0px
, и затем корректируем размер шрифта первой строки до 30px, используя div::first-line
. Иными словами, видными будут только символы первой строки, а так как ширина div всего 20px, отобразится только первый символ.
Далее мы можем использовать только что изученный прием для загрузки разных шрифтов. Когда я загружаю шрифт "fa", поскольку символ "A" не отображается на экране, никаких изменений не произойдет. Но когда я загружаю шрифт "fc", поскольку символ "C" отображается на экране, он будет отображен с использованием Comic Sans MS, что увеличит высоту и вызовет появление полосы прокрутки. Затем мы можем использовать это для отправки запроса, вот так:
Итак, как нам продолжать использовать новые шрифты? Для этого мы можно воспользоваться CSS-анимацией. Можно постоянно загружать разные шрифты и задавать разные переменные --leak
с помощью CSS-анимации.
Таким образом, мы можем узнать, что это за первый символ на экране.
Как только мы узнаем первый символ, можно увеличить ширину div, например, до 40px, чтобы он мог вместить два символа. То есть первая строка будет состоять из первых двух символов. Затем можно загружать разные семейства шрифтов тем же способом, чтобы просочиться до второго символа. Детальный процесс таков:
Предположим, что символы на экране - "ACB".
Скорректировать ширину до 20px, и только первый символ "A" появится на первой линии.
Загрузить шрифт "fa", так что "A" будет отображаться с более крупным размером шрифта, что вызовет появление полосы прокрутки. Загрузить фон полосы прокрутки и отправить запрос на сервер.
Загрузить шрифт "fb", но поскольку "B" не отображается на экране, никаких изменений не произойдет.
Загрузить шрифт "fc", но так как "C" не отображается на экране, никаких изменений не произойдет.
Скорректировать ширину до 40px, и первая строка будет отображать первые два символа "AC".
Загрузить шрифт "fa" снова, так что "A" будет отображаться с более крупным размером шрифта, что вызовет появление полосы прокрутки. Но на этом этапе фон уже загружен, так что новый запрос не будет отправлен.
Загрузить шрифт "fb", "B" будет отображаться с более крупным размером шрифта, провоцируя появление полосы прокрутки. Загрузить фон полосы прокрутки.
Загрузите шрифт "fc", "C" будет отображаться с более крупным размером шрифта, но поскольку однажды фон уже загружен, запрос не будет отправлен.
Скорректировать ширину до 60px, и все три символа "ACB" появятся на первой линии.
Загрузить шрифт "fa" снова, как в шаге 7.
Загрузить шрифт "fb", "B" будет отображаться с более большим размером шрифта, вызывая появление полосы прокрутки. Загрузите фон полосы прокрутки.
Загрузить шрифт "fc" снова, "C" будет отображаться с более большим размером шрифта, но поскольку фон уже загружен, запрос не будет отправлен.
Конец.
Из вышеприведенного процесса мы видим, что сервер получит три запроса в порядке A, C, B, что представляет собой порядок символов на экране. Изменение ширины и семейства шрифтов непрерывно может быть достигнуто с помощью CSS-анимации.
Хотя это решение и решает проблему "неизвестности порядка символов", оно по-прежнему не может решить проблему дублирования символов, потому что повторяющиеся символы не вызывают новые запросы.
Коротко говоря, этот ход может решить все вышеупомянутые проблемы и позволит "узнать порядок символов и повторяющиеся символы", что позволяет нам украсть полный текст.
Так как это помогает нам?
Мы можем создать уникальный шрифт сами, где мы устанавливаем ab
как лигатуру и отрисовываем его как элемент с очень большой шириной. Затем устанавливаем ширину определенного div с фиксированным значением и сочетаем его с трюком полосы прокрутки, о котором мы упоминали ранее: "Если "ab" появляется, он станет очень широким, что вызовет появление полосы прокрутки, и мы можем загрузить запрос, чтобы сообщить серверу; если его нет, полоса прокрутки не появится, и ничего не произойдет."
Процесс таков, предполагая, что на экране символы "acc":
Загрузить шрифт с лигатурой "aa", ничего не происходит.
Загрузить шрифт с лигатурой "ab", ничего не происходит.
Загрузить шрифт с лигатурой "ac". Успешно отрендерить огромный экран, появляется полоса прокрутки, загрузить изображения с сервера.
Сервер знает, что "ac" находится на экране.
Загрузить шрифт с лигатурой "aca", ничего не происходит.
Загрузить шрифт с лигатурой "acb", ничего не происходит.
Загрузить шрифт с лигатурой "acc". Успешно отрендерить, появляется полоса прокрутки, отправить результат на сервер.
Сервер знает, что "acc" на экране.
Сочетая лигатуры с полосой прокрутки, мы можем медленно получать все символы на экране, даже код JavaScript!
Вы знали, что содержимое скрипта может быть отображено на экране?
Добавив этот CSS, содержимое скрипта может быть отображено на экране, поэтому мы также можем использовать ту же технику для кражи содержимого скрипта!
Упрощенная демонстрация, чтобы показать, что это возможно.
Я добавил два отрезка кода JavaScript в скрипт. Содержимым являются var secret = "abc123"
и var secret2 = "cba321"
. Затем, используя CSS, я загружаю подготовленный шрифт. Всякий раз, когда есть лигатура с "a
, ширина становится чрезмерной.
Затем, если появляется полоса прокрутки, я устанавливаю фон в синий цвет для лучшей видимости. Финальный результат выглядит следующим образом:
Выше, поскольку содержимое состоит из var secret = "abc123"
, оно соответствует лигатуре "a
, так что ширина становится шире, и появляется полоса прокрутки.
Ниже, поскольку нет "a
, полоса прокрутки не появляется (места с "a" - это отсутствующие символы, что, вероятно, связано с тем, что не были определены другие глифы, но это не влияет на результат).
Изменяя фон полосы прокрутки на URL, мы можем знать результат утечки с сервера.
Если вы хотите увидеть фактическую демонстрацию и реализацию на стороне сервера, вы можете обратиться к двум упомянутым выше статьям.
Наконец, давайте поговорим о мерах защиты. Самый простой и прямой способ - это просто отключить использование стилей, что в основном устранит проблему инъекции CSS (если не считать случаев с уязвимостями в реализации).
Если вы действительно хотите разрешить стили, вы также можете использовать CSP для блокировки загрузки определенных ресурсов. Например, font-src
не нужно полностью открывать, а style-src
также можно задать в виде списка разрешений для блокировки синтаксиса @import
.
Кроме того, вы также можете рассмотреть, что произойдет, если что-то на странице будут украдено. Например, если CSRF-токен взят, наихудший сценарий - это CSRF. В этот момент вы можете реализовать защиту от CSRF, даже если злоумышленник получает CSRF-токен, они не смогут выполнить CSRF (например, проверяя заголовок происхождения).
CSS велик.
Эти две статьи главным образом представляют технику атаки инъекции CSS, поэтому здесь мало фактического кода. Эти техники атаки все заимствованы из статей предшественников, и я их перечислю ниже. Если вам интересно, вы можете прочитать оригинальные статьи, которые предоставят более подробные объяснения.
Ссылки:
Кроме того, вышеупомянутый метод не работает в Firefox. Даже если ответ на первый запрос приходит, Firefox не обновляет стили сразу. Он ждет, пока все запросы не будут выполнены, прежде чем обновиться. Для решения этой проблемы вы можете обратиться к этой статье автора Michał Bentkowski: . Удалите первый шаг импорта и оберните каждый импорт символов в дополнительные стили, как это:
В CSS есть свойство, называемое "unicode-range", которое позволяет нам загружать разные шрифты для разных символов. Вот пример, взятый с :
Мы можем сначала уменьшить ширину div, чтобы отобразить только один символ, так что другие символы будут размещены на второй строке. Затем мы можем использовать селектор для того, чтобы специально настроить стиль для первой строки, подобно следующему:
Этот сложный, но гениальный способ был придуман не мной, а пользователями @cgvwzq и @terjanq. Если вы хотите увидеть оригинальную демонстрацию, вы можете посетить эту веб-страницу (источник: ):
Прежде чем понять, как это сделать, нам нужно знать термин лигатура. В некоторых шрифтах определенные комбинации символов отображаются как связанные формы, как показано на картинке ниже (источник: ):
На практике вы можете использовать SVG с другими инструментами для быстрого создания шрифтов на сервере. Если вы хотите увидеть детали и связанный код, вы можете обратиться к статье Michał Bentkowski's:
Masato Kinugawa также создал версию демо для Safari. Поскольку Safari поддерживает шрифты SVG, нет необходимости генерировать шрифты с сервера. Оригинальная статья находится здесь: