首页 / 资讯中心 / 文章详情

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

DVWA文件包含漏洞:LFI/RFI、路径穿越与PHP伪协议实战 ★ FEATURED ARTICLE
很多人在 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);请求?pagemy_page.php它正常显示页面请求?page/etc/passwd它把系统账户文件的内容一屏刷出来请求?pagehttp://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 之后默认是 OffPHP 7.x 某些发行版默认 OnPHP 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 / Highphp://filterphp://filter/convert.base64-encode/resourceindex.php无特殊要求读取文件内容而不执行其中的 PHP 代码Low / Mediumphp://inputphp://input配合 POST 体allow_url_include On把请求体当成 PHP 代码执行Low / Mediumdata://data://text/plain;base64,xxxxallow_url_include On直接在 URL 里嵌入内容Low / Mediumphar://phar://./upload/a.phar/x.php需要 phar 扩展触发反序列化配合上传使用Low / Mediumzip://zip://./upload/a.zip%23x.php需要 zip 扩展从压缩包里取文件Low / Mediumhttp:///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/?pagephp://filter/convert.base64-encode/resourceinclude.php返回的内容应该是一串 base64拿去解码就是include.php的源码。这里有个小细节值得注意如果直接传?pageinclude.php页面显示的是被包含文件执行之后的结果可能看起来就是一些 HTML但加上 filter 之后就能看到里面的 PHP 代码本体两者差别很大。这个对比最能说明“执行”和“读取”的区别。第三个请求用来确认远程包含是否开启。http://127.0.0.1:8080/vulnerabilities/fi/?pagehttp://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/?pagehthttp://tp://127.0.0.1:8000/test.txththttp://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/?pagefile:///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 到文件名的映射表找不到就拒绝。语言包切换也是同理前端传langzh后端从白名单里查查不到就用默认语言永远不要拼路径。白名单还有一个隐性好处它天然防住了路径穿越和伪协议因为这两类攻击的前提都是“你能控制路径字符串”而白名单根本不给这个控制权。所以从工程角度说这比加十个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调用方传了用户输入进来光看函数本身完全看不出问题。所以搜索完之后还要顺着调用链往上找一层确认参数到底从哪来。
阅读完成 · 觉得有帮助?
咨询建站