Lemon's blog

PHP反序列化漏洞学习(一)

Record my learning process of 对象注入.

字数统计: 1.9k阅读时长: 7 min
2019/08/12 Share

前言:PHP反序列化也是web安全中常见的一种漏洞,这次就先来大致了解一下PHP反序列化漏洞的基础知识。


一、PHP序列化和反序列化

在学习PHP反序列化漏洞时,先来了解一下基础的知识。

(一)PHP序列化

函数 : serialize()

所有php里面的值都可以使用函数serialize()来返回一个包含字节流的字符串来表示。
序列化一个对象将会保存对象的所有变量,但是不会保存对象的方法,只会保存类的名字。

一开始看这个概念可能有些懵,看了很多大师傅们的博客后,慢慢明白这个概念的道理。

在程序执行结束时,内存数据便会立即销毁,变量所储存的数据便是内存数据,而文件、数据库是“持久数据”,因此PHP序列化就是将内存的变量数据“保存”到文件中的持久数据的过程。

1
2
$s = serialize($变量); //该函数将变量数据进行序列化转换为字符串
file_put_contents(‘./目标文本文件’, $s); //将$s保存到指定文件

下面通过一个具体的例子来了解一下序列化:
在这里插入图片描述
输出序列化后的结果:

1
2
User lemon is 20 years old. 
O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:5:"lemon";}

可以看到序列化一个对象后将会保存对象的所有变量,并且发现序列化后的结果都有一个字符,这些字符都是以下字母的缩写。

1
2
3
4
5
6
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

了解了缩写的类型字母,便可以得到PHP序列化格式

1
2
O:4:"User":2:{s:3:"age";i:20;s:4:"name";s:5:"lemon";}
对象类型:长度:"类名":类中变量的个数:{类型:长度:"值";类型:长度:"值";......}

通过以上例子,便可以理解了概念中的通过serialize()函数返回一个包含字节流的字符串这一段话。

(二)PHP反序列化

函数:unserialize()

unserialize() 对单一的已序列化的变量进行操作,将其转换回 PHP 的值。 在解序列化一个对象前,这个对象的类必须在解序列化之前定义。

简单来理解起来就算将序列化过存储到文件中的数据,恢复到程序代码的变量表示形式的过程,恢复到变量序列化之前的结果。

1
2
$s = file_get_contents(‘./目标文本文件’); //取得文本文件的内容(之前序列化过的字符串)
$变量 = unserialize($s); //将该文本内容,反序列化到指定的变量中

通过一个例子来了解反序列化:
在这里插入图片描述
输出结果:

1
User lemon is 20 years old.

注意:在解序列化一个对象前,这个对象的类必须在解序列化之前定义。否则会报错

在先知上看大师傅举得例子对序列化和反序列化的介绍,也很好理解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
class A{
var $test = "demo";
}

$a = new A(); // 生成a对象
$b = serialize($a); // 序列化a对象为b
$c = unserialize($b); // 反序列化b对象为c

print_r($b); // 输出序列化之后的值:O:1:"A":1:{s:4:"test";s:4:"demo";}
echo "\n";
print_r($c->test); // 输出对象c中test的值:demo

?>

二、PHP反序列化漏洞

在学习漏洞前,先来了解一下PHP魔法函数,对接下来的学习会很有帮助

PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__construct   当一个对象创建时被调用,
__destruct 当一个对象销毁时被调用,
__toString 当一个对象被当作一个字符串被调用。
__wakeup() 使用unserialize时触发
__sleep() 使用serialize时触发
__destruct() 对象被销毁时触发
__call() 在对象上下文中调用不可访问的方法时触发
__callStatic() 在静态上下文中调用不可访问的方法时触发
__get() 用于从不可访问的属性读取数据
__set() 用于将数据写入不可访问的属性
__isset() 在不可访问的属性上调用isset()或empty()触发
__unset() 在不可访问的属性上使用unset()时触发
__toString() 把类当作字符串使用时触发,返回值需要为字符串
__invoke() 当脚本尝试将对象调用为函数时触发

这里只列出了一部分的魔法函数,具体可见PHP 手册

下面通过一个例子来了解一下魔法函数被自动调用的过程

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 test{
public $varr1="abc";
public $varr2="123";
public function echoP(){
echo $this->varr1."<br>";
}
public function __construct(){
echo "__construct<br>";
}
public function __destruct(){
echo "__destruct<br>";
}
public function __toString(){
return "__toString<br>";
}
public function __sleep(){
echo "__sleep<br>";
return array('varr1','varr2');
}
public function __wakeup(){
echo "__wakeup<br>";
}
}

$obj = new test(); //实例化对象,调用__construct()方法,输出__construct
$obj->echoP(); //调用echoP()方法,输出"abc"
echo $obj; //obj对象被当做字符串输出,调用__toString()方法,输出__toString
$s =serialize($obj); //obj对象被序列化,调用__sleep()方法,输出__sleep
echo unserialize($s); //$s首先会被反序列化,会调用__wake()方法,被反序列化出来的对象又被当做字符串,就会调用_toString()方法。
// 脚本结束又会调用__destruct()方法,输出__destruct
?>

显示结果:
在这里插入图片描述
例子载自于脚本之家,通过这个例子就可以清晰的看到魔法函数在符合相应的条件时便会被调用。


二、对象注入

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

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

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

1
2
3
4
5
6
7
8
9
10
<?php
class A{
var $test = "demo";
function __destruct(){
echo $this->test;
}
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>

比如这个列子,直接是用户生成的内容传递给unserialize()函数,那就可以构造这样的语句

1
?test=O:1:"A":1:{s:4:"test";s:5:"lemon";}

在脚本运行结束后便会调用_destruct函数,同时会覆盖test变量输出lemon。
在这里插入图片描述
发现这个漏洞,便可以利用这个漏洞点控制输入变量,拼接成一个序列化对象。
再看下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
class A{
var $test = "demo";
function __destruct(){
@eval($this->test);//_destruct()函数中调用eval执行序列化对象中的语句
}
}
$test = $_POST['test'];
$len = strlen($test)+1;
$pp = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}"; // 构造序列化对象
$test_unser = unserialize($pp); // 反序列化同时触发_destruct函数
?>

其实仔细观察就会发现,其实我们手动构造序列化对象就是为了unserialize()函数能够触发__destruc()函数,然后执行在__destruc()函数里恶意的语句。

所以我们利用这个漏洞点便可以获取web shell了
在这里插入图片描述

总结:这次PHP反序列化漏洞的基础大致都了解了,但对于对象注入还是需要继续学习,这次就先学习到这里。


参考博客:
PHP反序列化漏洞与Webshell
实战经验丨PHP反序列化漏洞总结

CATALOG
  1. 1. 一、PHP序列化和反序列化
    1. 1.0.1. (一)PHP序列化
    2. 1.0.2. (二)PHP反序列化
  • 2. 二、对象注入