安全声明:本文仅用于 CTF 学习与授权环境复现。
可直接运行的 PHP shell 示例已替换为<PHP_WEB_SHELL_PAYLOAD_OMITTED_FOR_SAFETY>。
对应十六进制 PHP payload 已替换为<HEX_ENCODED_PHP_PAYLOAD_OMITTED_FOR_SAFETY>。
文中的危险脚本文件名发布时统一改为output.txt,避免触发杀软或被误用为可执行脚本。
php反序列化
魔术方法

从序列化到反序列化这几个函数的执行过程是:__construct() ->__sleep() -> __wakeup() -> __toString() -> __destruct()
绕过__wakeup()
当成员属性数目大于实际数目
反序列化过程中,会先调用wakeup()方法再进行unserilize(),但如果序列化字符串中表示对象属性个数的值大于真实的属性个数时,wakeup()的执行会被跳过
攻防世界的unserialize3
将设置属性值为2,可导致反序列化异常,如下所示。
1 | O:4:"xctf":2:{s:4:"flag";s:3:"111";} |

&=
把两个变量指向同一内存地址,这样当对其中一个变量进行操作时,另一个也会随之变化

在__destruct中,条件为:if(!isset($this->wakeup)||!$this->wakeup)。这个条件的意思是:如果$this->wakeup不存在,或者存在但为假(即False),则执行echo。
我们的目标是让程序输出”You get it!”。但是,当我们反序列化一个对象时,会先调用__wakeup方法(如果存在),然后当脚本结束时,会调用__destruct。
通过让 key 和 wakeup 指向同一个内存地址(引用),在 __wakeup 中设为 True,再在 __destruct 中通过修改 key 间接把 wakeup 改成 False,从而绕过检查,触发 “You get it!”
1 | $a = new ctf(); |
fast destruct?
在反序列化攻击中,我们往往要利用某个类的 __destruct() 或 __wakeup() 等魔术方法来执行恶意代码。但默认情况下,反序列化完成后,对象会一直存活直到脚本结束,__destruct() 才会被调用。如果我们希望它在反序列化过程中立即执行(例如为了绕过某些检查,或者让恶意代码更早触发),就可以利用 fast destruct 技巧。
如何实现 fast destruct?
核心思路是 让 unserialize() 在解析过程中遇到语法错误,从而提前终止,并销毁已经部分构建的对象。有两种常用手法:
1. 修改序列化数组的元素个数
对于一个数组的序列化字符串,格式是:a:{元素个数}:{内容}
例如一个包含两个元素的数组:a:2:{i:0;O:7:"myclass":1:{s:1:"a";O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}}}
如果我们把元素个数改成 比实际多的数字,比如 a:3:{...},那么 unserialize() 在解析时,会期望数组有 3 个元素,但实际只有 2 个。当它读完第 2 个元素后,发现字符串已经结束,就会报错(”unexpected end of string”),此时已经创建的两个对象(myclass 和 Hello)就会被立即销毁,触发它们的 __destruct()。
2. 去掉序列化字符串结尾的 }
每个序列化结构都有明确的结束符,比如数组的 }、对象的 }。如果故意去掉最后一个 },例如:a:1:{i:0;O:7:"myclass":1:{s:1:"a";O:5:"Hello":1:{s:3:"qwb";s:5:"/flag";}}
(注意末尾少了一个 })unserialize() 会一直解析直到字符串末尾,发现缺少预期的结束符,同样会报错,并销毁已创建的对象。
php issue#9618 (wakeup与destruct在不同类)
原理
声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上\0*\0的前缀。这里的\0 表示 ASCII 码为 0 的字符(不可见字符),而不是 \0 组合。也就是说当实例化的类里存在私有属性时比如private时,序列化它时会出现字符长度那里会出现不可见字符
但事实上只有这种情况能够绕过wakeup,也就是destruct和wakeup在不同的类的时候,如果他们存在同一个类时输入直接serialize得到的payload是没有回显的

使用C绕过

分析ArrayObject序列化后以C开头的特性,将恶意对象“打包”进ArrayObject,生成以C开头的序列化字符串,绕过正则检查(正则只拦O/a开头)。反序列化时,ArrayObject会“拆包”并触发内部恶意对象的__destruct,实现命令执行。
payload
原生类反序列化
C开头
1 | ArrayObject::unserialize |
绕MD5/SHA1 XSS
使用Error/Exception内置类 这两个类使用方法一样的
例如
1 | <?php |
1 | <?php |

这两个原生类返回的信息除了行号一模一样
发现可以绕过MD5和shal
遍历文件
1 | DirectoryIterator |
DirectoryIterator 基础用法

glob:// 伪协议突破限制

读取文件
SplFileObject类
1 | <?php |
SplFileObject优势:- 自动处理大文件(不会内存溢出)
- 内置行号跟踪(
$file->key()可获取当前行号)
SSRF
SoapClient 类
该内置类有一个 __call 方法,当 __call 方法被触发后,它可以发送 HTTP 和 HTTPS 请求
示例
1 | public SoapClient :: SoapClient(mixed $wsdl [,array $options ]) |
第一个参数是用来指明是否是 wsdl 模式,将该值设为 null 则表示非 wsdl 模式。
·第二个参数为一个数组,如果在 wsdl 模式下,此参数可选;如果在非 wsdl 模式下,则必须设置 location 和 uri 选项,其中 location 是要将请求发送到的 SOAP 服务器的 URL,而 uri 是 SOAP 服务的目标命名空间。
1 | <?php |
- location:是我们要攻击的目标服务器地址(即我们想让PHP服务器去访问的地址)。
- uri:只是一个命名空间标识符,通常设置为与location相关或任意合法URI,但它不代表本机或目标服务器。它不会触发网络请求到该URI(除非SOAP消息被解析时有外部实体引用,但SoapClient在生成请求时不会因为uri而发起请求)。
phar反序列化


大体来说 Phar 结构由4部分组成
1.stub :phar文件标识
1 | <?php |
可以理解为一个标志,格式为xxx<?php xxx; __HALT_COMPILER();?>,前面内容不限,但必须以__HALT_COMPILER();?>来结尾,否则phar扩展将无法识别这个文件为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)
签名,放在文件末尾
前提:生成phar文件需要修改php.ini中的配置,将phar.readonly设置为Off
phar文件生成
1 | <?php |

PHP session反序列化
什么是session?其实就是服务器为了保存用户状态而创建的一个保存用户信息的特殊对象,是存储在服务端的,有session那肯定就有sessionid了,他是怎么来的?当我们浏览器第一次访问服务器时,服务器创建一个session对象并且该对象有一个唯一的id,叫做sessionId,服务器会将sessionid以cookie的方式发送给浏览器,当浏览器再次访问服务器时,会将sessionId发送过来,服务器依据sessionId就可以找到对应的session对象,在这创建session对象的时候服务端先进行序列化再存储到session文件里(session文件就是专门保存序列化后的对象的),相反服务器依据sessionId找到对应的session对象的过程就是通过提取session文件来反序列化生成对象的,就是在这过程中就有可能产生session反序列化漏洞了,前提条件使用不同的引擎来处理session文件
当session_start()被调用或者php.ini中session.auto_start为1时,
php内部调用会话管理器,访问用户session被序列化以后,存储到指定目录(默认为/tmp).
存取数据的格式有多种,常用的有三种
