ThinkPHP审计(2) Thinkphp反序列化链5.1.X原理分析&从0编写POC

04-11 1094阅读

ThinkPHP审计(2) Thinkphp反序列化链子5.1.X原理分析&从0编写POC

文章目录

  • ThinkPHP审计(2) Thinkphp反序列化链子5.1.X原理分析&从0编写POC
  • 动态调试环境配置
  • Thinkphp反序列化链5.1.X原理分析
    • 一.实现任意文件删除
    • 二.实现任意命令执行
      • 真正的难点
      • Thinkphp反序列化链5.1.x 编写 Poc
      • 汇总POC

        动态调试环境配置

        比较简洁的环境配置教程:

        https://sn1per-ssd.github.io/2021/02/09/phpstudy-phpstorm-xdebug%E6%90%AD%E5%BB%BA%E6%9C%AC%E5%9C%B0%E8%B0%83%E8%AF%95%E7%8E%AF%E5%A2%83/

        Thinkphp反序列化链5.1.X原理分析

        原理分析仅仅是遵循前辈的已有的道路,而不是完全探究每一种链子所带来的情况和可能性

        前提:存在反序列化的入口

        1. unserialize()
        2. phar反序列化
        3. session反序列化

        __destruct/__wakeup可以作为PHP反序列链的入口

        这里简单介绍一下__destruct垃圾回收机制与生命周期的含义

        __destruct可以理解为PHP的垃圾回收机制,是每次对象执行结束后必须执行的内容,但是执行的先后顺序往往和反序列化的生命周期有关

        例如:

         
        

        ThinkPHP审计(2) Thinkphp反序列化链5.1.X原理分析&从0编写POC

        这里$test = new test("test",18, 'Test String');

        对象被赋值给了$test变量,而不是直接的new test("test",18, 'Test String'); 传递给对象延长了对象的生命周期

        所以是在echo '第二种执行完毕'.'
        ';执行后才执行了__destruct内容

        类似的比如快速销毁(Fast-destruct)

         
        

        ThinkPHP审计(2) Thinkphp反序列化链5.1.X原理分析&从0编写POC

        这里直接__construct后执行__destruct

        因为unset — 清除指定变量直接销毁储存对象的变量,达到快速垃圾回收的目的

        现在开始分析链子 Windows 类中__destruct执行了自身的removeFiles()方法

        ThinkPHP审计(2) Thinkphp反序列化链5.1.X原理分析&从0编写POC

        跟进removeFiles

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

        ThinkPHP审计(2) Thinkphp反序列化链5.1.X原理分析&从0编写POC

        发现遍历$this->files,而且$this->files可控,作为数组传递

        一.实现任意文件删除

        @unlink($filename);删除了传递的filename

        简单编写poc

         
        

        可以实现任意文件的的删除

        ThinkPHP审计(2) Thinkphp反序列化链5.1.X原理分析&从0编写POC

        二.实现任意命令执行

        除了任意文件删除,危害还可以更大吗?

        通过POP链可以实现任意命令执行

        ThinkPHP审计(2) Thinkphp反序列化链5.1.X原理分析&从0编写POC

        全局逻辑图

        ThinkPHP审计(2) Thinkphp反序列化链5.1.X原理分析&从0编写POC

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

        file_exists函数用于判断文件是否存在

        预期传入 String $filename但是如果我们控制$filename作为一个对象,就可以隐形的调用类的__toString()方法

        ThinkPHP审计(2) Thinkphp反序列化链5.1.X原理分析&从0编写POC

        在thinkphp/library/think/model/concern/Conversion.php中

        public function __toString()
            {
                return $this->toJson();
            }
        
          public function toJson($options = JSON_UNESCAPED_UNICODE)
            {
                return json_encode($this->toArray(), $options);
            }
        
        public function toArray()
            {
                $item       = [];
                $hasVisible = false;
                foreach ($this->visible as $key => $val) {
                    if (is_string($val)) {
                        if (strpos($val, '.')) {
                            list($relation, $name)      = explode('.', $val);
                            $this->visible[$relation][] = $name;
                        } else {
                            $this->visible[$val] = true;
                            $hasVisible          = true;
                        }
                        unset($this->visible[$key]);
                    }
                }
                foreach ($this->hidden as $key => $val) {
                    if (is_string($val)) {
                        if (strpos($val, '.')) {
                            list($relation, $name)     = explode('.', $val);
                            $this->hidden[$relation][] = $name;
                        } else {
                            $this->hidden[$val] = true;
                        }
                        unset($this->hidden[$key]);
                    }
                }
                // 合并关联数据
                $data = array_merge($this->data, $this->relation);
                foreach ($data as $key => $val) {
                    if ($val instanceof Model || $val instanceof ModelCollection) {
                        // 关联模型对象
                        if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
                            $val->visible($this->visible[$key]);
                        } elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
                            $val->hidden($this->hidden[$key]);
                        }
                        // 关联模型对象
                        if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
                            $item[$key] = $val->toArray();
                        }
                    } elseif (isset($this->visible[$key])) {
                        $item[$key] = $this->getAttr($key);
                    } elseif (!isset($this->hidden[$key]) && !$hasVisible) {
                        $item[$key] = $this->getAttr($key);
                    }
                }
                // 追加属性(必须定义获取器)
                if (!empty($this->append)) {//在poc中定义了append:["peanut"=>["whoami"]
                    foreach ($this->append as $key => $name) { //$key =paenut; $name =["whoami"]
                        if (is_array($name)) {//$name=["whoami"]所以进入
                            // 追加关联对象属性
                            $relation = $this->getRelation($key);
                            if (!$relation) {
                                $relation = $this->getAttr($key);
                                if ($relation) {
                                    $relation->visible($name);//$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法
                                }
                            }
                            $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);
                        }
                    }
                }
                return $item;
            }
        

        关键的几个判断和赋值

        public function getRelation($name = null)
            {
                if (is_null($name)) {
                    return $this->relation;
                } elseif (array_key_exists($name, $this->relation)) {
                    return $this->relation[$name];
                }
                return;
            }
        
         if (!empty($this->append)) {//在poc中定义了append:["peanut"=>["whoami"]
                    foreach ($this->append as $key => $name) { //$key =paenut; $name =["whoami"]
                        if (is_array($name)) {//$name=["whoami"]所以进入
                            // 追加关联对象属性
                            $relation = $this->getRelation($key);
                            if (!$relation) {
                                $relation = $this->getAttr($key);
                                if ($relation) {
                                    $relation->visible($name);//$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法
                                }
                            }
        
        public function getAttr($name, &$item = null)//此时$name = 上一层的$key = peanut
            {
                try {
                    $notFound = false;
                    $value    = $this->getData($name);
                } catch (InvalidArgumentException $e) {
                    $notFound = true;
                    $value    = null;
                }
        
        public function getData($name = null)//$name = $key =peanut
            {
                if (is_null($name)) {
                    return $this->data;
                } elseif (array_key_exists($name, $this->data)) {//poc中定义$this->data = ['peanut'=>new request()]
                    return $this->data[$name];
                } elseif (array_key_exists($name, $this->relation)) {
                    return $this->relation[$name];
                }
                throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
            }
        

        $relation->visible($name);中$relation可控,可以实现任意类的visible方法,如果visible方法不存在,就会调用这个类的__call方法

        如何达到$relation->visible($name); 触发点 访问

                if (!empty($this->append)) {//在poc中定义了append:["peanut"=>["whoami"]]
                    foreach ($this->append as $key => $name) { //$key =paenut; $name =["whoami"]
                        if (is_array($name)) {//$name=["whoami"]所以进入
        
        1. 保证$this->append不为空
        2. $this->append 数组的值$name为数组 也就是二维数组

        比如传入append:["peanut"=>["whoami"]]

        接着向下走

        $relation = $this->getRelation($key);
                            if (!$relation) {
        
        public function getRelation($name = null)
            {
                if (is_null($name)) {
                    return $this->relation;
                } elseif (array_key_exists($name, $this->relation)) {
                    return $this->relation[$name];
                }
                return;
            }
        

        不会进入if/elseif中 直接return;回来 为null

        if (!$relation)为空进入判断

         $relation = $this->getAttr($key);
                                if ($relation) {
                                    $relation->visible($name);//$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法
                                }
        
        public function getAttr($name, &$item = null)//此时$name = 上一层的$key = peanut
            {
                try {
                    $notFound = false;
                    $value    = $this->getData($name);
                } catch (InvalidArgumentException $e) {
                    $notFound = true;
                    $value    = null;
                }
        

        进入$this->getData

        public function getData($name = null)//$name = $key =peanut
            {
                if (is_null($name)) {
                    return $this->data;
                } elseif (array_key_exists($name, $this->data)) {//poc中定义$this->data = ['peanut'=>new request()]
                    return $this->data[$name];
                } elseif (array_key_exists($name, $this->relation)) {
                    return $this->relation[$name];
                }
                throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
            }
        

        判断了$this->data传递的键存在,如果存在,返回其数组对应的键值

        比如可以控制$this->data = ['peanut'=>new request()]

         $relation = $this->getAttr($key);
                                if ($relation) {
                                    $relation->visible($name);//$relation可控,找到一个没有visible方法或不可访问这个方法的类时,即可调用_call()魔法方法
                                }
        

        $relation->visible($name);中$relation可控为任意类

        现在寻找调用__call的类

        在thinkphp/library/think/Request.php中

          public function __call($method, $args)
            {
                if (array_key_exists($method, $this->hook)) {
                    array_unshift($args, $this);
                    return call_user_func_array($this->hook[$method], $args);
                }
                throw new Exception('method not exists:' . static::class . '->' . $method);
            }
        

        这里存在敏感关键函数call_user_func_array

        __call($method, $args)接受的参数`

        $method固定是visible

        $args是传递过来的$name

         if (array_key_exists($method, $this->hook)) {
                    array_unshift($args, $this);
                    return call_user_func_array($this->hook[$method], $args);
        

        可以控制$this->hook['visible']为任意值,可以控制函数名

        call_user_func()的利用方式无非两种

        __call_user_func($method, $args) __

        call_user_func_array([ o b j , obj, obj,method], $args)

        如果执行第一种方式call_user_func($method, $args)

        但是这里array_unshift($args, $this); 参数插入$this作为第一个值

        ThinkPHP审计(2) Thinkphp反序列化链5.1.X原理分析&从0编写POC

        参数是不能被正常命令识别的,不能直接RCE

        那我们最终的利用点可以肯定并不是这里

        如果选择第二种方式

        call_user_func_array([$obj,$method], $args)

        **通过调用 任意类 的 任意方法 **,可供选择的可能性更多

        call_user_func_array([ o b j , " 任 意 方 法 " ] , [ obj,"任意方法"],[ obj,"任意方法"],[this,任意参数])

        也就是 o b j − > obj-> obj−>func( t h i s , this, this,argv)

        真正的难点

        曲线救国的策略

        难点理解:

        __call魔术方法受到array_unshift无法可控触发call_user_func_array

        利用_call调用isAjax类找可控变量再触发到filterValue里的call_user_func

        为什么这里选Request类isAjax方法 接着POP链的调用了?

        为什么当时的链子发现的作者会想到通过isAjax接着执行命令?

        网上文章千篇一律,无非就是拿个poc动态调试,粘贴个poc就完了

        Thinkphp反序列化漏洞 核心在于 逆向的思考 倒推

        开发者不会傻乎乎写个system,shell_exec,exec等系统函数给你利用的可能

        而我们又希望最终实现RCE的效果

        我们最终应该更多关注于 不明显的回调函数或者匿名函数执行命令

        比如call_user_func,call_user_func_array,array_map,array_filter...

        在thinkphp/library/think/Request.php中

        private function filterValue(&$value, $key, $filters)
            {
                $default = array_pop($filters);
                foreach ($filters as $filter) {
                    if (is_callable($filter)) {
                        // 调用函数或者方法过滤
                        $value = call_user_func($filter, $value);
        

        $filter , $value 可控

        通过传递 $filter , $value实现任意命令执行

        那么什么地方调用了filterValue?回溯调用filterValue的地方

        ThinkPHP审计(2) Thinkphp反序列化链5.1.X原理分析&从0编写POC

        在thinkphp/library/think/Request.php中input调用

        $this->filterValue($data, $name, $filter);

        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', '
VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]