PHP开发中的不安全反序列化

序列化是开发语言中将某个对象转换为一串字节流的过程,转换后的字节流可以方便存储在数据库中,也可以方便在网络中进行传输。而反序列化则是将数据库取出的字节流或从网络上接收到的字节流反向转换为对象的过程。概念虽如此,但不同的开发语言的序列化和反序列化的过程又略有不同。

以下代码是PHP序列化和反序列化的简单示例:

如果应用的反序列化字符串能够最终被用户操控,那么恶意攻击者可以操控序列化对象将恶意代码植入到应用中执行,从而造成诸如命令执行的漏洞。因此,应用开发中对不可信的输入来源如无必要不要做反序列化操作。

PHP的序列化基本类型如下表所示。

以上文中的序列化输出为例:

复制代码
O:3:"Car":3:{...}

开头大写的O表示后面的字符串是对象类型,之后的3表示类名的长度(即Car的长度),最后的3表示类中属性的个数(即brand、model、year)。

复制代码
s:5:"brand";s:6:"Toyota";

这是Car类的第一个属性,包括属性名称和属性值,根据上面的序列化基本类型,s表示是字符串,5和6表示字符串长度,最后是字符串值。

复制代码
s:5:"model";s:5:"Camry";

同上,这是Car类的第二个属性,包括属性名称model和属性值Camry。

复制代码
s:4:"year";i:2022;

这是Car类的第三个属性,包括属性名称year和属性值2022,其中i表示整型类型,后续接数值2022。

可见,字符串在序列化后是不会转义的,但上例中属性都是公有的(public),因此没有类名做前缀,如果是保护类型(protected),则会表示为\x00*\x00,如果是私有类型(private),则会表示为\x00Car\x00。

假设上述序列化字符串的model属性存在注入漏洞,且该序列化字符串可被攻击者控制,那么攻击者可以构造类似下面的序列化字符串:

复制代码
O:3:"Car":3:{s:5:"brand";s:6:"Toyota";s:5:"model";s:17:"Camry\' or 1=1 -- ";s:4:"year";i:2022;}

CVE-2018-18702和CVE-2019-13292便是由于用户输入的内容经过反序列化之后执行数据库操作从而导致的SQL注入漏洞,因此CVSS评分高达9.8分。

CVE-2019-13292构造的POC是:

复制代码
echo base64_encode(serialize(["0" => "' or sleep(5) and '1'='1"]));

实际场景中很少会有这么简单且直接的反序列化漏洞。这时攻击者可以利用PHP的魔法函数,魔法函数以双下划线开头,并会在反序列化被调用过程中执行。 PHP中的魔法函数包括:

__destruct:析构函数

__wakeup:反序列化时先被调用,而后再执行反序列化,用于准备对象需要的资源

__sleep:序列化时先被调用,而后再执行序列化,用于清理对象

__toString:类被当做字符串时调用,该方法必须返回字符串

__invoke:类被当做函数使用时调用

比如下面的示例代码:

上述代码的类对象序列化之后的结果是:

复制代码
O:12:"Serialkiller":4:{s:24:"%00Serialkiller%00cache_file";s:16:"cache/john.cache";s:22:"%00Serialkiller%00log_file";s:13:"logs/john.log";s:21:"%00Serialkiller%00content";s:12:"Starting log";s:18:"%00Serialkiller%00user";s:4:"john";}

因此,可以控制cache_file变量和log_file变量执行任意操作。比如利用__wakeup函数写入shell代码:

复制代码
O:12:"Serialkiller":4:{s:24:"%00Serialkiller%00cache_file";s:5:"1.txt";s:22:"%00Serialkiller%00log_file";s:13:"logs/rce1.php";s:21:"%00Serialkiller%00content";s:24:"<?php%20system(%27ls%20~%27);%20?>";s:18:"%00Serialkiller%00user";s:4:"john";}

在实际开发中,为了防止可能出现的反序列化漏洞有多种办法,其中一种是检查序列化字符串的类型。比如下面的代码:

复制代码
<?php
function _safely_unserialize($input) {
 if (is_string($input) &&
   in_array(substr($input, 0, 1), array('a', 'O', 'b'))) {
   $input = "#" . $input;
 }

 return @unserialize($input);
}

_safely_unserialize($_POST['data']);?>
?>

这段代码中,_safely_unserialize函数通过in_array方法判断用户可控的data参数是否是PHP序列化的三种类型,即数组(a)、对象(O)和布尔值(b),如果是其中之一的类型,则在字符串前增加#,从而导致在反序列化时造成反序列化失败,最终返回false值。该函数旨在判断序列化字符串是否是安全的,但判断方法仅仅是判断序列化类型。

黑名单的过滤方式漏掉了PHP的另一种序列化对象类(C),因此可以构造类的序列化字符串绕过黑名单检查。 比如通过下述代码构造C开头的序列化字符串:

复制代码
class obj implements Serializable {
 private $data;

 public function __construct() {
    $this->data = "My private data";
 }

 public function serialize() {
   return serialize($this->data);
 }

 public function unserialize($data) {
   $this->data = unserialize($data);
 }

 public function getData() {
   return $this->data;
 }
}

$obj = new obj;
$ser = serialize($obj);

var_dump($ser);

从PHP 8.1版本开始,官方不再鼓励从Serializable类继承serialize()和unserialize(),而是建议直接使用魔术方法__serialize()和__unserialize(),但生成的序列化类型会是O。

复制代码
class obj {
 public $message;

 public function __construct() {
    $this->message = "My private data";
 }

 public function __serialize() {
   return ['msg' => $this->message];
 }

  public function __unserialize(array $data) {
   $this->message = $data['msg'];
 }
}

$obj = new obj;
$ser = serialize($obj);

var_dump($ser);
var_dump(unserialize($ser));

作者:裴伟伟

2024年5月15日

洞源实验室

相关推荐
Nick同学4 分钟前
GatewayWorker 使用总结
后端·php
饮长安千年月22 分钟前
JavaSec-SpringBoot框架
java·spring boot·后端·计算机网络·安全·web安全·网络安全
大咖分享课1 小时前
容器安全最佳实践:云原生环境下的零信任架构实施
安全·云原生·架构
淡水猫.2 小时前
ApacheSuperset CVE-2023-27524
安全·web安全
恰薯条的屑海鸥2 小时前
零基础在实践中学习网络安全-皮卡丘靶场(第九期-Unsafe Fileupload模块)(yakit方式)
网络·学习·安全·web安全·渗透测试·csrf·网络安全学习
Bruce_Liuxiaowei2 小时前
Web安全深度解析:源码泄漏与未授权访问漏洞全指南
安全·web安全
代码搬运媛3 小时前
React 中 HTML 插入的全场景实践与安全指南
安全·react.js·html
CRMEB定制开发4 小时前
CRMEB 中 PHP 快递查询扩展实现:涵盖一号通、阿里云、腾讯云
阿里云·php·腾讯云·商城系统·商城源码
CRMEB定制开发4 小时前
PHP 打印扩展开发:从易联云到小鹅通的多驱动集成实践
php·小程序源码·商城源码·微信商城·php商城源码
Bruce_Liuxiaowei4 小时前
PHP文件包含漏洞详解:原理、利用与防御
开发语言·网络安全·php·文件包含