PHP 反序列化漏洞:身份标识

文章目录

  • 参考
  • 环境
  • 访问修饰符
      • 访问修饰符
      • PHP 与访问修饰符
  • 手写身份标识
      • 身份标识
      • 定义身份标识
      • 控制字符 NUL
          • 在 PHP 中如何表示空字符?
      • 通过空字符尝试构建包含非公共属性对象的序列化文本
  • 空字符的传输
      • 控制字符的不可打印性
      • 结论
      • 另辟蹊径
          • URL 字符编码
            • 将非 ASCII 字符文本转换为 ASCII 字符文本
            • 去除语义
            • 将不可见字符(如空格)转换为可见文本
          • URL Encode

参考

项目 描述
搜索引擎 BingGoogle
AI 大模型 文心一言通义千问讯飞星火认知大模型ChatGPT
PHP 手册 PHP Manual

环境

项目 描述
PHP 8.0.0
PHP 编辑器 PhpStorm 2023.1.1(专业版)

访问修饰符

访问修饰符

访问修饰符在面向对象编程中起着重要的作用,它们提供了 对类的属性和方法的访问级别控制。访问修饰符能够为你的程序带来以下优点:

  1. 封装性(Encapsulation)
    访问修饰符允许将类的内部实现细节隐藏起来,仅暴露必要的公有接口给外部使用。这样可以有效地封装数据和行为,提高代码的可维护性和可重用性。

  2. 访问控制(Access Control)
    通过访问修饰符,可以控制类的成员对外部的可见性和可访问性。不同的访问修饰符可以限制属性和方法的访问范围,只允许特定的代码段访问,从而提供了更好的安全性和数据保护。

PHP 反序列化漏洞:身份标识_第1张图片

PHP 与访问修饰符

PHP 支持三种访问修饰符:publicprotected 以及 private,这些修饰符所提供的访问级别具体如下。

修饰符
(Modifier)
访问级别
(Access Level)
类内部
(Inside Class)
子类
(In Subclass)
外部
(Outside Class)
public 公有 可访问 可访问 可访问
protected 受保护 可访问 可访问 不可访问
private 私有 可访问 不可访问 不可访问

注:

在 PHP 中,访问修饰符是可以省略的,但这仅仅针对于类中的方法而言。默认情况下,如果没有指定访问修饰符,方法都将被视为公有成员。

手写身份标识

身份标识

在 PHP 中,当对象被序列化时,对象的 非公共属性的名称会被特殊处理以表示其可见性(访问修饰符)。对此,请参考如下示例:




class MyClass
{
    # 私有属性
    private $name = 'RedHeart';
    # 受保护属性
    protected $nation = 'China';
}

var_dump(serialize(new MyClass()));

执行效果

string(82) "O:7:"MyClass":2:{s:13:" MyClass name";s:8:"RedHeart";s:9:" * nation";s:5:"China";}"

MyClass 类中的私有属性名称 name 在转化为序列化文本后,成了 s:13:" MyClass name;",相比原先的 name 增加了两个空格(实际上是 控制字符 NUL,无法通过空格进行代替)及所属类的名称 MyClass

这样的处理方式是为了 在反序列化对象时能够正确地还原属性的可见性。当 PHP 在反序列化时遇到这些特殊的前缀,PHP 会 知道如何正确地设置属性的可见性。公共属性不会有这种特殊处理,它们在序列化后的文本中保持原始的属性名。

定义身份标识

身份标识即 对象的属性所使用的访问修饰符在序列化文本中的体现,私有属性与受保护属性的身份标识分别是 NUL所属类的名称NULNUL*NUL ,公有属性则无身份标识以与前两者相区分。

PHP 反序列化漏洞:身份标识_第2张图片

控制字符 NUL

控制字符 NUL 也被称为 空字符,是 ASCII 表中的第一个字符,其 十进制值0。在 ASCII 表中,控制字符是一系列 非打印字符用于控制硬件设备或通信协议的行为,而 NUL 的主要作用是标记字符串的结束

在 PHP 中如何表示空字符?

在 PHP 中,你可以通过转移字符 \0 表示空字符 NUL。但需要注意的是,在 PHP 中,转义字符仅有在 双引号 内才会被认为是转义字符,若转义字符存在于 单引号 中,则该转义字符将被视为普通字符。对此,请参考如下示例:




# 位于单引号中的 “转义字符”
var_dump('\0');
var_dump('Hello\0World');

# 位于双引号中的 转义字符
var_dump("\0");
var_dump("Hello\0World");

执行效果

string(2) "\0"
string(12) "Hello\0World"
string(1) " "
string(11) "Hello World"

通过空字符尝试构建包含非公共属性对象的序列化文本

既然知道了如何在 PHP 中表示 空字符,那么我们就能够通过这一特性来 手写(不使用 serialize() 函数来获取需要使用到的 PHP 序列化文本) 包含非公共属性对象的序列化文本。对此,请参考如下示例:




class MyClass {}

# 构造需要使用到的序列化文本
$serialize_text = 'O:7:"MyClass":2:{s:13:"' . "\0MyClass\0" . 'name";s:8:"RedHeart";s:9:"' . "\0*\0" . 'nation";s:5:"China";}';

# 对序列化文本执行反序列化操作
var_dump(unserialize($serialize_text));

执行效果

object(MyClass)#1 (2) {
  ["name":"MyClass":private]=>
  string(8) "RedHeart"
  ["nation":protected]=>
  string(5) "China"
}

由于序列化文本中的引号需要使用 双引号,而该序列化文本中需要使用到的引号个数 较多(若较少的话,可以在字符串最外层使用双引号,对字符串内部的双引号进行转义),故在序列化文本最外侧使用了单引号。通过 拼接双引号包裹的含有转义字符的文本实现身份标识的书写

空字符的传输

控制字符的不可打印性

在 ASCII 表中,控制字符是一系列 非打印字符用于控制硬件设备或通信协议的行为。因为控制字符产生的本意并不是为了展示内容,因此当你尝试将控制字符进行 复制粘贴 等操作时,对于不同的环境可能会有不同的结果,应具体分析。可能的情况如下:

  • 在一些文本编辑器中,这些字符可能会显示为 特定的符号(空格、 等)完全不显示(此时,你连复制 ”控制字符“ 都没有可能)
  • 在一些终端或控制台应用中,这些字符可能会直接 按照其原始的控制功能来执行

结论




# 尝试输出空字符 NUL
var_dump("\0");

# 将复制到的空字符 NUL 作为 ord 函数的参数进行输出
var_dump(ord(' '));

执行效果

在 PHP 编辑器 PHPStorm 中,我得到 "\0" 的输出并尝试将其中的空字符复制并粘贴于 ord() 函数中以作为该函数的参数。

在尝试 两次(第一次执行示例代码是为了获得空字符,第二次则是为了获得 ord() 函数的输出结果) 后,得到上述界面(蓝色部分即复制内容,是空字符 NUL 在 PHPStorm 中的显示效果)。ord() 函数能够得到某个字符对应的 ASCII 码,此处的结果是 32,对应的字符是可打印字符 空格

所以,结论是 空字符这类控制字符是无法通过复制粘贴这一操作进行传输的

另辟蹊径

URL 字符编码

URL 字符编码是一套转换依据,其功能主要有以下三点:

  1. 将非 ASCII 字符文本转换为 ASCII 字符文本
  2. 去除语义
  3. 将不可见字符(如空格)转换为可见文本
将非 ASCII 字符文本转换为 ASCII 字符文本

URL 中的中文等非 ASCII 字符在 通过网络传输前 需要通过 URL 编码进行转换以使其符合 URL 的设计原则(URL 基于 ASCII 字符集进行设计)

在 PHP 中,可通过内置函数 urlencode() 将非 ASCII 文本转换为 URL 编码字符。对此,请参考如下示例:




var_dump(urlencode('Hello World'));
var_dump(urlencode('你好,中国'));
var_dump(urlencode('你好I am a space世界'));

执行效果

string(11) "Hello+World"
string(45) "%E4%BD%A0%E5%A5%BD%EF%BC%8C%E4%B8%AD%E5%9B%BD"
string(48) "%E4%BD%A0%E5%A5%BDI+am+a+space%E4%B8%96%E7%95%8C"
去除语义

URL 中的 特殊字符 需要通过 URL 字符编码来进行转换以使其 失去其在 URL 中的特殊含义

举个栗子

查询字符串由 一个或多个参数 组成,每个参数之间使用 & 符号 进行 分隔。如果查询字符串中的某一个参数中包含 & 符号,请问 阁下如何让程序将这个 & 理解为参数中的内容而不是参数与参数之间的连接标识呢?

此时,URL 字符编码 去除语义 的作用就体现出来了。通过将参数中的 & 进行 URL 编码以使得程序不再将其作为参数与参数之间的连接符来进行看待而只是将其视为普通的文本内容。

具体而言

?username=RedHeart&myflag=&x&

这段查询字符串仅包含两个参数,username 的参数值为 RedHeart,而 myflag 的参数值为 &x&

但实际上,这段查询字符串将被程序理解为三个参数,其中 username 的参数值为 RedHeart,而 myflagx 的参数值均为 空(什么也没有)

解决方案

将参数中的具有 URL 语义的特殊字符 & 转换为其对应的 URL 编码 %26,使 & 失去其语义即可。

?username=RedHeart&myflag=%26x%26
将不可见字符(如空格)转换为可见文本

空格和其他 空白字符 在 URL 中 不易阅读,可能导致 混淆或误解。通过将空白字符转换为 可识别 的形式,能够增强 URL 的 可读性及准确性

注:

在 URL 中,空格将被转换为 +,而制表符将被转换为 %09

URL Encode

在尝试利用 PHP 反序列化漏洞时,往往需要使用到 URL。如果将 包含身份标识的序列化文本进行 URL 编码处理,那么其中的 控制字符就能同 URL 一同转移了。对此,请参考如下示例:

网页源码文件 index.php 中的内容




class MyClass {}

# 尝试将 URL 中的查询字符串中的 X 参数的值进行反序列并将结果进行输出
var_dump(unserialize($_GET['X']));

为了获得 URL 编码后的序列化文本,我们可以使用如下两种方案:

通过序列化构造的对象来获得序列化文本并将其进行 URL 编码处理




class MyClass
{
    private $name = 'RedHeart';
    protected $nation = 'China';
}

# 将序列化结果进行 URL 编码处理
var_dump(urlencode(serialize(new MyClass)));

将构造的序列化文本进行 URL 编码处理




# 构造需要使用到的序列化文本
$serialize_text = 'O:7:"MyClass":2:{s:13:"' . "\0MyClass\0" . 'name";s:8:"RedHeart";s:9:"' . "\0*\0" . 'nation";s:5:"China";}';

# 将构造的序列化文本进行 URL 编码处理
var_dump(urlencode($serialize_text));

URL 编码处理后的序列化文本如下:

O%3A7%3A%22MyClass%22%3A2%3A%7Bs%3A13%3A%22%00MyClass%00name%22%3Bs%3A8%3A%22RedHeart%22%3Bs%3A9%3A%22%00%2A%00nation%22%3Bs%3A5%3A%22China%22%3B%7D

使用这段文本作为 X 参数的值并尝试通过浏览器访问 index.php 页面,得到如下界面:

你可能感兴趣的:(安全,php,PHP,反序列化漏洞,身份标识,URL,编码,控制字符,访问修饰符)