Table of Contents


Send us your writeups!

Hack Dat Kiwi 2015 Auto-Write-Up

Before we get into the challenge write-ups, we want to thank you for your interest in Hack Dat Kiwi 2015! Participation was much higher than our anticipation and we had to rapidly expand our cloud to accommodate.

We will be providing a stand-alone challenge installer which you can grab from our website, run on a VM machine and have a copy of our challenge server ready to use. With that, you can easily hone your skills and practice this write-up.

The stand-alone challenge installer is ready to download. You need to unzip and run it on a flat Ubuntu 14.04 64bit machine. Just put it somewhere in the VM, and run the following commands:

unzip installer-public.zip
cd installer
sudo bash install.sh

Phone Lock 1

A very simple Javascript challenge. A look at the page's source code gives us the following important lines of code:

salt="a7274495d5e26749f2421c90c045a8a1";
valid="69b476f2868891b082e2ad7a309f6fc1";
//md5(salt+answer)

function buttonClick(e)
{
	if (locked) return false;
	var t=$("#result");
	t.val(t.val()+"X");
	result+=e.target.text;
	if (t.val().length>=4)
	{
		if (md5(salt+result)==valid)
		{
			alert("Flag is: "+md5(salt+result+result));
		}

On different runs, the value of salt and valid will change (time-dependent). This allows the server to detect cheating easier, and is also fundamental in the next challenge.

Basically the check to prevent brute-forcing of PIN is Javascript based, and all the information necessary for brute-forcing is already available in the JS code. We just have to find a pin that when concatenated to salt and hashed using md5, equals valid. Open up the Javascript console of your browser, and use the following code to get the valid pin:

for (i=1000;i<10000;++i) if (md5(salt+i)===valid) alert(i);

The script will find the valid pin and alert it. Feed it to the system to get the flag.

SSL Sniff

We have a pcap file that is the dump of some network activity. The challenge description tells us that it has been MITMd (man in the middle attack). Since it is SSL traffic (hence the challenge name), MITM should include the attacker's forged X509 certificate.

Since the X509 certificate will be passed on SSL traffic unencrypted (before initiating encryption), there is no encryption in this challenge. We just have to extract the certificate. SSLdump or Wireshark can easily do that.

An easier way though, is to do the following:

tcpdump -A -r dump.pcap | grep -i -e key -e flag

Extract ASCII text from PCAP file, then grep for key or flag.

Gaychal

A very simple reverse engineering challenge. Typical PHP worms use layers of encoding and decoding to hide their malicious behavior.

It is not always the best idea to manually decode these codes. An automated tool in a sandboxed environment is the best bet.

As for this challenge, there are about 50 layers of encodings on the PHP code. On top of that, each encoding layer uses a slightly different encoding function, so it is harder to create automated code to decode them. Finally, the first encodings are zipping and binarifying, so when you decode them, you will receive a huge chunk of data that will probably crash your text-editor or your system entirely.

Don't go at it the wrong way. You have to create code that solves this challenge, just like most of our other challenges. Since you don't know beforehand what kind of encoding functions are there, its best to use the PHP's interpretive functionality:

<?php
ini_set("memory_limit",-1);
$code=file_get_contents("gaychal.txt");
$code=str_replace("<php", "", $code);
while(1)
{
	$t=str_replace("eval","return", $code);
	$res=eval($t);
	if (!$res) break;
	echo strlen($res),PHP_EOL;
	$code=$res;
} 
echo $code,PHP_EOL;

The code handles your manual reverse engineering automatically. It first gets the content of the file, then in a loop, replaces EVAL with RETURN, so that instead of that code running on its own, we can run it and get its return value. Then, it evals it, and if a return value is available, keeps decoding. Otherwise, we have reached the end of the decoding and have run the actual code, so we need the code from step back, which is in $code. We echo that to see the original code of the challenge:

echo "the flag is ".md5("5+5=9<-- fix this"),PHP_EOL;

Just replace the 9 and re-md5. That would be the flag.

Phone Lock 2

This one is a tricky one. It probably deserved more than 100 points!!

The description is very important in this challenge. It tells us that they have a phone, and know the password (pin), and are pretty sure they know the right value, but no matter how much they enter it, it doesn't work.

So the pin is known, and has been entered a lot. A quick look at the code helps us understand why the system does not work. Some of its data is corrupted (that's common when you use cheap hardware). Some hex digits of salt and valid are replaced with x, which denotes they are corrupted. We have to find the correct value to fix the phone and find the flag.

salt ="6849af5387f6e7d94959e8faxxxxxxxx";
valid="a065a5134e775f5de5xxxxxxxxxxxxxx"; //md5(salt+answer)

function buttOnClick(e)
{
	if (locked) return false;
	var t=$("#result");
	t.val(t.val()+"X");
	result+=e.target.text;
	if (t.val().length>=4)
	{
		if (md5(salt+result)===valid)
		{
			alert("Flag is: "+md5(result+valid));
		}
		else

Since flag is combination of result and valid, we need to know both of them. Now valid misses 14 hex digits, each hex digit being 4 bits. That means it misses 56 bits. Salt on the other hand, misses 8 digits=32 bits. This is the fundamental part of this challenge. The hacker needs to have an idea of how much time is needed to brute force those numbers.

Lets get to the math. Possibilities of 10 bits is 210=1024. Thus, 32 bit is 4x1 billion (4G). Computers live in Gs. A 3 GHz CPU does 3Gs of instructions per second. An MD5 operation is roughly 1000-3000 instructions. You can easily benchmark that by running the following command:

time php -r 'for ($i=0;$i<1024*1024;++$i) md5($i);'

It should roughly take one second. To run 4096 of those will take 4096 seconds (1.13 hours). It is feasible, but not very fast. Fortunately, to break MD5 we don't need to cover the entire space. Based on the Birthday Paradox and the fact that many bits of valid are optional, we will find the result much much faster than in 1.13 hours. On an average computer, this will take about 10 minutes. However, 'valid' still can't be broke, as it is 24 bits bigger. Even finding the correct half part takes the same time.

So, valid is infeasible, but salt is not. Fortunately, valid is md5(salt+answer) so if we find the answer and salt, we also have valid and can find the flag.

Now to find the correct salt, we have to fill the Xs in salt with different hex digits, concatenate the answer, MD5, and then check if the first few bytes of the result match the first few bytes of valid. The pseudo-code would be:

for a very long time:
	seq = new 8 digit hex string
	temp = md5(first_24_digits_of_salt + seq + answer)
	if ( temp[0..18] == first_18_digits_of_valid )
		break; //win

Sounds straightforward. However, we also don't know the pin (answer in the above code)! The whole space for pin is 104, which is roughly 13 bits. That would make it too much (still 32+13<56) and infeasible. Knowing that all possible pins will make this infeasible (a thousand ten minutes instead of 10 minutes!) is the most important part in solving this challenge.

Taking a look at other parts of the challenge, we realize that there's nothing else that we can use at first sight. However, there are hints given to us. First, we know that the phone owner knows the password. So why didn't they give it to us? Did they feel like we don't need it? Did they omit because they wanted to test our hacker skills?

Another look at the keypad shows that some buttons are different in color! Those must be the digits used in the pin, because the phone owner frequently typed it in (and its cheap hardware, so leaves traces). In the page source, they are marked with css class 'b1' instead of 'bl', which looks pretty similar, and that's why you probably didn't realize by just looking at the code.

Ok, so we made some progress. Instead of 104, now we have 4!=24 possibilities (if we're not lucky), which is much better. This is now brute-forcable, taking roughly 10x24 minutes = 4 hours. Still, a sane mind would spend some more time digging, so that he can possibility reduce that time.

After 20 tries the code tells you 'maybe come back in a few minutes?'. That is another important piece. The challenge resets every (few) minutes. You should catch it at a time that has a password not comprosing of 4 digits. For example, you can come back at a luckier time, and realize that only the digit 3 is different in color. Voila! 3333 is the pin, and 10 minutes to finding the valid answer.

Finally, using a fast MD5 implementation is key here. Fortunately, Python and PHP and other high level languages have MD5 implemented in C, and are pretty fast at running it. You can also use OpenCL implementations if you're up for it. And you should minimize the high level code overhead in these kind of challenges.

Vigenre 1

A very simple Vigenere crypto. The chosen plaintext flavor, allows you to input anything you like and get the equivalent ciphertext. Since Vigenere is a classic crypto, its space is [A-Z], and A has a value of 0. Key is added to the plaintext to give the ciphertext, so providing an input of AAAAA... will show the key as ciphertext.

However, many participants made the error of repeating the key in their submitted flags. Key is KIWIKI, and it shouldn't be repeated. Of course if you repeat it, you get the same cipher, because that's a property of Vigenere, but the shortest key is the valid key.

Vigenre 2

An slightly harder variety, the known-plaintext. You can't choose the plaintext here, but you have the plaintext and the ciphertext. You just need to subtract plaintext from ciphertext (the first few words suffice) to get the ciphertext, considering that letters have values, e.g A is 0 and Z is 25.

Vigenre 3

This one is a little harder. It requires breaking the cipher, as no plaintext is available (cipher-text only mode). The first step in breaking Vigenere, is finding Key Length. That can be done using the Kasiski method for monoalphabetic ciphers.

However, we have chosen a sufficiently long key that most online tools will not work on first try. After finding the key length, a frequency analysis or index of coincidence attack can break the cipher.

Vigenre 4

All three previous challenges were there to give you a mindset about this one. Since this is called Advanced, it probably is an enhanced variety of Vigenere. These can be found on Wikipedia.

This is an autokey cipher, in which, first time the key is added to plaintext, but then the key itself is updated, by adding the plaintext to it. So the next piece of plaintext is encrypted using a different key.

This is still not a very hard challenge, but one must focus on the first few words of the challenge, because those are the only ones encrypted using the actual key. An algebraic solver can help a lot with solving such challenges. Ultimately, cryptographic knowledge is the main ingredient in solving these. So if you solved the first three, but couldn't figure out the fourth, you probably only used online tools!

SSL Sniff 2

This is a similar traffic from SSL Sniff, however, this time we have the server's private key. The private key was given to us to investigaet the MITM attack and realize what information was stolen.

By setting the private key in Wireshark and decrypting the traffic, key would be available.

This challenge was hard because it was based on a bug that exists in Wireshark. Wireshark has a bug that can't decrypt certain ciphers and key-exchanges on its own.

SSLDump, a rather old command line tool, can take care of that. However, SSLDump requires few patches to work with recent OpenSSL.

Kiwi Forum

You are given the source code to the forum software, and know that the flag is admin's avatar image. This avatar is available in db/ folder, with file format 00000.bmp where the number is the user-id. So admin should probably be 1 or 0 or 2.

The app/avatar.php code is as follows:

<?php #avatar.php
lib("users");
session_write_close();
$id=$_GET['id']*1;
$isAdmin=$_SESSION['user']['access'];
$initial_time=time();
function timeout()
{
	global $initial_time;
	static $timeout=null;
	if ($timeout===null) $timeout=rand(3,10);
	if (time()-$initial_time>$timeout) 
	{
		flush();
		die();
	}
}
if (!$isAdmin)
	declare(ticks=1);

	lib("download");

if (!$isAdmin)
	register_tick_function("timeout");
$dl=new \jf\DownloadManager();

if (!$isAdmin)
{
	$dl::$BandwidthLimitSpeed=1024/8; //128 bytes
	$dl::$BandwidthLimitInitialSize=1024; //1KB
}	
$dl->Feed(AvatarFile($id));

Basically, if the user is admin, it gives the avatar image out, but if the user is not admin, it limits the bandwidth to 128 byte per second for files which are larger than 1 KB to preserve bandwidth.

The fancy timing functions are there to kill this script somewhere between 3 and 10 seconds, to prevent long pulling and downloading the image 128 byte at a time!

Unfortunately, the admin avatar image is more than 1KB. How do we know? Look at the code for user signup:

<?php #users.php
function AvatarFile($ID)
{
	return 	$file=realpath(__DIR__."/../db/")."/".str_pad($ID, 5,"0",STR_PAD_LEFT).".bmp";
}
function GenerateUserKey($ID,$admin)
{
	lib ("purecaptcha");

	$key=implode("\n",str_split(RandomString("16"),6));
	$textRender=new PureTextRender();
	list($width,$height)=$textRender->text_size($key);
	$bitmap=$textRender->text_bitmap($key);
	if ($admin)
	{
		ini_set("memory_limit",-1);
		ini_set("max_execution_time",-1);
		$scale=200;
		$bitmap=$textRender->scale_bitmap($bitmap,$width,$height,$scale,$scale);
		$width*=$scale;
		$height*=$scale;
	}
	$bmpdata=$textRender->generate_bitmap($width,$height,$bitmap);
	$file=AvatarFile($ID);
	file_put_contents($file, $bmpdata);
	return true;
}
function CreateUser($username,$password,$email,$admin=false)
{
	if (sql("SELECT * from users WHERE username=?",$username))
		return "Username already in use.";
	lib("random");
	$salt=RandomString(32);
	$password=md5($salt.$password);
	$access=($admin===true);
	$ID=sql("INSERT INTO users (username,password,salt,email,access) VALUES (?,?,?,?,?)",$username,$password,$salt,$email,$access);
	if (!$ID)
		return "Unable to create user.";

	sql("INSERT INTO profile (ID,alias) VALUES (?,?)",$ID,$username);

	return GenerateUserKey($ID,$admin);
}

Apparently, if the user is admin, the avatar generated is scaled 200 times in width and height! This means that the typical 6KB avatar will be 40,000 times bigger, more than 100 MBs.

A look at other parts of the application show no apparent bug. SQL functions are properly used, and no XSS seems to exist. CSRF will not be helpful in this case as well. Lets take a look at the framework loader scripts:

<?php #loader.php
function do404()
{
	header("404 Not Found");
	echo "<h1>404 Not Found</h1>";
	echo "<p>The requested URL ".htmlspecialchars($_SERVER['REQUEST_URI'])." not found.

"; exit(0); } $request=$_GET['__r']; unset($_GET['__r']); $parts=explode("/",$request); if ($parts[count($parts)-1]=="") $parts[count($parts)]="index"; $file=realpath(__DIR__."/static/".implode("/",$parts)); if ($file and is_file($file)) { if (!$file) do404(); require_once __DIR__."/lib/download.php"; $x=new \jf\DownloadManager(); $x->Feed($file); } else { $file=realpath(__DIR__."/app/".implode("/",$parts).".php"); if (!$file) do404(); require_once __DIR__."/load.php"; require $file; }

Interesting! So all requests to this web app are forwarded to this loader script (by the web server). It seems like the request is put in __r get parameter. The application breaks the request down at slashes, and assumes index for the last part if empty. Then it checks to see if the request is for a static file (e.g css, js, img), or a dynamic PHP app. If the file exists in static folder, loader starts the download manager and feeds the file to the client.

If the file is not a static file, and it exists (realpath), it loads the framework (load.php) and then runs the file.

The second part is not very useful, because the file has to end in .php. The first part however, is useful, and doesn't enforce the path at all! The following get parameter will take care of downloading the flag bitmap:

?__r=../db/00001.bmp

Math Quiz

Since we don't have the source code or any indications of it, the first step is to try to cause the app to crash and give us error information.

That's easy in this case. Just replace one of the answers to the presented question with a single quote, and send the form. The following error will pop:

Error 4: syntax error, unexpected '';' (T_ENCAPSED_AND_WHITESPACE) in /var/www/html/web/math-quiz/index.php(44) : eval()'d code on line 1

Fortunately, that error is clickable, and gives us the source code! The important piece of the code is as follows:

function filter($expression)
{
    if (strpos($expression, ";")!==false) return null;
    $allowed_functions=explode(",","abs,ceil,cos,floor,log,max,min,md5,pi,pow,rand,round,sin,sqrt,tan");
    preg_match_all("/([a-z]+?)\s*\(/i", $expression, $matches);
    if ($matches)
        foreach ($matches[1] as $match)
            if (!in_array($match, $allowed_functions))
                return null;
    return $expression.";";
}
function evaluate($question,$answer)
{
    eval('$x='.filter($answer));
    $res=" ".$question[0];
    for ($i=1;$i<strlen($question);++$i)
    {
        if ($question[$i]=="X" and is_numeric($question[$i-1]))
            $res.="*X";
        else
            $res.=$question[$i];
    }
    $res=preg_replace("/(\d+|X)\s*\^\s*(.*?)\s/", "pow($1,$2)", $res);
    $res=str_replace("X", '$x', $res);
    
    $question=filter($res);
    if (!$question)
        return null;
    $question=str_replace("=", '==', $question);
    if (verbose()) echo $question.PHP_EOL;
    eval('$res='.$question);
    return $res;
}
function format($expression)
{
    $res="";
    $mode=null;
    for ($i=0;$i<strlen($expression);++$i)
    {
        $c=$expression[$i];
        if ($mode=="power")
            if ($c==" ")
                $c="</sup> ";
        if ($c=="^")
        {
            $c="<sup>";
            $mode="power";
        }
        elseif ($c==" ")
            $c="";
        $res.=$c;
    }
    return $res;
}
if (isset($_POST['answer']))
{
    $index=$_POST['index']*1;
    $correct=(evaluate($question[$index],$_POST['answer']));
 

The code sends our answer to the evaluate function, as well as the question. The evaluate function filters our answer (filter function), then evaluates it into $x. The rest of it is not important because it has nothing to do with our input.

The filter function first makes sure that there are no semicolons in our answer, so that we can't append code. This itself can be bypassed if needed, by closing and re-opening PHP tags, as a single statement in a PHP tag doesn't need semicolons. There are also other statements that don't need semicolons.

Next, filter extracts all function calls in our answer and makes sure that they are to allowed mathematical functions. MD5 is a red herring in the list, as only functions with a-z in their name will be checked. This means that functions with numbers or underlines are fine to use!

The regex '/([a-z]+?)\s*\(/i' basically means any name that is followed by some spacing and an open paranthesis, should be checked against the allowed function list. The regex assumes that function calls are in the format 'func(...)', however, that does not have to be true. For example, func/**/(...) is also a valid function call format that does not fall into that category.

Using these multiple vulnerabilities, the attacker can easily input answers that execute arbitrary PHP code. The flag is inside the questions.php code, at the beginning.

Virtual Virtue

A simple yet nasty reverse engineering challenge. The binary uses polymorphism (virtual function tables) and is compiled with no RTTI for a particular version of Mac OS X, so that we can't run it, yet we can't easily statically reverse engineer it either (because of lack of RTTI information and complicated virtual tables).

The equivalent C++ code would be as follows:

struct A {
	static int score;
	A(char *t):count(0)	{
		check(t);
	}
	int count;
	virtual void check(char *t)	{
		A::score-= !strcmp(t,"passwerd");
	}
};
int A::score=0;
struct B: public A {
	B(char *t):A(t)	{
		check(t+4);
	}
	virtual void check(char *t)	{
		if (strlen(t)>4)
			A::score+= *t=='Z';
		else
			A::score+= *t=='z';
	}
};
struct C: public A {
	C(char *t):A(t+1) {
		check (t+5);
	}
	virtual void check(char *t)	{
		int q=3;
		q+=3;
		A::score+= *(t+3)=='Z'+3;
	}
};
struct D: public B,C {
	D(char *t):B(t+1),C(t-1) {
		check(t);
	}
	virtual void check(char *t)	{
		B::score+= *t>'z' && (char)((*t)+132)<3 && (char)((*t)+132)>0;
	}
};

int main(int argc, char * argv[])
{
	int temp,temp1;
	if (argc<2)
		return -1;
	char *password=argv[1];
	char *t=password;
	D d(t);
	C c(t+1);
	B b(t+2);
	for (int i=0;i<3;++i) {
		d.check(t+i);
		c.check(t+i*2);
		b.check(t+i*3);
	}
	A::score-= t[4]!='e';
	A::score-= t[8]!='1';
	int sum=0;
	for (int i=0;i<strlen(t);++i)
		sum+=t[i];
	if ((int)(A::score-strlen(t))>0 && sum<1000) printf("You're the boss!\n");
	return 0;
}

Different characteristics of your input change your score. If your score is above 0 and the total ascii value of your input is below 1000, you win and your input will be a valid flag.

Your input (password pointer), is first sent to D's constructor. D's constructor does the following: 1. B's constructor with password+1 2. C's constructor with password-1 3. D's check with password

This process follows until the following things are called overall:

D(password)
-B(password+1)
--D::check(password+1+4)
-C(password-1)
--A(password-1+1)
---D::check(password-1+1)
--D::check(password-1+5)
-D::check(password)

C(password+1)
-A(password+1)
--C::check(password+1+1)
-C::check(password+1+5)

B(password+2)
-A(password+2)
--B::check(password+2)
-B::check(password+2+4)

D::check(password+0)
C::check(password+0)
B::check(password+0)

D::check(password+1)
C::check(password+2)
B::check(password+3)


D::check(password+2)
C::check(password+4)
B::check(password+6)

score-= password[4]!='e'
score-= password[8]!='1';

Now lets simply that before converting it to code!

D::check(password+5)
D::check(password)
D::check(password+4)
D::check(password)

C::check(password+2)
C::check(password+6)

B::check(password+2)
B::check(password+6)

D::check(password+0)
C::check(password+0)
B::check(password+0)

D::check(password+1)
C::check(password+2)
B::check(password+3)


D::check(password+2)
C::check(password+4)
B::check(password+6)

score-= password[4]!='e'
score-= password[8]!='1';

Ok. Those are the functions that will be called. The +6 means that the input should be at least 6 chars long. Since C also checks t+3, that means that the input should be 9 chars long. Any longer than that, and we get negative scores (-strlen(input)). Now let's see what B::check, C::check and D::check are, and correlate those with the list above:

void B::check(char *t)	{
	if (strlen(t)>4)
		A::score+= *t=='Z';
	else
		A::score+= *t=='z';
}
void C::check(char *t)	{
	int q=3;
	q+=3;
	A::score+= *(t+3)=='Z'+3;
}
void D::check(char *t)	{
	B::score+= *t>'z' && (char)((*t)+132)<3 && (char)((*t)+132)>0;
}

Score is a static variable and is shared by all.

B's check() gives score if (len(input)>4 and input[0]=='Z') or input[0]=='z'. We call this cond_B.

C's check() gives score if input[3]=='Z'+3, which is ']'. We call this cond_C. If we call the basic condition of C as cond_C2, then cond_C(x) == cond_C2(x+3).

D's check() gives score if input[0]>'z' and input[0]+132<3 and input[0]+132>0. We call this cond_D.

Now lets correlate these findings with the list of calls:

cond_D(input[0])
cond_D(input[4])
cond_D(input[0])
cond_C2(input[5])
cond_C2(input[9])

cond_C2(input[5])
cond_C2(input[9])

cond_B(input[2])
cond_B(input[6])

cond_D(input[0])
cond_C2(input[3])
cond_B(input[0])

cond_D(input[1])
cond_C2(input[5])
cond_B(input[3])

cond_D(input[2])
cond_C2(input[7])
cond_B(input[6])

score-= input[4]!='e'
score-= input[8]!='1'

And since none of the checks modify the values, we can group them together:

cond_D(input[0])
cond_D(input[0])
cond_D(input[0])
cond_B(input[0])

cond_D(input[1])

cond_B(input[2])
cond_D(input[2])

cond_B(input[3])
cond_C2(input[3])

cond_D(input[4]);

cond_C2(input[5])
cond_C2(input[5])
cond_C2(input[5])

cond_B(input[6])
cond_B(input[6])

cond_C2(input[7])

cond_C2(input[9])
cond_C2(input[9])

score-= input[4]!='e'
score-= input[8]!='1'	

Now things are much more obvious. We need cond_D on char 0, to get 3 points. cond_D on char 1, cond_B or cond_D on char 2, cond_B or cond_C2 on 3, cond_D on 4, cond_C2 on 5, cond_B on 6, cond_C2 on 7 and cond_C2 on 9. Each one of those combinations gives a flag.

Lets try the first choice. Char 125 fits cond_D (}). Z fits cond_B, and cond_C2 matches ]. 4 and 8 need to be e and 1 as well. That would give us '}}}Ze]z]1'.

Ugly Bin

Ugly bin is a self-modifying binary challenge. It does not modify itself in memory, but it modifies its own file. By static inspect, it is obvious that the file uses fork and exec to run itself. However, to run the same binary, fork should be enough. With closer inspection, the binary copies itself to bin, changes some bytes in the copy, and then fork/execs the temp binary.

This means that those bytes are either invalid or different in the original binary. A sequence of invalid bytes on a CISC binary means that the disassembler will most likely wrongly disassmble most of the instructions after that point.

What we need to do, is make a copy of the binary, modify the invalid bytes on that copy, and then decompile it. We will arrive at a binary with a set of condtions that when satisfied, gives us the key.

Kiwi Forum 2

This time, the LFD bug on loader is patched. We have to use a logical flaw to leak the image.

The avatar of admin, is just scaled of the normal avatars. It still has the same amount of useful data in its pixels, just repeated a hundred times each. The logical solution is to use the HTTP_RANGE available by the downloader to download certain points of the image, and then make a small image with it.

for j in 0 to small_avatar_height
	for i in 0 to small_avatar_width
		byte_position= header_size + scale * j * big_avatar_width + i * scale
		image[j,i]=download(byte_position)

And we will have the flag by downloading a multitude of bytes!

Leet Maze

You start by digging (TXT DNS record) track.dat.kiwi. You get another domain. You dig that too. You get another. This goes on for a while. Then suddenly the domain you dig gives you two or three domains instead of one! At this point, you choose to follow one, but it ends in a loop. You choose another one and go forward.

That's what a maze is. And no, we shouldn't solve mazes with trial and error. We should solve them using BFS or DFS. However, this maze is leet. After a while, the results are not in normal, ascii form. They are in leet form. U||$@C]{.dat.kiwi, is an example, meant to be unsack. Leet is not cool. It seriously isn't, but heck, people who use it think it is.

We need to de-leet, but then how do we make our tool de-leet? One good way is to use the share/words file, and grep for words ending in ack (all of those seem to share that). Then we figure out the characters that are obvious ($ is S right?), and find candidates in words file, and keep the DFS/BFS going.

Just like the rest of our challenges, if you manually follow this, it will take hours. But if you create a simple script, and use a DFS or BFS, it should solve in a few minutes. On the last node of the maze, you get flag is XXXX.

Blue Freeze

The write-up for this challenge is long and from one of our participants. Use this link to browse it.

Simple Stegano

For every message that we give, an image is returned. The image should have the message embedded via steganography. The message in the original image will give us the key.

Lets start off by comparing images. Use the following code to see the different pixels in images:

#!/usr/bin/python
import Image
import sys
img = [Image.open(sys.argv[i]) for i in range(1,3)]
pix = [img[i].load() for i in range(0,2)]
c = 0
for i in range(0, img[0].size[0]):
    for j in range(0, img[0].size[1]):
        if pix[0][i,j] != pix[1][i,j]:
			print "dif in %d,%d :" % (i,j), "({0}={0:b} vs {1:b}={1})".format(pix[0][i,j],pix[1][i,j])
			c += 1
print "total difs:", c

Now generate images for inputs A, AA and C. Compare A and C first. You see that they are different only in 1 pixel, and by 1 bit, and that pixel is in position (0,143). Now Compare A and AA. They are different in two spots, (0,146) and (0,152), still only in 1 bit.

The different bit is the LSB, and the difference positions correlate with bit positions in the text, i.e 0,146 and 0,152 are 6 bits apart (A is 1000001). It seems like the bits of the text are stored in LSB of the image, but not on all pixels.

Since it started at offset (0,143) we realize that it only stores in black pixels of the image. Now extracting the original message is just a matter of extracting all LSBs, and then converting them into ASCII and writing into a file.

In case we had problem with figuring out which pixels have the LSB, we can compare a big input of As with a big input of Cs and figure out the important pixels.

Captcha Password

This is our favorite challenge of this CTF! (aside from experimentals). This is a multi-step, chain-exploit challenge, that requires a lot of persistent and skill. Lets begin

First step is to obtain the source code, because we wouldn't want you to wander aimlessly. By converting the binary behind the page, you get the source code for the challenge:

<?php
session_start();
require_once __DIR__."/lib/timer.php";
require_once __DIR__."/lib/db.php";
class RunModes
{
	const Develop=1;
	const Deploy=2;
}
function binarify($data)
{
	for ($res="",$i=0;$i<strlen($data);++$i)
		$res.=str_pad(sprintf("%b",ord($data[$i])),8,"0",STR_PAD_LEFT) ;
	return $res;
}
if (isset($_SERVER['HTTP_HOST']) and strpos($_SERVER['HTTP_HOST'],"localhost")!==false)
	$runMode=RunModes::Develop;
else
	$runMode=RunModes::Deploy;
$timer=array(new Timer(),new Timer(),new Timer());
if (count($_POST) and $_SESSION['captcha']===$_POST['captcha'])
{
	$timer[1]->Count();
	$timer[2]->Reset();
	$secret=base64_decode(sql("SELECT * FROM secret WHERE ID=1"));
	$timer[2]->Count();
	$timer[1]->Reset();
	
	$input=str_pad($_POST['secret'],strlen($secret),"-");
	$input=str_split($input);
	$secret=str_split($secret);

	$match=0;
	foreach ($input as $k=>$v)
		for ($i=0;$i<256;++$i)
		if ($v==chr($i))
		for ($j=255;$j>=0;--$j)
		if ($secret[$k]==chr($j))
		if ($i==$j)
		for ($x=0;$x<count($secret);++$x)
		for ($y=$x;$y<count($input);++$y)
		if ($x==$k)
		if ($secret[$x]==$v)
		if ($k==$y)
			$match++;
	if ($match==count($secret))
		die(binarify(implode("",$secret)));
}
?>
<form method='post'>
	<input type='text' name='secret' size='60' value='' placeholder='enter the secret code'/><br/>
	<input type='text' name='captcha' placeholder='captcha' /><img height='16px' src='captcha.php' />
	<input type='submit' />
</form>
<?php

$timer[0]->Count();
$timer[1]->Count();
if ($runMode==RunModes::Develop)
	printf("Time: %.3f seconds (%.2f PHP, %.2f SQL)",$timer[0]->Time(),
		$timer[1]->Time(),$timer[2]->Time());
?>
<link rel="stylesheet" href="style.css"></link>
<div onselectstart='return false;' ondragstart='return false;' id='bg'>
<?php echo binarify(file_get_contents(__FILE__)); ?></div>

Ok. So we need to find $secret. The challenge has a captcha that prevents us from guessing too much, and also a debug timer which only works in localhost.

Bypassing the code that detects localhost is easy, because all $_SERVER[HTTP_*] parameters are controller by the client. Just send two Host headers, one with the original value, and the second with localhost, and the server will think you are in localhost mode and show you debugging information (including timing).

Also, the captcha is not used properly. The CAPTCHA is only renewed on <img src='captcha.php'> line, so if we don't browse that image, the captcha will not renew. Also, it is not invalidated after each check, so if we get one captcha, type its value in our script, and then don't get any other captcha, that original captcha should work as long as our session is valid!

Step three is to do the actual leak of the secret. There are a bunch of weird for loops in the middle of the script. Lets see what they do. First, our input is padded with dashes to be of equal size to the secret. Then comes the nasty loops.

The first for (foreach) loops over characters of input. Then checks all 256 ascii characters against it. On a match (which will always happen), checks all 256 ascii characters with the same position character in $secret. If that matches (which always will), checks if out match is the same as $secret's match. So far, it is basically checking if input[i]==secret[i], but doing so very stupidly and slowly.

The next two fors will only happen if this character of our input matches the same character position in secret. Two loops loop over all characters of input and secret, doing useless stuff. Match only will be incremented once there.

Since we have two 256 iteration loops that always execute, and two length(secret) iteration loops that only execute for each character that matches in input and secret, when characters match things will be much slower. This can be used to launch a timing attack.

However, this timing attack gets slower and slower the more characters that match between input and secret. But, we don't need to match all characters in each test, just the i-th position character.

So we bypass the captcha, and loop over characters in the input gracefully (or the firewall will block us), and every time match one character, putting dashes for the rest, or it gets super slow and also boggles down the server.

After a long loop (256 * length of secret) which can be parallelized, we figure out a secret that is binary data of size 1939 bytes.

Now the code is supposed to output binarify of this secret if we give it the right secret. So, we need to binarify that. Lets put that binary in a file.

The length 1939, is a little odd. The first thing any hacker will do, is figure out why that number. A mere factorization of that number gives us 7 * 277. Cool! Lets break down that binary by 7 and 277 chars separately. 7 doesn't give us anything useful, however, 277 seems informative: some patterns start to emerge in the binarified file.

Because only few bits are ones in the binary file, one can hypothetize that it is not actual data, but a pattern. The following pattern emerges after a little toying:

NTI

The experimental challenges are very beautiful! However, there was a stupid bug in the MySQL lexer available in these challenges that ruined everything. Most participants could leak the flag using that bug, and didn't even try to discover the beauty of the challenge!

Basically, these three are application firewalls for injection attacks (sql injection in this case). The first one, NTI, uses Negative Taint Inference to detect injections.

NTI compares user-input with the SQL query, and if a part matches (or is relatively similar), marks it as tainted. If a tainted part is an important SQL token (i.e is not just data, which is supposed to be from user input), NTI detects the attack and blocks the request.

NTI uses string distance to compute differences between input and query, for example if you enter UNION SELECT /**/ and it turns into UNION SELECT /*ABC*/ in the query, it can still detect it. However, it can only do so up to a certain threshold of changes. And that's how we crack it.

Taking a quick look at both source files, we see the following:

<?php #index.php
require_once "config.php";
if (isset($_GET['password']))
{
	$res=select("*", "secrets","password='{$_GET['password']}'");
	if ($res and $obj=$res[0])
		echo "flag as answer"," ( {$obj->flag} )",PHP_EOL;
	else
		echo "Invalid password.";
}

?>

Do you have the password? Enter it below.

<form method='get'>
	Password: <input type='text' name='password' /><input type='submit' />
</form>

<a href='ticket.php'>Contact Us
<?php #ticket.php
require_once "config.php";
if (isset($_POST['message']))
{
	$query="INSERT INTO tickets (sessionid,timestamp,title,message) VALUES 
		('".session_id()."',".time().",'".htmlspecialchars($_POST['title'])."','"
		.htmlspecialchars($_POST['message'])."')";
	$res=mysql_query_($query);
}
#...

$res=select("*","tickets","sessionid='".session_id()."'","", "timestamp DESC","10");
if ($res)
{
	echo "<h2>Your latest tickets:";
	foreach ($res as $record)
	{
		echo "<div class='message'>
		<span class='title'><span class='smaller'>
		(".date("Y-m-d H:i:s",$record->timestamp).") #{$record->ID}</span> {$record->title} </span>
		{$record->message}
		</div>",PHP_EOL;
	}
}

index.php is there for us to test different injections and fail, because NTI should protect them all. However, because of the lexer bug, some of the inputs make NTI not recognize important SQL tokens, even though it properly matches our input, and thus it will just ignore it!

The real challenge is in ticket.php. As you see, when you enter a ticket, it stores it in the database, but in an XSS-safe format. That is weird. Text should be stored normally in the database, and only XSS-safed when output to the browser. That htmlspecialchars() function makes enough changes for us to bypass NTI.

For example, try inserting the following in ticket, to see your message:

foo' ,(select flag from secrets limit 1)) -- >>>>>>>>

It should safely pass the NTI filter, because all those > characters will turn into &gt;, and that makes the difference between input and SQL query significant!

Now that NTI has passed, the flag should be visible in the message part of your submitted ticket. Grab it and enjoy!

PTI

Similar to NTI, PTI checks application-specific strings against the SQL query instead of user-input. If any important part of the SQL query is not coming from the application strings (i.e the strings that are used in the PHP application, not from the input), then it probably is used input and is attack.

Bypassing PTI is considerably harder than bypassing NTI, because every single thing that you use in your injection, should be available inside the application code!

The same query from NTI for example, will fail miserably, saying:

Attack Detected (PTI:108)
     Query: INSERT INTO tickets (sessionid,timestamp,title,message) VALUES ('2njpac43hrsomrhk0vkji7cn41',1448061983,'' ,(select flag from secrets limit 1)) -- <<<<<<<','')
  Markings: kkkkkk kkkk iiiiiii @iiiiiiiii@fffffffff@iiiii@iiiiiii@ kkkkkk @ssssssssssssssssssssssssssss@nnnnnnnnnn@ss @@kkkkkk iiii kkkk iiiiiii kkkkk n@@ cccccccccccccccccccccccccccccccccccc
     Match: 111111111111111111111111111111111111111111111111111111111111111110000000000000000000000000011000000000011101000000000000000000111111100000000000000000000000000000000000000000011111
  Position:                                                                                                             *

As you can see, the extra query (select flag ...) is not from within the application, and PTI recognizes that rather quickly. To bypass PTI, we have to adapt our query. We can grab a list of strings available in the application by having its source code (which we do).

After careful modifications, we figure out the following injection payload which comes from the application. The "SELECT " part comes from config.php, and should have two spaces to match exactly what we had there. The paranthesis come from index.php, where flag is supposed to be output. "flag as answer", is a string available in index.php. We have to use that, because we can't use flag directly and alone. "FROM" comes from config.php, and "secrets" comes from index.php. "LIMIT 1" also comes from config.php. The comment part "#" comes from ticket.php, where date is outputted!

foo' , ( SELECT  flag as answer FROM secrets LIMIT 1 )) #

And this will give us the flag in the ticket message.

HTI

HTI is considerably harder, because we have to satisfy both NTI and PTI. For breaking NTI, we need modifiable samples (e.g >) that are repeated, however, that does not exist in the code, and PTI will detect that. To bypass PTI, you have to use input that exactly appears in the source code, however, that input will be detected by NTI because it is nice and well formed!

The trick in this step is to exploit the parts that neither PTI nor NTI care about. The data parts of the query. After a little messing around, you realize that data fields, denoted by 's' or 'n' in markings, are ignored by both PTI and NTI, because they are data and pose no security threat in the SQL.

Simply put the modifiable inputs in a data block, and PTI will ignore them, and NTI will be unable to match the input because it changes too much:

foo' , ( SELECT  flag as answer FROM secrets WHERE 0='0>>>>>' LIMIT 1 )) #

The other parts still have to match PTI though. I bet you had a lot of fun if you actually did solve this!