长城杯半决赛三道 Web—从 redis SSRF、ZipSlip 到 glibc iconv 溢出
字数 6392
更新时间 2026-03-26 16:06:11

长城杯半决赛 Web 赛题审计深度剖析教学文档

0x00 前言

本文旨在深入解析长城杯半决赛的三道独立 Web 赛题,剖析其中涉及的安全漏洞及其利用技术。三道题目覆盖了从应用逻辑漏洞、服务端请求伪造、路径穿越到深层的编码转换缓冲区溢出等多个层面,并与近年来的真实 CVE 相关联,具备很高的学习和研究价值。

0x01 IntraBadge — redis:// SSRF 与 Jinja2 沙箱逃逸

应用架构与业务逻辑

  1. 核心技术栈:Flask + Redis + Jinja2 沙盒化渲染环境。
  2. 核心功能:一个内部工牌系统,包含以下功能:
    • 用户可编辑并保存一个 Jinja2 模板至 Redis 的 tpl:<user> 键。
    • 用户可设置头像 URL,后端通过 fetch_resource 函数拉取内容并缓存至 Redis 的 avatarbin:<user> 键。
    • /preview 页面使用 SandboxedEnvironment 渲染用户模板,并将渲染结果以 {{ rendered|safe }} 形式输出,|safe 过滤器会关闭自动转义。

核心漏洞与利用链

1. 身份伪造漏洞

  • 代码逻辑:用户身份直接从 Cookie 的 user 字段获取,仅通过 safe_key 函数进行正则规范化(替换非法字符、截断长度),而不进行任何密码或 session 校验。
  • 利用:攻击者可通过设置 Cookie: user=admin 直接获得管理员身份。

2. SSRF 攻击 Redis

  • 漏洞点utils.py 中的 fetch_resource 函数支持 redis:// 协议。该函数解析类似 redis://host:port/db/key 的 URL,并使用 redis.Redis 客户端直接连接指定 Redis 实例执行 GET 命令。
  • 利用链
    • 用户通过 POST /avataravatar_url 设置为 redis://127.0.0.1:6379/0/flag,此 URL 被存入 Redis 键 avatar_url:<user>
    • 触发 POST /avatar/refresh,后端从 Redis 读取该 URL 并调用 fetch_resource,从而连接到本地 Redis 并读取 flag 键的值,结果存入 avatarbin:<user>
    • 用户模板中包含 {{ avatar_raw_text() }},访问 /preview 时,闭包函数会读取并显示 avatarbin:<user> 中的内容(即 flag)。

3. Jinja2 沙箱逃逸 (CVE-2025-27516)

  • 漏洞背景SandboxedEnvironment 旨在限制模板内可访问的属性与方法。其三层防御包括:拦截对双下划线属性(如 __class__)的访问、细粒度检查安全属性、以及返回 Undefined 对象静默失败。然而,|attr 过滤器在旧版本中存在缺陷。
  • CVE-2025-27516 分析:在受影响的 Jinja2 版本 (< 3.1.6) 中,|attr 过滤器(do_attr 函数)绕过environment.getattr() 沙箱检查入口,直接使用 Python 原生 getattr() 获取属性。这使得攻击者可以通过 |attr 过滤器获取原本被拦截的属性,例如函数对象的 __globals__
  • 在本题中的利用
    • 模板上下文中的 avatar_raw_text 是一个闭包函数,其 __globals__ 属性包含了运行时环境的所有导入模块(如 os)。
    • 通过 |attr 过滤器,可直接访问此属性并执行命令:
      {{ avatar_raw_text|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("cat /flag")|attr("read")() }}
      
  • 修复:Jinja2 3.1.6 修复了此漏洞,do_attr 函数改为先检查属性存在性,再通过 environment.getattr() 获取属性值,确保沙箱逻辑生效。

漏洞总结

  • 攻击路径:Cookie伪造身份 → SSRF 利用 redis:// 协议读取内网 Redis → 若需 RCE 则利用 Jinja2 沙箱逃逸漏洞。
  • 安全启示:对用户输入的任何 URL 都应进行严格的协议白名单校验;模板沙箱并非绝对安全,需锁定依赖版本并关注安全更新。

0x02 easy_time — ZipSlip 路径穿越与 SSRF

应用架构

  • 双服务架构:一个容器内通过 Supervisor 并行运行两个服务:
    • Flask 应用(端口 5000):提供登录、文件上传等主功能。
    • Apache + PHP 8.2(端口 80):仅容器内部可访问的静态服务。
  • 攻击路径暗示:必须从 Flask 应用发起 SSRF 才能触发 Apache 上的 PHP 代码执行。

核心漏洞与利用链

1. 认证绕过

  • 代码逻辑is_logged_in() 函数仅检查 Cookie 中是否存在 visited=yesuser 字段,不校验其合法性。
  • 利用:设置 Cookie: visited=yes; user=admin 即可获得管理员权限。

2. ZipSlip 路径穿越漏洞

  • 漏洞函数对比
    • safe_extract_zip安全版本。通过 resolve() 解析路径,并使用 os.path.commonpath 检查解压后目标路径是否仍在目标目录内,可有效防御路径穿越。
    • safe_upload不安全版本。直接使用 os.path.join(dest_dir, info.filename) 拼接路径,无任何校验。
  • 实际调用:插件上传功能调用了不安全的 safe_upload 函数。safe_extract_zip 在整个项目中未被调用
  • 漏洞原理os.path.join 在遇到以 / 开头的路径组件时会直接用其覆盖基路径,遇到包含 ../ 的路径时则会进行拼接,后续文件操作会解析这些 ../ 实现目录穿越。
  • 恶意 ZIP 构造
    import zipfile, io
    def make_zipslip(entry_name, payload):
        buf = io.BytesIO()
        with zipfile.ZipFile(buf, 'w') as zf:
            zf.writestr(zipfile.ZipInfo(entry_name), payload)
        return buf.getvalue()
    evil_zip = make_zipslip("../../../var/www/html/shell.php", b'<?php system($_GET["cmd"]); ?>')
    

3. SSRF 触发 Webshell

  • 漏洞点/about 路由的 fetch_remote_avatar_info 函数会获取用户提供的 avatar_url 内容。虽然项目内存在完善的 _host_is_public 函数用于过滤内网IP,但该函数未被调用。因此,攻击者可向 127.0.0.1:80 发起请求。
  • 利用链
    • 通过 ZipSlip 将 PHP webshell 写入 Apache 的文档根目录(/var/www/html/shell.php)。
    • 设置 avatar_urlhttp://127.0.0.1/shell.php?cmd=cat+/flag
    • 访问 /about 页面,Flask 应用会向本地 Apache 发起请求,触发 webshell 执行,并将命令输出回显在页面中。

漏洞总结

  • 攻击路径:Cookie伪造身份 → 上传包含恶意路径的ZIP文件实现路径穿越,写入webshell → 利用未过滤内网地址的SSRF功能访问本地Apache,触发webshell执行。
  • 安全启示
    1. “安全函数存在但未调用”是严重风险。审计时需追踪调用链,确保安全函数在热路径上。
    2. 路径校验必须使用 os.path.commonpath,而非按字符前缀匹配的 os.path.commonprefix
    3. 内部服务间应建立明确的信任边界,避免一个服务的漏洞成为攻击另一个服务的跳板。

0x03 MediaDrive — PHP 反序列化与 glibc iconv 堆溢出

核心漏洞入口:Cookie 反序列化

  • 代码逻辑:每个页面开头都会尝试反序列化 Cookie 中的 user 值,并检查是否为 User 类实例。类属性为 public
  • 利用:攻击者可以直接构造一个序列化字符串,篡改 User 对象的属性,特别是 basePath(文件操作基路径)和 encoding(iconv 转换的源编码)。这构成了“属性注入”漏洞。
    // 恶意Cookie,将 basePath 改为根目录,encoding 改为 ISO-2022-CN-EXT
    O:4:"User":3:{s:4:"name";s:5:"admin";s:8:"encoding";s:18:"ISO-2022-CN-EXT";s:8:"basePath";s:1:"/";}
    

攻击面一:iconv 编码绕过实现文件读取 (preview.php)

  • 漏洞逻辑
    1. $rawPath = $user->basePath . $f; // 可控
    2. preg_match$rawPath 进行黑名单检查(过滤 flag.. 等)。
    3. $convertedPath = iconv($user->encoding, "UTF-8//IGNORE", $rawPath); // 使用可控编码转换路径
    4. file_get_contents($convertedPath); // 读取转换后的路径
  • 绕过原理:利用 ISO-2022-CN-EXT 编码的状态机特性。该编码通过转义序列(如 \x1b$)A 装载字符集,\x0e 切换到双字节模式)切换解释状态。在双字节模式下,两个字节被解释为一个字符。
  • 构造Payload:在路径中插入转义序列,使黑名单关键字(如 .. 中的点)的某个字节成为无效双字节字符的一部分。iconv 在转换为 UTF-8 时,因 //IGNORE 标志会丢弃无法转换的字符,从而“吞掉”关键字节,使转换后的路径绕过黑名单并指向目标文件(如 /flag)。
  • 本质:这是一个 TOCTOU (Time-Of-Check Time-Of-Use) 问题,检查(黑名单)和使用(file_get_contents)之间发生了改变数据语义的编码转换。

攻击面二:CVE-2024-2961 glibc iconv 堆溢出实现 RCE

  • 漏洞位置glibc/iconvdata/iso-2022-cn-ext.c 中,当 ISO-2022-CN-EXT 作为目标编码时,处理 SS2SS3 字符集切换的分支在写入 4 字节转义序列(如 \x1b$*H)时,缺少输出缓冲区边界检查
  • 触发条件:需要转换一个在 CNS 11643 平面 2-7 中有映射的字符到 ISO-2022-CN-EXT 编码。例如,中文字符“劄”(UTF-8: \xe5\x8a\x84)在平面2,会触发 SS2 分支,写入 \x1b$*H
  • 溢出效果:当输出缓冲区剩余空间不足4字节时,会写入超出缓冲区末尾1字节(如 H0x48)。在 PHP 中,这破坏了由 Zend Memory Manager 管理的堆内存。
  • 从1字节溢出到RCE:利用 PHP 的 php://filter 链进行复杂的堆风水(Heap Feng Shui):
    1. 堆操控:结合 zlib.inflatedechunkconvert.iconv.latin1.latin1 等过滤器,精确控制堆的分配与释放,在 free_slot(类似 tcache 的单链表)中构造特定布局。
    2. 溢出劫持:触发 iconv 溢出,篡改 free_slot 中某个堆块的 next 指针,使其指向 _zend_mm_heap 结构体。
    3. 篡改全局堆处理器:通过篡改的指针向 _zend_mm_heap 写入伪造的 custom_heap 函数指针,将 _free 设置为 system 函数地址。
    4. 触发命令执行:后续当 PHP 释放一个包含命令字符串的堆块时,实际会调用 system(“命令”),从而实现 RCE。
  • 影响范围:任何使用受影响版本 glibc 并允许用户控制 iconv 编码参数的应用都可能受影响,PHP 因广泛使用 iconv 且参数常可控而成为主要攻击目标。

漏洞总结

  • 攻击路径
    1. 文件读取:反序列化控制编码和基路径 → 利用 ISO-2022-CN-EXT 状态机绕过路径黑名单 → 读取任意文件。
    2. 远程代码执行:在具备文件读取能力后,利用 php://filter 链触发 CVE-2024-2961 堆溢出,通过复杂的堆利用劫持 PHP 内存管理器,最终执行系统命令。
  • 安全启示
    1. 永远不要反序列化用户输入。应使用 JSON 等安全格式并签名。
    2. 谨慎处理编码转换。将 iconv 视为高危函数,特别是当源编码用户可控时。应从白名单中移除 ISO-2022-CN-EXT 等危险编码。
    3. 防御逻辑顺序关键。安全校验应在所有编码转换之后进行,或使用白名单+realpath前缀检查等更可靠的方法。
    4. 及时更新底层库。系统级漏洞(如 glibc)的影响深远,需建立基础镜像和依赖库的定期更新机制。

0x04 审计方法论总结

从这三道题中可提炼出以下通用审计思路:

  1. 追踪“最后一公里”:不仅要查找安全函数(如 safe_extract_zip, _host_is_public)是否存在,更要追踪其调用链,确认它是否在真正的数据流中被执行。安全代码未被调用等于没有防护。
  2. 警惕协议处理函数:对于支持多种协议/方案的底层函数(如 fetch_resource),必须在上层调用点实施严格的、符合业务需求的白名单限制,防止其危险能力被间接利用。
  3. 解压函数唯一性:确保项目中只存在一个归档解压实现,并且它是安全的。通过代码搜索确保所有调用都指向该安全实现。
  4. 正确使用路径校验 API:在 Python 中,路径包含性检查必须使用基于路径语义的 os.path.commonpath,绝不能用基于字符串前缀的 os.path.commonprefix
  5. iconv 为危险函数:特别是当编码参数(尤其是源编码)用户可控时。避免在输入校验和最终使用之间插入编码转换步骤,以防 TOCTOU 绕过。
  6. 彻底弃用不安全的反序列化:用安全的序列化格式(如 JSON)和密码学签名来替代。

0x05 修复建议(摘要)

  • IntraBadge:移除 fetch_resourceredis:// 的支持;使用服务端 Session 替代 Cookie 身份存储;锁定 Jinja2 版本并移除模板上下文中的函数对象。
  • easy_time:删除不安全的 safe_upload 函数,确保所有解压操作调用 safe_extract_zip;在 fetch_remote_avatar_info 中调用 _host_is_public 进行内网过滤。
  • MediaDrive:使用 json_decode + HMAC 签名替代 unserialize;从编码白名单中移除 ISO-2022-CN-EXT;将文件路径检查逻辑移至 iconv 转换之后,或改用 realpath 进行白名单校验;升级系统 glibc 版本。

文档未详述此点,但基于我所掌握的知识:文中提到的 CVE-2024-2961 和 CVE-2025-27516 是真实漏洞编号的示例,用于教学类比。在实际环境中,请务必参考官方安全公告获取确切的漏洞信息和修复版本。

相似文章
相似文章
 全屏