Spring RCE漏洞分析2(CVE-2018-1273)

0x00 前言

继续分析Spring的历史漏洞,本篇记录Spring Data Commons的远程代码执行漏洞(CVE-2018-1273)的分析。

0x01 环境搭建

使用的Spring Data Example Projects提供的示例代码。
但该项目中包含有15个子模块,如果将其整体导入IDEA,则需要下载所有模块中的依赖,显然这是没有必要的。通过参考这篇文章,发现只需导入web模块中的example模块即可复现该漏洞:
Spring RCE漏洞分析2(CVE-2018-1273)_第1张图片
因此可以通过修改example子模块中的pom.xml中的配置将其变为一个单独的SpringBoot项目:
1、首先修改标签如下:

<parent>
		<groupId>org.springframework.bootgroupId>
		<artifactId>spring-boot-starter-parentartifactId>
		<version>2.0.0.RELEASEversion>
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.bootgroupId>
		<artifactId>spring-boot-starterartifactId>
dependency>

这样就可以把example子模块作为一个独立的SpringBoot项目导入到IDEA了。另外由于测试代码一般需要引入junit包,为了方便,我在导入项目之前把\example\src下的test文件夹删除掉了。
3、导入完成后,发现程序中使用了lombok包,因此还需添加如下依赖:

<dependency>
		<groupId>org.projectlombokgroupId>
		<artifactId>lombokartifactId>
		<scope>providedscope>
dependency>

这时,就可以运行example.Application类中main方法启动项目了。

0x02 漏洞利用

1、首先浏览器访问http://127.0.0.1:8080/users地址,即出现如下界面:
Spring RCE漏洞分析2(CVE-2018-1273)_第2张图片
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方法处设置断点:
Spring RCE漏洞分析2(CVE-2018-1273)_第3张图片
调试过程中发现该方法会处理所有GET/POST请求的参数名与参数值,且当propertyName值为Controller类中处理该请求的方法的形参对应的类中属性名时,才会进入上图中的else分支。下面解释一下这句话:
(1)首先看example.users.web.UserController#register方法如下,可以看到该方法是处理请求地址为/users的POST请求,看到其有一个类型为UserForm的形参:
Spring RCE漏洞分析2(CVE-2018-1273)_第4张图片
(2)查看UserForm的代码,发现其有如下三个属性,因此只有propertyName值为username、password、repeatedPassword时,才会进入后面的else分支。
Spring RCE漏洞分析2(CVE-2018-1273)_第5张图片
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分支后,就看到了熟悉的代码:
Spring RCE漏洞分析2(CVE-2018-1273)_第6张图片
在这里插入图片描述
这里只截取了关键的代码,可以看到先是使用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集合中的数据:
Spring RCE漏洞分析2(CVE-2018-1273)_第7张图片
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方法中,有如下代码:
Spring RCE漏洞分析2(CVE-2018-1273)_第8张图片
这段代码很熟悉了,在前一篇分析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表达式中的异常:
Spring RCE漏洞分析2(CVE-2018-1273)_第9张图片
(2)当payload使用#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc.exe")时,在
org.springframework.expression.spel.ast.CompoundExpression#getValueRef方法中,可以看到nextNode类型为VariableReference:
Spring RCE漏洞分析2(CVE-2018-1273)_第10张图片
然后运行到org.springframework.expression.spel.ast.VariableReference#getValueInternal方法处,直接返回如下对象,进而通过反射的方式来利用Runtime类执行命令。
Spring RCE漏洞分析2(CVE-2018-1273)_第11张图片
这里只是通过对比两种payload的调用栈进行了简单分析,后面有时间的话再深入分析一下。到了这里,关于漏洞原理方面的问题大部分都解决了。

0x04 漏洞修复

修改SpringBoot的版本为2.0.1.RELEASE,其对应的Spring Data Commons版本为2.0.6.RELEASE,在该版本中漏洞已被修复。从下图可以看到修复方案与CVE-2018-1270中的修复方案相同,即使用SimpleEvaluationContext替换StandardEvaluationContext,该方案的原理在前一篇也分析了,就不赘述了。
Spring RCE漏洞分析2(CVE-2018-1273)_第12张图片

0x05 参考

  1. https://github.com/spring-projects/spring-data-examples
  2. https://xz.aliyun.com/t/2269

你可能感兴趣的:(java代码审计)