1. 威客安全首页
  2. 安全资讯

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

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


作者:mengchen@知道创宇404实验室

时间:2019年7月31日




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


在进行分析之前,我们需要了解下Typo3TCA(Table Configuration Array),在Typo3的代码中,它表示为$GLOBALS['TCA']

Typo3中,TCA算是对于数据库表的定义的扩展,定义了哪些表可以在Typo3的后端可以被编辑,主要的功能有


  • 表示表与表之间的关系

  • 定义后端显示的字段和布局

  • 验证字段的方式


这次漏洞的两个利用点分别出在了CoreEngineFormEngine这两大结构中,而TCA就是这两者之间的桥梁,告诉两个核心结构该如何表现表、字段和关系。


TCA的第一层是表名:


$GLOBALS['TCA']['pages'] = [    ...];$GLOBALS['TCA']['tt_content'] = [    ...];


其中pagestt_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]


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


很明显,补丁分别禁用了backendDatabaseLanguageRows.phpcore中的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接口,因此静态分析寻找谁调用的DatabaseLanguageRowsaddData方法根本不现实,但是根据文章中的演示视频,我们可以知道网站中修改page这个功能中进入了漏洞点。在addData方法加上断点,然后发出一个正常的修改page的请求。


当程序断在DatabaseLanguageRowsaddData方法后,我们就可以得到调用链。


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


DatabaseLanguageRows这个addData中,只传入了一个$result数组,而且进行反序列化操作的目标是$result['databaseRow']中的某个值。看命名有可能是从数据库中获得的值,往前分析一下。


进入OrderedProviderListcompile方法。


路径: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方法。


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


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


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


路径: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 CVE-2019-12747 反序列化漏洞分析


路径: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);
    // 省略代码...}


很明显,通过调用FormDataCompilercompile方法,将$initialData中的数据存储到了$result中。


再往前走,来到了EditDocumentController类中的makeEditForm方法中。


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


在这里,$formDataCompilerInput['overrideValues']获取了$this->overrideVals[$table]中的数据。


$this->overrideVals的值是在方法preInit中设定的,获取的是通过POST传入的表单中的键值对。


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


这样一来,在这个请求过程中,进行反序列化的字符串我们就可以控制了。


在表单中提交任意符合数组格式的输入,在后端代码中都会被解析,然后后端根据TCA来进行判断并处理。比如我们在提交表单中新增一个名为a[b][c][d],值为233的表单项。


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


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


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


可以看到我们传入的键值对在经过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


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


可以看到,我们的输入成功的到达了反序列化的点。


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);        }        $currentRecord = $checkValueRecord;    } else {        // We must use the current values as basis for this!        $currentRecord = ($checkValueRecord = $this->recordInfo($table, $id, '*'));        // This is done to make the pid positive for offline versions; Necessary to have diff-view for page translations in workspaces.        BackendUtility::fixVersioningPid($table, $currentRecord);    }
    // Get original language record if available:    if (is_array($currentRecord)        && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']        && $GLOBALS['TCA'][$table]['ctrl']['languageField']        && $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0        && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']        && (int)$currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0       ) {        $originalLanguageRecord = $this->recordInfo($table, $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']], '*');        BackendUtility::workspaceOL($table, $originalLanguageRecord);        $originalLanguage_diffStorage = unserialize($currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']]);    }    ......//省略代码


看代码,如果我们要进入反序列化的点,需要满足前面的if条件


if (is_array($currentRecord)        && $GLOBALS['TCA'][$table]['ctrl']['transOrigDiffSourceField']        && $GLOBALS['TCA'][$table]['ctrl']['languageField']        && $currentRecord[$GLOBALS['TCA'][$table]['ctrl']['languageField']] > 0        && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']        && (int)$currentRecord[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] > 0    )


也就是说要满足以下条件


  • $currentRecord是个数组

  • TCA$table的表属性中存在transOrigDiffSourceFieldlanguageFieldtransOrigPointerField字段。

  • $table的属性languageFieldtransOrigPointerField$currentRecord中对应的值要大于0


查一下TCA表,满足第二条条件的表有


sys_file_referencesys_file_metadatasys_file_collectionsys_collectionsys_categorypages


但是所有sys_*的字段的adminOnly属性的值都是1,只有管理员权限才可以更改。因此我们可以用的表只有pages


它的属性值是


[languageField] => sys_language_uid[transOrigPointerField] => l10n_parent[transOrigDiffSourceField] => l10n_diffsource


再往上,有一个对传入的参数进行处理的if-else语句。


从注释中,我们可以知道传入的各个参数的功能:

  • 数组 $fieldArray 是默认值,这种一般都是我们无法控制的

  • 数组 $incomingFieldArray 是你想要设置的字段值,如果可以,它会合并到$fieldArray中。


而且如果满足if (strpos($id, 'NEW') !== false)条件的话,也就是$id是一个字符串且其中存在NEW字符串,会进入下面的合并操作。


$checkValueRecord = $fieldArray;......if (is_array($incomingFieldArray) && is_array($checkValueRecord)) {    ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);}$currentRecord = $checkValueRecord;


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



如果不满足上面的if条件,$currentRecord的值就会通过recordInfo方法从数据库中直接获取。这样后面我们就无法利用了。


简单总结一下,我们需要

  • $tablepages

  • $id是个字符串,而且存在NEW字符串

  • $incomingFieldArray中要存在payload

接下来我们看在哪里对该函数进行了调用。


全局搜索一下,只找到一处,在typo3/sysext/core/Classes/DataHandling/DataHandler.php:954处的process_datamap方法中进行了调用。


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


整个项目中,对process_datamap调用的地方就太多了,尝试使用xdebug动态调试来找一下调用链。从RIPS团队的那一篇分析文章结合上面的对表名的分析,我们可以知道,漏洞点在创建page的功能处。


接下来就是找从EditDocumentController.phpmainAction方法到前面我们分析的fillInFieldArray方法的调用链。


尝试在网站中新建一个page,然后在调用fillInFieldArray的位置下一个断点,发送请求后,我们就拿到了调用链。


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


看一下mainAction的代码。


public function mainAction(ServerRequestInterface $request): ResponseInterface{    // Unlock all locked records    BackendUtility::lockRecords();    if ($response = $this->preInit($request)) {        return $response;    }
    // Process incoming data via DataHandler?    $parsedBody = $request->getParsedBody();    if ($this->doSave        || isset($parsedBody['_savedok'])        || isset($parsedBody['_saveandclosedok'])        || isset($parsedBody['_savedokview'])        || isset($parsedBody['_savedoknew'])        || isset($parsedBody['_duplicatedoc'])    ) {        if ($response = $this->processData($request)) {            return $response;        }    }    ....//省略代码}


当满足if条件是进入目标$response = $this->processData($request)


if ($this->doSave        || isset($parsedBody['_savedok'])        || isset($parsedBody['_saveandclosedok'])        || isset($parsedBody['_savedokview'])        || isset($parsedBody['_savedoknew'])        || isset($parsedBody['_duplicatedoc'])    )


这个在新建一个page时,正常的表单中就携带doSave == 1,而doSave的值就是在方法preInit中获取的。


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


这样条件默认就是成立的,然后将$request传入了processData方法。


public function processData(ServerRequestInterface $request = null): ?ResponseInterface{// @deprecated Variable can be removed in TYPO3 v10.0    $deprecatedCaller = false;
    ......//省略代码    $parsedBody = $request->getParsedBody(); // 获取Post请求参数    $queryParams = $request->getQueryParams(); // 获取Get请求参数
    $beUser = $this->getBackendUser(); // 获取用户数据
    // Processing related GET / POST vars    $this->data = $parsedBody['data'] ?? $queryParams['data'] ?? [];    $this->cmd = $parsedBody['cmd'] ?? $queryParams['cmd'] ?? [];    $this->mirror = $parsedBody['mirror'] ?? $queryParams['mirror'] ?? [];    // @deprecated property cacheCmd is unused and can be removed in TYPO3 v10.0    $this->cacheCmd = $parsedBody['cacheCmd'] ?? $queryParams['cacheCmd'] ?? null;    // @deprecated property redirect is unused and can be removed in TYPO3 v10.0    $this->redirect = $parsedBody['redirect'] ?? $queryParams['redirect'] ?? null;    $this->returnNewPageId = (bool)($parsedBody['returnNewPageId'] ?? $queryParams['returnNewPageId'] ?? false);
    // Only options related to $this->data submission are included here    $tce = GeneralUtility::makeInstance(DataHandler::class);
    $tce->setControl($parsedBody['control'] ?? $queryParams['control'] ?? []);
    // Set internal vars    if (isset($beUser->uc['neverHideAtCopy']) && $beUser->uc['neverHideAtCopy']) {        $tce->neverHideAtCopy = 1;    }    // Load DataHandler with data    $tce->start($this->data, $this->cmd);    if (is_array($this->mirror)) {        $tce->setMirror($this->mirror);    }
    // Perform the saving operation with DataHandler:    if ($this->doSave === true) {        $tce->process_uploads($_FILES);        $tce->process_datamap();        $tce->process_cmdmap();    }    ......//省略代码}


代码很容易懂,从$request中解析出来的数据,首先存储在$this->data$this->cmd中,然后实例化一个名为$tce,调用$tce->start方法将传入的数据存储在其自身的成员datamapcmdmap中。


typo3/sysext/core/Classes/DataHandling/DataHandler.php:735public function start($data, $cmd, $altUserObject = null){   ......//省略代码    // Setting the data and cmd arrays    if (is_array($data)) {        reset($data);        $this->datamap = $data;    }    if (is_array($cmd)) {        reset($cmd);        $this->cmdmap = $cmd;    }}


而且if ($this->doSave === true)这个条件也是成立的,进入process_datamap方法。


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


代码有注释还是容易阅读的,在第985行,获取了datamap中所有的键名,然后存储在$orderOfTables,然后进入foreach循环,而这个$table,在后面传入fillInFieldArray方法中,因此,我们只需要分析$table == pages时的循环即可。


$fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);


大致浏览下代码,再结合前面的分析,我们需要满足以下条件:

  • $recordAccess的值要为true

  • $incomingFieldArray中的payload不会被删除

  • $table的值为pages

  • $id中存在NEW字符串


既然正常请求可以直接断在调用fillInFieldArray处,正常请求中,第一条、第三条和第四条都是成立的。


根据前面对fillInFieldArray方法的分析,构造payload,向提交的表单中添加三个键值对。


data[pages][NEW5d3fa40cb5ac4065255421][l10n_diffsource] ==> serialized_shell_datadata[pages][NEW5d3fa40cb5ac4065255421][sys_language_uid] ==> 4data[pages][NEW5d3fa40cb5ac4065255421][l10n_parent] ==> 4


其中NEW*字符串要根据表单生成的值进行对应的修改。


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


发送请求后,依旧能够进入fillInFieldArray,而在传入的$incomingFieldArray参数中,可以看到我们添加的三个键值对。


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

进入fillInFieldArray之后,其中l10n_diffsource将会进行反序列化操作。此时我们在请求中将其l10n_diffsource改为构造好的序列化字符串,重新发送请求即可成功getshell


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




5. 写在最后


其实单看这个漏洞的利用条件,还是有点鸡肋的,需要你获取到typo3的一个有效的后台账户,并且拥有编辑page的权限。


而且这次分析Typo3给我的感觉与其他网站完全不同,我在分析创建&修改page这个功能的参数过程中,并没有发现什么过滤操作,在后台的所有参数都是根据TCA的定义来进行相应的操作,只有传入不符合TCA定义的才会抛出异常。而TCA的验证又不严格导致了变量覆盖这个问题。


官方的修补方式也是不太懂,直接禁止了反序列化操作,但是个人认为这次漏洞的重点还是在于前面变量覆盖的问题上,尤其是Backend的利用过程中,可以直接覆盖从数据库中取出的数据,这样只能算是治标不治本,后面还是有可能产生新的问题。


当然了,以上只是个人拙见,如有错误,还请诸位斧正。



6. 参考链接


[1] 详情: https://blog.ripstech.com/2019/typo3-overriding-the-database/
[2] 官方手册: https://docs.typo3.org/m/typo3/reference-tca/master/en-us/Introduction/Index.html
[3] 通告: https://typo3.org/security/advisory/typo3-core-sa-2019-020/
[4] 修复记录: 

https://github.com/TYPO3/TYPO3.CMS/commit/555e0dd2b28f01a2f242dfefc0f344d10de50b2a?diff=unified

[5]:https://www.php.net/manual/en/function.unserialize.php



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

往 期 热 门

(点击图片跳转)

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

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

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



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

觉得不错点个“在看”哦Typo3 CVE-2019-12747 反序列化漏洞分析

原文始发于微信公众号(Seebug漏洞平台):Typo3 CVE-2019-12747 反序列化漏洞分析

本文转为转载文章,本文观点不代表威客安全立场。

发表评论

登录后才能评论

联系我们

15110186328

在线咨询:点击这里给我发消息

邮件:zhanglei@jinlongsec.com

工作时间:周一至周五,9:30-18:30,节假日休息

QR code
X