【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)

admin 2021年11月27日01:04:05评论196 views字数 9518阅读31分43秒阅读模式


上方蓝色字体关注我们,一起学安全!
作者:lz2y@Timeline Sec
本文字数:3220
阅读时长:8~10min
声明:仅供学习参考使用,请勿用作违法用途,否则后果自负


0x01 漏洞复现

《CVE-2021-30179:Apache Dubbo RCE复现》


0x02 调试准备

暂无


0x03 漏洞分析

Dubbo使用DecodeHandler#received方法来接收来自socket的连接,当接收到请求时会先调用DecodeHandler#decode方法处理请求,其将调用DecodeableRpcInvocation.java#decode方法处理数据


在138行通过方法名和desc获取服务端中的方法描述,如果为空则判断是否为泛类引用


【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)


判断方法名是否为$invoke或者$invokeAsync,desc是否为Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;,如果不满足则直接抛出异常


【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)


decode完成之后将调用HeaderExchangeHandler.java#received方法处理请求,若为泛型引用,则将调用GenericFilter#invoke方法


判断是否满足泛型引用条件,通过getArguments()方法获取参数,并取第一个参数为方法名,第二个参数为方法名的类型,第三个参数为args。通过反射寻找服务端提供的方法,如果没有找到则抛出异常。


【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)


接下来将通过获取请求中的generic参数来选择通过raw.return/nativejava/bean反序列化参数成pojo对象,这个CVE漏洞的入口就在这里了


【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)


我们依次分析一下触发点

1、设置generic为raw.return或者true,将调用PojoUtils#realize方法

PojoUtils.realize(args, params, method.getGenericParameterTypes());
// PojoUtils#realize public static Object[] realize(Object[] objs, Class<?>[] types, Type[] gtypes) { ... Object[] dests = new Object[objs.length]; for (int i = 0; i < objs.length; i++) { dests[i] = realize(objs[i], types[i], gtypes[i]); }


调用PojoUtils#realize

public static Object realize(Object pojo, Class<?> type, Type genericType) {        return realize0(pojo, type, genericType, new IdentityHashMap<Object, Object>());


调用PojoUtils#realize0,若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方法


最后利用反射执行method.invoke(dest, value);,就可以使用org.apache.xbean.propertyeditor.JndiConverter的setAsText发起JNDI注入了

private static Object realize0(Object pojo, Class<?> type, Type genericType, final Map<Object, Object> history) {      ...        if (pojo instanceof Map<?, ?> && type != null) {            Object className = ((Map<Object, Object>) pojo).get("class");            if (className instanceof String) {                try {                    type = ClassUtils.forName((String) className);                } catch (ClassNotFoundException e) {                    // ignore                }            }            ...            Map<Object, Object> map;            if (!type.isInterface() && !type.isAssignableFrom(pojo.getClass())) {                try {                    ...                } catch (Exception e) {                    map = (Map<Object, Object>) pojo;                }            }        if (Map.class.isAssignableFrom(type) || type == Object.class) {...}        else if (type.isInterface()) {...}        else {            Object dest = newInstance(type);                history.put(pojo, dest);                for (Map.Entry<Object, Object> entry : map.entrySet()) {                    Object key = entry.getKey();                    if (key instanceof String) {                        String name = (String) key;                        Object value = entry.getValue();                        if (value != null) {                            // 获取对应的set方法                            Method method = getSetterMethod(dest.getClass(), name, value.getClass());                            Field field = getField(dest.getClass(), name);                            if (method != null) {                                if (!method.isAccessible()) {                                    method.setAccessible(true);                                }                                Type ptype = method.getGenericParameterTypes()[0];                                value = realize0(value, method.getParameterTypes()[0], ptype, history);                                try {                                    // 反射执行方法                                    method.invoke(dest, value);                                } catch (Exception e) {...}                                else if (field != null) {...}                                        }


调用栈如下:

setAsText:59, AbstractConverter (org.apache.xbean.propertyeditor)invoke0:-1, NativeMethodAccessorImpl (sun.reflect)invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:497, Method (java.lang.reflect)realize0:483, PojoUtils (org.apache.dubbo.common.utils)realize:211, PojoUtils (org.apache.dubbo.common.utils)realize:99, PojoUtils (org.apache.dubbo.common.utils)invoke:91, GenericFilter (org.apache.dubbo.rpc.filter)invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)invoke:38, ClassLoaderFilter (org.apache.dubbo.rpc.filter)invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)invoke:41, EchoFilter (org.apache.dubbo.rpc.filter)invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)reply:145, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)received:152, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)received:177, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)received:51, DecodeHandler (org.apache.dubbo.remoting.transport)run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)runWorker:1142, ThreadPoolExecutor (java.util.concurrent)run:617, ThreadPoolExecutor$Worker (java.util.concurrent)run:745, Thread (java.lang)


2、设置generic为bean,将遍历args,如果args[i]为JavaBeanDescriptor的实例,则调用JavaBeanSerializeUtil#deserialize进行处理

JavaBeanSerializeUtil.deserialize((JavaBeanDescriptor) args[i]);
// JavaBeanSerializeUtil#deserialize public static Object deserialize(JavaBeanDescriptor beanDescriptor) { return deserialize( beanDescriptor, Thread.currentThread().getContextClassLoader()); }


调用

JavaBeanSerializeUtil#instantiateForDeserialize以获得对象,name2Class负责把 Class.forName 的返回值转换为 Class,而instantiate则实例化对象,最终返回一个JavaBeanDescriptor描述的对象   

private static Object instantiateForDeserialize(JavaBeanDescriptor beanDescriptor, ClassLoader loader,IdentityHashMap<JavaBeanDescriptor, Object> cache) {        ...        Object result;        if (beanDescriptor.isArrayType()) {...}        else {            try {                Class<?> cl = name2Class(loader, beanDescriptor.getClassName());                result = instantiate(cl);                cache.put(beanDescriptor, result);            } catch (Exception e) {                throw new RuntimeException(e.getMessage(), e);            }        }        return result;    }


接着调用JavaBeanSerializeUtil#deserializeInternal进行反序列化,如果beanDescriptor.isBeanType()(只需要实例化JavaBeanDescriptor时指定即可),则将遍历beanDescriptor,获取property及value,调用getSetterMethod获取对应的set方法


最后利用反射执行method.invoke(dest, value);,就可以使用org.apache.xbean.propertyeditor.JndiConverter的setAsText发起JNDI注入了

private static void deserializeInternal(Object result, JavaBeanDescriptor beanDescriptor, ClassLoader loader,IdentityHashMap<JavaBeanDescriptor, Object> cache) {        if (beanDescriptor.isEnumType() || beanDescriptor.isClassType() || beanDescriptor.isPrimitiveType()) {            return;        }        if(beanDescriptor.isArrayType()){...}        ...        else if (beanDescriptor.isBeanType()) {            for (Map.Entry<Object, Object> entry : beanDescriptor) {                String property = entry.getKey().toString();                Object value = entry.getValue();                if (value == null) {                    continue;                }                if (value instanceof JavaBeanDescriptor) {...}                // 获取对应的set方法                Method method = getSetterMethod(result.getClass(), property, value.getClass());                boolean setByMethod = false;                try {                    if (method != null) {                        // 反射执行方法                        method.invoke(result, value);                        setByMethod = true;                    }                }catch (Exception e) {                    ...                }        }


调用栈如下:

setAsText:59, AbstractConverter (org.apache.xbean.propertyeditor)invoke0:-1, NativeMethodAccessorImpl (sun.reflect)invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:497, Method (java.lang.reflect)deserializeInternal:282, JavaBeanSerializeUtil (org.apache.dubbo.common.beanutil)deserialize:215, JavaBeanSerializeUtil (org.apache.dubbo.common.beanutil)deserialize:204, JavaBeanSerializeUtil (org.apache.dubbo.common.beanutil)invoke:115, GenericFilter (org.apache.dubbo.rpc.filter)invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)invoke:38, ClassLoaderFilter (org.apache.dubbo.rpc.filter)invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)invoke:41, EchoFilter (org.apache.dubbo.rpc.filter)invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)reply:145, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)received:152, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)received:177, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)received:51, DecodeHandler (org.apache.dubbo.remoting.transport)run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)runWorker:1142, ThreadPoolExecutor (java.util.concurrent)run:617, ThreadPoolExecutor$Worker (java.util.concurrent)run:745, Thread (java.lang)


3、设置generic为nativejava,将遍历args,如果args[i]的类型为byte,以args[]为参实例化一个UnsafeByteArrayInputStream,再通过反射获得NativeJavaSerialization,再调用NativeJavaSerialization#readObject方法

if (byte[].class == args[i].getClass()) {    try (UnsafeByteArrayInputStream is = new UnsafeByteArrayInputStream((byte[]) args[i])) {        args[i] = ExtensionLoader.getExtensionLoader(Serialization.class).getExtension(GENERIC_SERIALIZATION_NATIVE_JAVA).deserialize(null, is).readObject();          } catch (Exception e) {     throw new RpcException("Deserialize argument [" + (i + 1) + "] failed.", e);    }}else{...}


NativeJavaSerialization#readObject调用inputStream的readObject()方法,相当于调用了UnsafeByteArrayInputStream的readObject()方法


由于UnsafeByteArrayInputStream没有readObject()故调用ObjectInputStream#readObject()方法进行反序列化,我们可以通过以此为入口触发CC4的gadget  

 public Object readObject() throws IOException, ClassNotFoundException {        return inputStream.readObject();    }


调用栈如下:

readObject:371, ObjectInputStream (java.io)readObject:50, NativeJavaObjectInput (org.apache.dubbo.common.serialize.nativejava)invoke:98, GenericFilter (org.apache.dubbo.rpc.filter)invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)invoke:38, ClassLoaderFilter (org.apache.dubbo.rpc.filter)invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)invoke:41, EchoFilter (org.apache.dubbo.rpc.filter)invoke:83, ProtocolFilterWrapper$1 (org.apache.dubbo.rpc.protocol)reply:145, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)received:152, DubboProtocol$1 (org.apache.dubbo.rpc.protocol.dubbo)received:177, HeaderExchangeHandler (org.apache.dubbo.remoting.exchange.support.header)received:51, DecodeHandler (org.apache.dubbo.remoting.transport)run:57, ChannelEventRunnable (org.apache.dubbo.remoting.transport.dispatcher)runWorker:1142, ThreadPoolExecutor (java.util.concurrent)run:617, ThreadPoolExecutor$Worker (java.util.concurrent)run:745, Thread (java.lang)


0x04 个人总结

个人认为CVE-2021-30179的主要思路就是Apache Dubbo在处理泛类引用时,提供了多种通过反序列化方式得到对象再生成pojo对象的选择。


在进行反序列化过程中没有做好防护,轻易相信用户提供的数据,直接将其进行反序列化操作,导致一些恶意对象的实例化以及相对应Gadget的触发,从而造成RCE。


在2.7.10中,使用了黑名单来阻断raw.return和bean这两条链

orgapachedubbocommonutilsPojoUtils.java#realize0


【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)


orgapachedubbocommonbeanutilJavaBeanSerializeUtil.java#name2Class


【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)


orgapachedubbocommonutilsSerializeClassChecker.java#validateClass


【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)


而nativejava则通过判断配置文件是否允许nativejava的反序列化


【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)


参考链接:

https://www.anquanke.com/post/id/197658


【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)

【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)
阅读原文看更多复现文章
Timeline Sec 团队
安全路上,与你并肩前行





本文始发于微信公众号(Timeline Sec):【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年11月27日01:04:05
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   【漏洞分析】Dubbo Pre-auth RCE(CVE-2021-30179)http://cn-sec.com/archives/442512.html

发表评论

匿名网友 填写信息