Flask-Jinja2 RCE漏洞CVE-2019-8341

jinja2模板语言:

作为一些主流web框架(如Flask,Djngo)的数据渲染的底层调用,尤其是其模板语言,相当方便。

利用Jinja2进行后台(python)与前端(HTML)之间进行数据交互。

(1)创建html模板-Demo:

20210910222816.png

(2)加载模板

后台import jinja2

import jinja2
//path需要为当前python文件所在目录的完整路径
env = jinja2.Environment(loader=jinja2.FileSystemLoader(path))
//get_template内部的参数为html模板相对于该python文件所在目录的路径(相对路径)
temp = env.get_template('statics/template.html')

(3)模拟数据

确认中的数据的可选值

render_dict={}

(4)render渲染模板

temp_out = temp.render(content=render_dict['Content']...)
with open(os.path.join(path, 'statics/out.html'), 'w', encoding='utf-8') as f:
    f.writelines(temp_out)
    f.close()
CVE-2019-8341复现

环境搭建版本:

image-20210805004726262

证明存在SSTI:

image-20210816220359402

读取任意文件:

http://localhost:2333/?username=

image-20210809224240714

image-20210809224310770

命令执行:

http://localhost:2333/?username=

image-20210811213340692

image-20210811213401148

python魔术方法(沙箱)

绕过模拟的 Python 终端,最终实现命令执行

在 Python 的内建函数中,有一些函数可以帮助我们实现任意命令执行:

os.system() os.popen()
commands.getstatusoutput() commands.getoutput()
commands.getstatus()
subprocess.call(command, shell=True) subprocess.Popen(command, shell=True)
pty.spawn()

在 Python 中导入模块的方法通常有三种(xxx 为模块名称):

import xxx
from xxx import *
__import__('xxx')
#可以通过路径引入模块:如linux系统中的Python的os模块的路径一般都是在/usr/lib/python2.7/os.py
>>import sys
>>sys.modules['os']='/usr/lib/python2.7/os.py'
>>import os

常见的危险函数:

execfile('/usr/lib/python2.7/os.py')
system('cat /etc/passwd')
---
import timeit
timeit.timeit("__import__('os').system('dir')",number=1)
---
eval('__import__("os").system("dir")')
---
import platform
print platform.popen('dir').read()

python内建函数

当我们不能导入模块,或者想要导入的模块被禁,那么我们只能寻求 Python 本身内置函数(即通常不用人为导入,Python 本身默认已经导入的函数)。我们可以通过可以通过 dir __builtin__ 来获取内置函数列表

在 Python 中,不引入直接使用的内置函数被成为 builtin 函数,随着 *builtin* 这个模块自动引入到环境中。

image-20210811221316487

可以通过 *dict* 引入我们想要引入的模块。*dict* 的作用是列出一个模组/类/对象 下面 所有的属性和函数。这在沙盒逃逸中是很有用的,可以找到隐藏在其中的一些东西

创建对象及引用

常见两种方法:

#__mro类似于__base__,但是__mro__是追根溯源的,不是向上查找一级
().__class__.__bases__[0]
''.__class__.__mro__[2]

image-20210811224309383

__subclasses__获取类的所有子类

image-20210811224628533

前面同理就是为了获取两个类,这两个类中的__init__.__globals__中有os模块,用来执行命令:<class ‘site._Printer’>、<class ‘site.Quitter’>然后可以获取到os模块,利用os模块的popen执行命令,read函数获取回显。

image-20210811225504863

image-20210811231116609

jinjia2漏洞

Environment的实例方法from_string,存在RCE,该函数在内部实现逻辑中,存在exec函数去执行了,from_string函数参数中的jinja2的代码指令。

jinja2.Environment().from_string()

漏洞点:

image-20210811231444090

jinja2.Environment().from_string('Hello'+username).render()

这行代码表示,将前端输入的username拼接到模板,此时username的输入没有经过任何检测

除了这种使用方式外,还有一种是使用Template的方法:

t=Template("Hello "+username)

return t.render()

image-20210816222330208

可以看到同样可以成功

image-20210816222346927

跟进Template可以看到同样使用了from_string方法

image-20210816222529834

compile函数

跟进该函数前,先科普一个python内置函数compile

compile() 函数将一个字符串编译为字节代码。

#source -- 字符串或者AST(Abstract Syntax Trees)对象。。
#filename -- 代码文件名称,如果不是从文件读取代码则传递一些可辨认的值。
#mode -- 指定编译代码的种类。可以指定为 exec, eval, single。
compile(source, filename, mode[, flags[, dont_inherit]])

image-20210814193911754

调试过程

username可控,传入对应的payload

此处payload为:``

image-20210814194751182

跟进from_string方法

image-20210814195252422

跟进compile方法

image-20210814201546704

在该函数中,主要关注三个函数

①Enviroment.parse()

image-20210814201747048

分析源代码并返回抽象语法树。编译器使用此节点树将模板转换为可执行源代码或字节码。这对于调试或从模板中提取信息很有用。

image-20210814202217478

②generate ->已编译的模板,并用于对其进行计算(步骤①)。

API — Jinja Documentation (3.0.x) (osgeo.cn)

image-20210814202736204

③compile()->将步骤②中计算好的字符编译为字节代码。

image-20210814203153257

跟进完Environment.compile()

跟进from_code方法

image-20210814203505458

此处触发命令执行

官方修复

维护者和多个第三方认为此漏洞无效,因为用户不应在没有砂箱的情况下使用不受信任的模板。

修复措施

防止服务器端模板注入的最佳方法是不允许任何用户修改或提交新模板。但是,由于业务需求,有时这是不可避免的。

避免引入服务器端模板注入漏洞的最简单方法之一是,除非绝对必要,否则始终使用“无逻辑”模板引擎,例如Mustache。尽可能将逻辑与表示分离开来,可以大大减少您遭受最危险的基于模板的攻击的风险。

另一措施是仅在已完全删除潜在危险模块和功能的沙盒环境中执行用户的代码。不幸的是,对不受信任的代码进行沙箱处理固有地困难,并且容易被绕过。

最后,另一种补充方法是接受几乎不可避免的任意代码执行,并通过在例如锁定的Docker容器中部署模板环境来应用自己的沙箱。

若使用Template的方法,如果使用一个固定好了的模板,在模板渲染之后传入数据,就不存在模板注入,就好像SQL注入的预编译一样

image-20210816222851668