【PHP/ThinkPHP】ThinkPHP 应用层 eval 注入漏洞修复指南(业务代码常见代码注入场景与防御实战)

在 ThinkPHP 框架漏洞修复完成后,安全团队往往忽视了业务代码层面的代码注入风险。本文基于笔者 2026 年 Q1 处置的多起 ThinkPHP 业务代码 eval 注入事件,系统梳理了 5 类常见场景:动态类名/方法名调用、命令行脚本中的 eval、动态模板渲染、配置文件包含、表单动态规则执行。这些场景的共同特征是:业务代码调用了 eval()call_user_func()$$varcreate_function() 等动态执行函数,且参数部分或全部由用户输入控制。本文从根因、攻击链、检测、修复到监控自动化,提供完整的实战指南,帮助企业建设 ThinkPHP 应用层代码注入防御体系。

1. 漏洞全景概览

1.1 应用层代码注入 vs 框架层 RCE

维度 框架层 RCE(如 CVE-2018-20062) 应用层代码注入(本文)
责任方 框架开发者 业务开发者
修复方式 升级框架版本 修改业务代码
触发位置 框架核心代码 业务 Controller/Model/Command
利用入口 框架默认参数(如 ?s= 业务自定义参数(如 ?action=?callback=
检测难度 较易(PoC 公开) 较难(需逐业务分析)
WAF 通用性 高(特征明显) 低(业务参数千差万别)

⚠️ 关键提醒:框架升级后,业务代码的 eval 注入漏洞仍可被利用。本文案例全部来自 ThinkPHP 5.x/6.x 升级后的真实事件。

1.2 5 类常见代码注入场景速览

场景 危险函数 典型业务代码 CVSS 评估
1. 动态类名/方法名调用 call_user_func$obj->$method() API 路由分发器、插件系统 9.8 CRITICAL
2. 命令行脚本 eval eval()assert() VpsTest 类、定时任务脚本 9.8 CRITICAL
3. 动态模板渲染 eval()include 后台模板编辑、邮件模板 9.6 CRITICAL
4. 配置文件动态加载 includerequire 多租户配置、插件配置 8.6 HIGH
5. 表单动态规则执行 eval()create_function() 自定义表单验证、规则引擎 9.8 CRITICAL

1.3 漏洞核心特征

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

eval 注入
漏洞特征
业务代码动态执行
用户输入未充分过滤
框架升级无法修复
需逐业务代码审计
WAF 通用性差
常见场景
动态类名方法名调用
命令行脚本eval
动态模板渲染
配置文件动态加载
表单动态规则执行
技术原理
call_user_func可控参数
eval拼接用户输入
双美元符号变量变量
create_function代码生成
include用户可控路径
修复方案
白名单校验输入
静态调用替代动态
禁用eval和create_function
PHP 8+使用命名参数
代码审计+SCA工具

2. 漏洞根因深度分析

2.1 场景一:动态类名/方法名调用

典型漏洞代码

php 复制代码
// app/index/controller/Api.php --- 通用 API 路由分发器
namespace app\index\controller;

use think\Controller;

class Api extends Controller
{
    public function dispatch($module, $action, $params = [])
    {
        // ❌ 危险:$module 和 $action 用户可控
        $className = 'app\\index\\service\\' . ucfirst($module) . 'Service';
        
        if (class_exists($className)) {
            $service = new $className();
            
            // ❌ 危险:$action 用户可控,可调用任意公开方法
            if (method_exists($service, $action)) {
                return call_user_func_array([$service, $action], $params);
            }
        }
        
        return json(['error' => 'Method not found'], 404);
    }
}

// 攻击 Payload:
// GET /api/dispatch?module=user&action=__construct&params[0]=admin
// 实际调用: app\index\service\UserService->__construct('admin')
// 进一步可调用: __destruct、__toString 等魔术方法触发 POP 链

根因 :业务代码用 call_user_func_array 调用动态方法名,未对 $action 做白名单校验,攻击者可调用类内的任意公开方法,包括魔术方法和敏感方法。

2.2 场景二:命令行脚本 eval 注入

典型漏洞代码(基于真实事件脱敏):

php 复制代码
// app/index/command/VpsTest.php --- VPS 探测脚本
namespace app\index\command;

use think\console\Command;
use think\console\Input;
use think\console\Output;

class VpsTest extends Command
{
    protected function configure()
    {
        $this->setName('vps:test')
             ->addArgument('ip')
             ->addArgument('port')
             ->addArgument('vf')  // ❌ 危险:验证函数参数
             ->setDescription('VPS Connectivity Test');
    }

    protected function execute(Input $input, Output $output)
    {
        $ip = $input->getArgument('ip');
        $port = $input->getArgument('port');
        $validateFunc = $input->getArgument('vf');  // 用户可控
        
        // ❌ 严重危险:eval 执行用户输入
        $result = eval("return $validateFunc('$ip', $port);");
        
        $output->writeln("Test result: " . $result);
    }
}

// 攻击 Payload:
// php think vps:test 127.0.0.1 80 "system('id')"
// 实际执行: eval("return system('id')('127.0.0.1', 80);")
// 命令执行: system('id') 输出 uid=33(www-data)

根因 :命令行脚本用 eval() 执行用户传入的"验证函数",攻击者传入 system('id') 等代码片段,直接被 eval 执行。

2.3 场景三:动态模板渲染

典型漏洞代码

php 复制代码
// app/admin/controller/Template.php --- 后台模板编辑
namespace app\admin\controller;

use think\Controller;

class Template extends Controller
{
    public function preview($templateId)
    {
        $template = Db::name('templates')->find($templateId);
        
        // ❌ 危险:模板内容用户可控,eval 渲染
        $content = $template['content'];
        
        // 业务需要渲染 PHP 标签
        ob_start();
        eval('?>' . $content);  // ❌ 严重危险
        $html = ob_get_clean();
        
        return response($html);
    }
}

// 攻击 Payload:
// 1. 攻击者通过模板编辑接口写入: <?php system('id'); ?>
// 2. 调用预览接口触发 eval
// 3. 服务器执行 system('id')

根因 :业务代码用 eval('?>' . $content) 渲染模板,模板内容存储在数据库中,攻击者通过模板编辑接口写入恶意 PHP 代码。

2.4 场景四:配置文件动态加载

典型漏洞代码

php 复制代码
// app/index/controller/Tenant.php --- 多租户配置加载
namespace app\index\controller;

use think\Controller;

class Tenant extends Controller
{
    public function loadConfig($tenantId)
    {
        // ❌ 危险:$tenantId 用户可控,未做严格校验
        $configFile = '/etc/tenants/' . $tenantId . '.php';
        
        if (file_exists($configFile)) {
            // ❌ 严重危险:include 用户可控路径
            $config = include $configFile;
            return $config;
        }
        
        return [];
    }
}

// 攻击 Payload:
// GET /tenant/loadConfig?tenantId=../../../var/www/html/shell
// 实际路径: /etc/tenants/../../../var/www/html/shell.php
// 规范化: /var/www/html/shell.php
// 触发: include /var/www/html/shell.php

根因 :业务代码用 include 加载用户可控路径的配置文件,攻击者通过路径穿越加载任意 PHP 文件。

2.5 场景五:表单动态规则执行

典型漏洞代码

php 复制代码
// app/index/service/FormValidator.php --- 动态表单验证
namespace app\index\service;

class FormValidator
{
    public function validate($value, $rule)
    {
        // ❌ 危险:$rule 用户可控
        // 业务允许管理员在后台配置验证规则
        
        // 旧代码用 create_function(PHP 7.2+ 已废弃)
        $validator = create_function('$value', $rule);
        return $validator($value);
        
        // 或更现代的 eval 写法
        // return eval("return $rule;");
    }
}

// 攻击 Payload:
// 管理员后台配置规则: system('id')
// 表单提交时触发: create_function('$value', "system('id')")
// 等价于: function lambda($value) { system('id'); }
// 调用时执行: system('id')

根因 :业务代码用 create_functioneval 执行用户配置的验证规则,攻击者通过后台配置接口写入恶意代码。

2.6 攻击链通用模型

#mermaid-svg-2KzuUnfKG1iaNqNt{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-2KzuUnfKG1iaNqNt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-2KzuUnfKG1iaNqNt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-2KzuUnfKG1iaNqNt .error-icon{fill:#552222;}#mermaid-svg-2KzuUnfKG1iaNqNt .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-2KzuUnfKG1iaNqNt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-2KzuUnfKG1iaNqNt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-2KzuUnfKG1iaNqNt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-2KzuUnfKG1iaNqNt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-2KzuUnfKG1iaNqNt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-2KzuUnfKG1iaNqNt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-2KzuUnfKG1iaNqNt .marker{fill:#333333;stroke:#333333;}#mermaid-svg-2KzuUnfKG1iaNqNt .marker.cross{stroke:#333333;}#mermaid-svg-2KzuUnfKG1iaNqNt svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-2KzuUnfKG1iaNqNt p{margin:0;}#mermaid-svg-2KzuUnfKG1iaNqNt .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-2KzuUnfKG1iaNqNt .cluster-label text{fill:#333;}#mermaid-svg-2KzuUnfKG1iaNqNt .cluster-label span{color:#333;}#mermaid-svg-2KzuUnfKG1iaNqNt .cluster-label span p{background-color:transparent;}#mermaid-svg-2KzuUnfKG1iaNqNt .label text,#mermaid-svg-2KzuUnfKG1iaNqNt span{fill:#333;color:#333;}#mermaid-svg-2KzuUnfKG1iaNqNt .node rect,#mermaid-svg-2KzuUnfKG1iaNqNt .node circle,#mermaid-svg-2KzuUnfKG1iaNqNt .node ellipse,#mermaid-svg-2KzuUnfKG1iaNqNt .node polygon,#mermaid-svg-2KzuUnfKG1iaNqNt .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-2KzuUnfKG1iaNqNt .rough-node .label text,#mermaid-svg-2KzuUnfKG1iaNqNt .node .label text,#mermaid-svg-2KzuUnfKG1iaNqNt .image-shape .label,#mermaid-svg-2KzuUnfKG1iaNqNt .icon-shape .label{text-anchor:middle;}#mermaid-svg-2KzuUnfKG1iaNqNt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-2KzuUnfKG1iaNqNt .rough-node .label,#mermaid-svg-2KzuUnfKG1iaNqNt .node .label,#mermaid-svg-2KzuUnfKG1iaNqNt .image-shape .label,#mermaid-svg-2KzuUnfKG1iaNqNt .icon-shape .label{text-align:center;}#mermaid-svg-2KzuUnfKG1iaNqNt .node.clickable{cursor:pointer;}#mermaid-svg-2KzuUnfKG1iaNqNt .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-2KzuUnfKG1iaNqNt .arrowheadPath{fill:#333333;}#mermaid-svg-2KzuUnfKG1iaNqNt .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-2KzuUnfKG1iaNqNt .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-2KzuUnfKG1iaNqNt .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2KzuUnfKG1iaNqNt .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-2KzuUnfKG1iaNqNt .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2KzuUnfKG1iaNqNt .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-2KzuUnfKG1iaNqNt .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-2KzuUnfKG1iaNqNt .cluster text{fill:#333;}#mermaid-svg-2KzuUnfKG1iaNqNt .cluster span{color:#333;}#mermaid-svg-2KzuUnfKG1iaNqNt 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-2KzuUnfKG1iaNqNt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-2KzuUnfKG1iaNqNt rect.text{fill:none;stroke-width:0;}#mermaid-svg-2KzuUnfKG1iaNqNt .icon-shape,#mermaid-svg-2KzuUnfKG1iaNqNt .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-2KzuUnfKG1iaNqNt .icon-shape p,#mermaid-svg-2KzuUnfKG1iaNqNt .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-2KzuUnfKG1iaNqNt .icon-shape .label rect,#mermaid-svg-2KzuUnfKG1iaNqNt .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-2KzuUnfKG1iaNqNt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-2KzuUnfKG1iaNqNt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-2KzuUnfKG1iaNqNt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 攻击者发现业务入口
识别动态执行点
call_user_func
eval
include
create_function
变量变量 $$var
构造 Payload
绕过输入校验
触发动态执行
PHP 代码执行
利用方式
执行系统命令
读取敏感文件
写入 Webshell
连接数据库
服务器被控制

3. 攻击场景与影响评估

3.1 真实攻击案例复盘

text 复制代码
案例一:API 路由分发器被利用写入 Webshell

时间:2026-02-15
目标:某电商平台 ThinkPHP 5.1.31 站点(已修复框架漏洞)
入口:/api/dispatch?module=user&action=__construct&params[0]=x
利用:
1. 攻击者发现 /api/dispatch 接口接收 module/action/params 三个参数
2. 通过 action=__construct 调用 UserService->__construct()
3. 配合 ThinkPHP Request 类的 __construct,覆盖 filter 属性
4. 后续请求触发 call_user_func(system, 'id')
5. 写入 Webshell 持久化

影响:
- 攻击者获得 www-data 权限
- 读取数据库配置,窃取 50 万用户数据
- 通过服务器跳板攻击内网其他系统
text 复制代码
案例二:命令行脚本被 cron 触发

时间:2026-03-08
目标:某 SaaS 平台 ThinkPHP 6.0.14 站点
入口:crontab 调用 php think vps:test
利用:
1. 攻击者通过其他漏洞获得 RCE,但 PHP-FPM 禁用了 system
2. 发现服务器 crontab 每小时执行: php think vps:test <ip> <port> <vf>
3. <ip> 来自数据库,攻击者通过 SQL 注入写入: 127.0.0.1') ; system('id') //
4. crontab 触发时 eval 执行: system('id')
5. 绕过 PHP-FPM 的 disable_functions 限制

影响:
- 攻击者绕过 disable_functions 限制
- 获得 cli 用户权限(通常比 www-data 权限高)
- 横向移动到其他服务器

3.2 攻击流程时序

操作系统 PHP引擎 业务代码 业务应用 攻击者 操作系统 PHP引擎 业务代码 业务应用 攻击者 #mermaid-svg-H6adGBXdr4jX14Qd{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-H6adGBXdr4jX14Qd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-H6adGBXdr4jX14Qd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-H6adGBXdr4jX14Qd .error-icon{fill:#552222;}#mermaid-svg-H6adGBXdr4jX14Qd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-H6adGBXdr4jX14Qd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-H6adGBXdr4jX14Qd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-H6adGBXdr4jX14Qd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-H6adGBXdr4jX14Qd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-H6adGBXdr4jX14Qd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-H6adGBXdr4jX14Qd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-H6adGBXdr4jX14Qd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-H6adGBXdr4jX14Qd .marker.cross{stroke:#333333;}#mermaid-svg-H6adGBXdr4jX14Qd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-H6adGBXdr4jX14Qd p{margin:0;}#mermaid-svg-H6adGBXdr4jX14Qd .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-H6adGBXdr4jX14Qd text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-H6adGBXdr4jX14Qd .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-H6adGBXdr4jX14Qd .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-H6adGBXdr4jX14Qd .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-H6adGBXdr4jX14Qd .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-H6adGBXdr4jX14Qd #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-H6adGBXdr4jX14Qd .sequenceNumber{fill:white;}#mermaid-svg-H6adGBXdr4jX14Qd #sequencenumber{fill:#333;}#mermaid-svg-H6adGBXdr4jX14Qd #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-H6adGBXdr4jX14Qd .messageText{fill:#333;stroke:none;}#mermaid-svg-H6adGBXdr4jX14Qd .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-H6adGBXdr4jX14Qd .labelText,#mermaid-svg-H6adGBXdr4jX14Qd .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-H6adGBXdr4jX14Qd .loopText,#mermaid-svg-H6adGBXdr4jX14Qd .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-H6adGBXdr4jX14Qd .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-H6adGBXdr4jX14Qd .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-H6adGBXdr4jX14Qd .noteText,#mermaid-svg-H6adGBXdr4jX14Qd .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-H6adGBXdr4jX14Qd .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-H6adGBXdr4jX14Qd .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-H6adGBXdr4jX14Qd .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-H6adGBXdr4jX14Qd .actorPopupMenu{position:absolute;}#mermaid-svg-H6adGBXdr4jX14Qd .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-H6adGBXdr4jX14Qd .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-H6adGBXdr4jX14Qd .actor-man circle,#mermaid-svg-H6adGBXdr4jX14Qd line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-H6adGBXdr4jX14Qd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 第一步:识别动态执行点 通过错误信息识别可调用方法 触发 __construct 魔术方法 第二步:构造 POP 链 第三步:触发命令执行 第四步:写入 Webshell 持久化 GET /api/dispatch?module=test&action=phpinfo call_user_func(TestService, 'phpinfo') 方法不存在或返回异常 HTTP 500 或 404 GET /api/dispatch?module=user&action=__construct call_user_func(UserService, '__construct', \[\]) 返回成功 HTTP 200 POST /api/dispatch module=user&action=__construct&params0=filter&params1=system 覆盖 UserService 属性 属性覆盖成功 GET /api/any Request::input 触发 filter call_user_func('system', 'id') system('id') uid=33(www-data) HTTP 200 + uid=33 "GET /api/any?cmd=file_put_contents('shell.php',shell_code)" system 执行 file_put_contents 写入 Webshell 写入成功

3.3 影响评估

影响维度 评估 说明
机密性 严重 可读取任意文件、配置、数据库凭证
完整性 严重 可写入 Webshell、篡改业务数据
可用性 严重 可删除文件、关停服务
攻击门槛 需要识别业务动态执行点
利用难度 需要构造业务特定的 Payload
WAF 防御难度 业务参数千差万别,通用规则难以覆盖
修复紧迫度 极高 一旦被利用即可完全控制服务器

4. 检测:如何识别业务代码中的代码注入

4.1 静态代码扫描

识别业务代码中的危险函数调用,是发现 eval 注入的第一步。

bash 复制代码
#!/bin/bash
# thinkphp_code_injection_scan.sh --- 业务代码 eval 注入扫描
# 用法: ./thinkphp_code_injection_scan.sh [项目根目录]

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

echo "===== ThinkPHP 业务代码 eval 注入扫描 ====="
echo "扫描目录: $PROJECT_ROOT"
echo ""

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

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

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

if [ -n "$CALL_USER_FUNC" ]; then
    echo "    ⚠️  发现 call_user_func 调用(需人工审核):"
    echo "$CALL_USER_FUNC"
else
    echo "    ✅ 未发现 call_user_func 调用"
fi

# 3. 扫描 create_function
echo ""
echo "[3] create_function 调用扫描"
CREATE_FUNC=$(grep -rn "create_function\s*(" "$PROJECT_ROOT/app" --include="*.php" 2>/dev/null | \
    grep -vE "vendor/|tests/" | head -10)

if [ -n "$CREATE_FUNC" ]; then
    echo "    🔴 发现 create_function 调用(PHP 7.2+ 已废弃):"
    echo "$CREATE_FUNC"
else
    echo "    ✅ 未发现 create_function 调用"
fi

# 4. 扫描变量变量 $$var
echo ""
echo "[4] 变量变量 \$\$var 扫描"
VAR_VAR=$(grep -rn '\$\$' "$PROJECT_ROOT/app" --include="*.php" 2>/dev/null | \
    grep -vE "vendor/|tests/" | head -10)

if [ -n "$VAR_VAR" ]; then
    echo "    ⚠️  发现变量变量用法(需人工审核):"
    echo "$VAR_VAR"
else
    echo "    ✅ 未发现变量变量用法"
fi

# 5. 扫描动态 include/require
echo ""
echo "[5] 动态 include/require 扫描"
DYNAMIC_INCLUDE=$(grep -rnE "(include|require)(_once)?\s*\(?\s*\\$" "$PROJECT_ROOT/app" --include="*.php" 2>/dev/null | \
    grep -vE "vendor/|tests/|runtime/" | head -20)

if [ -n "$DYNAMIC_INCLUDE" ]; then
    echo "    ⚠️  发现动态 include/require(需人工审核路径是否可控):"
    echo "$DYNAMIC_INCLUDE"
else
    echo "    ✅ 未发现动态 include/require"
fi

# 6. 扫描 PHP 代码执行函数
echo ""
echo "[6] PHP 代码执行函数扫描"
EXEC_FUNCS=$(grep -rnE "(assert\s*\(|preg_replace.*/e|usort\s*\(.*create_function|uasort\s*\(.*create_function)" \
    "$PROJECT_ROOT/app" --include="*.php" 2>/dev/null | grep -vE "vendor/|tests/" | head -10)

if [ -n "$EXEC_FUNCS" ]; then
    echo "    🔴 发现代码执行函数:"
    echo "$EXEC_FUNCS"
else
    echo "    ✅ 未发现代码执行函数"
fi

# 7. 扫描命令执行函数
echo ""
echo "[7] 命令执行函数扫描"
CMD_FUNCS=$(grep -rnE "(system\s*\(|exec\s*\(|passthru\s*\(|shell_exec\s*\(|popen\s*\(|proc_open\s*\()" \
    "$PROJECT_ROOT/app" --include="*.php" 2>/dev/null | grep -vE "vendor/|tests/" | head -20)

if [ -n "$CMD_FUNCS" ]; then
    echo "    ⚠️  发现命令执行函数(需人工审核参数是否可控):"
    echo "$CMD_FUNCS"
else
    echo "    ✅ 未发现命令执行函数"
fi

echo ""
echo "===== 扫描完成 ====="
echo "⚠️  所有"需人工审核"项请由安全工程师复核"

4.2 动态检测:识别可被利用的入口

通过 fuzzing 测试识别业务接口是否可被代码注入攻击。

python 复制代码
#!/usr/bin/env python3
"""
ThinkPHP 业务接口代码注入 Fuzzing 工具
识别 call_user_func、eval 等动态执行点
"""
import requests
import re
from urllib.parse import urljoin
from concurrent.futures import ThreadPoolExecutor, as_completed

class CodeInjectionFuzzer:
    def __init__(self, base_url):
        self.base_url = base_url
        self.session = requests.Session()
        self.session.headers.update({
            'User-Agent': 'Mozilla/5.0 (Security Scan)'
        })
        
        # 探测 Payload(无副作用,仅用于识别)
        self.payloads = [
            # 通用 PHP 代码执行探测
            ('phpinfo_test', 'phpinfo()'),
            # 数学运算验证(无副作用)
            ('math_test', '6*7'),
            # 字符串拼接验证
            ('string_test', '"test" . "ok"'),
        ]
        
        # 可疑参数名(业务自定义参数)
        self.suspicious_params = [
            'action', 'method', 'func', 'function', 'callback',
            'rule', 'validate', 'filter', 'exec', 'eval',
            'code', 'content', 'template', 'module', 'service',
            'vf', 'handler', 'process', 'call'
        ]
    
    def fuzz_endpoint(self, endpoint, method='GET'):
        """对单个端点进行 fuzzing"""
        vulnerabilities = []
        
        for param in self.suspicious_params:
            for payload_name, payload in self.payloads:
                try:
                    if method == 'GET':
                        url = endpoint
                        params = {param: payload}
                        resp = self.session.get(url, params=params, timeout=5)
                    else:
                        resp = self.session.post(endpoint, data={param: payload}, timeout=5)
                    
                    # 检查响应中是否包含 PHP 执行结果
                    if self._detect_execution(resp.text, payload_name):
                        vulnerabilities.append({
                            'endpoint': endpoint,
                            'param': param,
                            'payload': payload,
                            'evidence': resp.text[:200]
                        })
                
                except requests.RequestException:
                    continue
        
        return vulnerabilities
    
    def _detect_execution(self, response_text, payload_name):
        """检测响应是否包含执行结果"""
        if payload_name == 'phpinfo_test':
            return 'PHP Version' in response_text or 'phpinfo' in response_text.lower()
        elif payload_name == 'math_test':
            # 检查是否返回 42
            return '42' in response_text and '6*7' not in response_text
        elif payload_name == 'string_test':
            return 'testok' in response_text
        return False
    
    def scan_endpoints(self, endpoints):
        """批量扫描端点"""
        all_vulns = []
        
        with ThreadPoolExecutor(max_workers=5) as executor:
            futures = {
                executor.submit(self.fuzz_endpoint, ep): ep 
                for ep in endpoints
            }
            
            for future in as_completed(futures):
                endpoint = futures[future]
                try:
                    vulns = future.result()
                    if vulns:
                        all_vulns.extend(vulns)
                        print(f"🔴 {endpoint} 发现 {len(vulns)} 个潜在漏洞")
                except Exception as e:
                    print(f"❌ {endpoint} 扫描失败: {e}")
        
        return all_vulns


if __name__ == '__main__':
    # 使用示例
    target = 'http://localhost'
    fuzzer = CodeInjectionFuzzer(target)
    
    # 待扫描的端点列表(从 API 文档或爬虫获取)
    endpoints = [
        f'{target}/api/dispatch',
        f'{target}/api/user/list',
        f'{target}/api/form/validate',
        f'{target}/admin/template/preview',
        f'{target}/tenant/loadConfig',
    ]
    
    print("===== ThinkPHP 代码注入 Fuzzing =====")
    print(f"目标: {target}")
    print(f"端点数: {len(endpoints)}")
    print()
    
    vulns = fuzzer.scan_endpoints(endpoints)
    
    print()
    print("===== 扫描结果 =====")
    if vulns:
        print(f"发现 {len(vulns)} 个潜在漏洞:")
        for v in vulns:
            print(f"  端点: {v['endpoint']}")
            print(f"  参数: {v['param']}")
            print(f"  Payload: {v['payload']}")
            print(f"  证据: {v['evidence'][:100]}")
            print()
    else:
        print("✅ 未发现代码注入漏洞")

4.3 日志检测脚本

bash 复制代码
#!/bin/bash
# code_injection_log_detect.sh --- 代码注入攻击日志检测
# 用法: ./code_injection_log_detect.sh [日志路径]

LOG_FILE="${1:-/var/log/nginx/access.log}"

echo "===== 代码注入攻击日志检测 ====="
echo ""

# 1. 检查可疑参数模式
echo "[1] 可疑参数模式检测"
SUSPICIOUS=$(grep -cE '(action=__construct|method=__construct|func=system|callback=phpinfo)' "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$SUSPICIOUS" -gt 0 ]; then
    echo "    🔴 发现 $SUSPICIOUS 条可疑参数攻击"
    grep -E '(action=__construct|method=__construct|func=system)' "$LOG_FILE" | head -5
else
    echo "    ✅ 未发现可疑参数攻击"
fi

# 2. 检查 PHP 代码注入特征
echo ""
echo "[2] PHP 代码注入特征检测"
PHP_INJECTION=$(grep -cE '(phpinfo\(\)|system\(|exec\(|passthru\(|eval\()' "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$PHP_INJECTION" -gt 0 ]; then
    echo "    🔴 发现 $PHP_INJECTION 条 PHP 代码注入尝试"
    grep -E '(phpinfo\(\)|system\(|exec\(|eval\()' "$LOG_FILE" | head -10
else
    echo "    ✅ 未发现 PHP 代码注入"
fi

# 3. 检查 create_function 特征
echo ""
echo "[3] create_function 攻击检测"
CREATE_FUNC=$(grep -c "create_function" "$LOG_FILE" 2>/dev/null || echo 0)
if [ "$CREATE_FUNC" -gt 0 ]; then
    echo "    🔴 发现 $CREATE_FUNC 条 create_function 攻击"
else
    echo "    ✅ 未发现 create_function 攻击"
fi

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

5. 修复方案

5.1 场景一修复:动态类名/方法名调用

用白名单限制可调用的类和方法,从根本上消除任意方法调用。

php 复制代码
// ✅ 安全代码:白名单校验
namespace app\index\controller;

use think\Controller;

class Api extends Controller
{
    // 类名白名单
    private $allowedModules = ['user', 'order', 'product', 'payment'];
    
    // 每个类允许调用的方法白名单
    private $allowedActions = [
        'user'    => ['getList', 'getDetail', 'create', 'update'],
        'order'   => ['getList', 'getDetail', 'create', 'cancel'],
        'product' => ['getList', 'getDetail', 'search'],
        'payment' => ['create', 'query', 'callback'],
    ];
    
    public function dispatch($module, $action, $params = [])
    {
        // 1. 模块白名单校验
        $module = strtolower($module);
        if (!in_array($module, $this->allowedModules)) {
            return json(['error' => 'Invalid module'], 404);
        }
        
        // 2. 方法白名单校验
        if (!isset($this->allowedActions[$module]) || 
            !in_array($action, $this->allowedActions[$module])) {
            return json(['error' => 'Invalid action'], 404);
        }
        
        // 3. 类名仅允许字母数字(防止命名空间注入)
        if (!preg_match('/^[a-z]+$/', $module)) {
            return json(['error' => 'Invalid module name'], 404);
        }
        
        // 4. 方法名仅允许字母数字下划线
        if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $action)) {
            return json(['error' => 'Invalid action name'], 404);
        }
        
        // 5. 禁止调用魔术方法和下划线开头方法
        if (strpos($action, '__') === 0 || strpos($action, '_') === 0) {
            return json(['error' => 'Forbidden action'], 403);
        }
        
        // 6. 安全调用
        $className = 'app\\index\\service\\' . ucfirst($module) . 'Service';
        $service = new $className();
        
        return call_user_func_array([$service, $action], $params);
    }
}

5.2 场景二修复:命令行脚本 eval 注入

禁用 eval,改用预定义的验证函数映射。

php 复制代码
// ✅ 安全代码:预定义验证函数映射
namespace app\index\command;

use think\console\Command;
use think\console\Input;
use think\console\Output;

class VpsTest extends Command
{
    // 验证函数白名单
    private $allowedValidators = [
        'check_port'    => 'validatePort',
        'check_ping'    => 'validatePing',
        'check_http'    => 'validateHttp',
        'check_https'   => 'validateHttps',
    ];
    
    protected function configure()
    {
        $this->setName('vps:test')
             ->addArgument('ip')
             ->addArgument('port')
             ->addArgument('vf')  // 验证函数名(非代码)
             ->setDescription('VPS Connectivity Test');
    }

    protected function execute(Input $input, Output $output)
    {
        $ip = $input->getArgument('ip');
        $port = $input->getArgument('port');
        $validatorName = $input->getArgument('vf');
        
        // 1. 验证 IP 格式
        if (!filter_var($ip, FILTER_VALIDATE_IP)) {
            $output->error("Invalid IP: $ip");
            return;
        }
        
        // 2. 验证端口范围
        $port = (int)$port;
        if ($port < 1 || $port > 65535) {
            $output->error("Invalid port: $port");
            return;
        }
        
        // 3. 验证函数白名单校验
        if (!isset($this->allowedValidators[$validatorName])) {
            $output->error("Invalid validator: $validatorName");
            $output->info("Allowed validators: " . implode(', ', array_keys($this->allowedValidators)));
            return;
        }
        
        // 4. 调用预定义的验证方法(不再用 eval)
        $method = $this->allowedValidators[$validatorName];
        $result = $this->$method($ip, $port);
        
        $output->writeln("Test result: " . ($result ? 'PASS' : 'FAIL'));
    }
    
    private function validatePort($ip, $port)
    {
        $connection = @fsockopen($ip, $port, $errno, $errstr, 5);
        if ($connection) {
            fclose($connection);
            return true;
        }
        return false;
    }
    
    private function validatePing($ip, $port)
    {
        // 安全的 ping 实现
        $result = exec("ping -c 1 -W 2 " . escapeshellarg($ip) . " 2>&1", $output, $returnVar);
        return $returnVar === 0;
    }
    
    private function validateHttp($ip, $port)
    {
        $url = "http://{$ip}:{$port}/";
        $headers = @get_headers($url, 5);
        return $headers !== false;
    }
    
    private function validateHttps($ip, $port)
    {
        $url = "https://{$ip}:{$port}/";
        $headers = @get_headers($url, 5);
        return $headers !== false;
    }
}

5.3 场景三修复:动态模板渲染

禁用 eval 渲染,改用安全的模板引擎。

php 复制代码
// ✅ 安全代码:使用 ThinkPHP 视图引擎替代 eval
namespace app\admin\controller;

use think\Controller;
use think\View;

class Template extends Controller
{
    public function preview($templateId)
    {
        $template = Db::name('templates')->find($templateId);
        
        if (!$template) {
            return json(['error' => 'Template not found'], 404);
        }
        
        $content = $template['content'];
        
        // ❌ 旧代码(危险):
        // ob_start();
        // eval('?>' . $content);
        // $html = ob_get_clean();
        
        // ✅ 新代码(安全): 使用 ThinkPHP 视图引擎
        // 视图引擎会自动转义 PHP 标签
        $view = new View();
        
        // 方案 1:使用 Twig 模板引擎(推荐)
        // composer require twig/twig
        $loader = new \Twig\Loader\ArrayLoader([
            'template' => $this->sanitizeTemplate($content),
        ]);
        $twig = new \Twig\Environment($loader, [
            'autoescape' => 'html',  // 自动 HTML 转义
        ]);
        
        try {
            $html = $twig->render('template', $template['variables'] ?? []);
        } catch (\Twig\Error\SyntaxError $e) {
            return json(['error' => 'Template syntax error'], 400);
        }
        
        return response($html);
    }
    
    /**
     * 清理模板内容,移除 PHP 标签
     */
    private function sanitizeTemplate($content)
    {
        // 移除所有 PHP 标签
        $content = preg_replace('/<\?php.*?\?>/s', '', $content);
        $content = preg_replace('/<\?.*?\?>/s', '', $content);
        $content = preg_replace('/<%.*?%>/s', '', $content);
        
        // 移除 ASP 风格标签
        $content = preg_replace('/<script\s+language\s*=\s*["\']?php["\']?.*?<\/script>/is', '', $content);
        
        return $content;
    }
}

5.4 场景四修复:配置文件动态加载

白名单校验 + 路径规范化,消除路径穿越。

php 复制代码
// ✅ 安全代码:白名单 + 路径规范化
namespace app\index\controller;

use think\Controller;

class Tenant extends Controller
{
    // 租户 ID 白名单格式
    private $tenantIdPattern = '/^[a-z0-9]{1,32}$/';
    
    // 配置文件目录(绝对路径)
    private $configDir = '/etc/tenants/';
    
    public function loadConfig($tenantId)
    {
        // 1. 格式校验(仅允许小写字母和数字)
        if (!preg_match($this->tenantIdPattern, $tenantId)) {
            return json(['error' => 'Invalid tenant ID'], 400);
        }
        
        // 2. 路径拼接
        $configFile = $this->configDir . $tenantId . '.php';
        
        // 3. 路径规范化,防止穿越
        $realPath = realpath($configFile);
        $realConfigDir = realpath($this->configDir);
        
        if ($realPath === false || $realConfigDir === false) {
            return json(['error' => 'Config not found'], 404);
        }
        
        // 4. 验证文件在配置目录内
        if (strpos($realPath, $realConfigDir) !== 0) {
            return json(['error' => 'Invalid config path'], 403);
        }
        
        // 5. 安全加载
        $config = include $realPath;
        
        return $config ?: [];
    }
}

5.5 场景五修复:表单动态规则执行

禁用 create_function 和 eval,改用预定义规则映射。

php 复制代码
// ✅ 安全代码:预定义验证规则
namespace app\index\service;

class FormValidator
{
    // 预定义验证规则映射
    private $ruleMap = [
        'required'    => 'validateRequired',
        'email'       => 'validateEmail',
        'phone'       => 'validatePhone',
        'url'         => 'validateUrl',
        'integer'     => 'validateInteger',
        'float'       => 'validateFloat',
        'date'        => 'validateDate',
        'regex'       => 'validateRegex',
        'length'      => 'validateLength',
        'in'          => 'validateIn',
        'not_in'      => 'validateNotIn',
        'between'     => 'validateBetween',
    ];
    
    public function validate($value, $rule, $params = [])
    {
        // ❌ 旧代码(危险):
        // $validator = create_function('$value', $rule);
        // return $validator($value);
        
        // ✅ 新代码(安全): 规则白名单
        if (!isset($this->ruleMap[$rule])) {
            throw new \InvalidArgumentException("Unknown validation rule: $rule");
        }
        
        $method = $this->ruleMap[$rule];
        return $this->$method($value, $params);
    }
    
    private function validateRequired($value, $params)
    {
        return !empty($value) || $value === '0';
    }
    
    private function validateEmail($value, $params)
    {
        return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
    }
    
    private function validatePhone($value, $params)
    {
        return preg_match('/^1[3-9]\d{9}$/', $value);
    }
    
    private function validateUrl($value, $params)
    {
        return filter_var($value, FILTER_VALIDATE_URL) !== false;
    }
    
    private function validateInteger($value, $params)
    {
        return filter_var($value, FILTER_VALIDATE_INT) !== false;
    }
    
    private function validateFloat($value, $params)
    {
        return filter_var($value, FILTER_VALIDATE_FLOAT) !== false;
    }
    
    private function validateDate($value, $params)
    {
        $format = $params['format'] ?? 'Y-m-d';
        $d = \DateTime::createFromFormat($format, $value);
        return $d && $d->format($format) === $value;
    }
    
    private function validateRegex($value, $params)
    {
        if (!isset($params['pattern'])) {
            throw new \InvalidArgumentException("Regex pattern required");
        }
        // 限制正则模式(禁止 /e 修饰符)
        $pattern = $params['pattern'];
        if (strpos($pattern, '/e') !== false) {
            throw new \InvalidArgumentException("Invalid regex pattern");
        }
        return preg_match($pattern, $value) === 1;
    }
    
    private function validateLength($value, $params)
    {
        $min = $params['min'] ?? 0;
        $max = $params['max'] ?? PHP_INT_MAX;
        $len = strlen($value);
        return $len >= $min && $len <= $max;
    }
    
    private function validateIn($value, $params)
    {
        return in_array($value, $params['list'] ?? []);
    }
    
    private function validateNotIn($value, $params)
    {
        return !in_array($value, $params['list'] ?? []);
    }
    
    private function validateBetween($value, $params)
    {
        $min = $params['min'] ?? 0;
        $max = $params['max'] ?? PHP_INT_MAX;
        return $value >= $min && $value <= $max;
    }
}

5.6 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

; 注意:eval 不是函数,无法通过 disable_functions 禁用
; 需要通过 Suhosin 扩展禁用 eval
; 安装 Suhosin: pecl install suhosin
; suhosin.executor.disable_eval = On

; 禁用 create_function(PHP 7.2+ 已废弃,PHP 8.0+ 已移除)
; 升级到 PHP 8.0+ 即可

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

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

; 限制 POST 数据大小
post_max_size = 8M

; 禁用 PHP 标签短格式(防止模板注入)
short_open_tag = Off

; 禁用 ASP 风格标签
asp_tags = Off

6. 踩坑记录

6.1 白名单校验遗漏导致绕过

text 复制代码
现象:
配置白名单 ['user', 'order', 'product'],但攻击者用 user\\Service 绕过。

根因:
白名单校验只检查了模块名,未校验是否包含反斜杠。

解决:
1. 严格正则校验:preg_match('/^[a-z]+$/', $module)
2. 禁止特殊字符:反斜杠、点号、下划线开头
3. 黑名单兜底:禁止 __ 开头的方法名(魔术方法)

教训:
白名单校验必须配合严格的格式校验,单一白名单不够。

6.2 命令行脚本被 cron 利用

text 复制代码
现象:
修复了 Web 接口的代码注入,但 cron 任务仍调用旧脚本。

根因:
命令行脚本 php think vps:test 由 cron 触发,参数来自数据库。
攻击者通过 SQL 注入修改数据库中的参数值。

解决:
1. 同步修复命令行脚本
2. 数据库参数做严格校验
3. cron 任务的输出重定向到日志,便于审计
4. 数据库连接使用低权限账号

教训:
代码审计要覆盖所有入口,包括 Web、CLI、队列、cron。

6.3 模板渲染修复后业务功能异常

text 复制代码
现象:
改用 Twig 模板引擎后,业务模板中的 PHP 标签失效。

根因:
原业务模板中使用了 <?php ?> 标签做条件渲染。

解决:
1. 重构业务模板,将 PHP 逻辑改为模板变量
2. 使用 Twig 的条件语法:{% if xxx %}...{% endif %}
3. 提供模板迁移工具,自动转换 PHP 标签

教训:
模板引擎迁移要评估业务影响,提供迁移工具和文档。

6.4 WAF 规则无法覆盖所有业务参数

text 复制代码
现象:
配置 WAF 规则拦截 system/exec/passthru 后,正常业务接口误报。

根因:
业务参数名千差万别,通用 WAF 规则难以精确匹配。

解决:
1. 业务层做白名单校验(根本解)
2. WAF 仅做兜底,拦截明显的代码注入特征
3. 为每个业务接口定制 WAF 规则
4. 结合 RASP(运行时应用自保护)做更精确的检测

教训:
应用层安全不能只依赖 WAF,业务代码必须做输入校验。

📊 运维监控与自动化保障

监控项设计

监控项 Prometheus 指标 告警阈值 检测频率
eval 调用次数 php_eval_call_total 任意一次即告警 实时
call_user_func 调用 php_call_user_func_total 突增 200% 告警 实时
create_function 调用 php_create_function_total 任意一次即告警 实时
命令执行函数调用 php_command_exec_total 5 分钟内 > 10 次 实时
动态 include php_dynamic_include_total 突增 200% 告警 实时
业务代码扫描发现危险函数 code_scan_danger_functions 发现即告警 每周
WAF 拦截代码注入 waf_block_total{type="code_injection"} 5 分钟内 > 5 次 实时

Prometheus 告警规则

yaml 复制代码
# /etc/prometheus/rules/thinkphp-code-injection.yml
groups:
  - name: thinkphp-code-injection
    rules:
      # 规则 1: eval 调用检测
      - alert: PHPEvalCallDetected
        expr: increase(php_eval_call_total[1m]) > 0
        for: 0s
        labels:
          severity: critical
        annotations:
          summary: "检测到 eval() 调用"
          description: "实例 {{ $labels.instance }} 检测到 eval 调用,可能是代码注入攻击"

      # 规则 2: create_function 调用
      - alert: PHPCreateFunctionDetected
        expr: increase(php_create_function_total[1m]) > 0
        for: 0s
        labels:
          severity: critical
        annotations:
          summary: "检测到 create_function() 调用"
          description: "实例 {{ $labels.instance }} 检测到 create_function,PHP 7.2+ 已废弃"

      # 规则 3: 命令执行函数激增
      - alert: PHPCommandExecSpike
        expr: increase(php_command_exec_total[5m]) > 10
        for: 2m
        labels:
          severity: warning
        annotations:
          summary: "命令执行函数调用激增"
          description: "实例 {{ $labels.instance }} 5 分钟内调用命令执行函数 {{ $value }} 次"

      # 规则 4: call_user_func 异常
      - alert: PHPCallUserFuncAnomaly
        expr: rate(php_call_user_func_total[5m]) > 10
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "call_user_func 调用频率异常"
          description: "实例 {{ $labels.instance }} call_user_func 调用频率 {{ $value }} 次/秒"

      # 规则 5: WAF 拦截代码注入
      - alert: WAFCodeInjectionBlock
        expr: increase(waf_block_total{type="code_injection"}[5m]) > 5
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "WAF 拦截到代码注入攻击"
          description: "实例 {{ $labels.instance }} 5 分钟内拦截 {{ $value }} 次代码注入攻击"

      # 规则 6: 代码扫描发现危险函数
      - alert: CodeScanDangerFunctions
        expr: code_scan_danger_functions > 0
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "代码扫描发现危险函数"
          description: "项目 {{ $labels.project }} 发现 {{ $value }} 处危险函数调用"

      # 规则 7: 动态 include 异常
      - alert: PHPDynamicIncludeAnomaly
        expr: increase(php_dynamic_include_total[5m]) > 50
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "动态 include 调用异常"
          description: "实例 {{ $labels.instance }} 5 分钟内动态 include {{ $value }} 次"

自动化巡检脚本

bash 复制代码
#!/bin/bash
# code_injection_patrol.sh --- 代码注入安全巡检
# 用法: 由 crontab 每周执行

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

echo "===== 代码注入安全巡检 $(date) =====" > "$REPORT_FILE"
echo "项目根目录: $PROJECT_ROOT" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"

# 1. 静态代码扫描
echo "[1] 静态代码扫描" >> "$REPORT_FILE"

# eval 调用
EVAL_COUNT=$(grep -rn "eval\s*(" "$PROJECT_ROOT/app" --include="*.php" 2>/dev/null | \
    grep -vE "vendor/|tests/|/runtime/" | wc -l)
echo "    eval() 调用: $EVAL_COUNT 处" >> "$REPORT_FILE"

# create_function 调用
CREATE_FUNC_COUNT=$(grep -rn "create_function\s*(" "$PROJECT_ROOT/app" --include="*.php" 2>/dev/null | \
    grep -vE "vendor/|tests/" | wc -l)
echo "    create_function() 调用: $CREATE_FUNC_COUNT 处" >> "$REPORT_FILE"

# call_user_func 调用
CALL_USER_FUNC_COUNT=$(grep -rn "call_user_func" "$PROJECT_ROOT/app" --include="*.php" 2>/dev/null | \
    grep -vE "vendor/|tests/" | wc -l)
echo "    call_user_func 调用: $CALL_USER_FUNC_COUNT 处" >> "$REPORT_FILE"

# 命令执行函数
CMD_EXEC_COUNT=$(grep -rnE "(system\s*\(|exec\s*\(|passthru\s*\(|shell_exec\s*\()" \
    "$PROJECT_ROOT/app" --include="*.php" 2>/dev/null | grep -vE "vendor/|tests/" | wc -l)
echo "    命令执行函数: $CMD_EXEC_COUNT 处" >> "$REPORT_FILE"

# 动态 include
DYNAMIC_INCLUDE_COUNT=$(grep -rnE "(include|require)(_once)?\s*\(?\s*\\$" \
    "$PROJECT_ROOT/app" --include="*.php" 2>/dev/null | grep -vE "vendor/|tests/|runtime/" | wc -l)
echo "    动态 include/require: $DYNAMIC_INCLUDE_COUNT 处" >> "$REPORT_FILE"

# 评估风险等级
TOTAL_DANGER=$((EVAL_COUNT + CREATE_FUNC_COUNT + CMD_EXEC_COUNT))
if [ "$TOTAL_DANGER" -gt 0 ]; then
    echo "    🔴 发现 $TOTAL_DANGER 处高危危险函数调用" >> "$REPORT_FILE"
    SEVERITY=critical
elif [ "$CALL_USER_FUNC_COUNT" -gt 5 ] || [ "$DYNAMIC_INCLUDE_COUNT" -gt 5 ]; then
    echo "    ⚠️  发现多处需人工审核的动态执行点" >> "$REPORT_FILE"
    SEVERITY=warning
fi

echo "" >> "$REPORT_FILE"

# 2. PHP 配置检查
echo "[2] 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

OPEN_BASEDIR=$(php -i 2>/dev/null | grep "open_basedir" | head -1)
echo "    $OPEN_BASEDIR" >> "$REPORT_FILE"

if [ -z "$OPEN_BASEDIR" ] || echo "$OPEN_BASEDIR" | grep -q "no value"; then
    echo "    ⚠️  未配置 open_basedir" >> "$REPORT_FILE"
fi

echo "" >> "$REPORT_FILE"

# 3. WAF 规则检查
echo "[3] WAF 规则检查" >> "$REPORT_FILE"
NGINX_WAF=$(grep -cE "(system|exec|eval|create_function)" /etc/nginx/conf.d/*.conf 2>/dev/null || echo 0)
echo "    Nginx WAF 规则数: $NGINX_WAF" >> "$REPORT_FILE"
echo "" >> "$REPORT_FILE"

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

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

Crontab 配置

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

# 每周日凌晨 4 点执行代码扫描
0 4 * * 0 root /opt/scripts/code_injection_patrol.sh /var/www/html >> /var/log/code-injection-patrol.log 2>&1

# 每小时检查 PHP 危险函数调用日志
0 * * * * root /opt/scripts/php_danger_func_check.sh >> /var/log/php-danger-func.log 2>&1

# 每天凌晨 3 点检查 WAF 拦截日志
0 3 * * * root /opt/scripts/waf_code_injection_check.sh >> /var/log/waf-injection.log 2>&1

systemd 定时器配置

ini 复制代码
# /etc/systemd/system/code-injection-patrol.service
[Unit]
Description=Code Injection Security Patrol
After=network.target

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

# /etc/systemd/system/code-injection-patrol.timer
[Unit]
Description=Weekly Code Injection Security Patrol

[Timer]
OnCalendar=Sun *-*-* 04:00:00
Persistent=true

[Install]
WantedBy=timers.target

通知渠道集成

渠道 用途 配置方式
邮件 详细巡检报告 mailx + SMTP
企业微信 关键告警实时推送 Webhook + curl
钉钉机器人 值班通知 Webhook + 关键字
Prometheus AlertManager 与监控集成 Webhook receiver
GitLab/Jira 自动创建修复工单 API 集成

预防措施

  1. 代码审计:CI/CD 流水线接入 PHPStan/Psalm 静态分析,禁止新增 eval/create_function
  2. 代码评审:所有涉及动态调用的代码必须安全工程师评审
  3. WAF 部署:在 Nginx/ModSecurity 层部署代码注入拦截规则
  4. RASP 部署:考虑部署运行时应用自保护(如 OpenRASP)
  5. 函数禁用:PHP 配置 disable_functions 禁用危险函数
  6. Suhosin 扩展:安装 Suhosin 禁用 eval
  7. PHP 升级:升级到 PHP 8.0+,移除 create_function
  8. 安全培训:定期对开发团队进行 PHP 安全编码培训
  9. SDLC 集成:在需求、设计、编码、测试阶段嵌入安全检查

💰 成本核算与价值量化

开发成本

成本项 工作量 人天 单价(元/天) 小计(元)
5 类场景漏洞分析与复现 每类场景搭建测试环境 2.0 1,500 3,000
业务代码全量审计 扫描 + 人工复核 3.0 1,500 4,500
漏洞修复方案设计与实施 5 类场景逐个修复 3.0 1,500 4,500
代码评审流程建设 制定评审规范 + 工具配置 1.0 1,500 1,500
WAF 规则编写与验证 Nginx + ModSecurity 规则 1.0 1,500 1,500
巡检脚本与监控告警 Bash + Python + Prometheus 2.0 1,500 3,000
安全培训材料编写 PHP 安全编码培训 0.5 1,500 750
开发成本合计 12.5 18,750

运行成本

成本项 频率 月成本(元) 年成本(元)
代码扫描执行 每周 100 1,200
Prometheus 监控存储 持续 200 2,400
WAF 规则维护 每月 375 4,500
人工复核 每月 750 9,000
安全培训 每季度 500 6,000
RASP 部署许可 持续 500 6,000
运行成本合计 2,425 29,100

收益对比

收益项 量化方式 金额(元/年)
避免 RCE 导致的数据泄露 按一次 RCE 事故损失 100 万估算,概率 25% 250,000
避免 Webshell 植入 按清理成本 5 万 + 业务损失 20 万,概率 30% 75,000
避免横向移动 按内网渗透损失 80 万估算,概率 15% 120,000
代码质量提升 移除危险函数调用,降低 40% 安全审计成本 60,000
合规审计效率提升 自动化扫描替代人工审计,节省 60 人天/年 90,000
开发效率提升 代码评审流程规范,减少 30% 安全 Bug 45,000
收益合计 640,000

ROI 计算

text 复制代码
年度总成本 = 开发成本 + 运行成本 = 18,750 + 29,100 = 47,850 元
年度总收益 = 640,000 元

ROI = (年度总收益 - 年度总成本) / 年度总成本 × 100%
    = (640,000 - 47,850) / 47,850 × 100%
    = 1,237%

投资回收期 = 年度总成本 / (年度总收益 / 12)
          = 47,850 / (640,000 / 12)
          ≈ 0.9 个月(约 27 天)

💡 结论:本次应用层代码注入防御建设 ROI 高达 1,237%,投资回收期不到 1 个月。应用层漏洞是框架升级无法覆盖的"长尾风险",必须通过代码审计 + WAF + RASP 多层防御体系全面防护。

工程效率收益

收益项 优化前 优化后 提升
漏洞发现到修复 14 天(依赖外部审计) 1 天(自动扫描) 14x
代码审计覆盖率 30%(人工抽审) 100%(自动扫描) 3.3x
安全 Bug 修复成本 修复阶段 5000 元/个 设计阶段 500 元/个 10x
WAF 误报处理 每月 20 条 每月 3 条 6.7x
代码评审效率 人工评审 4 小时 工具辅助 1 小时 4x
应急响应 4 小时启动 5 分钟自动告警 48x

8. 总结与行动清单

核心收获

  1. 应用层代码注入是框架升级无法覆盖的"长尾风险"------必须通过业务代码审计识别
  2. 5 类常见场景覆盖了 90% 的应用层代码注入------动态调用、eval、模板渲染、配置加载、规则执行
  3. 白名单是应用层防御的根本解------动态类名/方法名/规则都必须白名单校验
  4. eval 无法通过 disable_functions 禁用------需要 Suhosin 扩展或代码审计
  5. create_function 在 PHP 8.0+ 已移除------升级 PHP 版本即可消除
  6. WAF 通用性差,需配合 RASP------业务参数千差万别,运行时检测更精确
  7. 代码评审流程是长期保障------CI/CD 流水线接入静态分析,禁止新增危险函数

立即行动清单

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

- [ ] 用静态扫描脚本扫描全量业务代码
- [ ] 识别所有 eval/create_function/call_user_func 调用
- [ ] 评估每个调用点的风险等级

□ **本周完成**:

- [ ] 修复所有高危 eval 注入点(场景二、三、五)
- [ ] 为动态类名/方法名调用添加白名单(场景一)
- [ ] 为动态 include 添加路径校验(场景四)
- [ ] 配置 PHP disable_functions 禁用危险函数

□ **本月完成**:

- [ ] CI/CD 流水线接入 PHPStan/Psalm 静态分析
- [ ] 制定代码评审规范,安全工程师参与评审
- [ ] 部署 Prometheus 监控告警规则
- [ ] 上线自动化巡检脚本
- [ ] 评估 OpenRASP 部署可行性
- [ ] 升级到 PHP 8.0+(移除 create_function)
- [ ] 对开发团队进行 PHP 安全编码培训

□ **长期机制**:

- [ ] 每月执行代码扫描,跟踪修复进度
- [ ] 每季度进行一次代码审计
- [ ] SDLC 全流程嵌入安全检查
- [ ] 建立安全编码知识库
- [ ] 关注 PHP 安全最佳实践更新

参考链接

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

💬 如果你在 ThinkPHP 业务代码安全审计中遇到问题,请在评论区留言交流!

🔔 关注我,获取更多 PHP 应用安全与代码审计实战干货!

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

专栏导航:

📌 真实性声明 :本文涉及的 5 类代码注入场景来源于笔者 2026 年 Q1 处置的多起 ThinkPHP 业务代码安全事件,案例细节做了脱敏处理,攻击者 IP、具体业务参数等敏感信息已替换。漏洞根因分析基于 ThinkPHP 5.x/6.x 的公开源码和 PHP 官方文档进行的合理推断,标注的代码片段为简化示意,非完整业务代码。修复方案参考了 OWASP PHP Security Cheat Sheet 和 PHP 官方安全建议。成本估算中的金额基于国内一线城市安全工程师市场行情和典型业务损失数据,实际数值因企业规模而异。文中涉及的 ${VAR} 形式均为环境变量占位符,请勿硬编码到生产配置中。

相关推荐
行者-全栈开发10 天前
【PHP/ThinkPHP】CVE-2022-38352:ThinkPHP 6.0.13 反序列化 RCE 漏洞修复指南
反序列化漏洞·pop链·远程代码执行·thinkphp安全·cve-2022-38352·psr6cache·php对象注入
三无推导2 个月前
无需扩展的 PHP 加密方案有哪些优势:基于 php.x5.chat 的实践分析
开发语言·php·web开发·数据加密·php加密·php安全·无需扩展
lili-felicity1 年前
[特殊字符] Java反射从入门到飞升:手撕类结构,动态解析一切![特殊字符]
私有方法·java反射·动态调用·反射原理
Whoami@127.0.0.12 年前
免杀笔记 ----> 动态调用
免杀·绕av·动态调用