DotNetToJScript: Weaponizing .NET Assemblies Through Script Serialization
Hook
In 2017, a single tool demonstrated that any .NET assembly could execute from a simple JScript file without touching disk, fundamentally changing how red teams approached application whitelisting bypasses.
Context
Before DotNetToJScript emerged, executing .NET code in restricted environments meant getting a compiled executable past application whitelisting controls—a challenging proposition when tools like Device Guard and AppLocker were maturing. While PowerShell provided a scriptable avenue for .NET execution, organizations increasingly deployed PowerShell logging and constrained language mode to counter offensive tooling. The security community needed alternative vectors.
JScript and VBScript represented an overlooked attack surface. Despite being legacy scripting languages, they shipped with Windows, ran under Windows Script Host with surprising privileges, and received far less monitoring than PowerShell. The challenge was bridging these interpreted scripting environments with compiled .NET assemblies. James Forshaw (tyranid) solved this by exploiting .NET's binary serialization capabilities and COM interop, creating a tool that transforms any .NET assembly into a self-contained script file. The technique proved so effective that it became standard in penetration testing toolkits and spawned numerous variants attempting to evade the inevitable security detections.
Technical Insight
DotNetToJScript operates through a clever exploitation of .NET's BinaryFormatter serialization combined with COM interop bridges. At its core, the tool takes your compiled .NET assembly, wraps it in a serializable object structure, converts it to a base64-encoded byte array, and generates JScript code that reverses this process at runtime.
The architecture hinges on a key Windows capability: Windows Script Host can instantiate COM objects, and the .NET Framework registers itself as a COM-visible runtime. When JScript calls GetObject("script:" + monikerName) or instantiates specific .NET Framework classes through COM, it can bootstrap the Common Language Runtime within the script process. Once the CLR loads, DotNetToJScript's generated code uses BinaryFormatter to deserialize the embedded assembly bytes directly into memory, then invokes the target class's parameterless constructor.
Here's what a typical usage pattern looks like. First, you create a .NET assembly with a ComVisible class and parameterless constructor:
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
[ComVisible(true)]
public class TestClass
{
public TestClass()
{
MessageBox.Show("Executing from JScript!");
}
}
After compiling this to TestAssembly.dll, you run DotNetToJScript:
DotNetToJScript.exe TestAssembly.dll -l JScript -o output.js -c TestClass
The generated output.js contains code resembling this structure (simplified for clarity):
var serialized_obj = [
0x00, 0x01, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF,
// ... hundreds of bytes representing your assembly ...
];
var stm = new ActiveXObject("ADODB.Stream");
stm.Type = 1; // Binary
stm.Open();
stm.Write(serialized_obj);
stm.Position = 0;
var fmt = new ActiveXObject("System.Runtime.Serialization.Formatters.Binary.BinaryFormatter");
var al = new ActiveXObject("System.Collections.ArrayList");
var d = fmt.Deserialize_2(stm);
var o = d.DynamicInvoke(al).CreateInstance("TestClass");
The genius lies in the layering. The tool doesn't just serialize your assembly—it serializes a carefully constructed object graph that includes delegate chains. These delegates, when deserialized, provide a generic way to load and instantiate your assembly without requiring specific knowledge of its contents. The BinaryFormatter essentially becomes an execution engine, with the serialized payload containing all the instructions for loading, reflecting, and invoking your .NET code.
For .NET 4+ assemblies, the tool generates additional code to handle runtime versioning issues. The v4 runtime has stricter security policies around cross-version COM interop, so DotNetToJScript includes fallback mechanisms using WScript.Shell to probe the environment and select appropriate COM objects. This adaptability allows the same generation technique to work across .NET 2.0, 3.5, and 4.x targets, though with varying reliability.
The tool also supports VBScript and VBA output formats with nearly identical architectures, simply translating the JScript syntax to VBScript or generating VBA macros. The VBA option proved particularly valuable for phishing campaigns, as it allowed red teams to embed .NET post-exploitation tools directly in macro-enabled Office documents without dropping files or making network requests during initial execution.
One advanced feature is the custom JScript injection capability. After your class constructor executes, you can specify additional JScript code to interact with the instantiated object if it exposes ComVisible methods. This enables scenarios like configuring a C2 agent, extracting credential material, or orchestrating multi-stage payloads—all from a single script file.
Gotcha
The technique's primary limitation is visibility. Modern endpoint detection systems explicitly watch for the COM object instantiation patterns DotNetToJScript uses. When JScript suddenly calls GetObject with script monikers or instantiates BinaryFormatter through ActiveXObject, it triggers high-confidence alerts in virtually every EDR product. The serialized payload itself is trivially detectable through static analysis—YARA rules matching BinaryFormatter's magic bytes inside script files are standard defensive measures.
Beyond detection concerns, the tool has practical execution constraints. It requires a full-trust scripting context, meaning it won't work under Internet Explorer's security zones, won't execute from restricted locations, and fails when Group Policy disables Windows Script Host. The .NET 4+ support is particularly fragile, often breaking in hardened environments where certain COM registrations are removed or disabled. Additionally, the generated scripts can be enormous—a 50KB assembly might produce a 200KB+ JScript file due to serialization overhead and base64 encoding, making them unsuitable for size-constrained delivery mechanisms. Finally, the tool hasn't been meaningfully updated since 2017, reflecting its origins in a specific era of offensive tooling. It targets legacy .NET Framework runtimes and has no path forward for .NET Core, .NET 5+, or modern cross-platform scenarios. You're fundamentally limited to Windows-only, legacy runtime execution.
Verdict
Use DotNetToJScript if you're conducting red team assessments against organizations still running legacy .NET applications, need to test detection coverage for script-based .NET loading, or are researching historical offensive techniques for defensive insights. It remains valuable for understanding how COM interop bridges scripting and managed code, and the underlying principles still inform modern evasion research. Skip it if you need operational stealth in mature environments (the detection signatures are universal), if you're targeting modern .NET Core applications (incompatible), or if you have alternative execution primitives available like direct process injection or kernel-mode execution. In 2024, this is primarily an educational tool and a detection benchmark rather than a frontline offensive asset. Modern alternatives like Donut or execute-assembly BOFs provide significantly better operational security, while defenders should ensure their monitoring catches BinaryFormatter instantiation from scripting contexts as a foundational detection rule.