I recently got approached by my favorite CTF platform to produce a challenge for them. Of course, I wasn't going to say no, so I decided, while I produced a challenge, I would also squeeze a blog post out of it, and demonstrate my thought process throughout challenge creation.

Warning, SPOILERS AHEAD!

Getting started.

To get started, I took a look at some of the challenges that already existed on the platform, to have a better idea of the currently introduced format they follow.

For this, I took "Compare The Pair" as a base. This is a PHP challenge where only the source code is provided, so naturally, I thought "how about some static analysis based challenges?"

Some vulnerabilities that I enjoy for PHP are type juggling and deserialization, so we can certainly implement them into our SAST challenge. Maybe some sort of signed cookie that can be forged to exploit the type juggling, and reach a deserialization vulnerability.

So I have this as a template:

<?php
  require_once('flag.php');
  $password_hash = "0e902564435691274142490923013038";
  $salt = "f789bbc328a3d1a3";
  if(isset($_GET['password']) && md5($salt . $_GET['password']) == $password_hash){
    echo $flag;
  }
  echo highlight_file(__FILE__, true);
?>

This is where I wanted to start making modifications, so I thought of ways to implement the type juggling. I ended up going with the cookie signature being the type juggling part of the challenge.

Generally, signatures are added to the end of valuable data and separated with a ".", so that will be my personal method of detecting the start of the signature too. In python, this would be done with the split function, and googling led me to believe that this function exists in PHP too! Let's give it a try:

<?php
  require_once('flag.php');
  if(isset($_COOKIE['sess']) && split(".", $_COOKIE['sess'],1) == NULL){
        echo "Welcome back! Here is your flag: ".$flag;
  }else{
  echo highlight_file(__FILE__, true);
  }
?>

After testing the above piece of code, it didn't return any highlighted
code, meaning we have an error somewhere. Log all the things!

chiv@Dungeon:/var/www/html$ cat /var/log/apache2/error.log | tail
[Tue May 05 19:15:50.463251 2020] [php7:error] [pid 20223] [client 127.0.0.1:56534] PHP Fatal error:  Uncaught Error: Call to undefined function split() in /var/www/html/index.php:3\nStack trace:\n#0 {main}\n  thrown in /var/www/html/index.php on line 3
[Tue May 05 19:15:50.642339 2020] [php7:error] [pid 20227] [client 127.0.0.1:56536] PHP Fatal error:  Uncaught Error: Call to undefined function split() in /var/www/html/index.php:3\nStack trace:\n#0 {main}\n  thrown in /var/www/html/index.php on line 3
[Tue May 05 19:15:50.848820 2020] [php7:error] [pid 20225] [client 127.0.0.1:56538] PHP Fatal error:  Uncaught Error: Call to undefined function split() in /var/www/html/index.php:3\nStack trace:\n#0 {main}\n  thrown in /var/www/html/index.php on line 3
[Tue May 05 19:18:05.796847 2020] [php7:error] [pid 20220] [client 127.0.0.1:56680] PHP Fatal error:  Uncaught Error: Call to undefined function split() in /var/www/html/index.php:3\nStack trace:\n#0 {main}\n  thrown in /var/www/html/index.php on line 3
[Tue May 05 19:18:08.641375 2020] [php7:error] [pid 20222] [client 127.0.0.1:56682] PHP Fatal error:  Uncaught Error: Call to undefined function split() in /var/www/html/index.php:3\nStack trace:\n#0 {main}\n  thrown in /var/www/html/index.php on line 3
[Tue May 05 19:18:21.511435 2020] [php7:error] [pid 20223] [client 127.0.0.1:56684] PHP Fatal error:  Uncaught Error: Call to undefined function split() in /var/www/html/index.php:3\nStack trace:\n#0 {main}\n  thrown in /var/www/html/index.php on line 3

Split is an undefined function? Oh, it was deprecated, now people use explode with a delimiter. After quickly fixing this error, I ended up with the following:

<?php
  require_once('flag.php');
  if(isset($_COOKIE['sess']) && explode(".", $_COOKIE['sess'])[1] == NULL){
        echo "Welcome back! Here is your flag: ".$flag;
  }else{
  echo highlight_file(__FILE__, true);
  }
?>

Let's verify it works this time:

chiv@Dungeon:/var/www/html$ curl localhost -b "sess=."
Welcome back!

Perfect, the basic type juggling is all setup. Now to implement our basic deserialization vulnerability.

<?php
  class insert_log
  {
        public $new_data = "testing";
        function __destruct(){
                echo($this->new_data);
                }
  }

  require_once('flag.php');
  if(isset($_COOKIE['sess']) && explode(".", $_COOKIE['sess'])[1] == NULL){
        unserialize(base64_decode(explode(".",$_COOKIE['sess'])[0]));
        echo "Welcome back! Here is your flag: ".$flag;
  }else{
        echo highlight_file(__FILE__, true);
  }
?>

For this, I basically have once again told the cookie to be split into an array, using "." as a delimiter, and the first value of the array being base64 decoded, and deserialized. This is where we can call our "insert_log" class, and pass it any "new_data" value we want. In my case, just as a test, I base64 encoded a serialized PHP object, storing "new_data" as a string "It works!".

Basic PHP serialized data format:
type:length:string(:quantity)

In our case, we want a PHP Object to hold one value called "new_data", so that would translate to:
O:10:"insert_log":1:{s:8:"new_data";s:9:"It works!";};

To depict this payload, the O means Object, the 10 is the length of the name (insert_log has 10 characters), we then state that the object has one name vale pair, and we open the contents with {. Then we state the name is a string of 8 characters known as "new_data", with a pair of type string, 9 characters, that holds "It works!". Essentially, what this should do, when passed to unserialize, is be recognized as serialized version of data for the "insert_log" class, and will use the "new_data" value we pass to it as that variable.

We can base64 encode this payload and then curl it to the webserver:

chiv@Dungeon:/var/www/html$ curl -XGET localhost/ -b "sess=$(echo 'O:10:"insert_log":1:{s:8:"new_data";s:9:"It works!";};' | base64 -w 0)."
It works!Welcome back!

Perfect, it echo's our payload back to us. Now we can start to complicate things.

[EDIT]
I later decided that type juggling + deserialization wasn't enough for my "feature" on 247CTF, so during the deserialization process, why not add a funky SQL injection?

<?php
  class insert_log
  {
        public $new_data = "testing";
        function __destruct(){
                require_once('connect.php');
                $mysqli->query("INSERT INTO logs(message) VALUES ('".$this->new_data."');");
                }
  }


  if(isset($_COOKIE['sess']) && explode(".", $_COOKIE['sess'])[1] == NULL){
        unserialize(base64_decode(explode(".",$_COOKIE['sess'])[0]));
        echo "Welcome back!";
  }else{
        echo highlight_file(__FILE__, true);
  }
?>

This means, we can remove the flag.php import, and just store the flag in the database.

As seen in the previous section, we currently have a basic type juggling and deserialization challenge. Now that we have our base, my usual challenge creation methodology would be to improve upon what we have, so let's start by splitting the challenge into three stages, type juggling, deserialization, and SQL Injection.

Type juggling is a tough vulnerability to make complex, as solving type juggling challenges basically consist of figuring out what parameters you control, what they translate to in the final string, and then try to comprehend how our parameter affects the final comparison. Due to this, I modified it slightly, but not massively. I think the only vulnerability I can properly play with throughout this challenge is the SQL Injection.

<?php
  class insert_log
  {
        public $new_data = "testing";
        function __destruct(){
                require_once('connect.php');
                $mysqli->query("INSERT INTO logs(message) VALUES ('".$this->new_data."');");
                }
  }


  if(isset($_COOKIE['sess']) && explode(".", $_COOKIE['sess'])[1].rand(0,13337) == "0"){
        unserialize(base64_decode(explode(".",$_COOKIE['sess'])[0]));
        echo "Welcome back!";
  }else{
        echo highlight_file(__FILE__, true);
  }
?>

This new modification makes it so that one in every 13338 requests will work, unless you find some other way of the comparison returning true.

Next we have an unserialize function which will unserialize the first part of our cookie, this can be abused to call the insert_log class, and lead to a SQL Injection, the reason for implementing this in my mind was, to exploit a blind SQL injection, you need to script it to be efficient. So having the script build your PHP serialization payload automatically is a cool programming challenge, aswell as learning how PHP serialized objects are built. I believe that exploiting the INSERT SQLi is already a challenge enough, and no further modification is needed.

Remember, this is not a writeup for the challenge, the solution will be posted in another post. This is my thought process through development of the challenge.

Overall, the solution of the challenge consists of the development of a specially crafted cookie to bypass the rand() check, and then deserialize into the INSERT query, where you can exploit a SQL Injection, and leak the flag from the flag table.

Finally, I need to clean up the challenge:

<?php
  class insert_log
  {
        public $new_data = "Logged! Don't worry about when ;)";
        function __destruct(){
                require_once('connect.php');
                $mysqli->query("INSERT INTO logs(message) VALUES ('".$this->new_data."');");
                }
  }

  if(isset($_COOKIE['247']) && explode(".", $_COOKIE['247'])[1].rand(0,133337) == "0"){
        file_put_contents("/dev/null",unserialize(base64_decode(explode(".",$_COOKIE['247'])[0])));
  }else{
        echo highlight_file(__FILE__, true);
  }
?>