Spring Framework 历史漏洞研究

本文的主要目标是分析、总结、归纳历史上出现过的 Spring 框架漏洞,从而尝试找出其中的潜在模式,以达到温故知新的目的。当然作为一个 Java 新手,在直接分析漏洞之前,还是会先从开发者的角度去学习 Spring 中的一些核心概念,从而为后续的理解奠定基础。

前言

由于早期 J2EE 规范过于复杂且不采纳社区的建议,Spring 于 2003 年最初作为 J2EE 的竞争者而诞生了。随着时代的发展,Spring 与 JavaEE 形成了一种互补的关系,不拥抱 JavaEE 的平台规范,但是从其中精心挑选了个别规范进行整合,比如 Servlet、JPA、JMS 等。

当我们在谈论 Spring 的时候,可以是指代 Spring 框架项目本身,也可以是指代基于 Spring 框架之上的整个项目家族,即 Spring 全家桶,在后文介绍具体漏洞的时候也可以看到更多漏洞出现在框架之上的项目中。

Spring Framework

Spring 框架是一个功能强大的 Java 应用程序框架,旨在提供高效且可扩展的开发环境。其本身也是模块化的,应用程序可以选择所需要的模块。比如包含核心容器、配置模型和依赖注入机制的模块 spring-core,脚本引擎 SpEL,以及基于 Servlet 的 Spring MVC Web 和 Spring WebFlux 框架等。

除了 Spring 框架本身,还有其他项目,如 Spring Boot、Spring Security、Spring Data、Spring Cloud、Spring Batch 等,每个项目都有自己的源代码仓库和版本管理。完整的项目列表可以参考 spring.io/projects。本节主要介绍 Spring Framework 本身的核心概念以及历史漏洞,其他项目会在下面的章节中单独介绍。

注: 关于 Spring Framework 的详细介绍建议直接参考官方的文档 Spring Framework Documentation,这里只是摘录了一些笔者自以为的关键内容,仅作为补充理解。

凡介绍 Spring 框架的文章都离不开 IoC,其全称为 Inversion of Control,即控制反转。看了网上很多文章,都说 IoC/依赖注入就是将 XML 配置文件中的 Bean 通过反射构造成实例的过程,却没有解释这么做的目的。

要理解 IoC 就需要先理解依赖注入(Dependency Injection,简称 DI)。在 Java 中,依赖注入本质上是一种设计模式。依赖注入的基本原则是应用组件不应该负责查找资源或者其他依赖的协作对象,配置对象的工作应该由容器负责,查找资源的逻辑应该从应用组件的代码中抽取出来,交给 DI 容器来完成。

举例来说,假设类 A 需要用到接口 B 的方法,那么就需要为类 A 和接口 B 建立关联,即依赖关系。最原始的方式是在 A 类中创建一个 B 接口的实例,但这种方法需要开发者手动维护二者的依赖关系,一旦依赖关系发生变动就需要对代码进行重构和修改。依赖注入就是为了解决这个问题而诞生的。通过依赖注入,二者的依赖关系由 DI 容器进行管理,当依赖关系变化时候只需要修改容器的配置文件即可。

简而言之,如果一个 Bean 是另一个 Bean 的依赖,这通常意味着一个 Bean 被设置为另一个 Bean 的一个属性。

在 Spring 中构成用户应用程序的骨干且由 Spring IoC 容器管理的对象称为 Bean。其结构和 Java Bean 类似,但生命周期由 Spring IoC 容器管理,Spring Bean 以及它们之间的依赖关系都反映在容器使用的配置元数据之中,配置元数据可以来自 XML 配置文件或者 Java 注解。

org.springframework.beansorg.springframework.context 包是 Spring Framework 的 IoC 容器的基础。其中 BeanFactory 接口提供了一种配置机制,能够管理任何类型的对象。

ApplicationContext 是 BeanFactory 的一个子接口,代表 Spring IoC 容器,负责实例化、配置和组装 bean。该接口有几个常见的内置实现,比如 ClassPathXmlApplicationContextFileSystemXmlApplicationContext

Spring 框架核心功能之一就是 IoC 容器,而对于开发者而言,需要的只是告诉容器哪些 bean 需要被管理,这个过程通常也是 Spring 的配置过程。Spring 中提供了两种配置 IoC 容器的方式,分别是基于 XML 的配置和基于注解的配置。

基于 XML 的配置示例如下:

<bean id="userService" class="com.example.UserService">
   <property name="userDao" ref="userDao"/>
</bean>

<bean id="userDao" class="com.example.UserDao">
</bean>

Spring 读取该配置文件后会通过 IoC 容器创建对应的 POJO 对象(Bean),管理其生命周期并将对应依赖进行动态注入。

使用注解方式也可以达到类似的效果,如下所示:

@Component
public class UserService {

    @Autowired
    private UserDao userDao;

    // Required setters and methods
}

两种方式各有利弊,且二者 Spring 都提供了大量的细化配置方法,比如设置作用域(Scope),工厂类和构造函数调用规则等,详细配置方法可以参考官方的文档。

Spring Framework 作为一个 Web 框架,其核心功能自然要包含传统的 Web MVC 功能。前面说过,Spring 框架是以模块划分的,而该 MVC 框架自一开始就是 Spring 中的核心模块,其代码在 github.com/spring-projects/spring-framework 中的 spring-webmvc 子目录中,其正式名称为 “Spring Web MVC”,通常也被称为 Spring MVC。注意这只是 Spring Framework 中的一个模块,而并不是单独的项目。

在前文 Java 安全研究初探 中介绍过,Servlet 是 Java EE Web 服务器中的重要标准之一,满足 Servlet 规范的应用便可以被标准的 Servlet 容器加载执行。对于 Spring MVC 而言同样拥抱了这个标准,其中重要的 Servlet 即为 DispatcherServlet。从 6.0 的源码中看,DispatcherServlet 的继承链为:

DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet

与所有其他 Servlet 一样,我们可以通过 web.xml 或者 ServletContext 接口来注册和初始化 DispatcherServlet,并通过添加控制器来实现 Web 请求处理。Spring MVC 提供了一个基于注解的编程模型,其中 @Controller@RestController 组件使用注解来表达请求映射、请求输入、异常处理等内容。注解的控制器具有灵活的方法签名,不需要继承基类,也不需要实现特定的接口。下面是一个简单的例子:

@Controller
public class HelloController {

    @GetMapping("/hello")
    public String handle(Model model) {
        model.addAttribute("message", "Hello World!");
        return "index";
    }
}

上述控制器示例接受一个 Model 实例,并返回一个字符串,表示视图名称;实际上可以支持各种灵活的函数签名,Spring 框架会对其进行合适的解析。一个更常见的示例如下:

@RestController
@RequestMapping("/persons")
class PersonController {

    @GetMapping("/{id}")
    public Person getPerson(@PathVariable Long id) {
        return PersonDao.getById(id);
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public void add(@RequestBody Person person) {
        // ...
    }
}

@RequestMapping 表示类层面的共享映射,而 @RestController 可以认为是 @Controller@ResponseBody 的组合。@ResponseBody 表示 controller 函数的返回直接绑定到 Web 请求的返回 body 中。

Controller 函数中可以通过返回 DeferredResultCallable 来实现单个异步结果的返回,或者通过 ResponseBodyEmitterSseEmitter 来实现多个异步结果的返回,最近比较火的 ChatGPT 流式响应就是基于 SSE (Server-Sent Events) 实现的。

Spring MVC 中对于请求和响应提供了灵活的处理接口,这里只是对其基本功能有个大致理解,后面介绍具体漏洞的时候再展开介绍。

Spring WebFlux 是一个与 Spring MVC 经常相提并论的一个新兴 Web 框架,在 Spring Framework 5.0 中引入,模块名称为 spring-webflux。这是一个基于 reactive (响应式) 技术栈的 Web 框架,具有完全的异步支持,支持 Reactive Streams 背压等。

背压: 专业术语,指消费者可以告诉生产者自己需要多少的量,避免生成速率高于消费速率造成的缓冲溢出或者数据丢失。

Spring WebFlux 的使用与 Spring MVC 类似,最大的不同点在于 WebFlux 支持模型中的响应式类型,如 Mono<User> 或者 io.reactivex.Single<User>,下面是一个简单的示例:

@RestController
public class UserController {
    private final UserRepository userRepository;

    public UserController(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @GetMapping("/users")
    public Flux<User> getAllUsers() {
        return userRepository.findAll();
    }

    @GetMapping("/users/{id}")
    public Mono<User> getUserById(@PathVariable String id) {
        return userRepository.findById(id);
    }
}

Spring MVC 本来就支持异步非阻塞,那为什么还要引入 WebFlux 呢?一个主要原因是性能,完全异步的处理可以减少并发的线程数量。而且引入异步 API 可以充分利用 Java 8 中引入的 lambda 表达式特性实现函数式编程,允许异步逻辑的声明式组合,如下所示:

@PostMapping("/owners/{ownerId}/pets/{petId}/edit")
public Mono<String> processSubmit(@Valid @ModelAttribute("pet") Mono<Pet> petMono) {
    return petMono
        .flatMap(pet -> {
            // ...
        })
        .onErrorResume(ex -> {
            // ...
        });
}

SpEL 即为 Spring 表达式语言,其语法和标准的 JUEL 类似,但提供了额外的功能,如方法调用和字符串模版等丰富的功能。SpEL 是 Spring 框架中的核心部分之一,在 Spring 框架的 XML 配置、注解、MVC 控制器中都有所应用。

在 XML 中使用 SpEL 设置 bean 的属性:

<bean id="numberGuess" class="org.spring.samples.NumberGuess">
    <property name="randomNumber" value="#{ T(java.lang.Math).random() * 100.0 }"/>

    <!-- other properties -->
</bean>

在注解中使用 SpEL 设置字段默认值:

public class FieldValueTestBean {

    @Value("#{ systemProperties['user.region'] }")
    private String defaultLocale;

    // ...
}

SpEL 虽然出自 Spring,但也可以独立使用,例如:

ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("new String('hello world').toUpperCase()"); (1)
String message = exp.getValue(String.class);

值得一提的是,Expression.getValue 方法有多个重载,其中部分重载接受一个额外的 EvaluationContext 类型参数。EvaluationContext 接口在评估表达式以解析属性、方法或字段时使用,并帮助执行类型转换。Spring提供了两种实现,分别是:

  1. StandardEvaluationContext: 支持 SpEL 语言的所有功能和配置选项,这是默认值;
  2. SimpleEvaluationContext: 仅提供 SpEL 语言的基本功能和配置选项的子集,适用于不需要 SpEL 语言全部功能,且对表达式有限制的情况;

一般来说,使用默认的 StandardEvaluationContext 且表达式用户可控的情况下会导致任意的 Java 代码执行,此时通常会用 SimpleEvaluationContext 来限制表达式只能访问有限的属性。后文可以看到类似的漏洞。

历史漏洞

本节记录和分析一些历史上出现过的 Spring Framework 的漏洞。由于 Spring Framework 本身是 Spring 元宇宙的根基,因此一旦出现漏洞影响就非常广泛,值得我们重点关注。由于篇幅原因,这里的分析不会太过深入,并尽可能给出已有的优秀分析文章链接。

Spring MVC 允许开发者将业务对象 (Bean) 绑定到 HTML 表单中,并通过请求对其进行修改,例如下述请求:

POST /adduser HTTP/1.0
...
firstName=Tavis

如果绑定了业务对象 User,那么该请求背后最终会执行 user.firstName = "Tavis",这是基于 JavaBean 的接口去实现的。Spring 还支持使用复杂的设置方式,比如 user.address.street=Test 会转换为:

frmObj.getUser().getAddress().setStreet("Test")  

由于 Bean 类默认继承自 Object,且内省的时候没有过滤掉父类的属性,因此攻击者可以通过 class.classLoader 的方式调用 frmObj.getClass().getClassLoader() 从而进一步修改 ClassLoader 中的属性,导致任意代码执行。最初作者的建议是通过 Introspector.getBeanInfo(Person.class, Object.class) 指定 stopClass 的方式防止该问题,但 Spring 实际上通过黑名单的方式进行了修复,这也为后续的绕过埋下伏笔。

详细分析可以参考:

Spring MVC 中可以将请求的数据绑定到 Bean 对象中,请求数据可以是表单、XML、JSON 等,该漏洞就是通过 XML 请求绑定到 Bean 对象时解析 XML 外部实体导致的 XXE 注入问题。示例 Controller 如下:

@Controller
public class HomeController {
    @RequestMapping(value="/home", method=RequestMethod.POST, consumes="application/xml")
    public ModelAndView home(@RequestBody User user) {
        //System.out.println(user);
        return new ModelAndView("home", "message", user.getUserName());
    }
} 

User 定义如下:

import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "user")
public class User {
   private String userID;
   private String userName;
   @XmlElement
   public String getUserID() { return userID; }
   public void setUserID(String userID) { this.userID = userID; }
   @XmlElement
   public String getUserName() { return userName; }
   public void setUserName(String userName) { this.userName =userName; }
} 

这类漏洞的修复方式取决于使用的 XML API:

  • DOM: 使用 DocumentBuilderFactory.setExpandEntityReferences(false); 禁用外部实体;
  • SAX: 使用 XMLReader.setFeature("http://xml.org/sax/features/external-general-entities", false); 禁用外部实体;
  • StAX: XMLInputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
  • Spring OXM: 类似 DOM,并使用 Jaxb2Marshaller 指定 DOMSource;

针对这个漏洞,关键的修复代码如下:

--- spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java	(revision 2843b7d2ee12e3f9c458f6f816befd21b402e3b9)
+++ spring-web/src/main/java/org/springframework/http/converter/xml/Jaxb2CollectionHttpMessageConverter.java	(revision )
@@ -226,7 +226,9 @@
 	 * @return the created factory
 	 */
 	protected XMLInputFactory createXmlInputFactory() {
-		return XMLInputFactory.newInstance();
+		XMLInputFactory inputFactory = XMLInputFactory.newInstance();
+		inputFactory.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
+		return inputFactory;
 	}
 
 }

详细信息可以参考:

另外还有几个类似的外部实体注入漏洞,如 CVE-2013-7315、CVE-2013-6429,另外由于上述修复不完全导致的绕过 CVE-2014-0054,因为上面只修了 Jaxb2CollectionHttpMessageConverter,没有修复 Jaxb2RootElementHttpMessageConverter。总的来说还是要仔细审计 XML 解析处理相关的功能,个人认为这也是由于 XXE 功能没有默认禁用导致的一大问题。

Spring MVC 虽然作为 MVC 框架,但同时也支持 Web 服务器中常见的功能,比如静态资源访问。例如我们可以通过配置 XML 去设定需要访问的静态资源,下面是一个简单的 spring-web-config.xml 示例:

 <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
     <property name="prefix" value="/WEB-INF/views/" />
     <property name="suffix" value=".jsp" />
 </bean>
<mvc:resources mapping="/resources/**" location="/resources/">

其中 <mvc:resources> 标签表示将 URL 路径 /resources 的请求映射到静态资源,路径为 Web 应用根目录的相对路径 /resources 下的文件。

或者也可以通过 Java 代码添加,参考 spring-showcase:

// org.springframework.samples.mvc.config.WebMvcConfig
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
}

该漏洞的 POC 比较简单,见下述 demo 代码:

$ git clone https://github.com/ilmila/springcss-cve-2014-3625/ && cd springcss-cve-2014-3625/
$ mvn jetty:run
$ curl 'http://localhost:8080/spring-css/resources/file:/etc/passwd'
root:x:0:0:root:/root:/bin/bash
...

漏洞成因出在静态资源解析的地方,调用链路为:

org.springframework.web.servlet.resource.ResourceHttpRequestHandler#handleRequest
org.springframework.web.servlet.resource.ResourceHttpRequestHandler#getResource
org.springframework.web.servlet.resource.PathResourceResolver#getResource
org.springframework.core.io.Resource#createRelative
org.springframework.core.io.PathResource#createRelative

相关代码如下 (v4.1.1.RELEASE):

  protected Resource getResource(HttpServletRequest request) throws IOException{
    String path = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
    if (path == null) {
      throw new IllegalStateException("Required request attribute '" +
          HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE + "' is not set");
    }
    if (!StringUtils.hasText(path) || isInvalidPath(path)) {
      if (logger.isTraceEnabled()) {
        logger.trace("Ignoring invalid resource path [" + path + "]");
      }
      return null;
    }
    Resource resource = resolveChain.resolveResource(request, path, getLocations());
    if (resource == null || getResourceTransformers().isEmpty()) {
      return resource;
    }
    ResourceTransformerChain transformChain = new DefaultResourceTransformerChain(resolveChain, getResourceTransformers());
    resource = transformChain.transform(request, resource);
    return resource;
  }
  protected boolean isInvalidPath(String path) {
    return (path.contains("WEB-INF") || path.contains("META-INF") || StringUtils.cleanPath(path).startsWith(".."));
  }

问题在于 Resource#createRelative 返回的结果是 file:/etc/passwd,这就导致了子目录的限制实现穿越。该漏洞的修复经历了多个 commit,详情可以参考下面的链接:

从这个漏洞引申出来的另外一个漏洞是 CVE-2018-1271,即 Spring MVC Windows 下任意文件读取,因为路径过滤的时候仅考虑了 ../,没有考虑 Windows 中 \ 的情况,详情可以参考:

该漏洞是 Spring Messaging 远程代码执行漏洞。Spring Messaging 也是 Spring 框架中的一个可选模块,前面并没介绍,其代码模块目录为 spring-messaging。主要提供应用间的消息传递机制,最典型的就是 Websocket 功能,以及在 Websocket 之上的 STOMP (Simple Text Oriented Messaging Protocol) 协议,此外还提供了针对不支持 Websocket 情况下的模拟替代方案 SockJS 等。

Messaging 主要针对全双工的异步传输,因此也采用了常见的发布/订阅机制。在多个客户端可以订阅同一个主题等待消息,以实现类似聊天室的效果。

回到该漏洞,其 PoC 如下,主要功能是订阅 /topic/greetings 时添加了自定义 selector 头:

function connect() {
    var header  = {"selector":"T(java.lang.Runtime).getRuntime().exec('calc.exe')"};
    var socket = new SockJS('/gs-guide-websocket');
    stompClient = Stomp.over(socket);
    stompClient.connect({}, function (frame) {
        setConnected(true);
        console.log('Connected: ' + frame);
        stompClient.subscribe('/topic/greetings', function (greeting) {
            showGreeting(JSON.parse(greeting.body).content);
        },header);
    });
}

该 selector 在服务端会被取出并当成 SpEL 表达式执行,如下:

// org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java
  private MultiValueMap<String, String> filterSubscriptions(
      MultiValueMap<String, String> allMatches, Message<?> message) {
    EvaluationContext context = null;
    for (String sessionId : allMatches.keySet()) {
      for (String subId : allMatches.get(sessionId)) {
        // ...
        Expression expression = sub.getSelectorExpression();
        if (context == null) {
          context = new StandardEvaluationContext(message);
          context.getPropertyAccessors().add(new SimpMessageHeaderPropertyAccessor());
        }
        // ..
        if (Boolean.TRUE.equals(expression.getValue(context, Boolean.class))) {
        result.add(sessionId, subId);
        }
      }
    }
    // ...

版本: 5.0.4.RELEASE

前面说过,expression.getValue 会执行对应的表达式,而如果表达式的内容可控,就可能会导致任意代码执行。除了这里,在该类的许多其他地方也有类似的问题,因此这一类问题的修复方案是将执行上下文即 EvaluationContext 设置为了 SimpleEvaluationContext,限制表达式的行为。具体的 patch 可以参考下面的链接:

从这个漏洞中可以看出,Spring 框架内有许多地方使用了 SpEL 来实现动态过滤、属性访问等功能,如果不加限制就可能造成非预期的危害。

这是一个 Spring Framework 的 RFD 漏洞。RFD,全称为 Reflected File Download,是在 Blackhat EU 2014 提出的一种新型 Web 攻击手法。其主要攻击思路是通过某些 Web 接口返回用户请求的特性,结合浏览器自动下载文件以及在 URL 中伪造文件后缀的技巧,实现针对目标域名的文件下载功能。例如,下面的域名通过路径参数 ; 添加额外的后缀,让浏览器自动识别下载文件为 ChromeSetup.bat,并且 HTTP 的响应中包含 GET 参数 q 的内容,通过 Windows 命令的 || 特性绕过前面的无用数据最终执行 calc.exe

https://www.google.com/s;/ChromeSetup.bat?q=||calc

这只是一个粗略的介绍,关于 RFD 漏洞的细节可以参考后文中原作者的白皮书。

回到 Spring,这个漏洞导致 RFD 的原因是通过 Spring 的接口返回文件时,如果用户可以控制返回的部分文件名,就能实现 RFD 的效果。具体来说,是在使用 org.springframework.http.ContentDisposition 类返回的 Content-Disposition 头时,用户通过控制恶意的文件名影响该返回头。主要受影响的下面两个接口:

ContentDisposition.Builder#filename(String)
ContentDisposition.Builder#filename(String, US_ASCII)

这个漏洞即近期比较知名的 Spring4Shell 漏洞,但从漏洞原理来说,其核心其实是前文介绍过的 CVE-2010-1622 的一个绕过。前面说过,在修复 CVE-2010-1622 时,开发者使用了黑名单的方式,限制 Bean 属性名称不能为 classLoader 或者 protectionDomain,这在当时确实是解决了问题。但随着 Java 9 的推出,又引入了一些新的属性,比如 module,因此可以通过这个属性去绕过上面的限制再次对 classLoader 进行修改从而实现 RCE。

不过,该漏洞的 PoC 实际是利用 Tomcat 日志写文件的特性去实现的 webshell 写入,因为直接修改 classLoader 的方式有较多限制。网上虽然有很多分析文章,但大多数只是对着 PoC 单步调试,并没有提及该漏洞的核心。笔者认为,该漏洞的核心还是在于 Spring 对于 JavaBean 的内省 API 使用不当,即 Introspector.getBeanInfo 没有指定 stopClass 导致,不过看 Spring 的修复方式,还是想要在这条缝缝补补的路走到黑了。

该漏洞的补丁分了几个 commit,首先是第一次补丁:

diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java
index 8332045197..bd234eb58f 100644
--- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java
+++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java
@@ -22,6 +22,7 @@ import java.beans.Introspector;
 import java.beans.PropertyDescriptor;
 import java.lang.reflect.Method;
 import java.lang.reflect.Modifier;
+import java.net.URL;
 import java.security.ProtectionDomain;
 import java.util.Collections;
 import java.util.HashSet;
@@ -292,10 +293,12 @@ public final class CachedIntrospectionResults {
                                        // Only allow all name variants of Class properties
                                        continue;
                                }
-                               if (pd.getWriteMethod() == null && pd.getPropertyType() != null &&
-                                               (ClassLoader.class.isAssignableFrom(pd.getPropertyType()) ||
-                                                               ProtectionDomain.class.isAssignableFrom(pd.getPropertyType()))) {
-                                       // Ignore ClassLoader and ProtectionDomain read-only properties - no need to bind to those
+                               if (URL.class == beanClass && "content".equals(pd.getName())) {
+                                       // Only allow URL attribute introspection, not content resolution
+                                       continue;
+                               }
+                               if (pd.getWriteMethod() == null && isInvalidReadOnlyPropertyType(pd.getPropertyType())) {
+                                       // Ignore read-only properties such as ClassLoader - no need to bind to those
                                        continue;
                                }

限制了内省的获取属性的类型。黑名单的属性类型包括:

  • 属性名为 content 且类型为 URL
  • 没有 setter 的属性,且类型为黑名单,包括:
    • AutoCloseable
    • ClassLoader
    • ProtectionDomain

然后过了两周又再次增强了一下:

diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java
index 7b7a67d91c..4187097ce3 100644
--- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java
+++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java
@@ -286,9 +287,13 @@ public final class CachedIntrospectionResults {
                        // This call is slow so we do it once.
                        PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
                        for (PropertyDescriptor pd : pds) {
-                               if (Class.class == beanClass &&
-                                               ("classLoader".equals(pd.getName()) ||  "protectionDomain".equals(pd.getName()))) {
-                                       // Ignore Class.getClassLoader() and getProtectionDomain() methods - nobody needs to bind to those
+                               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;
                                }

Class 类进行了进一步的限制,只允许获取其中 name 相关的属性。虽然满满的补丁感,但是有用!

后续的代码更新把 classLoader 等黑名单属性放到了 isInvalidReadOnlyPropertyType 之中,也就是现在我们看到的样子。

参考文章:

小结

本节简单学习了 Spring Framework 本身的一些特性和功能,并在此基础上随意选择了几个相关的漏洞进行分析,以尝试对 Spring 框架的整体结构、历史漏洞和代码质量有个初步认识。从中可以发现,Spring Framework 作为流行 Web 框架的基石,历史上出现过的严重漏洞并不多,RCE 类漏洞更多出现在 SpEL 中,MVC 框架里的 JavaBean 绑定也可以算是一个独特的攻击面。作为安全研究者,一方面可以对这些历史攻击面进行深入分析,以发掘新的绕过方式;另一方面也可以寻找一些新的攻击面,后者往往能造成更大的影响效果。

值得一提的是,受限于笔者的认知水平,这里介绍的漏洞并不完整,更多 Spring Framework 的漏洞可以参考官方的公告:

同时,对于想要深入 Spring 框架内部以寻找新攻击面的研究者来说,仔细阅读其官方文档并结合源码进行调试分析或许是个更好的选择:

Spring 项目除了 Framework 本身,还有许多丰富的项目,如 Spring Boot、Spring Data、Spring Security 等,每个项目都有独立的代码仓库,这与 Spring Framework 的模块有所不同。项目列表可以参考:

后续有时间会继续对一些主流的 Spring Project 进行研究,结合整体的 Spring 生态也能对局部的设计有更深的理解。


版权声明: 自由转载-非商用-非衍生-保持署名 (CC 4.0 BY-SA)
原文地址: https://evilpan.com/2023/04/21/spring-framework-bugs/
微信订阅: 有价值炮灰
TO BE CONTINUED.