搜索
热搜: 活动 交友 discuz
Hi~登录注册
黑客技术论坛 最新事件 漏洞预警 查看内容

Typo3 CVE-2019-12747 反序列化漏洞分析

2019-8-7 16:00| 发布者: 21027264| 查看: 27| 评论: 0

摘要: 1. 前言TYPO3是一个以PHP编写、采用GNU通用公共许可证的自由、开源的内容管理系统。2019年7月16日,RIPS的研究团队公开了Typo3 CMS的一个关键漏洞详情,CVE编号为CVE-2019-12747,它允许后台用户执行任意PHP代码。漏 ...
1. 前言
TYPO3是一个以PHP编写、采用GNU通用公共许可证的自由、开源的内容管理系统。
2019年7月16日,RIPS的研究团队公开了Typo3 CMS的一个关键漏洞详情[1],CVE编号为CVE-2019-12747,它允许后台用户执行任意PHP代码。
漏洞影响范围:Typo3 8.x-8.7.26 9.x-9.5.7。
 
2. 测试环境简述
Nginx/1.15.8PHP 7.3.1 + xdebug 2.7.2MySQL 5.7.27Typo3 9.5.7
 
3. TCA
在进行分析之前,我们需要了解下Typo3的TCA(Table Configuration Array),在Typo3的代码中,它表示为$GLOBALS[‘TCA’]。
在Typo3中,TCA算是对于数据库表的定义的扩展,定义了哪些表可以在Typo3的后端可以被编辑,主要的功能有
表示表与表之间的关系
定义后端显示的字段和布局
验证字段的方式
这次漏洞的两个利用点分别出在了CoreEngine和FormEngine这两大结构中,而TCA就是这两者之间的桥梁,告诉两个核心结构该如何表现表、字段和关系。
TCA的第一层是表名:
$GLOBALS['TCA']['pages'] = [
    ...];
$GLOBALS['TCA']['tt_content'] = [
    ...];
其中pages和tt_content就是数据库中的表。
接下来一层就是一个数组,它定义了如何处理表,
$GLOBALS['TCA']['pages'] = [
    'ctrl' => [ // 通常包含表的属性
        ....    ],
    'interface' => [ // 后端接口属性等
        ....    ],
    'columns' => [
        ....    ],
    'types' => [
        ....    ],
    'palettes' => [
        ....    ],
];
在这次分析过程中,只需要了解这么多,更多详细的资料可以查询官方手册[2]。
 
4. 漏洞分析
整个漏洞的利用流程并不是特别复杂,主要需要两个步骤,第一步变量覆盖后导致反序列化的输入可控,第二步构造特殊的反序列化字符串来写shell。第二步这个就是老套路了,找个在魔术方法中能写文件的类就行。这个漏洞好玩的地方在于变量覆盖这一步,而且进入两个组件漏洞点的传入方式也有着些许不同,接下来让我们看一看这个漏洞吧。
4.1 补丁分析
从Typo3官方的通告[3]中我们可以知道漏洞影响了两个组件——Backend & Core API (ext:backend, ext:core),在GitHub上我们可以找到修复记录[4]:

很明显,补丁分别禁用了backend的DatabaseLanguageRows.php和core中的DataHandler.php中的的反序列化操作。
4.2 Backend ext 漏洞点利用过程分析
根据补丁的位置,看下Backend组件中的漏洞点。
路径:typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseLanguageRows.php:37
public function addData(array $result){
if (!empty($result['processedTca']['ctrl']['languageField'])
        && !empty($result['processedTca']['ctrl']['transOrigPointerField'])
    ) {
        $languageField = $result['processedTca']['ctrl']['languageField'];
        $fieldWithUidOfDefaultRecord = $result['processedTca']['ctrl']['transOrigPointerField'];
if (isset($result['databaseRow'][$languageField]) && $result['databaseRow'][$languageField] > 0
            && isset($result['databaseRow'][$fieldWithUidOfDefaultRecord]) && $result['databaseRow'][$fieldWithUidOfDefaultRecord] > 0
        ) {// Default language record of localized record
            $defaultLanguageRow = $this->getRecordWorkspaceOverlay(
                $result['tableName'],
                (int)$result['databaseRow'][$fieldWithUidOfDefaultRecord]
            );
if (empty($defaultLanguageRow)) {
throw new DatabaseDefaultLanguageException('Default language record with id ' . (int)$result['databaseRow'][$fieldWithUidOfDefaultRecord]
                    . ' not found in table ' . $result['tableName'] . ' while editing record ' . $result['databaseRow']['uid'],1438249426
                );
            }
            $result['defaultLanguageRow'] = $defaultLanguageRow;
// Unserialize the "original diff source" if givenif (!empty($result['processedTca']['ctrl']['transOrigDiffSourceField'])                && !empty($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']])            ) {                $defaultLanguageKey = $result['tableName'] . ':' . (int)$result['databaseRow']['uid'];                $result['defaultLanguageDiffRow'][$defaultLanguageKey] = unserialize($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']]);            }//省略代码        }//省略代码    }//省略代码}
很多类都继承了FormDataProviderInterface接口,因此静态分析寻找谁调用的DatabaseLanguageRows的addData方法根本不现实,但是根据文章中的演示视频,我们可以知道网站中修改page这个功能中进入了漏洞点。在addData方法加上断点,然后发出一个正常的修改page的请求。
当程序断在DatabaseLanguageRows的addData方法后,我们就可以得到调用链。

在DatabaseLanguageRows这个addData中,只传入了一个$result数组,而且进行反序列化操作的目标是$result[‘databaseRow’]中的某个值。看命名有可能是从数据库中获得的值,往前分析一下。
进入OrderedProviderList的compile方法。
路径:typo3/sysext/backend/Classes/Form/FormDataGroup/OrderedProviderList.php:43
public function compile(array $result): array{
    $orderingService = GeneralUtility::makeInstance(DependencyOrderingService::class);
    $orderedDataProvider = $orderingService->orderByDependencies($this->providerList, 'before', 'depends');
    foreach ($orderedDataProvider as $providerClassName => $providerConfig) {
        if (isset($providerConfig['disabled']) && $providerConfig['disabled'] === true) {
            // Skip this data provider if disabled by configuration
            continue;
        }
        /** @var FormDataProviderInterface $provider */
        $provider = GeneralUtility::makeInstance($providerClassName);
        if (!$provider instanceof FormDataProviderInterface) {
            throw new UnexpectedValueException(
                'Data provider ' . $providerClassName . ' must implement FormDataProviderInterface',
                1485299408
            );
        }
        $result = $provider->addData($result);
    }
    return $result;}
我们可以看到,在foreach这个循环中,动态实例化$this->providerList中的类,然后调用它的addData方法,并将$result作为方法的参数。
在调用DatabaseLanguageRows之前,调用了如图所示的类的addData方法。

经过查询手册以及分析代码,可以知道在DatabaseEditRow类中,通过调用addData方法,将数据库表中数据读取出来,存储到了$result[‘databaseRow’]中。

路径:typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseEditRow.php:32
public function addData(array $result){
    if ($result['command'] !== 'edit' || !empty($result['databaseRow'])) {// 限制功能为`edit`
        return $result;
    }
    $databaseRow = $this->getRecordFromDatabase($result['tableName'], $result['vanillaUid']); // 获取数据库中的记录
    if (!array_key_exists('pid', $databaseRow)) {
        throw new UnexpectedValueException(
            'Parent record does not have a pid field',
            1437663061
        );
    }
    BackendUtility::fixVersioningPid($result['tableName'], $databaseRow);
    $result['databaseRow'] = $databaseRow;
    return $result;
}
再后面又调用了DatabaseRecordOverrideValues类的addData方法。

路径:typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseRecordOverrideValues.php:31
public function addData(array $result){
    foreach ($result['overrideValues'] as $fieldName => $fieldValue) {
        if (isset($result['processedTca']['columns'][$fieldName])) {
            $result['databaseRow'][$fieldName] = $fieldValue;

 $result['processedTca']['columns'][$fieldName]['config'] = [
                'type' => 'hidden',
                'renderType' => 'hidden',
            ];
        }
    }
    return $result;}
在这里,将$result[‘overrideValues’]中的键值对存储到了$result[‘databaseRow’]中,如果$result[‘overrideValues’]可控,那么通过这个类,我们就能控制$result[‘databaseRow’]的值了。
再往前,看看$result的值是怎么来的。
路径:typo3/sysext/backend/Classes/Form/FormDataCompiler.php:58
public function compile(array $initialData){
    $result = $this->initializeResultArray();
    //省略代码
    foreach ($initialData as $dataKey => $dataValue) {
        // 省略代码...
        $result[$dataKey] = $dataValue;
    }
    $resultKeysBeforeFormDataGroup = array_keys($result);
    $result = $this->formDataGroup->compile($result);
    // 省略代码...}
很明显,通过调用FormDataCompiler的compile方法,将$initialData中的数据存储到了$result中。
再往前走,来到了EditDocumentController类中的makeEditForm方法中。

在这里,$formDataCompilerInput[‘overrideValues’]获取了$this->overrideVals[$table]中的数据。
而$this->overrideVals的值是在方法preInit中设定的,获取的是通过POST传入的表单中的键值对。

这样一来,在这个请求过程中,进行反序列化的字符串我们就可以控制了。
在表单中提交任意符合数组格式的输入,在后端代码中都会被解析,然后后端根据TCA来进行判断并处理。比如我们在提交表单中新增一个名为a[b][c][d],值为233的表单项。

在编辑表单的控制器EditDocumentController.php中下一个断点,提交之后。

可以看到我们传入的键值对在经过getParsedBody方法解析后,变成了嵌套的数组,并且没有任何限制。
我们只需要在表单中传入overrideVals这一个数组即可。这个数组中的具体的键值对,则需要看进行反序列化时取的$result[‘databaseRow’]中的哪一个键值。
if (isset($result['databaseRow'][$languageField]) && $result['databaseRow'][$languageField] > 0 && isset($result['databaseRow'][$fieldWithUidOfDefaultRecord]) && $result['databaseRow'][$fieldWithUidOfDefaultRecord] > 0) {    // 省略代码    if (!empty($result['processedTca']['ctrl']['transOrigDiffSourceField']) && !empty($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']])) {        $defaultLanguageKey = $result['tableName'] . ':' . (int) $result['databaseRow']['uid'];        $result['defaultLanguageDiffRow'][$defaultLanguageKey] = unserialize($result['databaseRow'][$result['processedTca']['ctrl']['transOrigDiffSourceField']]);    }    //省略代码}
要想进入反序列化的点,还需要满足上面的if条件,动态调一下就可以知道,在if语句中调用的是
$result['databaseRow']['sys_language_uid']$result['databaseRow']['l10n_parent']
后面反序列化中调用的是
$result['databaseRow']['l10n_diffsource']
因此,我们只需要在传入的表单中增加三个参数即可。
overrideVals[pages][sys_language_uid] ==> 4overrideVals[pages][l10n_parent] ==> 4overrideVals[pages][l10n_diffsource] ==> serialized_shell_data

可以看到,我们的输入成功的到达了反序列化的点。
4.3 Core ext 漏洞点利用过程分析
看下Core中的那个漏洞点。
路径:typo3/sysext/core/Classes/DataHandling/DataHandler.php:1453
public function fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $realPid, $status, $tscPID){
    // Initialize:    $originalLanguageRecord = null;
    $originalLanguage_diffStorage = null;
$diffStorageFlag = false;
    // Setting 'currentRecord' and 'checkValueRecord':
    if (strpos($id, 'NEW') !== false) {
        // Must have the 'current' array - not the values after processing below...
        $checkValueRecord = $fieldArray;
        if (is_array($incomingFieldArray) && is_array($checkValueRecord)) {
            ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);

鲜花

握手

雷人

路过

鸡蛋