Rebuilding IronNetInjector – Turla’s IronPython Toolkit

Click the icon to Follow me:- twitterTelegramRedditDiscord

During a recent engagement, we were asked to employ Turla’s Tactics, Techniques, and Procedures (TTPs) using IronNetInjector. This is not a toolkit that we had a lot of experience with and upon doing some OSINT, it seemed to be pretty hard to come by. Specifically, no one seemed to be able to determine how Turla was running the initial IronPython interpreter. We decided that if we were going to build something similar, we would put our own spin on it and focus on building things in phases. So, we got started from the ground up with development.

We recently expanded the Empire Python agent to include IronPython3 capability, so we had some insight into how we could accomplish this. We also created Invoke-IronPython3 for running IronPython3 scripts natively through PowerShell. So we weren’t complete novices in this area. However, there was still plenty for us to learn when it came to the Bring Your Own Interpreter (BYOI) concept. BYOI is not a new concept and has been around for a few years. The most prevalent of the BYOI frameworks is SILENTTRINITY developed by Byt3bl33d3r, which is centered around the idea of developing implants in any language and bringing their packages along.


IronNetInjector is a Turla toolkit that contains IronPython and a .NET injector with one or more embedded payloads. The payloads can be .NET assemblies (x86/64) or native PEs (x86/64), such as ComRAT.  When an IronPython script is run, the .NET injector gets loaded, which in turn injects the payload into a process.

decoded ironnetinjector
Decoded IronPython script with embedded .NET injector and ComRAT payload – Unit42

The interesting point is that Turla had two separate versions of IronNetInjector, which used different Portable Executable (PE) Injection implementations. The first was developed in 2018 using Empire’s ReflectivePEInjection converted to C#. The second is from 2019 and used PeNet to reflectively load unmanaged code into remote processes.

Turla built IronNetInjector to replace the PowerShell loader, PowerStallion, that they were using since AMSI instrumentation is minimal when moving to other Dynamic Language Runtime (DLR) languages. When IronNetInjector is executed, it will load the IronPython interpreter using C#. Next, the embedded .NET injector written in IronPython is used to reflectively load the ComRAT DLL.

Turla uses a RAT derived from Agent.BTZ named ComRAT. Their latest version, ComRAT v4, was released in 2017 and is still being actively used. Besides a refactored code base, ComRAT v4 is developed in C++ and uses the Gmail web UI as its primary C2 channel. However, it can still use HTTP(S) if it enters into a legacy mode. Looking at ComRAT’s architecture, you will notice that Turla’s TTPs previously relied heavily on PowerShell to load their implants. IronNetInjector replaces most of their attack chain and shifts their TTPs away from PowerShell.

computer virus
ComRAT v4 Architecture – ESET Whitepaper

Seems simple enough, right? We wanted to break things into phases for our development since IronNetInjector is a pretty sophisticated toolkit to recreate. The first phase, which this blog post will focus on, involves embedding the IronPython interpreter with the necessary DLLs and loading the standard library via a C# executable. We decided that this part of the attack chain is when things are most vulnerable and where our customers would be most interested in catching us. Also, building the .NET injector into IronPython and creating a C++ agent was a bit more work than we intended. Additionally, there is a lot of value for keeping the agent entirely hosted within the IronPython environment instead of only using IronPython to spawn another implant.

Next, we will dive into building the IronPython Engine, but if you want to learn more about IronNetInjector, there is an excellent technical write-up from Unit 42.

Building the IronPython Engine

Even though IronNetInjector used IronPython2, we decided that IronPython3 would be our best bet since it fits with our recent work converting Empire’s codebase from Python 2 to 3. Plus this would give us a cutting-edge capability that would be scalable in the future. While not completely the same IronNetInjector, it would have similar characteristics for indicators.

You may have noticed that in the recent 4.2 release we added an IronPython agent to Empire. This is the finished IronPython launcher and loads directly into the DLR and injects the DLL without hitting disk. The first part of building the launcher is creating a payload containing the necessary DLLs for the IronPython interpreter. By borrowing some code, we can use C# to load embedded assemblies with the required libraries to generate a self-contained IronPython executable.

// Load DLLs from embedded resources
AppDomain.CurrentDomain.AssemblyResolve += (sender, args) =>
      String resourceName = new AssemblyName(args.Name).Name + ".dll";
      // Load embedded resources
      using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName))
            Byte[] assemblyData = new Byte[stream.Length];
            stream.Read(assemblyData, 0, assemblyData.Length);
            return Assembly.Load(assemblyData);

Next, we need to generate the IronPython Engine, which will contain the IronPython interpreter. We will be running our IronPython code inside of this engine (almost like code inception). The easiest way for us to do this is to embed a base64 Empire launcher inside the code passed to the interpreter at startup.

// Setup IronPython engine
string PyCode = "";
byte[] ScriptBytes = Convert.FromBase64String(B64PyCode);
PyCode = Encoding.ASCII.GetString(ScriptBytes);
ScriptEngine engine = Python.CreateEngine();

// Execute IronPython code
var script = engine.CreateScriptSourceFromString(PyCode, SourceCodeKind.Statements);

Once the Python Engine is working correctly, you will quickly realize that you can’t do much without loading the Python Standard Libraries. There are a few different ways to approach this. First, you can hope that the target has IronPython installed and use their existing standard libraries. Obviously, this isn’t a great option because you have a better chance of winning the lotto than finding IronPython in the wild.

The second option is to send the libraries as you need, which would require a bit extra work since the Empire agent code was never designed to be used this way.

Third, you can bring along all the libraries, but this introduces some additional challenges. Specifically, IronPython expects to be passed a file path for the location of the standard libraries. As a result, most solutions using IronPython drop this library to disk and then call the libraries. From an offensive perspective, this obviously is not ideal. Luckily Byt3bl33d3r and checkymander had working solutions to solve this problem for IronPython2. Unfortunately, moving to .Net 4 and IronPython3 prevented their solutions from working out of the box, but after a little tweaking (and a lot of testing), Cx01N and Kevin got it working.

To accomplish this our implementation leverages an embedded resource file that contains a zip of all the Python Standard Libraries, which nicely aligned with IronNetInjector’s configuration. Turla used the IronPython2 libraries and zipped them as an embedded resource.

alien vault std lib
Analysis of the file used by IronNetInjector from AlienVault

To load the libraries, you will need to get the executing assembly from the embedded resources. Once we have the file, we need to pull the path of the standard library directory using ResourceMetaPathImport. Finally, we can patch the directory to the Python Engine and we can use all the Standard Libraries. As previously mentioned, our code was heavily influenced by the work Byt3bl33d3r created for an IronPython implant and the project Zolom, which are both centered around IronPython2.

// Load Python Std Lib from embedded resources
Assembly asm = Assembly.GetExecutingAssembly();
dynamic sysScope = engine.GetSysModule();
var importer = new ResourceMetaPathImporter(asm, "");

Loading the IronPython Script

The next phase to replicate IronNetInjector, is to embed other types of executables and load them using IronPython since it has direct access to the .NET API. Turla leverages this to inject its RAT, however, we went with the easier solution and used our Python agent and modified it to be compatible with IronPython. This eliminates the need to load another stage to reflectively load an unmanaged DLL.

Taking a look at the IronNetInjector Python script from AlienVault, you can see some of the characteristics that we wanted to build into our version to properly emulate indicators. Things such as Base64Encoding, obfuscated variable names, and AES encryption.

alienvault ironpython analysis
alienvault ironpython analysis 2
Analysis of the IronPython payload used by IronNetInjector from AlienVault

Since we will be using our existing Python agent, we can test our C# executable to make sure the IronPython interpreter and standard libraries are properly embedded. You can embed the IronPython code in a few different ways. First, you can attach a python script and have it be loaded as an embedded resource, similar to how we load the zip file. Otherwise, you can go with the easier of the options and just Base64 encode the script and embed the text before compilation.

string B64PyCode = "{{ INSERT BASE64 CODE HERE }}";


IronPython Engine Console Issues

In order to test our solution, we compiled our C# launcher as a Console Application which provides a window to see output during execution. However, we don’t want to get a popup window during execution which would be a big red flag. So we had everything working, switch to our operational compilation settings, go to test and…we don’t get a callback. How can that be? Nothing was changed except turning the executable into a Windows Application.

It turns out that when you change the application type, there is no longer a console for the IronPython Interpreter to bind to, which causes it to fail. We decided that there was a simple solution for fixing this. We start a thread for the IronPython Engine to run inside of and we spawn a console. The trick is to open and hide the window, so we don’t get any strange indicators from flashing consoles. So we did what all good programmers do and borrowed code from Stackoverflow.

[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AllocConsole();

static extern IntPtr GetConsoleWindow();

static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);

const int SW_HIDE = 0;
const int SW_SHOW = 5;

public static void Main()
            new Thread(() =>


public static void ShowConsoleWindow()
      var handle = GetConsoleWindow();
      if (handle == IntPtr.Zero)
         ShowWindow(handle, SW_HIDE);

public static void HideConsoleWindow()
      var handle = GetConsoleWindow();
      ShowWindow(handle, SW_HIDE);

This is a similar issue that Hubbl3 experienced when he was building his Offensive VBA project. Excel required the login prompt to be spawned, but we did not want it to be visible to the user. We know this all seems a bit silly, but we are used to it when it comes to Windows products at this point. Plus, we didn’t want to dive into another rabbit hole while developing the basic functionality. (If anyone knows why IronPython requires a console, please tell us)

IronPython Agent Operations

Here is the final test to see how IronPython stacks up against our other implants. Generation is relatively simple since Empire 4.2 introduced the necessary files to generate IronPython as either an executable or shellcode. First, you will start the CSharp plugin (Roslyn compiler) and a listener. Next, you will use the csharp_exe stager to generate the IronPython executable.

empire stager
IronPython agent generation in Empire

Assuming everything is successful, we will launch the executable on our target machine and we will see the agent begin staging. When you go to the agents menu, you should see that the reported language report back as “ironpython” with the target process as “System.Diagnostics.Process”.

empire checkin
IronPython agent callback in Empire

Since the agent successfully connected back, we can explore some of the toolings that we are able to use. We are often operating within a shell, so making sure the interactive shell was compatible was an essential test. This required a bit of re-engineering since the original shell for Python was only compatible with Bash. We updated the code so we can interact directly with PowerShell commands while inside the interactive shell window, a huge advantage for our team.

empire shell
Interactive shell in Empire

Python modules tend to be something that our team does not use very often. Typically we rely on getting a Python agent back on a Linux box and we do all our operations through the interactive shell. However, with the shift to IronPython, there is now a good reason for us to integrate tools such as PyPyKatz into the Python modules in the future. Our example below uses the port scan Python module, which is compatible with both the IronPython and Python agents. Unfortunately, not all the Python modules are written in a way that works with both agents. So this is something we will have to update in the future.

empire portscan
Python portscan module using IronyPython in Empire

Finally, we have Phase 1 completed for rebuilding the IronNetInjector! We are able to fully host the IronPython Engine with the standard libraries within a single executable and then spawn an IronPython agent. The next steps from here involve the more intricate functionality like reflection that you would find in the PowerShell and C# agents. This functionality is not available within the original Python agent due to it not having access to the .NET APIs that IronPython can access. We plan to continue to evolve this capability and have some big plans on the horizon.

The post Rebuilding IronNetInjector – Turla’s IronPython Toolkit appeared first on BC Security.

If you like the site, please consider joining the telegram channel or supporting us on Patreon using the button below.


Original Source