Tags: web random 

Rating: 5.0

# Two Sides of a Coin

two-sides-of-a-coin.zip (the flask app provided for the challenge, all the code, listed out below)

The challenge was a bulletin board, going to the url gave you a list with the following posts:

=> Buying insomnia pills

=> Selling PlayStation 4

=> Selling baby toys

=> All for $1

=> Not selling anything, just kidding

=> Buying digital piano

=> Selling Ferrari

=> Cargo delivering

=> Will code for food

=> Still buying insomnia pills

First we checked the suspicious "Not selling anything, just kidding".

It said it was posted exactly at 09:00.

We had the flask app code for tha challege:

=> Dockerfile
FROM python:3.6

RUN pip install flask

COPY app/ /app/



CMD python /app/app.py

=> app/templates/add.html
<title>Bulletin Board</title>
<form action="/add" method="POST">

Title: <input name="title">


<textarea name="text"></textarea>

Extra notes (visible only to yourself):

<textarea name="text_extra"></textarea>

<input type="submit">

=> app/templates/index.html
<title>Bulletin Board</title>
Bulletin Board
{% for item in data %}

{{ item.title }}

{% endfor %}
{% if not readonly %}


{% endif %}

=> app/templates/view.html
<title>Bulletin Board</title>
{{ data.title }}

{{ data.text }}

{% if data.text_extra %}

{{ data.text_extra }}

{% endif %}
{% if data.url_viewer %}

See as guest

{% endif %}
<font color="lightgray">Posted at {{ data.posted_at }}</font>

=> app/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():
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)

In view route we see we have text_extra if the id passed to the route is an editor_id for the post.

Also we can see the way the post is saved and the viewer and editor ids are created using the time for posted_at as seed for random. So we can make our own code to test it out.

After that we supposed the "Buying insomnia pills" and "Still buying insomnia pills" were suspicious. We supposed one is posted at exactly midnight and the other, 0.0001 millisecond before midnight. Which we tested and we got the same viewer_id. We got the editor_id and saw that the flag is somewhere in the middle. So we thought to bruteforce the timestamps.

**IMPORTANT NOTE**: We needed to see the timezone offset, since maybe the server time and our time is different and we need the exact number used for random seed. The "Not selling anything, just kidding" was a good thing to test it on. We got a 1hr offset.

Here's the code we used for the solution. Don't judge it too harshly it is just a POC. :)

=> solver.py
#!/usr/bin/env python3
import string
import random
import time
from dateutil.parser import parse

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

# Gotten experimentally
my_timezone_offset = 3600

# Ones we were sure of
exact_datetimes = [
'2020-09-22 00:00:00.0000 UTC',
'2020-09-22 09:00:00.0000 UTC',
'2020-09-22 23:59:59.9999 UTC',

for datetime_str in exact_datetimes:
timestamp = parse(datetime_str)
time_in_ms = float(timestamp.strftime("%s.%f")) + my_timezone_offset
id_viewer = get_random_id()
print('id_viewer', id_viewer)
id_editor = get_random_id()
print('id_editor', id_editor)

# Timestamps unsure of and the 4-letter start of their viewer_id
uncertain_datetimes = [
['2020-09-22 00:00', 'n3jx'],
['2020-09-22 00:07', 'zzdu'],
['2020-09-22 04:25', 'bctf'],
['2020-09-22 13:37', 'vpir'],
['2020-09-22 14:12', 'wanl'],
['2020-09-22 20:09', '6645'],
['2020-09-22 21:09', 'cfdd'],
# We can bruteforce the seconds and miliseconds for all uncertain timestamps
# and when we get the correct viewer_id we know we have the correct editor_id
for datetime_str, view_id in uncertain_datetimes:
found = False
for s in range(60):
# No need to go on after it finds the correct timestamp
if found:
for m in range(10000):
timestamp = parse(datetime_str+":"+str(s)+"."+str(m)+" UTC")
time_in_ms = float(timestamp.strftime("%s.%f")) + my_timezone_offset
id_viewer = get_random_id()
if id_viewer.startswith(view_id):
print('id_viewer', id_viewer)
id_editor = get_random_id()
print('id_editor', id_editor)
found = True

Here are all the posts data:

title => Buying insomnia pills
posted_at => 2020-09-22 00:00:00.0000 UTC
id_viewer => 69if9kbky7rhhabku227u2vbdjahhp5j
id_editor => gcncpaj4wqlk3zexnsgakwocdcrz7jrv
text => I can't sleep anymore. Does anyone have insomnia pills? I can drive to your place right now if needed.
text_extra => I am tired, so posting this exactly at midnight.

title => Selling PlayStation 4
posted_at => 2020-09-22T00:00:33.3333 UTC
id_viewer => n3jxzus2yv2vujsnxx2vgsu94b90mn4c
id_editor => 51vnjdckam1dqil9lgy22ykbuko2e6fp
text => Price $500.
text_extra => Bought it previously for $400 :-)

title => Selling baby toys
posted_at => 2020-09-22T00:07:33.7937 UTC
id_viewer => zzduxgm7wn8fireq07cgpk4nf8n34ea0
id_editor => o0lbdfdrw8kawpk2yt8d44d4vwk43016
text => My son grown up, so I don't need some toys anymore. Come and see.
text_extra => Let's see how much I can get from it.

title => All for $1
posted_at => 2020-09-22T04:25:06.3345 UTC
id_viewer => bctf0eua8o7nl8uv9bpn6yho41p52wel
id_editor => v6s6flflu64wq8mcebwywwgue3d3ot6s
text => Come to my backyard and check yourself.
text_extra => URL looks interesting indeed. However, the flag is not here.

title => Not selling anything, just kidding
posted_at => 2020-09-22 09:00:00.0000 UTC
id_viewer => eqgqdsvvfuizy8zn1albdpq7szjd13py
id_editor => brd6ogfmhuyc2unkuwv6yzy7kstkvg0a
text => Look how cool I am. Posted this exactly at 9 a.m.
text_extra => (Like a boss)

title => Buying digital piano
posted_at => 2020-09-22T13:37:10.1010 UTC
id_viewer => vpir71pn503fw1cxhd8bwz8e9fcqvnl7
id_editor => 3fbl8598ogc9bmdokyq20z9bpxrpovrr
text => Yamaha or similar.
text_extra => You're on the right track.

title => Selling Ferrari
posted_at => 2020-09-22T14:12:12.9998 UTC
id_viewer => wanlon9q86hqarsilau6y6s499g4eixv
id_editor => dhdgweruwpd25cndsa2kyrqzq42wkbuj
text => 105,000 EUR -- for the same price I bought it before.
text_extra => It's too cool for me.

title => Cargo delivering
posted_at => 2020-09-22T20:09:39.1234 UTC
id_viewer => 6645nz5nxux67n69yx6armiov12hzgox
id_editor => raf54kfwox47uypahp1ngpxucify5e45
text => Any time, any weight, anywhere!
text_extra => Almost there, mate...

title => Will code for food
posted_at => 2020-09-22T21:09:31.3371 UTC
id_viewer => cfddcxulrhohtilq03qke9v0iqwddmvz
id_editor => k6wfi4zwhdcwqtqgay57djxpcv9gb7fk
text => If you're interested, drop me a message.
text_extra => BCTF{numb3rs_from_PRNG_are_n0t_really_rand0m} <- FLAG

title => Still buying insomnia pills
posted_at => 2020-09-22 23:59:59.9999 UTC
id_viewer => 50gwkm297ip10bif2trwe58ylxj15xeo
id_editor => zgka068rfqqejrv20htxrmjgk6brhq2k
text => Anyone? Please help.
text_extra => It is the end! Flag should be somewhere earlier.

ideaengine007Sept. 27, 2020, 7:53 p.m.

Clear and concise one!