PHP反序列化从浅入深

PHP反序列化由浅入深

序列化和反序列化

有时需要把一个对象在网络上传输,为了方便传输,可以把整个对象转化为二进制串,等到达另一端时,再还原为原来的对象,这个过程称之为串行化(也叫序列化)。这种将原本的数据通过某种手段进行”压缩”,并且按照一定的格式存储的过程就可以称之为序列化,而将压缩后的数据还原为原有的对象方式则是反序列化。

漏洞的成因

PHP 反序列化漏洞又叫做 PHP 对象注入漏洞,是因为程序对输入数据处理不当导致的.

反序列化漏洞的成因在于代码中的 unserialize()接收的参数可控,而可能在对应的类中又使用了危险方法,最终导致了危险方法的输入可控,可能会导致远程代码执行(RCE),本地文件包含(LFI),任意文件读取等问题。

下面我们来了解一下一切的开始:序列化与反序列化函数。

序列化与反序列化函数

序列化函数:serialize(),用于将一个类序列化为一个二进制字符串,可用于网络传输后保留类的完整性

反序列化函数:unserialize(),用于将一个二进制字符串反序列化为已有的类,并会根据预定义的魔术方法自动进行操作.

在常见的CTF题目中,unserialize()函数往往允许用户输入并作为PHP反序列化的入口点,而最终结束于被传入危险函数的远程代码执行或文件读取。而利用的传递属性的介质往往是自动触发的PHP魔术方法.

常见魔术方法介绍

魔术方法 调用时机 备注
__construct () 实例化时调用 构造函数
__destrct() 销毁时调用 析构函数
__call() 在对象中调用一个不可访问方法时 类比java 外部想调用类的内部函数
__callStatic() 在静态上下文中调用一个不可访问方法时 同上
__get() 读取不可访问(protected 或 private)或不存在的属性的值时 类比Java中的Getter
__set() 给不可访问(protected 或 private)或不存在的属性赋值时 类比Java中的Setter
__isset() 对不可访问(protected 或 private)或不存在的属性调用 isset() 或 empty() 时 用于在外部判断一个内部变量的状态
__unset() 对不可访问(protected 或 private)或不存在的属性调用 unset() 时 用于在外部删除一个内部变量
__sleep() 执行 serialize() 时 长眠于此
__wakeup() 执行 unserialize() 时 苏醒吧,我的爱人
__toString() 把类当成字符串时 设计到字符串处理均会触发
__invoke() 把对象当成函数调用时 用于直接调用类作为函数的处理逻辑
__debugInfo() 使用 var_dump, print_r 时 想要对类进行调试的时候使用
__set_state() 调用var_export()导出类时
__autoload() 尝试加载未定义的类
__clone() 当对象复制完成时 调用 clone函数可以考虑一下,使用浅拷贝创建了一份新的对象引用

__toString()的触发时机比较多,常见如下:

  1. echo($obj)/print($obj)打印时会触发
  2. 反序列化对象与字符串连接时
  3. 反序列化对象参与格式化字符串时
  4. 反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)
  5. 反序列化对象参与格式化SQL语句,绑定参数时
  6. 反序列化对象在经过php字符串处理函数,如strlen()strops()strcmp()addslashes()
  7. in_array()方法中,第一个参数时反序列化对象,第二个参数的数组中有__toString()返回的字符串的时候__toString()会被调用
  8. 反序列化的对象作为class_exists()的参数的时候

反序列化函数对字符串处理的工作原理

PHP中的unserialize函数根据类型标记和长度进行分词和属性还原。一个常见的被序列化的PHP对象一般如下

1
O:1:"a":1:{s:3:"act";s:24:"show_source('flag.php');";}

PHP通过类型标识符:长度:内容的格式来处理字符串,并对其进行类型和值的重建,而访问控制则是通过特定格式来实现的.它首先接受到类型并重建,根据其后跟随的长度读取其后对应长度的内容,并进行内容还原,在匹配到结束符号后结束反序列化.

反序列化依托已有的类进行攻击,若未定义将会被反序列化为__PHP_Incomplete_Class.(不完整的PHP类)

类型标识符

PHP 序列化字符串使用特定的 类型标识符 来表示数据类型和结构。例如:

O:表示对象(object),后接长度及类名。对象声明的格式通常为 O:<class name length>:"<class name>":<property count>:{<properties>},用于说明对象的类名、属性数量及具体的属性数据。

s:表示字符串(string),后接长度及字符串内容。字符串的格式为 s:<length>:"<value>";

i:表示整数(integer),格式为 i:<value>;

a:表示数组(array),包括键值对和键值数量的详细说明,格式为 a:<num>:{<key-value pairs>}

内容中的访问控制

在序列化对象属性时,PHP 使用特定的字符串前缀来表示属性的可见性和访问控制,特别是在 publicprotectedprivate 访问控制方面。例如:

Public 属性:无前缀,例如 s:3:"act";

Protected 属性:使用前缀 *,标记格式为 s:<length>:"*<property_name>";

Private 属性:使用类名和 \0 前缀,格式为 s:<length>:"\0<classname>\0<property_name>";

要注意的是,传递参数时\0前缀为空白符,无法被正常复制,需要使用Urlencode后发送Payload.

危险函数利用

危险函数提供了PHP反序列化中的构造链终点,也是我们要想办法进行传参的目标函数.

函数类型 函数列表 描述和风险
代码执行函数 eval(), assert(), preg_replace() (e), create_function(), array_map(), call_user_func(), call_user_func_array(), array_filter(), usort(), uasort() 这些函数允许将字符串作为 PHP 代码执行。如果字符串内容由用户控制,则可能引入代码注入漏洞,特别是 eval() 和带 /e 修饰符的 preg_replace()
命令执行函数 system(), exec(), shell_exec(), passthru(), pcntl_exec(), popen(), proc_open() 用于执行系统命令。如果传入参数未经过严格验证,可能导致命令注入攻击,使攻击者执行任意系统命令。
文件包含函数 require, include, require_once, include_once 用于包含并执行 PHP 文件。如果文件路径受用户控制(如不正确过滤的 URL 参数),可能导致文件包含漏洞,甚至执行恶意代码。
文件读取函数 copy(), file_get_contents(), highlight_file(), fopen(), readfile(), fread(), fgetss(), fgets(), parse_ini_file(), show_source(), file() 这些函数可以读取文件内容,如果路径由外部控制,可能导致信息泄露甚至文件读取。
信息泄露函数 phpinfo() 输出 PHP 环境信息,可能泄露服务器的配置、已安装模块等敏感信息。
常见漏洞函数 intval(), switch(), in_array(), unset(), ini_set(), md5(), ereg(), strcmp() (5.3以前), is_numeric(), sha1() 某些函数(如 md5()sha1())会导致哈希碰撞,intval()in_array() 等可能因类型不严格导致逻辑漏洞。
变量覆盖 extract(), parse_str(), import_request_variables(), $$ 这些函数允许动态创建变量或批量导入变量,可能导致变量覆盖、逻辑错误和安全隐患,尤其是在处理用户输入时。

POP链构造

PHP反序列化的攻击是基于属性的攻击,通过提供恶意参数,依托于已定义的类中的恶意函数中来完成攻击。而从反序列化到最终完成恶意操作中的参数传递链,被称为POP链.

POP 面向属性编程(Property-Oriented Programing) 常用于上层语言构造特定调用链的方法,与二进制利用中的面向返回编程ROP(Return-Oriented Programing)的原理相似,都是从现有运行环境中寻找一系列的代码或者指令调用,然后根据需求构成一组连续的调用链,最终达到攻击者邪恶的目的.

CTF实战中,POP链往往由多个魔术方法串联起来,以unserialize()开头,魔术方法作为中间传递链,恶意函数作为链尾.

高阶利用

这里的大部分方法都是依托基础的反序列化来实现的.拓展在没有直接给出unserialize()函数时的攻击面.

__wakeup魔术方法的绕过(CVE-2016-7124)

影响范围

  • PHP5 < 5.6.25
  • PHP7 < 7.0.10

正常来说在反序列化过程中,会先调用__wakeup()方法再进行unserialize(),但如果序列化字符串中表示对象属性个数的值大于真实的属性个数时,__wakeup()的执行会被跳过。

Session反序列化

Session反序列化相当于是对于攻击面的扩展,帮助寻找反序列化传参入口点.

session_start()被调用或者php.inisession.auto_start为1时,PHP内部调用会话管理器,访问用户 session被序列化以后,存储到指定目录(默认为/tmp).

序列化机制 描述 特性及差异 常见用途 示例
PHP PHP 默认的会话序列化格式 使用 serialize()unserialize() 解析,支持 PHP 类型和对象,但易引发安全问题。 适用于传统会话存储 O:1:"a":1:{s:3:"act";s:24:"show_source('flag.php');"},表示对象 a 有一个属性 act,值为 show_source('flag.php');
php_serialize 使用标准的 serialize 序列化机制 类似 PHP 格式,但更严格,防止反序列化漏洞,对象重建更安全。 高安全性会话数据存储 a:2:{s:4:"name";s:4:"John";s:3:"age";i:30;} 表示数组包含两个键值对:name => Johnage => 30
php_binary 将数据以紧凑二进制格式进行序列化 提高存储效率,但可读性低,复杂数据结构处理速度快 高效存储需求的会话,如大型应用或分布式系统 \x04\x0a 表示压缩的二进制数据,在数据传输效率上更优。
json 使用 json_encode / json_decode 兼容性强,支持跨平台,但不支持复杂 PHP 对象和资源类型 前后端分离和跨平台应用场景 {"name":"John","age":30} 表示 JSON 格式的键值对,结构简单,便于前后端共享。
msgpack 使用 MessagePack 格式 更高效率、更小存储空间,适用于高性能场景,但不支持 PHP 对象 高性能和大数据量的 PHP 应用 \x82\xa4name\xa4John\xa3age\x1e,表示 name => Johnage => 30,采用高效的二进制编码。
wddx 使用 WDDX 格式(XML) 兼容性好但效率低,适用于 PHP 和其他语言共享 需要与非 PHP 系统的数据交换 <wddxPacket version='1.0'><struct><var name='name'><string>John</string></var><var name='age'><number>30</number></var></struct></wddxPacket>
igbinary 使用 igbinary 扩展进行二进制序列化 更紧凑,减少内存使用,通常用于缓存系统和性能优化 适用于 Redis 等缓存系统的会话数据存储 二进制格式:\x00\x02\x04name\x04John\x03age\x1e,相较 PHP 序列化更省空间。
memcache memcache 扩展将数据存储到内存 提高数据共享和访问速度,不适用于持久化存储 速度要求高、数据频繁读取的会话管理 将会话数据以 memcache 的格式存储,便于在不同进程间快速共享数据。
redis 使用 redis 存储会话数据 提供数据持久化与快速访问,同时支持分布式和高可用 分布式环境和高可用要求的会话存储 会话数据直接存储到 Redis 中,适合大规模分布式系统。
custom_handler 自定义会话存储机制 实现 SessionHandlerInterface 自定义序列化逻辑 需要定制会话存储逻辑的特殊业务场景 需开发者实现自定义逻辑,如加密存储或符合特定需求的格式。

主要关注前三种PHP,php_serialize,php_binary的反序列化利用;Session文件的格式一般为sess_${PHPSESSID}格式.

HTTP请求一个页面后,如果用到开启session,会去读COOKIE中的PHPSESSID是否有,如果没有,则会新生成一个session_id,先存入COOKIE中的PHPSESSID中,再生成一个sess_前缀文件。当有写入$_SESSION的时候,就会往sess_文件里序列化写入数据。当读取session变量的时候,先会读取COOKIE中的PHPSESSID,获得session_id,然后再去找这个sess_session_id文件,来获取对应的数据。由于默认的PHPSESSID是临时的会话,在浏览器关闭后就会消失,所以,当我们打开浏览器重新访问的时候,就会新生成session_idsess_session_id这个文件。

PHP

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

image-20241026041246357

1
session|s:6:"sakura";

序列化格式为session|s:<length>:<content>

PHP_binary

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_binary');
session_start();
$_SESSION['sessionsessionsessionsessionsession'] = $_GET['session'];
?>

image-20241026042709344

1
#sessionsessionsessionsessionsessions:6:"sakura";

起始符号为键名长度对应的 ASCII 的值:len(sessionsessionsessionsessionsessions)=35;chr(35)=#

序列化格式为${chr(len(key))}+key+s:<value>

php_serialize

1
2
3
4
5
6
<?php
error_reporting(0);
ini_set('session.serialize_handler','php_serialize');
session_start();
$_SESSION['session'] = $_GET['session'];
?>

image-20241026044107354

1
a:1:{s:7:"session";s:6:"sakura";}

序列化格式为标准化数组$_SESSION携带传入的键值对序列化后的结果.

漏洞的产生

不同页面混用PHP序列化机制,导致session内容被加载反序列化时触发带有漏洞的反序列化漏洞.

例题
1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php');
session_start();
class Test{
public $code;
function __wakeup(){
eval($this->code);
}
}
?>

简单的命令执行,反序列化后直接eval;使用php作为反序列化机制

1
2
3
4
5
6
7
8
<?php
highlight_file(__FILE__);
ini_set('session.serialize_handler', 'php_serialize');
session_start();
if(isset($_GET['test'])){
$_SESSION['test']=$_GET['test'];
}
?>

使用了不一样的php_serialize机制,会导致session内容被控制,从而在漏洞页面上进行解析时触发session反序列化漏洞.

PHP内置类反序列化利用

Error/Exception - XSS

Error/Exception - Bypass Hash Comparison

SoapClient - SSRF

DirectoryIterator -Bypass open_basedir

SimpleXMLElement - XXE

字符替换WAF导致反序列化逃逸

在php中,反序列化的过程中必须严格按照序列化规则才能成功实现反序列化.然而底层代码是以 ; 作为字段的分隔,以 } 作为结尾,并且是根据长度判断内容的.这就导致了单纯使用字符替换WAF,并且替换字符串长度不同的情况下,会导致反序列化逃逸漏洞,没有达到原本的预期效果.

过滤后字符减少

替换后设定长度大于实际长度,会导致实际反序列化时向后读取多余字符串,从而导致反序列化类属性的逃逸。

过滤后字符增多

替换后设定长度小于实际长度,会导致实际反序列化时向前截断字符串,从而导致反序列化类属性的逃逸。

Phar://反序列化

什么是Phar

PHP Archive, like a Java Jar, but for PHP.

phar(PHP Archive)是类似于JAR的一种打包文件。PHP ≥5.3对Phar后缀文件是默认开启支持的,不需要任何其他的安装就可以使用它。

phar扩展提供了一种将整个PHP应用程序放入.phar文件中的方法,以方便移动、安装。.phar文件的最大特点是将几个文件组合成一个文件的便捷方式,.phar文件提供了一种将完整的PHP程序分布在一个文件中并从该文件中运行的方法。

说白了,就是一种压缩文件,但是不止能放压缩文件进去。

在做进一步探究之前需要先调整配置,因为对于Phar文件的相关操作,php缺省状态是只读的(也就是说单纯使用Phar文件不需要任何的调整配置)。但是因为我们现在需要创建一个自己的Phar文件,所以需要允许写入Phar文件,这需要修改一下 php.ini.生成phar文件需要修改php.ini中的配置,将phar.readonly设置为Off.

Phar文件结构

  1. a stub是一个文件标志,格式为 :xxx<?php xxx;__HALT_COMPILER();?>
  2. manifest是被压缩的文件的属性等放在这里,这部分是以序列化存储的,是主要的攻击点。
  3. contents是被压缩的内容。
  4. signature签名,放在文件末尾。

就是这个文件由四部分组成,每种文件都是有它独特的一种文件格式的,有首有尾。而__HALT_COMPILER();就是相当于图片中的文件头的功能,没有它,图片无法解析,同样的,没有文件头,php识别不出来它是phar文件,也就无法起作用。

生成的phar文件,打开该文件可以看到文件头是<?php __halt_compiler(); ?>以及中间的部分内容是序列化的形式存在于这个文件中.

漏洞利用方法

1.phar文件能够上传至服务器
//即要求存在include、file_get_contents、file_put_contents、copy、file、file_exists、is_executable、is_file、is_dir、is_link、is_writable、fileperms、fileinode、filesize、fileowner、filegroup、fileatime、filemtime、filectime、filetype、getimagesize、exif_read_data、stat、lstat、touch、md5_filefopen(),copy(),file_exist(),filesize()这种函数

2.要有可利用的魔术方法
//这个的话用一位大师傅的话说就是利用魔术方法作为”跳板”

3.文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
//一般利用姿势是上传Phar文件后通过伪协议Phar来实现反序列化,伪协议Phar格式是Phar://这种,如果这几个特殊字符被过滤就无法实现反序列化

4.php版本大于5.3.0

5.phar.readonly选项为OFF