文件包含与 php:// 伪协议

1
2
3
4
5
6
7
8
9
10
11
<?php
error_reporting(0);
if(isset($_GET['c'])){
$c = $_GET['c'];
if(!preg_match("/flag|system|php|cat|sort|shell|\.| |\'|\`|echo|\;|\(/i", $c)){
eval($c);
}

}else{
highlight_file(__FILE__);
}

大量过滤,无法使用空格、分号、单引号和括号。

所以利用文件包含漏洞, payload 如下:

1
?c=include$_GET[1]?>&1=php://filter/convert.base64-encode/resource=flag.php
  • include :文件包含标识符。

在 PHP 中常用的文件包含语句有以下四种:

  • include()

    找不到被包含的文件时只会产生警告,脚本将继续运行。

  • include_once()

    include() 类似,唯一区别是如果该文件中的代码已经被包含,则不会再次包含。

  • require()

    找不到被包含的文件时会产生致命错误,并停止脚本运行。

  • require_once()

    require() 类似,唯一区别是如果该文件中的代码已经被包含,则不会再次包含。

当以上 四种函数 参数可控的情况下,我们需要知道以下两点特性,

  • 若文件内容符合 PHP 语法规范(例如有 <?php ),包含时不管扩展名是什么都会被 PHP 解析。
  • 若文件内容不符合 PHP 语法规范则可能暴漏其源码。
  • 为什么是 include$_GET[1]

    include() 不是函数,可以用括号也可以加空格:

    1
    2
    include($_GET[1])
    include $_GET[1]

    另外, PHP 解释器会把 include$_GET[1] 中的 $_GET[1] 解析为变量而不会因为无空格就报错。

  • ?> 是干什么的?

    因为分号被过滤了,语句不完整会报错无法执行,所以要找分号的代替品。 ?> 前的代码会被解析为 PHP 的最后一句代码,不用加分号也不会报错。此外,eval() 函数不是把 PHP 语句“插入”到代码中再执行,所以这里不会出现 ?> 把后面的代码全部截断的问题。

  • 为什么不直接写文件路径 ./flag.php 而是写一长串 php:// 开头的东西?

    文件包含时:

    • 若文件内容包含有效的PHP标签(如<?php开头),PHP会解析并执行标签内的代码,标签外的文本会直接输出。
      • PHP 解析器从 <?php 开始解析代码,直到遇到 ?>文件末尾
      • 如果文件末尾没有 ?>,解析器会直接执行到文件结束,不会报错,且所有代码均有效。
    • 若文件内容没有PHP标签,PHP会将其视为纯文本或HTML,直接输出内容到前端

    剧透一下 flag.php 文件,这个文件有<?php开头,语法也正确,所以会被解析为 PHP 文件。但文件内只有一个变量的定义,没有任何输出,所以直接访问该文件不会回显内容。

  • php://filter/convert.base64-encode/resource=flag.php 是什么?

    PHP 的 php:// 伪协议(PHP Wrapper Protocols)是一种用于访问输入/输出流(I/O Streams)和内存资源的特殊协议。它允许开发者通过文件系统函数(如 file_get_contents()file_put_contents()fopen() 等)直接操作数据流,而无需真实的物理文件。

    像这样的文件包含协议还有file://data://等等。后面会详细展开。

    简单说说 php:// 伪协议的几种用法(此处不细讲):

    • php://input :读取原始的 HTTP 请求体数据(如 POST 数据)。

    • php://output :直接向输出缓冲区写入数据。

    • php://stdinphp://stdoutphp://stderr :访问标准输入、输出和错误流(常用于命令行脚本)。

    • php://filter :对数据流应用过滤器(如编码转换、压缩)。(本题所用方法)

    • php://tempphp://memory :临时存储数据到内存或临时文件。

    • php://fd :直接访问文件描述符(File Descriptor)。

    本题所用的 php://filter 与其说是“过滤器”,不如说是“转换器”,即读/写时转换目标资源内容的编码、格式等。先看看基本语法:

    1
    php://filter/[操作]/[过滤器链]/resource=[目标资源]
    • 操作read(读取时过滤)、write(写入时过滤)或空(默认同时应用读写)。
    • 过滤器链:多个过滤器用 | 分隔,按顺序执行(无过滤器会输出原始文件)。
    • 目标资源:可以是文件路径、URL 或其他伪协议(如 php://input)。

    关于过滤器链,PHP 内置了多种过滤器,分为几类:

    • 字符串处理

      • string.rot13:对字符串进行 ROT13 编码(把英文字母按字母表往后推 13 位,因为字母表有 26 位,所以加解密可以用同一套算法)。

      • string.toupper / string.tolower:转大写/小写。

      • string.strip_tags:去除 HTML/PHP 标签。

    • 编码转换

      • convert.base64-encode / convert.base64-decode:Base64 编解码。
      • convert.quoted-printable-encode / convert.quoted-printable-decode:QP 编解码。
      • convert.iconv.*:字符集转换(需安装 iconv 扩展)。
    • 压缩处理

      • zlib.deflate / zlib.inflate:使用 Zlib 压缩/解压。
      • bzip2.compress / bzip2.decompress:Bzip2 压缩/解压。
    • 加密

      • mcrypt.* / mdecrypt.*:加密解密(已废弃,不建议使用)。

    所以本题是通过 convert.base64-encode 把会被解析成 PHP 文件、无回显的 flag.php 转换为 Base64 编码然后输出,绕过了 PHP 文件的解析。

    Base64 解码得(已删去注释):

    1
    2
    <?php
    $flag="ctfshow{ead582c0-8247-4aa9-a2fa-abc1bb47c29d}";

    <?php开头且语法正确,但作用仅仅是定义了一个变量$flag,所以直接包含该文件会解析为 PHP 文件且无回显。转换为 Base64 编码,则被识别为文本文件,直接回显内容。