搜集信息,先F12查看网页源代码。看到提示,访问/source,得到部分源代码。
const express = require('express');
const bodyParser = require('body-parser')
const path = require('path');
const crypto = require('crypto');
const fs = require('fs');
const app = express();
const FLAG = require('./config').FLAG;
app.set('view engine', 'html');
app.engine('html', require('hbs').__express);
app.use(express.urlencoded());
app.use(bodyParser.urlencoded({extended: true})).use(bodyParser.json())
var glowworm=[];
var content=[];
function sha1(string) {
return crypto.createHash("sha1").update(string).digest("hex");
}
app.get('/', (req, res) => {
const { page } = req.query;
if (!page) res.redirect('/?page=index');
else res.render(page, { FLAG, 'insect': 'glowworm' });
});
app.get('/source', function(req, res) {
res.sendFile(path.join(__dirname + '/app.js'));
});
app.post('/data', function(req, res) {
var worm = req.body;
content[worm.wing][worm.fire] = worm.data;
res.end('data success')
});
app.get('/refresh', (req, res) => {
let files = [];
var paths = path.join(__dirname,'views/sandbox')
if(fs.existsSync(paths)){
files = fs.readdirSync(paths);
files.forEach((file, index) => {
let curPath = paths + "/" + file;
if(fs.statSync(curPath).isFile()){
fs.unlinkSync(curPath);
}
});
}
res.end('refresh success')
});
app.post('/', (req, res) => {
const key = "worm";
const { content , a, b} = req.body;
if (!a || !b || a.length !== b.length) {
res.send("no!!!");
return;
}
if (a !== b && sha1(key + a) === sha1(key + b)) {
if(glowworm.token1 && req.query.token2 && sha1(glowworm.token1) === req.query.token2){
if (typeof content !== 'string' || content.indexOf('FLAG') != -1) {
res.end('ban!!!');
return;
}
const filename = crypto.randomBytes(8).toString('hex');
fs.writeFile(`${path.join('views','sandbox',filename)}.html`, content, () => {
res.redirect(`/?page=sandbox/${filename}`);
})
}else{
res.send("no no no!!!");
}
}else{
res.send("no no!!!");
}
});
app.listen(8888, '0.0.0.0');
容易发现如下代码,
content[worm.wing][worm.fire] = worm.data;
nodejs声明变量的方式和php非常像,不用声明具体类型。nodejs的路由写法app.get(’/’, (req, res) => {expression})
提供两个对象req和res。和其他语言一样,拥有对请求和响应的处理函数。路由绑定的方法和flask比较像。
nodejs原型链污染
之前遇到nodejs原型链污染,一般都是直接略过了。这次是不得不做此题了,就差一个就ak了web怎么能放弃。
在javascript中一切皆对象,因为所有的变量,函数,数组,对象 都始于object的原型即object.prototype,任何对象都拥有__proto__
属性,正确的用法是建立两个对象之间的关联。(一个对象可以使用__proto__
关联另外一个对象,被关联的对象就叫原型)
var animal={
eat:function(){
...
}
};
var dog={
__proto__:animal
};
在以上代码中,dog对象可以使用eat方法。
结合上面我们看到的content[worm.wing][worm.fire] = worm.data;
可以发现,如果我们可以控制worm的值,以此来控制该程序的变量。如果worm.wing的值是__proto__
,那么就有如下结构
var content={
__proto__:worm.fire=worm.data
}
worm.fire可以是我们想要的任何变量。
再看下面的if语句
if(glowworm.token1 && req.query.token2 && sha1(glowworm.token1) === req.query.token2)
在上下文中,token1是没有进行定义的,那么这里就需要用到原型链污染,对token1进行赋值。
这是一个单独的请求。如果我们访问/的时候,这个值还会在程序的上下文中。为什么在,我也不知道。
javascript弱类型
凡是在定义变量时不声明类型的操作,都有可能触发弱类型比较。以上代码有
if (a !== b && sha1(key + a) === sha1(key + b))
javascript在进行拼接字符的时候,会忽略类型,也就是这里传数组[1]和’0’,与给定字符串key相加后,会获得相同的值。
这里req.query.token2,简单说明一下req.query会获取URL中get请求获得的值。这里获取的值经过sha1加密应该和原型链污染的data值相同。
模板注入
app.get('/', (req, res) => {
const { page } = req.query;
if (!page) res.redirect('/?page=index');
else res.render(page, { FLAG, 'insect': 'glowworm' });
});
这里调用了res.render将参数渲染到前端,常见的模板注入方式为{{}}。这里进行简单尝试__proto__.toString()
。可以发现并没有将字符直接打印出来。
在javascript中,几乎任何数据类型都是对象,对象使用this关键字调用。那么利用模板注入遍历this关键字指向的属性。即可获取本题的flag。
关于该模板的文档:https://www.handlebarsjs.cn/guide/hooks.html#helpermissing
最终利用存在模板注入的路由来读取flag,这里路由/后的代码会创建一个随机文件名的html文件,然后将文件名显示在前端页面。