如果你在搜索引擎里敲下"文件包含"这四个字,大概率会撞见两种完全不相干的结果:一种是 C/C++ 编译时报出的#include找不到头文件、提示你去检查 IncludePath 的报错;另一种是安全圈里说的文件包含漏洞,也就是常被写成 LFI / RFI 的那一类问题。名字撞车,性质却差着十万八千里——前者是编译期的静态文本替换,找不到就报错,全程可预测;后者是运行期的动态加载,加载什么完全取决于运行时拿到的那个字符串。很多刚入门的朋友搜"文件包含"搜到一半就迷糊了,就是因为没先把这两个语境分开。
这篇要聊的是后者,而且不是孤立的文件包含。单独一个文件包含,很多时候只能做到"读文件",离真正的 RCE(远程代码执行)还差一层窗户纸;而当它和"任意文件上传"凑到一起,这层窗户纸就被捅破了。以通达OA这类协同办公系统为代表的一批内网应用,历史上出现过的典型问题,正是这条组合链:上传点负责把内容送进去,包含点负责把内容当代码执行,两段拼起来就是完整的 RCE。这篇文章站在防守和应急的视角,把这条链从头到尾拆一遍——它为什么成立、条件是什么、告警长什么样、怎么排查、怎么从根上剪断。做运维的、做应急响应的、写后端代码的同学,都能从中找到对得上自己活儿的部分。
1. 先把"文件包含"这个词拆开:编译期包含与运行时包含
1.1 从 #include 报错说起
C/C++ 里的#include是预处理指令,它的工作发生在编译之前,做的是纯文本层面的事:把指定文件的内容原样搬到当前位置,然后交给编译器。所以一旦路径写错、目录没配、头文件缺失,编译器会立刻甩给你一句"检测到 #include 错误,请更新你的 IncludePath",因为在这个阶段,所有依赖关系都是静态可见的,缺什么、在哪缺,一清二楚。构建系统甚至会把每个头文件的依赖关系画成一张有向图,用来做增量编译。这类"包含"是确定的、可枚举的、能被工具完整分析的。
PHP、JSP、ASP 这类脚本语言里的include/require则完全是另一码事。它们发生在运行期,而且最要命的一点是:被包含的路径可以是一个变量,变量的值可以来自请求参数。这一下就把"静态可分析的依赖图"变成了"运行时才知道的字符串"。你没法在部署前画出一张完整的包含关系图,因为这张图取决于每一个请求带来了什么。
用个不太严谨但很好懂的类比:编译期的 include 像装订一本手册,附录在装订时就已经按目录订进去了,缺页当场就能发现;运行期的 include 像读到某一页时,书上写着"请现在翻开另一个文件夹里的第 N 页继续读"——翻哪本、第几页,是读的时候才决定的。如果这句话是编辑写死的,没事;如果这句话是读者自己填的,那就出事了。
1.2 运行时包含为什么危险:include 本质是"把文件当代码执行"
很多人对包含漏洞的理解停在"能读文件"这一层,比如读到/etc/passwd就算胜利。这其实是低估了它。include的语义不是"打印文件内容",而是"把文件内容当作当前语言的代码参与解析执行"。也就是说,被包含的文件只要落在 PHP 标签内,内容就会被当成 PHP 代码跑起来,而不是被当成文本输出。
这个区别决定了漏洞的严重程度上限。如果能读文件但不会执行,最坏情况是敏感信息泄露;如果能执行,那就是服务器权限范围内的任意代码执行。所以判断一个包含点到底有多大危害,第一个要问的不是"能包含什么路径",而是"包含进去之后,文件里的内容会不会被解析"。
在某个固定的目标环境里,这又取决于几个具体的配置:目标文件的后缀是否在解析范围内、内容里是否带了脚本起始标签、allow_url_include这类开关是否打开。这也是为什么同一条利用思路,在 A 环境一次就通,到了 B 环境怎么试都不行——不是思路错了,是环境不满足。
1.3 单点包含为什么不够,为什么要配上"上传"
纯粹的文件包含,攻击者能控制的只有"路径",控制不了"内容"。他能指向服务器上已有的文件,但没法往服务器上写新文件。而服务器上现成的文件,内容基本都是业务代码或系统数据,带脚本标签的极少。所以孤立地看,它通常只能做到信息读取,或者包含一些"本来就存在但设计者没打算让人直接访问"的脚本。
要突破这一层,就需要一个能把"攻击者可控内容"落到服务器磁盘上的通道。任意文件上传正好提供了这个通道。上传负责落内容,包含负责给这份内容授予执行权——两段链条各司其职,缺一不可。这也是为什么在应急响应里,看到包含告警一定要顺手去翻上传接口的日志,两者往往是成对出现的。
2. 一条链的成立条件:上传与包含是怎么被"焊"在一起的
2.1 任意文件上传真正的问题不是"能上传",而是"上传后能被执行"
先把一个常见的认知偏差纠正掉:上传漏洞的危害,不取决于"能不能把文件传上去",而取决于"传上去之后这个文件处于什么位置、以什么身份存在"。把一张图片传到 Web 根目录下的静态资源目录,那个目录没有脚本解析权限,传上去一万个文件也只是占硬盘;反过来,如果上传目录恰好是脚本可解析的,而且文件名和后缀没被约束,那一个文件就足以掀桌子。
所以评估上传点时,我一般按这个顺序问三个问题:
- 这份文件最终落在哪个物理路径?这个路径是否在 Web 服务器的可访问根目录内?
- 这个目录是否配置了脚本解析?还是只当作纯静态资源?
- 文件名、扩展名、内容三者里,服务端实际校验了哪几个?
很多老系统的上传逻辑,只做了最省事的那一层——查扩展名。而扩展名校验一旦做成黑名单,就天然存在漏项问题,因为它需要穷举所有"危险后缀",而服务器能解析的后缀组合、不同中间件的映射关系,永远比开发者写进黑名单的那几行长。
2.2 文件包含在下半场的角色:给静态文件注入"执行权"
上传把内容放到了磁盘上,但这些内容此刻还是一堆躺在文件系统里的字节,没人去"读它"。这时候包含点就登场了。它做的事情是:把某个路径的字符串交给解析器,让解析器去读这个文件,并且把它当作代码处理。
这里有个特别值得玩味的点:很多场景下,上传上来的文件本身扩展名是被"改造"过的、或者根本不是什么脚本后缀,从静态检查的角度看毫无威胁。但只要包含点愿意去读它,后缀是什么就完全不重要了——解析器不看后缀,只看内容。
一句话概括这条链的本质:上传负责把"数据"放进来,包含负责把"数据"提拔成"代码"。数据本身是无害的,代码才有杀伤力。中间的这次身份转换,就是整条链最关键的铰链。
2.3 链条成立的三个隐性前提
把这条链拆开看,它的成立需要三个前提同时满足。做防守的时候,把这三个前提里任意一个掐掉,链条就断了——这也是后面加固思路的来源。
| 前提 | 具体含义 | 常见失效表现 | 防守侧的抓手 |
|---|---|---|---|
| 路径可达 | 包含点能通过可控字符串定位到目标文件 | 参数直接拼进 include,且未做白名单 | 拒绝外部输入参与路径构造 |
| 位置可控 | 上传的文件落在包含点能到达的目录 | 上传目录固定、路径可穿越 | 上传目录移出 Web 根、目录隔离 |
| 权限足够 | Web 进程有读取该文件的权限 | 运行账户权限过大 | 最小权限、open_basedir 类限制 |
这三个前提里,最容易被忽视的是第一个和第三个。第一个是代码问题,写代码的人未必意识到风险;第三个是配置问题,装机器的人未必意识到影响。两者叠加,才让链条从"理论可行"变成"实际打通"。
3. 上传点常见校验失效的几种形态
3.1 只查后缀不查内容:黑名单的天然缺陷
后缀黑名单的问题不在于实现得多烂,而在于它的思路本身是"枚举坏东西"。任何枚举都面临两个不可控变量:一是漏项,二是变体。漏项指的是开发者没想到的后缀,比如某些中间件会把一些冷门扩展名也交给脚本引擎解析;变体指的是同一个后缀能写出的多种等价形式,比如大小写、多后缀叠加、结尾带特殊字符,等等。
这两种问题在实战里几乎必然出现。原因很简单:写黑名单的人和做中间件配置的人,往往不是同一批人,而中间件到底把哪些后缀交给了哪个解析器,只有翻配置文档才知道。所以从工程角度,黑名单可以作为一道辅助手段,但它绝不能是唯一的防线。
我在实际排查里发现过一个很典型的规律:凡是只做了后缀黑名单的上传接口,出问题的概率远高于平均值。因为它的安全性完全依赖"开发者记得全",而人是不可能记得全的。
3.2 只信前端和 Content-Type:服务端信任链断裂
第二类失效形态更隐蔽一些,它的问题不在校验规则,而在校验的位置。常见的有两种:
一种是校验写在前端 JS 里。用户在页面上选文件,JS 拦住不合规的文件名。这套逻辑对正常用户是有用的,因为它能提前给出提示;但对攻击者毫无意义,因为绕过前端校验只需要构造一个请求就行,浏览器的 JS 根本不在链路上。前端校验的唯一定位是"提升用户体验",不是"安全防护"。
另一种是把Content-Type当证据。这个字段是请求体里的一个普通头部,客户端想写什么就写什么,服务端拿它当判断依据等于自欺欺人。更常见的是代码里用文件扩展名反推 MIME 类型,或者反过来用 MIME 类型决定扩展名,这两种逻辑都容易被构造出不一致的结果。
判断一个上传接口是否可靠,看一个点就够了:它的校验逻辑是否完全在服务端,且是否同时校验了扩展名、内容类型和文件内容特征这三者。三者缺一,就等于给攻击者留了一道侧门。
3.3 路径与文件名拼接:目录穿越与覆盖
第三类问题出在"保存路径"这一步。很多老代码的写法是把上传目录和文件名拼起来:
$savePath = $uploadDir . $_POST['filename']; move_uploaded_file($tmp, $savePath);这段代码里,filename完全由客户端提供。如果这个字段里带了../这样的相对路径片段,保存位置就可能跑到上传目录之外——跑到 Web 根目录、跑到配置文件目录、甚至跑到可以覆盖已有文件的位置。目录穿越的后果比单纯上传更严重,因为它把"写入一个受限目录"变成了"写入任意位置"。
还有一类变体是覆盖。如果目标路径存在同名文件,某些实现会直接覆盖,于是攻击者可以用上传来覆盖一个本就在预期内的文件,从而改变系统行为。严格来说这已经不算"上传漏洞",而是"任意文件写入"了,危害等级更高。
3.4 上传目录被当成静态资源站:最容易被忽略的一环
前面三点都是代码层面的事,这一点是配置层面的,而且特别容易被忽略:上传目录被直接暴露在 Web 根目录下,且该目录具备脚本解析能力。
这两件事单独看都不算大问题——目录暴露只是让人能下载文件,脚本解析是服务器的正常能力。但当"上传的文件"和"可解析的目录"重叠在一起,"上传"这个动作就等价于"在服务器上部署一段可执行代码"。
我在做加固方案时,对上传目录通常有三个硬性要求:物理路径移出 Web 根、或者至少在 Web 服务器层面单独配一段 location 禁止脚本解析;上传的文件一律随机重命名,不保留用户提供的文件名;文件访问统一走一个下载脚本做权限校验,而不是让 Web 服务器直接吐文件。这三条做完,即使上传接口的校验出了纰漏,攻击者拿到手的也只是一个"能在服务器上存文件"的能力,而不是"能在服务器上执行代码"的能力。
4. 包含点的三类典型写法:从变量拼接到日志落点
4.1 直接拼接 $_GET / $_POST 的经典写法
最直白的写法长这样:
include $_GET['page'] . '.php';这种代码在早期项目里非常常见,思路是"用一个参数切换页面"。写法本身简洁,但把路径构造权完全交了出去。攻击者要做的事只有一件:构造一个能指向目标文件的字符串。注意,末尾那个.php拼上去,并不意味着安全——它是前缀还是后缀、能不能被路径穿越消化掉、服务器对特殊路径片段的处理方式,都会影响最终指向哪里。
这里我想强调一个经验:不要用"这个拼接看起来很难绕"来论证安全。路径的规范化、软链接、相对路径的层级、不同文件系统的差异,组合起来的情况多得超出直觉。稳妥的判断标准只有一条:外部输入有没有参与路径构造。参与了,就当它不安全。
4.2 白名单没做全:前缀、后缀、截断的擦边
比裸拼高一级的写法是加了个白名单,但加得不彻底。典型形态有三种:
第一种是前缀拼接,形如$baseDir . $_GET['f']。前缀在左边,看似把范围限制在了$baseDir里,但只要参数里带上返回上一级的相对路径片段,就能一层层爬出去。限制范围靠的是"过滤掉向上跳的片段",而不是"加个前缀"。
第二种是后缀拼接,形如$_GET['f'] . '.php'。后缀的限制力比前缀还弱,因为末尾拼接的东西通常可以用路径片段消化掉,或者被当成目录名的一部分处理。
第三种是字符串截断类问题,在特定历史环境下靠超长或特殊字符让校验逻辑与解析逻辑看到不同的字符串。这类问题的现代版本基本都修掉了,但老系统里依然存在,做审计时不要因为"这个是老问题"就跳过。
真正可靠的白名单是"键值映射",不是"字符串过滤"。这两者的差别等下在加固那节会给出具体代码。
4.3 日志文件包含:不是漏洞的错,是"日志被当代码读"的错
日志文件包含(业内常简称日志包含)是这条链里最有趣的一个玩法,也是最容易被误判为"这不是漏洞"的一类。它的逻辑是:Web 服务器会把每个请求的 URL、User-Agent 之类的信息原样写进访问日志,如果攻击者把这些信息构造得带上了脚本特征,那么日志文件里就混进了"看起来像代码"的内容;再通过一个包含点去读这个日志文件,日志里的内容就被当代码解析了。
注意,这个链条里没有任何一个环节是"设计缺陷":日志记录请求信息是它的本职工作,包含点是代码自己写的,日志文件是正常存在的。问题出在这三件事同时成立的时候——一份原本只应该被人看的文本文件,被程序当成了代码来读。
这个案例有个很实用的推论:排查系统里的包含风险时,不要只看业务模板目录。日志目录、缓存目录、上传目录、临时文件目录、会话文件目录,这些都是"内容可能被外部影响"的地方,只要包含点能指过去,就有戏。
4.4 无参数与短标签依赖:为什么有些环境"打通了"有些"打不通"
经常有朋友问:"同样的思路,为什么我在测试环境怎么都通不了?" 这个问题背后一般有几个具体变量在作祟,值得单独列一下:
| 变量 | 影响 | 排查方式 |
|---|---|---|
| 脚本起始标签配置 | 短标签是否启用,决定某些载荷是否会被解析 | 查 php.ini 相关开关 |
| 远程包含开关 | 决定能否直接从远端地址加载内容 | 查远程包含相关配置项 |
| 目录访问限制 | 限制脚本能读写的目录范围,越界直接失败 | 查目录限制类配置 |
| Web 运行账户权限 | 决定能不能读到目标文件 | 看进程属主与文件权限 |
| 日志格式与落盘位置 | 决定日志里内容的具体形态 | 直接看日志文件样本 |
还有一个现象叫"无参数执行",指的是在某些特定写法下,攻击者不需要传入自己构造的参数,而是借助系统里已有的函数组合完成执行。这类技巧对环境的依赖更强,可移植性很差,经常是"在这个版本能用、换个版本就不行"。从防守角度看,这类技巧的存在提醒我们:不能因为某个具体载荷在当前环境跑不通,就判定这里没有风险。判断风险的依据是代码逻辑,不是某一次尝试的结果。
5. 应急响应实战:从告警到定位落地文件
5.1 先看什么:Web 日志里的三个可疑信号
一旦收到相关告警或者怀疑被打了这条链,第一步永远是看日志,不要急着删文件。访问日志里,这几类信号出现的组合度很高:
第一类是包含点的异常参数。业务正常的包含参数值通常是固定的几个短字符串,突然出现带有路径特征、带有多层相对路径、或者是绝对路径的取值,就是强烈信号。
第二类是上传接口的异常请求。重点看请求体的类型和大小、上传后的响应状态、以及紧接着对上传路径的访问。正常用户上传完会看到成功提示然后继续操作,攻击者的行为模式是"上传 → 立刻访问 → 立刻带参数调用包含点",时间间隔往往在秒级。
第三类是状态码的异常跳变。比如连续几个 404 之后突然出现 200,或者某个平时从不被访问的路径突然被高频请求。这类模式在日志里画成折线图会非常显眼。
我的习惯是先把日志按"可疑时间窗口"切出来,把那个窗口内所有对上传接口和包含点的请求列出来,按时间排序,基本上整条链的动作序列就摆在眼前了。
5.2 文件系统排查:时间线、体积、权限三个维度
日志看完,接下来是落地文件的排查。这里给一个我自己常用的排查顺序:
- 按修改时间筛。从可疑时间窗口起,把该时间段内新增或修改的文件列出来。命令层面可以用
find配合时间参数,注意时区要和日志对齐,否则会错开一段。 - 看体积。攻击者放上去的文件通常很小,几百字节到几 KB,因为它们只需要装一小段代码。而正常业务文件的大小分布是相对固定的,极小文件本身就是异常特征。
- 看权限和属主。正常上传的文件属主应该是 Web 运行账户,权限通常是常规的读写权限。如果出现可执行位、或者属主不对,就要重点看。
- 看扩展名与实际内容是否一致。这是最关键的一步。把文件头读出来对比,一个声称是图片的文件,内容里却是脚本标签,这就是明证。
- 检查 Web 根目录之外的路径。有余力的话,把整个 Web 根的外层目录也扫一遍,因为目录穿越类的写入很可能落在外面。
排查的时候有个原则要守住:先记录、再处置。把文件的路径、大小、修改时间、哈希、内容摘要全部记下来,拷贝一份样本留存,然后再动手清理。跳过这一步,后面复盘就无从谈起。
5.3 进程与网络:确认是否已进入持久化阶段
文件找到不代表事情结束了,还要确认攻击者有没有留下来。重点看这几个地方:
进程列表里,找那些由 Web 运行账户启动的、非常规的进程。正常情况下一台 Web 服务器上,Web 账户不该起命令行解释器、不该起下载工具、不该起任何交互式的程序。看到就是问题。
网络连接方面,看有没有从这台机器主动外连到陌生地址的长连接,特别是非常规端口的。持久化往往需要回连通道。
再往下是几个容易被忽略的落点:计划任务、开机启动项、Web 应用自身的定时脚本、以及账号体系里有没有被新增的账户。这几个位置不查,很可能出现"清理完了过两天又活了"的情况。
还有一个常被忽视的点是二次落脚点。攻击者进来之后,通常会在多个位置放东西,删掉其中一个并不影响其他。所以清理要以"完整梳理"为前提,而不是"看到什么删什么"。
5.4 处置顺序:为什么不能先删文件
应急响应的处置顺序,我总结成一句话:先隔离,再取证,后清理,最后验证。
先隔离,指的是把受影响的机器从网络里摘出来,或者至少把对外访问切断,防止攻击者继续操作、也防止横向扩散。这一步要快。
再取证,就是前面说的记录和留存。内存里的东西(进程、连接、临时文件)一旦重启就没了,所以取证要抢在重启之前。
后清理,才是删文件、清任务、改配置。这里有个特别容易犯的错:先改密码。很多人的第一反应是"赶紧把密码都换了",但如果后门还在,改密码对攻击者毫无影响,反而打草惊蛇,而且新密码可能直接被后门抓到。正确的顺序是先把后门清干净,再改凭据。
最后验证,指的是清理完要重新做一轮排查,确认没有遗漏,并且在接下来一段时间里保持监控。经验上,被打过的系统在清理后的一两周内是最需要盯着的窗口。
6. 加固方案:把这条链从三个位置同时剪断
6.1 上传侧:白名单 + 重命名 + 隔离存储
上传侧的加固,我一般按"四步走"来落地。
第一步,扩展名白名单,而且要基于业务实际需要来定。图片业务就只允许图片格式,附件业务就只允许文档格式,绝不出现"允许上传任意文件"这种需求。白名单的写法是判断"是否在允许集合内",而不是"是否在禁止集合外"。
第二步,服务端强制重命名。文件名由服务端生成,用户提供的名字只作为展示用途存在元数据表里,不参与磁盘路径构造。名字的生成方式建议用随机值加时间戳,避免可预测。
第三步,校验文件内容,不只是看扩展名和 MIME。对图片类,可以用图像库尝试解析,解析失败直接拒;对文档类,可以校验文件头魔数。这一步能挡住大量"改后缀"的尝试。
第四步,存储隔离。上传目录移出 Web 根,或者至少在 Web 服务器配置里明确禁止该目录下的脚本解析。这是最后一道保险:即使前面三道都被绕过,攻击者拿到的也只是一个"存文件"的能力。
6.2 包含侧:拒绝动态路径,只允许映射表
包含侧的加固思路很干脆:外部输入永远不参与路径构造。具体做法是把"参数值到文件路径"的映射关系写死在代码里:
<?php // 只允许映射表里存在的键,路径完全由服务端决定 $views = [ 'detail' => __DIR__ . '/tpl/detail.php', 'list' => __DIR__ . '/tpl/list.php', 'edit' => __DIR__ . '/tpl/edit.php', ]; $key = $_GET['view'] ?? 'list'; if (!isset($views[$key])) { http_response_code(400); exit('invalid view'); } include $views[$key];这段代码和"拼字符串"的写法,最大的区别在于:攻击者能控制的只有$key这个键名,而键名的取值必须存在于映射表里,路径本身一个字节都动不了。就算攻击者传了带路径片段的键名,isset直接把它挡掉。
如果业务确实需要动态加载,那就退一步,做一个严格的字符集校验加上目录固定:只允许特定字符、不允许路径分隔符、不允许上级目录片段,并且最终路径要经过规范化之后再确认它还在允许的目录内。这个逻辑写起来不复杂,但千万别省。
6.3 运行环境侧:最小权限与目录隔离
代码层面的修复之外,环境层面能做的事其实更多,而且收益往往更大。几个我认为性价比最高的措施:
- 限制脚本可访问的目录范围。通过配置把脚本的读写范围圈定在业务目录内,越界的访问直接失败。这样即使包含点被利用,能指过去的位置也极其有限。
- Web 进程用低权限账户运行。不要图省事用高权限跑 Web 服务。低权限意味着即使代码被执行,攻击者拿到的东西也少得多。
- 上传目录、缓存目录、日志目录单独分区或单独挂载,并去掉执行属性。这几个目录是"内容可能被外部影响"的重灾区,给它们最严格的待遇是值得的。
- 禁用不必要的危险函数。把业务用不到的、容易被利用的函数在配置里禁掉。这一步要注意别把业务搞崩,所以上线前一定要在测试环境完整回归。
6.4 检测侧:用规则兜住漏网的场景
前面三节是"不让它发生",这一节是"发生了要能发现"。检测规则可以从三个维度建:
请求维度,对包含点和上传接口做参数基线。正常参数值会形成一个很小的集合,偏离基线的取值就告警。这类规则误报率低、收益高,是很划算的一步。
文件维度,做上传目录的文件完整性监控。这个目录里除了业务正常写入的文件,任何新增都值得看一眼,特别是体积小于某个阈值、或者扩展名不在白名单内的。
行为维度,监控"上传后短时间内访问该文件"和"访问包含点时携带路径特征"这两类行为序列。单独看每个请求都可能是正常的,但组合起来就是典型的攻击动作序列。
规则建好之后要有一个调整期。一开始误报多是正常的,花一两周把白名单补全,规则的可用性会上一个台阶。千万不要因为误报就整个关掉——那就等于把检测能力一起关掉了。
7. 几个容易走偏的认知误区
7.1 "改了后缀就安全了"
这是最普遍的一个误区。把上传后缀白名单做严,确实能挡住一大批尝试,但它挡不住"内容被执行"这条路径——因为执行的判定依据是内容,不是后缀。我见过一些系统,上传校验做得很严,只允许特定几种图片后缀,MIME 也查了;但包含点依然用参数拼路径,结果攻击者靠包含一份本来就存在的日志文件就把事情办了。所以上传加固和包含加固是两件事,必须分别做,不能拿一个替代另一个。
7.2 "内网系统不用管"
协同办公系统这类应用,绝大多数部署在内网,于是很多团队默认"外网进不来就没风险"。这个假设在今天的网络环境下基本站不住。内网不是一个安全域,而是一个信任前提被过度放大的区域。一旦有任何一台终端被控,攻击者就在内网里了,而这些系统的默认配置、弱口令、老旧版本,恰好是横向移动最省力的抓手。把内网系统按"迟早会被访问到"来加固,是更符合实际的思路。
7.3 "打了补丁就万事大吉"
补丁必须打,但打完不等于结束。协同办公系统这类产品通常会被二次开发,官方补丁覆盖不到自定义的代码;而且升级过程中,老版本的文件经常因为各种原因被保留下来,成了没人管的"影子入口"。所以打完补丁之后,建议做两件事:一是把升级时保留下来的历史文件清理干净,二是对二次开发的代码做一次针对性的审计,重点看上传和包含这两类逻辑。这两件事做完,补丁的实际效果才落地。
7.4 "有 WAF 就够了"
WAF 是一层有效的缓冲,它能拦住大量自动化扫描和已知特征的攻击。但它的定位是"降低攻击效率",不是"消除风险"。绕过的本质是让请求看起来不像攻击,而这件事在 HTTP 协议的自由度下并不困难。所以正确的姿势是:WAF 继续用,但把它当最后一道,而不是第一道。第一道永远是代码里的校验逻辑和环境里的权限约束。
我从这条链上得到的最实际的一条体会是:真正稳固的防护,从来不是靠某一个点的"严防死守",而是靠多个位置上"即便这个点失守,下一个点也能兜住"。上传做白名单、包含用映射表、目录做隔离、账户给最小权限、日志建规则——单看每一条都不复杂,但它们叠在一起的时候,攻击者要穿透的成本是指数级上升的。反过来,只要其中任意一环想着"反正前面已经拦住了",整条链的强度就取决于最弱的那一环。这些年我复盘过的案例里,绝大多数不是被多高深的手法打穿的,而是一个不起眼的拼接、一个懒得改的配置、一句"内网应该没事"累积出来的。