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日

洞源实验室

相关推荐
BingoGo18 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack18 小时前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
cipher1 天前
ERC-4626 通胀攻击:DeFi 金库的"捐款陷阱"
前端·后端·安全
BingoGo2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php
JaguarJack2 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php·服务端
JaguarJack3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
后端·php·服务端
BingoGo3 天前
推荐 PHP 属性(Attributes) 简洁读取 API 扩展包
php
JaguarJack4 天前
告别 Laravel 缓慢的 Blade!Livewire Blaze 来了,为你的 Laravel 性能提速
后端·php·laravel
郑州光合科技余经理4 天前
代码展示:PHP搭建海外版外卖系统源码解析
java·开发语言·前端·后端·系统架构·uni-app·php
一次旅行4 天前
网络安全总结
安全·web安全