介绍 Bottle 框架
Bottle 是一个用 Python 写的 **微型 Web 框架,**整个框架只有一个 Python 文件。
SimpleTemplate 模板引擎 — Bottle 0.13-dev 文档
Bottle 框架常见的漏洞是 SSTI,pickle 反序列化。
bottle模板框架注入 - Echair - 博客园、VNCTF 2025 web wp - LamentXU - 博客园LilCTF做题笔记
https://ctf-files.bili33.top/VNCTF2025/VNCTF2025%20Official%20Writeup.pdf
实战
[VNCTF 2025]学生姓名登记系统
Infernity 师傅用某个单文件框架给他的老师写了一个 “学生姓名登记系统”,并且对用户的输入做了严格的限制,他自认为他的系统无懈可击,但是真的无懈可击吗?
看 wp 都说题目描述说:”某个单文件框架“,搜索或者问 ai 都能知道 python 本体的单文件框架是 bottle 框架。而我现在实践,发现确实搞不出来……搜索能力有待改进。
[XYCTF 2025]Signin
复现环境:Review::CTF
1 | # -*- encoding: utf-8 -*- |
路径穿越
1 |
|
下载文件的接口,过滤了../../和 \\,../和/开头,这里考的的路径穿越 bypass,只过滤两个../并排,那么./../=../,适当绕过一下就好了。
结合<font style="color:#080808;">../../secret.txt</font>,给<font style="color:#080808;">/download</font>传参<font style="color:#080808;">?filename=./.././../secret.txt</font>,得到:

我们得到了 secret.txt 的内容:Hell0_H@cker_Y0u_A3r_Sm@r7
pickle反序列化
[LilCTF 2025]ez_bottle
1 | from bottle import route, run, template, post, request, static_file, error |
1 | UPLOAD_DIR = .../uploads # 上传的文件存放目录 |
黑名单基本杜绝了 SSTI 的可能。
检查软链接 & 路径穿越
1 | def is_symlink(zipinfo): |
- 防止 zip 文件里有软链接。
1 | def is_safe_path(base_dir, target_path): |
- 防止解压 zip 的时候用
../跳出目录(路径穿越攻击)。
路由功能
/upload 上传页面(GET)
- 返回
upload.html(上传表单)
/upload 上传处理(POST)
- 必须是
.zip文件 - 文件大小不能超过 1MB
- 用
zipfile.ZipFile解压时:- 禁止软链接
- 禁止路径穿越
- 解压到一个用
md5(文件名 + 时间)生成的唯一目录里 - 返回文件列表,并提示访问路径
/view/<md5>/<filename>
/view/<md5>/<filename>
- 读取文件内容(纯文本)
- 如果命中黑名单 → 返回 “you are hacker!!!”
- 否则调用
1 | return template(content) |
- **这里的 **`**template()**`** 是 Bottle 框架的模板渲染函数**
- 如果文件内容里是模板语法(类似 `{{...}}`),它会直接执行 **SSTI**
打的过程
% x = “hello world”
x
输出 x, 忽略行尾纯变量,无法利用行尾表达式
过滤花括号和下划线,无法获取对象
一些尝试
1 | underscore = chr(95) |
正确思路
一个页面可以上传压缩包,会把他解压放在另一个目录里,并且这个目录可知。
禁止了软链接,路径穿越和 SSTI。
如果总结成这样看,确实是做不出来。但是题目里禁止做的事情,我们一般有两个思路,一是换别的方法做,但是更多情况我们要绕过 bypass 继续用这个事情。当我们上传压缩包的时候,只要是软链接,路径穿越直接上传不上去,那就没办法了。但是我们重新看查看文件的路由:
1 |
|
这里包含了黑名单,查看到的是"you are hacker!!!nonono!!!",但是实际上他还是渲染了,只是不给你看。所以我们通过别的文件看就可以了。
bottle 框架里面%可以执行 python 代码。
也就是我们先上传一个名为 1.tpl 的文件,内容是{{! open('/flag').read()}},来获得 flag。
在 /upload 处理函数里:
1 | current_time = str(time.time()) |
UPLOAD_DIR = os.path.join(os.path.dirname(__file__), 'uploads')
也就是说,所有上传文件都放在当前脚本目录下的uploads/目录里。
再上传一个内容为:
的文件用来读取 flag 就可以了。