Node.js is an open-source and cross-platform JavaScript runtime environment. It is a popular tool for almost any kind of project! Node.js runs the V8 JavaScript engine, Google Chrome's core, outside the browser. This allows Node.js to be very performant.
That said, NodeJS has become very popular in recent years for deploying enterprise-level applications and beyond, but at the same time, many vulnerabilities have surfaced.
Today we are going to look at 3 different vulnerabilities by analyzing the source code of an application and how you can detect and exploit them.
As always, we will rely on PWNX Labs. You can find the following lab in the retired machines.
The Lab presents an application that shows a simple store from PWNX and Pentesting Made Simple.
Let's analyze the code of the application in order to find the first vulnerability.
JS (In)security - First Vulnerability: Eval function()
The application implements a redirect function on any user login. The parameter used to redirect the user is then included in an eval function which leads to remote code execution allowing any user to execute code on the machine and potentially obtain a reverse shell.
Javascript has two unsafe functions that execute code provided in a variable, eval and the function constructor. Let's look at the application's source code and search for unsafe functions.
Here the eval function is used to parse the JSON document passed in the ReturnTo query parameter. The URL field from this JSON document is used after to redirect the user back to the page. In this scenario, the query string can be manipulated by an attacker.
Let's try to pass a simple expression that uses a global process object to crash the application.
We can notice that we didn't receive any response. The application log shows that the process was terminated because we passed the exit method.
Looking back at the source code, the called eval function is a direct invocation meaning that the injected code has access to the current scope of the application, including all the local variables.
On the login function, the res object(response) from ExpressJS allows us to manipulate the content sent back to the response.
Let's try to inject some code that leaks all the user databases in a JSON response.
`res.send(JSON.stringify(users))`
As we can see is a JSON array with the info about all the users of the application.
Of course, when we have an eval function, we can perform code execution on the application and perform server takeover. Let's upload a webshell to execute arbitrary commands.
This classic JS webshell starts an HTTP server listening on port 8000. The shell commands send the command query string parameter and are executed using the child_process module.
Injecting our webshell now, we can execute arbitrary commands connecting to port 8000.
JS (In)security -Second Vulnerability: Loose Comparison
The NodeJS application is affected by a loose comparison vulnerability, the issue allows an attacker to trigger a condition always to be true and return always true values. In the case of this application, triggering a condition to be always true, an attacker can enumerate all the users(aka user enumeration attack) on the application.
On utis.js, the loose comparison is used in the filter function and is called from the readProfile function. The loose comparison tries to compare a field of an object, provided as an input parameter, to a specific value and provided as an input parameter.
If it is possible to trick the filter condition into always being true, we would be able to retrieve information about all the users in the database.
If the field did not exist, the property retrieval and the bracket notation would return undefined. If the value is null, the loose comparison will always be true.
How How to trick loose comparison:
- Mixed data types
- Arrays and objects
- Missing properties
Let’s login into the application and intercept the profile request.
Here we can see the field name, and we need to make it null as the loose comparison will be true and we will be able to retrieve the data.
Delete the value param and insert a non-existing field parameter.
The function is returning undefined. If the value is null, the loose comparison will always be true, and we can retrieve all the data.
JS (In)security - Third Vulnerability: Prototype Pollution
In the case of the Prototype Pollution vulnerability in the NodeJS application, the issue allows an attacker to control the default values of an object's properties. This allows the attacker to tamper with the application's logic and can lead to denial of service or remote code execution in extreme cases.
The saveProfile function is the handler for the user profile management form. In the first step, we search the user database to find the user based on the user’s email.
If the user was found in the database, we clone fields into a clean temporary object named updatedUser.
After that is used, the Object.assign method is to update the user object retrieved from the user database.
The merge function in utils.js uses the for-in loop to iterate over the source object's properties.
Then it uses the bracket notation to add or write the properties based on their name.
The value comes also from the source object. The source object comes from the html form and could be manipulated by an attacker.
What can you achieve with this type of attack?:
- Denial of service
- Session fixation
Once again, we log in to the application and change the address via JSON request.
On the server side, the document is parsed, and the parsed object is cloned, as we saw from the source code before.
Here we can add an object overwriting the built-in object method to String.
We caused a DOS on the web application. The error message comes from the Express framework and is caused by the built-in object .toString is no longer a method but a string.
A simple modification of the object prototype chain caused a denial of service attack.
Prototype pollution allows us to add properties to objects when they are not expected.
In the login.js file, we notice that getting user credentials breeds them from the request body property.
What happens if these fields are missing from the request?
Let's take the POST request on the profile again, and this time we use prototype pollution to add two properties, email and password.
Log out with the browser dev tool & then remove the email and password fields to cause both values to be missing.
Now we can, for example, trick the user into sending a POST request with no values, and the user is logged as h4t4way@pms.com. This bug is called Session Fixation.
Even though the email and password fields were missing from the POST request, they were inherited by the request body object in the login function.
Conclusion
As with any other human-made technology, programming languages and environments present advantages and threats. Most technologies can be made as secure as possible with the proper use of certain principles, and Node.js is no exception.
In this article, we saw a few of the many vulnerabilities that could affect Node.js applications.