Baby PHP (Web Challenge WriteUp) — hacklu CTF 2018

Visiting the link, we are give the source code for the challenge:

<?php

require_once('flag.php');
error_reporting(0);


if(!isset($_GET['msg'])){
highlight_file(__FILE__);
die();
}

@$msg = $_GET['msg'];
if(@file_get_contents($msg)!=="Hello Challenge!"){
die('Wow so rude!!!!1');
}

echo "Hello Hacker! Have a look around.\n";

@$k1=$_GET['key1'];
@$k2=$_GET['key2'];

$cc = 1337;$bb = 42;

if(intval($k1) !== $cc || $k1 === $cc){
die("lol no\n");
}

if(strlen($k2) == $bb){
if(preg_match('/^\d+$/', $k2) && !is_numeric($k2)){
if($k2 == $cc){
@$cc = $_GET['cc'];
}
}
}

list($k1,$k2) = [$k2, $k1];

if(substr($cc, $bb) === sha1($cc)){
foreach ($_GET as $lel => $hack){
$$lel = $hack;
}
}

$‮b = "2";$a="‮b";//;1=b

if($$a !== $k1){
die("lel no\n");
}

// plz die now
assert_options(ASSERT_BAIL, 1);
assert("$bb == $cc");

echo "Good Job ;)";
// TODO
// echo $flag;

To make the task a little easier, I copied the code onto my machine continued on localhost.

@$msg = $_GET['msg'];
if(@file_get_contents($msg)!=="Hello Challenge!"){
die('Wow so rude!!!!1');
}

The first part (snippet given above) requires us to pass in a GET Parameter msg, such that on calling the file_get_contents on the value of the parameter, Hello Challenge! is echoed. There are a couple of ways to get past this;

  • Using php://input as the parameter value and passing Hello Challenge! in the body of the request. As
  • Using data://text/plain,Hello%20Challenge!as the parameter value.
@$k1=$_GET['key1'];
@$k2=$_GET['key2'];

$cc = 1337;$bb = 42;

if(intval($k1) !== $cc || $k1 === $cc){
die("lol no\n");
}

The next part basically involves sending a parameter key1 with a value such that the value is not equal to the variable $cc and the integer value of key1 should equal the value of $cc; i.e. 1337. Since $k1 reads the value from the parameter key1, they type of $k1 would always be a string; Thus, the condition $k1 === $cc is always false (as === compares the values along with their types. And here, even though the values are the same, the comparison is done between a string and an integer). As for the condition intval($k1) !== $cc; simply passing the value of key1 as 1337 would invalidate this condition and allow us to proceed further!

The string lol no changes to lel no; signifying that Part 2 is complete!

if(strlen($k2) == $bb){
if(preg_match('/^\d+$/', $k2) && !is_numeric($k2)){
if($k2 == $cc){
@$cc = $_GET['cc'];
}
}
}

Moving on, they check whether the value in parameter key2 is of length $bb which is 42 (as defined earlier).

Then they compare the value with the regex /^\d+$/ which basically matches any decimal value (0–9) one or more times from the start, followed by a$ symbol. This is the part that I spent the most time on as I thought the $ matched the end of the line (without noticing that this was a unicode character and not the actual $ character). So, the value sent should be of length 42, should have decimal characters followed by the special character $. Due to the presence of the special character, the second condition !is_numeric($k2) is validated by default.

The final condition ($k2 == $cc) checks whether the parameter $k2 passed is the same as $cc (1337). The comparison here is made using == , so we can simply pass in 1337, preceded by a bunch of 0s and followed by the special character (totaling up to a length of 42). The special character in URL Encoded format was: %EF%BC%84, which has a length of 3.

I modified the source a little, to confirm the correctness of the payloads I tried:

So our final payload for this part looked like:

list($k1,$k2) = [$k2, $k1];

if(substr($cc, $bb) === sha1($cc)){
foreach ($_GET as $lel => $hack){
$$lel = $hack;
}
}

Too much for a Baby question, don’t you think? This part first interchanged the values of $k1 and $k2. And then checked whether the last 42 characters in $cc are equal to the sha1 of $cc. In the last step, we could change the existing value of $cc. So we had to modify $cc in such a way that this condition was validated. I set aside a script to brute-force this case (very improbable of it actually working)and read about the first sha1 collision found by Google. And concluded that this particular case wasn’t possible (not sure about this claim though.). Then I found out somewhere that on passing an array into the sha1() function resulted in a Null Value! . And similarly, a Null is returned when an array is passed into the substr() function!

This basically validates the condition (substr($cc, $bb) === sha1($cc)), as Null === Null, is True!

I edited the code to confirm whether the condition is validated:

And this is what my payload looked like:

The for loop inside this Part will be used in the next part; and will be explained there too.

The next line, is printed in reverse. On pasting the same into a text editor, the right syntax is shown.

if($$a !== $k1){
die("lel no\n");
}

In the previous line, $a was assigned a value of “b”; $$a basically refers to $$a = $($a) = $b. Which was assigned a value of 2. Therefore, for this part, we must ensure that $k1=2. So this is where we will use the for loop from the previous step.

To understand what it’s doing, the easiest way was to print out the contents of all the variables in the for loop:

The response was:

So the for loop iterates over all of the key-value pairs sent as a GET request; the keys being $lel and their values in $hack. Inside, the value of $$lel (the format$$a as explained above) is set to the corresponding $hack value. So if we pass another parameter k1 in the GET request with the value 2. This will basically translate to $lel = k1 and $hack = 2. Which will then become $$lel = $hack => $k1 = 2; Thus validating the last if condition!

Now the question is, how do we print the flag? as the line which does that is commented out. Here’s where the assert comes to our rescue. Assert will basically execute the command passed to it!

assert("$bb == $cc");

We can thus manipulate the value of $bb (and not $cc since the command executed would be $bb = <some code here>, which doesn’t seem to be of any help to us), to print out the flag.

I tried using echo $flag;// (the comments at the end to ignore whats after the payload that is injected) but that didn’t print anything out. Maybe it had something to do with the space after the echo. Then I used var_dump($flag);// which did print the flag out!

The final payload used: http://arcade.fluxfingers.net:1819/?msg=php://input&key1=1337&key2=000000000000000000000000000000000001337%EF%BC%84&cc[]=1337&k1=2&bb=var_dump($flag);//

And the flag: flag{7c217708c5293a3264bb136ef1fadd6e}!

Not a Baby question for sure.