这个漏洞的原因在于对phar文件的过滤不严,导致可以远程代码执行。这里涉及到phar的反序列化,具体参见 Sam Thomas 关于phar unserialize的文章。
本地搭建了两个基于Ubuntu 18.04.2 LTS
的测试环境,TYPO3 8.7.16+PHP PHP 7.0.33
和TYPO3 9.3.0+7.3.7
。该漏洞的触发需要一个有效的后台用户。
这里有一个问题需要知晓,assert
函数在PHP7.2
之后的定义与之前版本略有不同。
1 | assert ( mixed $assertion [, string $description ] ) : bool |
php manual
中有一段话:
1 | Warning |
这在编写POC
的时候,需要避免在PHP7.3
中使用assert
来执行代码。
PHP7.3.7
中执行phpinfo
使用的poc
如下:
1 |
|
好了,现在开始进行漏洞分析。
TYPO3
中存在漏洞的代码在typo3/sysext/core/Classes/Database/SoftReferenceIndex.php
的getTypoLinkParts()
方法
上面说到存在风险的文件操作函数,其中就包括file_exists()
,当传给file_exists()
的参数是phar
压缩文档并通过phar://
伪协议解析时,就会反序列化其中的metadata
数据,一旦该数据被控制,就会形成漏洞。
我们可以构造$splitLinkParam
参数为phar
文件,其中包含恶意代码,传递给file_exists()
函数,便会触发漏洞。
在这里,利用phpggc
来构造POP
链。TYPO3
中包含类FnStream
,所以可以利用phpggc
中的guzzle/rce1
载荷将数据写入一张图片中。
然后上传这个附件,接着创建一个页面,将 Link
设置为phar://
,注意需要将:
转义
保存后就会触发漏洞
漏洞复现完毕。
最后说一下phpggc
中该POP
调用链。
在PHPGGC
中,pop
链在gadgets.php
文件中,pop
链的逻辑和描述在chain.php
文件中chain.php
1 |
|
从其中可以看到其对使用该组件的描述,要求GuzzleHttp\Psr7
的版本要小于1.5.0
,具体的逻辑在generate
成员方法中,其中入口参数为数组parameters
,其包括function
和parameter
两个参数,分别为要进行rce
的函数和函数的参数,其返回的即是\GuzzleHttp\Psr7\FnStream
的匿名对象,其入口参数为一个数组,数组包括一个数组元素,键名为close
,键值为一个数组,包括\GuzzleHttp\HandlerStack
匿名对象,以及resolve
字符串,至此构造序列化对象的逻辑结束,接下来结合gadgets.php
看一下整个链是如何连起来的:
gadgets.php
1 |
|
在类HandlerStack
的构造方法中传入了rce
要使用的函数及参数,并赋值给$this->stack
和$this->handler
,然后在类FnStream
的构造方法中传入包含键close
的数组,此时将会拼接出:_fn_close=[new\GuzzleHttp\HandlerStack($function, $parameter),'resolve']
_fn_close
的第一个元素其实已经实例化为一个匿名对象了,这里为了好理解先写成实例化前的形式。然后在FnStream的__destruct()
函数中将会调用$this->_fn_close
,即构成:call_user_func([new \GuzzleHttp\HandlerStack($function, $parameter),'resolve'])
以上这种调用的形式在php
官方文档中存在此种调用类中方法的形式:
所以此时关注类HandlerStack
的resolve
方法,其中将利用php
的动态函数的性质来构成rce
的函数调用,比如此时假设:[new \GuzzleHttp\HandlerStack($function, $parameter),'resolve']=>
[new \GuzzleHttp\HandlerStack("system", "id"),'resolve']
即此时$prev
参数首先经过$prev = $this->handler
以后为id
,接着经过foreach
(array_reverse($this->stack) as $fn)
,$fn
将为包含一个元素的数组["system"]
,然后经过$fn[0]
即为system
,即$prev
即为system("id")
;最后函数调用返回再传入call_user_func,即构成call_user_func(system("id"))
;
到此,整个调用链已经分析结束,实现的原理也清楚了。