第九届强网杯(2025)-ezphp

题目分析
打开题目发现是一个eval执行base64,可以解码得到如下内容
1 |
|
首先看看test类
1 | class test { |
在 test 里面有两个方法 __destruct() 与 __construct()
首先看看 __destruct()
将 $this->readflag 保存为 $GLOBALS['filename']
然后根据 key来看是进行新建类还是执行
然后 __construct()
在里面对 readflag 创建了一个匿名类
在这个匿名类中定义了3个方法 __construct() ,__wakeup() 与 readflag()
来看看这个匿名类的 __construct()
1 | public function __construct() { |
就是一个处理文件上传的地方
重点在!伪随机!
伪随机种子为时间与文件名的组合,由于文件名是我们可控,并且时间我们可以获取到
因此我们可以通过爆破文件名来得到一个==相对可控的文件名==(伏笔)
__wakeup()不重要不管
在 readflag() 里面定义了一个 readflag()函数
1 | public function readflag() { |
读取 $GLOBALS['file'] ,检查文件中是否含有敏感字符(伏笔)
就此整个代码就明了了
分析一
当我们include一个文件的时候,会调用一个叫做compile_filename的方法:
1 | zend_op_array *compile_filename(int type, zend_string *filename) |
这个函数 compile_filename 是 Zend 引擎(PHP 内核)的一个内部函数,他的作用是编译给定的 PHP 文件,返回其对应的 zend_op_array(即可执行的中间代码),并将文件路径加入全局已包含文件列表,防止重复 include,可以看到其中有一行调用了zend_compile_file,顾名思义,它的作用是使用 Zend 的编译器编译这个文件。
继续定位到phar对应的编译方法,需要看到phar_compile_file:
1 | static zend_op_array *phar_compile_file(zend_file_handle *file_handle, int type) /* {{{ */ |
可以看到
1 | if (strstr(file_handle->filename, ".phar") && !strstr(file_handle->filename, "://")) |
如果文件名中包含 .phar 且不是一个 URL(如 phar://),则认为它是本地的 .phar 文件
所以我们只有带有.phar 字符,随便啥名字都能解析(后面会讲)
当他判断到strstr(ZSTR_VAL(file_handle->filename), ".phar"),也就是发现文件名中包含字符串 .phar,会调用phar_open_from_filename,继续跟phar_open_from_filename:

可以看到这里调用了一个叫phar_open_from_fp的东西,继续跟一下:

1 | static int phar_open_from_fp(php_stream* fp, char *fname, size_t fname_len, char *alias, size_t alias_len, uint32_t options, phar_archive_data** pphar, int is_data, char **error) /* {{{ */ |
总结一下就是
从文件指针 fp 打开一个 .phar 文件并解析其结构,识别格式(phar/tar/zip/gz/bz2),找到 __HALT_COMPILER();,并初始化 phar_archive_data。
它可以去解析各种压缩的形式,最后如果整个文件都没找到 __HALT_COMPILER();,就报错
如果检测到 zip magic,交由 phar_parse_zipfile() 来处理。
如果文件结构像 TAR,就使用 phar_parse_tarfile() 解析。
这个过程会自动的去解压我们的文件
1 | 打开 phar 文件流 |
你可能会奇怪一个事,明明函数叫 phar_open_from_fp,不是“打开 Phar 文件”的吗?为什么还要判断 gzip、bzip2、zip、tar 呢?这些不是非 Phar 吗?其实这些格式也可以是合法的 Phar 文件容器,Phar 文件本质上是容器格式,不是文件后缀决定的,PHP 的 Phar 扩展支持将一个 Phar 文件封装成以下几种格式:
| 文件结构 | 是否支持作为 Phar | 是否需要特殊处理 |
|---|---|---|
纯 PHP 脚本(有 __HALT_COMPILER();) | ✅ | 默认支持 |
| gzip 压缩的 Phar | ✅ | 需要解压 |
| bzip2 压缩的 Phar | ✅ | 需要解压 |
| tar 格式的打包 | ✅ | 用 .phar.tar 或 .tar.phar |
| zip 格式的打包 | ✅ | .phar.zip 或 .zip.phar |
| 完全不是 Phar | ❌ | 报错 |
比如下面是一个合法的 gzip 压缩 Phar:
1 | php -d phar.readonly=0 -r ' |
生成的 test.phar:
- 外表是 gzip 格式;
- 里面是 tar + Phar 元数据;
- PHP 打开它的时候就需要:
- 判断是 gzip;
- 解压到临时流;
- 再继续扫描
__HALT_COMPILER();或 tar header;
要是我们打包成了zip,那么 PHP 会识别成 zip,通过 phar_parse_zipfile() 去解析。
最后的结论就是,比如我们生成了一个phar文件,然后把他打包成gz文件,当我们include这个gz文件时,php会默认把这个gz文件解压回phar进行解析
当然,在前面我们跟代码的时候应该还记得,他的判断逻辑是只要文件名里有.phar这几个字就行:

所以事实上我们完全不需要保证最后include的是一个xxx.phar.gzip文件,只要文件名里有.phar即可,所以说无论我们是include 1.phar.png还是1.phar.html均可以正常rce,甚至只要包含的路径里带了.phar这几个字就能解析 哪怕是目录也行:

所以题目的的一部分就浮出水面了
第一部分
由于我们可以控制文件名,并且知道时间,我们可以预测文件名为1234.pharxxxx.jpg
1 |
|

这样我们就可以得到文件名中包含.phar
生成phar的脚本如下
1 |
|
我们就可以得到文件exploit.phar
然后在使用gzip压缩即可得到exploit.phar.gz==(后面会用到)==
分析二
我们如何调用某一个类中的某个方法呢
可以通过类名加方法名放在一个数组中的方式

那问题来了,我们隐函数的类名是什么呢?
可以看看离别歌师傅的博客与php的bugs
PHP :: Bug #81111 :: Serialization is unexpectedly allowed on anonymous classes with __serialize()
我们可以看到

函数名很好获得就是readflag,php文件的绝对路径我们可以引起一个报错就可以很方便得到,(其实就是先乱写,然后看报错)

还有一个比较简单不用动调的方法,就是get_class()函数

成功拿到,将里面的文件路径和行号改一改就好了
然后就是调用分析
1 | class test { |
由于在__destruct()方法中读的 class 属性,为fun就调用方法,否则就是新建类
所以我们可以嵌套多个 test 类来实现访问其内部匿名类
首先最外层作用是文件上传我们的恶意文件,第二层是用来创建匿名类,调用其readflag()方法,注册我们的 readflag()函数,第三层可以去调用readflag()函数,使其include
第二部分
所以我们的序列化脚本如下
1 |
|
还可以是
1 |
|
两种方法都可以实现多个类的嵌套,然后就是
合成所有步骤
wjm.php
1 |
|
exp.py
1 | import os |
将exploit.phar.gz与这两个脚本放一起,运行py脚本

成功写入
蚁剑链接,密码1

查看suid权限

有一个base64有suid权限

拿下flag

- 标题: 第九届强网杯(2025)-ezphp
- 作者: tiran
- 创建于 : 2025-11-07 18:40:57
- 更新于 : 2025-11-07 19:11:30
- 链接: https://www.tiran.cc/2025/11/07/第九届强网杯-2025-ezphp/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。