前段時(shí)間有篇文章[^1]披露了開(kāi)源項(xiàng)目 Spring Cloud Gateway 的一個(gè)遠(yuǎn)程代碼執(zhí)行漏洞,編號(hào)為 CVE-2022-22947。
受影響版本
根據(jù) VMWare 和 Spring 的官方公告[^2][^3],受影響的版本為:
3.1.0
3.0.0 到 3.0.6
舊的不受支持的版本也受影響
修復(fù)方案
修復(fù)方案有:
3.1.x 版本用戶應(yīng)升級(jí)到 3.1.1+ 版本,3.0.x 版本用戶應(yīng)升級(jí)到 3.0.7+ 版本。
在不影響業(yè)務(wù)的前提下,通過(guò)將配置選項(xiàng) management.endpoint.gateway.enabled 設(shè)置為 false 禁用 gateway actuator endpoint。
檢測(cè)思路
流量檢測(cè):分析 HTTP 流量,檢測(cè)是否存在異常訪問(wèn) actuator gateway API 的請(qǐng)求。
主機(jī)端:
靜態(tài)檢測(cè):通過(guò)對(duì)比修復(fù)前后 ShortcutConfigurable.class 文件的區(qū)別指定特征碼,根據(jù)特征碼編寫(xiě) yara 規(guī)則,以查找服務(wù)器上是否存在受影響版本的 spring-cloud-gateway jar 包。 動(dòng)態(tài)檢測(cè):查找服務(wù)器上正在運(yùn)行的 Java 進(jìn)程,檢測(cè)其是否加載了 spring-cloud-gateway jar 包。
漏洞分析
目前已公開(kāi)的漏洞分析文章都在分析 3.x 版本,為了確認(rèn) 2.x 版本也受影響,本文對(duì) 2018 年發(fā)布的 Finchley.RELEASE 版本進(jìn)行了分析,Spring Cloud Gateway 的版本為 2.0.0.RELEASE。
環(huán)境搭建
演示項(xiàng)目代碼已上傳到 GitHub 倉(cāng)庫(kù)。
項(xiàng)目中,通過(guò)配置文件定義了一個(gè)路由。啟動(dòng)項(xiàng)目后,訪問(wèn) http://localhost:8080/ip,如果一切正常,則會(huì)得到以下結(jié)果:
利用方法
以 POST 方法請(qǐng)求 /actuator/gateway/routes/pentest,并提交以下數(shù)據(jù),用于創(chuàng)建一條惡意路由:
{ "id": "pentest", "filters": [ { "name": "AddResponseHeader", "args": { "name": "X-Request-Foo", "": "#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(getRuntime().exec(new String[]{\"wh\"}).getInputStream()))}" }, "uri": "http://httpbin.org/get", "predicates": [ { "name": "Method", "args": { "_key_0": "GET" } }, { "name": "Path", "args": { "_key_0": "/pentest" } } ] } ]}
id 字段指定新路由的名稱,必須全局唯一。
filters 字段給這條路由指定若干個(gè)過(guò)濾器。過(guò)濾器用于對(duì)請(qǐng)求和響應(yīng)進(jìn)行修改。
name 字段指定要添加的過(guò)濾器,這里添加了一個(gè) AddResponseHeader 過(guò)濾器,用于 gateway 給客戶端返回響應(yīng)之前添加一個(gè)響應(yīng)頭。
args.name 字段指定要添加的響應(yīng)頭。
args.value 字段指定響應(yīng)頭的值。這里的值是要執(zhí)行的 SpEL 表達(dá)式,用于執(zhí)行 whoami 命令。注意需要將命令輸出結(jié)尾的換行符去掉,否則過(guò)濾器執(zhí)行時(shí)會(huì)拋出異常說(shuō)「響應(yīng)頭的值不能以 \r 或 \n 結(jié)尾」。
uri 字段指定將客戶端請(qǐng)求轉(zhuǎn)發(fā)到 http://httpbin.org/get。
predicates 字段指定匹配此路由的條件。這里指定了兩個(gè)條件,一個(gè)是請(qǐng)求的方法為 GET,一個(gè)是請(qǐng)求的 URI 為 /pentest。
有關(guān)其它 actuator gateway 的 API,可查看官方文檔[^7]。
接著以 POST 方法請(qǐng)求 /actuator/gateway/refresh ,用于刷新路由,使剛添加的惡意路由生效。
最后以 GET 方法請(qǐng)求 /pentest,觸發(fā)惡意路由。在響應(yīng)中可以看到過(guò)濾器添加的響應(yīng)頭:
修復(fù)方案分析
代碼修復(fù)方案
首先在官方倉(cāng)庫(kù)中查看為了修復(fù)漏洞的 commit[^4]:
在 ShortcutConfigurable 接口中的 getValue 方法中,使用自定義的 Gatewayeva luationContext 類替換了原來(lái)的 Standardeva luationContext 類。查看 Gatewayeva luationContext 類的實(shí)現(xiàn)可知,其是對(duì) Simpleeva luationContext 類的簡(jiǎn)單封裝。
通過(guò)查詢文檔可知,Standardeva luationContext 和 Simpleeva luationContext 都類是執(zhí)行 Spring 的 SpEL 表達(dá)式的接口,區(qū)別在于前者支持 SpEL 表達(dá)式的全部特性,后者相當(dāng)于一個(gè)沙盒,限制了很多功能,如對(duì) Java 類的引用等[^5][^6]。因此通過(guò)將 Standardeva luationContext 類替換為 Gatewayeva luationContext 類,可以限制執(zhí)行注入的 SpEL 表達(dá)式。
禁用 actuator gateway
通過(guò)前面的漏洞利用過(guò)程可以看到,首先需要通過(guò) /actuator/gateway/routes/{id} API 創(chuàng)建一條路由。因此將此 API 禁止,也可實(shí)現(xiàn)漏洞的修復(fù)。根據(jù) Actuator 的 API 文檔[^7]可知,啟用 actuator gateway 需要設(shè)置以下兩個(gè)配置的值:
management.endpoint.gateway.enabled=true # default valuemanagement.endpoints.web.exposure.include=gateway
因此只要這兩個(gè)選項(xiàng)不同時(shí)滿足,就不會(huì)啟用 actuator gateway。
漏洞分析思路
以 ShortcutConfigurable 接口開(kāi)始,通過(guò) IntelliJ IDEA 可以看到,大多數(shù)內(nèi)置過(guò)濾器都繼承了 ShortcutConfigurable 接口。其次,RouteDefinitionRouteLocator 類(org/springframework/cloud/gateway/route/RouteDefinitionRouteLocator.class)的私有方法 loadGatewayFilters 中調(diào)用了 ShortcutConfigurable 接口的 normalize 方法:
通過(guò)簡(jiǎn)單的回溯,RouteDefinitionRouteLocator 類的公有方法 getRoutes 最終會(huì)調(diào)用 loadGatewayFilters 方法,調(diào)用鏈為:
loadGatewayFilters() -> getFilters() -> convertToRoute() -> getRoutes()
因此 /actuator/gateway/routes 這個(gè) URI 也會(huì)觸發(fā) SpEL 表達(dá)式的執(zhí)行。
再仔細(xì)看下 loadGatewayFilters 方法的關(guān)鍵功能:
參數(shù) id 為路由的名稱,也就是定義路由時(shí)參數(shù) id 的值。參數(shù) filterDefinitions 為該路由中定義的過(guò)濾器對(duì)象數(shù)組。
方法遍歷過(guò)濾器對(duì)象數(shù)組:
檢查指定的過(guò)濾器是否存在。不存在則拋出異常 Unable to find GatewayFilterFactory with name。
存在時(shí),獲取過(guò)濾器的參數(shù),并打印 debug 日志 RouteDefinition {id} applying filter {args} to {filter}。
調(diào)用 normalize 方法,如果參數(shù)的值是 SpEL 表達(dá)式則執(zhí)行,不是則直接返回。
使用處理后的參數(shù)創(chuàng)建配置對(duì)象,然后使用過(guò)濾器工廠創(chuàng)建過(guò)濾器實(shí)例并保存到數(shù)組中。
2.x 與 3.x 版本的區(qū)別
在產(chǎn)生漏洞的核心點(diǎn)上,二者沒(méi)有區(qū)別,都是 ShortcutConfigurable 接口的 getValue 方法中使用了 Standardeva luationContext 類來(lái)執(zhí)行 SpEL 表達(dá)式。
第一個(gè)區(qū)別在于,2.x 版本在刷新路由后需要額外一次請(qǐng)求才能觸發(fā) SpEL 表達(dá)式的執(zhí)行。而 3.x 版本在刷新路由后會(huì)立即執(zhí)行。
第二個(gè)區(qū)別在于對(duì)此方法的調(diào)用鏈。通過(guò)查找源代碼可知,只有 ConfigurationService 類的內(nèi)部類 ConfigurableBuilder 的 normalizeProperties 方法(重寫(xiě)了父類中的方法)中調(diào)用了 normalize 方法。而 ConfigurableBuilder 類繼承自內(nèi)部抽象類 AbstractBuilder。AbstractBuilder 類中有一公有方法 bind 調(diào)用了 normalizeProperties 方法。
繼續(xù)跟進(jìn)對(duì) bind 方法的引用,可知有三處:
-
AbstractRateLimiter 類的 onApplicationEvent 方法。
RouteDefinitionRouteLocator 類的 loadGatewayFilters 方法和 lookup 方法。
然后繼續(xù)回溯可以知道所有有可能觸發(fā) SpEL 表達(dá)式執(zhí)行的地方。