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日

洞源实验室

相关推荐
23zhgjx-NanKon39 分钟前
华为eNSP:QinQ
网络·安全·华为
23zhgjx-NanKon41 分钟前
华为eNSP:mux-vlan
网络·安全·华为
昔我往昔1 小时前
阿里云文本内容安全处理
安全·阿里云·云计算
棱角~~3 小时前
盘点和嗨格式一样好用的10款数据恢复!!
数据库·经验分享·安全·电脑·学习方法
Dingww10114 小时前
梧桐数据库中的网络地址类型使用介绍分享
数据库·oracle·php
NETFARMER运营坛4 小时前
如何优化 B2B 转化率?这些步骤你不可不知
大数据·安全·阿里云·ai·ai写作
安徽京准4 小时前
京准时钟:无人机卫星信号安全防护隔离装置
安全·无人机·信号安全防护装置·卫星安全隔离装置·北斗授时安全隔离·北斗对时防护隔离装置
mingzhi615 小时前
渗透测试-快速获取目标中存在的漏洞(小白版)
安全·web安全·面试·职场和发展
Coding~5 小时前
NewStar easygui re wp
安全
23zhgjx-zgx5 小时前
以太网交换安全:DHCP Snooping
linux·服务器·网络·安全·华为