0x00
最近看了国外几篇关于模板注入的文章, 自己也在这里加上自己的一些东西总结一下.
Server-Side Template Injection — James Kettle
0x01 万恶的拼接
我们先看这段处理网站404状态的代码
|
|
这段代码没有从模板文件而是用 render_template_string() 直接从一个字符串渲染到了html. 从模板文件还是从字符串倒不是什么大问题, 主要是它渲染的那个字符串是和用户的输入(request.url)拼接过的. 要知道这里的 template 存的并不是纯数据而是有一部分控制功能在里面的. 这就产生了代码域与数据域的混淆, 只要出现了这样的情况十有八九就会有洞. 首先最直接的, html模板渲染到html, 插入到html就肯定会有XSS.
0x02 不仅仅是客户端
我们都知道html里面拼接数据是XSS攻击的是客户端, 然而html模板并不仅仅是html, 还有能被模板渲染引擎解释的模板代码, 这样一来我们就能插入在服务器端执行的代码. 让我们试一下
看来是可以的, 再试一个
WOW, 连secret_key都爆出来了(还记得hitcon有个题就是这个套路)
0x03 读写文件
当然, 我们的目标肯定不能止步于一个 泄露出来的信息. 我们想的当然是最好能拿到一个shell.
要拿到shell, 就很难避免要执行命令, 而Jinja和Flask的template是不太可能提供这种功能的(事实上也没有), 所以在这种环境下, 肯定就要想办法调用python的 system()
或者 check_output()
之类可以执行命令的函数.
首先在Flask/Jinja的模板中, python的字符串,数字这类基本对象是肯定是支持的
其实根据以前在CTF里面的经验, 不难想到先试着调用一下这些对象的内置方法, 去看一下当前环境下能访问哪些对象 ''.__class__.__mro__[2].__subclasses__()
, 或者 (1).__class__.__base__.__subclasses__()
这里还是稍微写一下, 首先 ''.__class__
可以访问到字符串的类型对象(关于python中的类型对象参见Python Types and Objects)
因为python中所有的对象都是从Object逐级继承来的, 类型对象也不除外, 所有我们就可以调用对象的 __base__
方法访问该对象所继承的对象
或者使用 __mro__
(Method Resolution Order) 直接获得对象的继承链, python用这个方法来确定对象方法解析的顺序
当我们访问到Object的类型对象的时候, 就可以用 __subclasses__()
来获得当前环境下能够访问的所有对象.
因为调用对象的 __subclasses__()
方法会返回当前环境中所有继承于该对象的对象.
我们仔细过一遍环境里面存在的对象, 首先引起我们注意的肯定就是这个python内建的file对象
至少我们能读写文件了.
''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd', 'r').read()
''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test', 'w').write('AAAAAAAAAAAAAAAAAA')
但是即使可以写文件, 好像也拿不到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
|
|
flask有 from_json
, from_envvar
, from_object
, from_mapping
, from_pyfile
等好几个更新配置的方法, 但是相比于其它 from_pyfile
这个方法的实现有点特殊. 我们看上面的源码, 首先新建一个module对象d, 然后把传入的文件读出来用compile()编译成exec()可以执行的code对象, 然后执行, 并且把 d.__dict__
用作代码对象code执行的scope. 这句话可能比较抽象, 或者我说的不准确, 这里放一张调试的图, 相信大家一看就明白了
然后又将d传入了 from_object
方法, from_object
方法遍历 d.__dict__
将键名为大写的键值对更新到当前环境的config对象中.
所以如果我们能让 from_pyfile
去读这样的一个文件
|
|
那么我们访问 config['SHELL']
时, 实际上就能访问到 system
函数了. 而我们前面又已经做到了文件读写, 所以两个点结合起来我们就完全可以拿到SHELL
我们最终的payload为
|
|
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