本文首发于先知社区:https://xz.aliyun.com/t/11087

去年weblogic出白名单时研究了下怎么绕过,总结出了下面的思路,本想再找找有无新的攻击面的思路,但是找了几次都没找到,后来就搁置了。昨天看见https://xz.aliyun.com/t/11037这篇文章,提到了T3协议绕过,才想起自己也搞过这块的研究,遂将研究的内容分享出来抛砖引玉,希望能看到大佬们更多的分析文章。

0x01 协商

Weblogic处理T3基础信息协商的类如下

weblogic.rjvm.t3.MuxableSocketT3#readIncomingConnectionBootstrapMessage

客户端发送:

1
2
3
t3 10.3.6
AS:255
HL:19

服务端发送:

1
2
3
4
5
HELO:12.2.1.4.false
AS:2048
HL:19
MS:10000000
PN:DOMAIN

客户端发送的都是这种键值对的形式,存在以下可用键:

其中,AS和HL是两种常用的头信息,这里和我们利用的关系不大,就不分析了,需要注意的时候AS需要设置成01,尽可能小。

0x02 信息发送

信息处理的主要代码在weblogic.rjvm.MsgAbbrevInputStream#init函数

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
super.init(data, 4);#变量初始化,以及跳过一个int(数据总长度)
this.connection = connection;
this.responseId = -1;
this.user = null;
this.setValidatingClass(false);
this.header.readHeader(this, connection.getRemoteHeaderLength());#读取header信息
if (this.connectionManager.thisRJVM != null) {
this.header.src = this.connectionManager.thisRJVM.getID();
}

this.header.dest = JVMID.localID();
if (this.requiresUnauthenticatedFilter()) {
WebLogicObjectInputFilter.setUnauthenticatedFilterForStream(this.objectStream);
this.objectStream.setFilterType(MsgAbbrevInputStream.FilterType.UNAUTHENTICATED);
} else if (this.objectStream.getFilterType() == null) {
WebLogicObjectInputFilter.setWebLogicFilterForStream(this.objectStream);
this.objectStream.setFilterType(MsgAbbrevInputStream.FilterType.WLS);
}

if (KernelStatus.DEBUG && debugMessaging.isDebugEnabled()) {
}

this.mark(this.header.abbrevOffset);
this.skip((long)(this.header.abbrevOffset - this.pos()));
connection.readMsgAbbrevs(this);
this.reset();
if (JVMID.localID().equals(this.header.dest)) {
if (!this.header.getFlag(8)) {
this.read81Contexts();
} else {
this.readExtendedContexts();
}
}

第一步super.init()会进行一些初始化,并跳过前面的4个byte的长度信息,首先读取的就是header信息,处理函数如下weblogic.rjvm.JVMMessage#readHeader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try {
this.cmd = JVMMessage.Command.getByValue(is.readByte());
this.QOS = is.readByte();
this.flags = is.readByte() & 255;
this.hasJVMIDs = this.getFlag(1);
this.hasTX = this.getFlag(2);
this.hasTrace = this.getFlag(4);
this.responseId = is.readInt();
this.invokableId = is.readInt();
this.abbrevOffset = is.readInt();
int skip = remoteHeaderLen - 19;
if (skip > 0) {
is.skip((long)skip);
}

} catch (IOException var4) {
throw new AssertionError("Exception reading message header", var4);
}

总的一共是19个byte

这里说一些比较重要的

  • cmd,代表的是执行指令,这个值会影响代码进入不同的处理分支
  • flags,一个标志位,标识数据包中的信息种类,同样会影响代码进入不同分支
  • abbreOffset,一个int变量,标识header长度,在后面对流的控制会用到。

在读取完header后,会调用mark和skip函数,标记当前位置,并跳过一部分内容,跳过的长度为abbrevOffset 的值-当前读取的长度,然后调用connection.readMsgAbbrevs(this);读取信息。

1
2
this.skip((long)(this.header.abbrevOffset - this.pos()));

readMsgAbbrevs函数就会对流中的序列化数据进行反序列化,调用的是InboundMsgAbbrev类的readObject方法,并存储在栈中。调用栈如下

会调用到FilteringObjectInputStream的resolveClass函数。这里也就是之前weblogic的漏洞会触发的readObject的地方。但是在21年4月的补丁中,Weblogic使用了白名单,只有以下七种类可以被反序列化,因此所有Weblogic原本的漏洞都无法使用。

  • java.lang.String
  • weblogic.rmi.spi.ServiceContext
  • weblogic.rjvm.ClassTableEntry
  • weblogic.rjvm.JVMID
  • weblogic.security.acl.internal.AuthenticatedUser
  • weblogic.rmi.extensions.server.RuntimeMethodDescriptor
  • weblogic.utils.io.Immutable

读取结束后,执行reset()方法,流的指针回到之前mark处,根据header中flag值的不同,进入不同的分支。

1
2
3
4
5
if (!this.header.getFlag(8)) {
this.read81Contexts();
} else {
this.readExtendedContexts();
}

根据需求设定flag值,可以进入如下函数

该函数的关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (b == 4) {
ObjectStreamClass desc = this.readClassDescriptor();
Class cl = this.resolveClass(desc);
weblogic.utils.io.ObjectStreamClass osc = weblogic.utils.io.ObjectStreamClass.lookup(cl);
Externalizable e = (Externalizable)osc.newInstance();
int envelopeLength = this.readInt();
int startEnvelope = this.pos();
this.pushExternalizableInfo(startEnvelope + envelopeLength, cl.getName());
boolean var12 = false;

try {
var12 = true;
e.readExternal(this);
var12 = false;
}

这部分代码就是T3白名单绕过的关键部分了。

白名单绕过

上面的函数中实例化了一个实现了Externalizable接口的类,并调用了它的readExternal。这个类的Desc信息来源于this.readClassDescriptor();这个函数会从栈中的ClassTableEntry中读取descriptor属性作为desc,接着调用resovleClass方法生成类,这里调用的是MsgAbbrevInputStream的resolveClass方法,只存在黑名单判断,不存在白名单。接着会实例化这个类,并调用readExternal方法。栈中的ClassTableEntry类是在Weblogic T3的白名单中的,因此可以顺利被传入。

在后面在readExternal中需要注意,要想真正绕过白名单,不能在Externalizable 实例的readExternal里调用原生的readObject方法,不然还是会受黑名单影响。

举例,在传入的ClassTableEntry对象的descriptor属性中传入weblogic.cache.RefWrapper对应的ObjectStreamClass对象。该类实现了Externalizable接口,同时该类的readExternal方法如下:

1
2
3
4
5
6
7
8
9
10
11
public void readExternal(ObjectInput oi) throws IOException, ClassNotFoundException {
Object o = oi.readObject();
if (o != null) {
if (nosoftrefs) {
this.hardref = o;
} else {
this.softref = new SoftReference(o);
}

}
}

在这个in.readObject()处打个断点,,按照文章前面提到的数据发送流程发送数据后,再进入readObject跟几步,利用栈如下:

可以发现程序又进入了readObjectFromPreDiabloPeer方法。原因在于默认传入的ObjectInput和在MsgAbbrevInputStream中被readObject的是同一个流,依然会受白名单的影响。那么如何把这个流替换成其他的呢?其实也很简单。 

在Externalizable接口的实现类中,很常见的会看见这种写法,这里以com.tangosol.coherence.servlet.AttributeHolder为例

1
2
3
4
5
6
7
public void readExternal(DataInput in) throws IOException {
this.m_sName = ExternalizableHelper.readUTF(in);
this.m_oValue = ExternalizableHelper.readObject(in);
this.m_fActivationListener = in.readBoolean();
this.m_fBindingListener = in.readBoolean();
this.m_fLocal = in.readBoolean();
}

它在反序列化的过程中使用的是ExternalizableHelper.readObject方法。它在反序列化过程中根据序列化的数据类型不同,存在许多自定义的逻辑。其中在反序列化利用链中最常用是下面这两种,我们重点关注它们对流是否存在转换和处理

第一个是反序列化实现了ExternalizableLite接口的类。

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
try {
Class<?> clz = loadClass(sClass, loader, inWrapper == null ? null : inWrapper.getClassLoader());
if (in instanceof ObjectInputStream) {
ObjectInputStream ois = (ObjectInputStream)in;
if (!checkObjectInputFilter(clz, ois)) {
throw new InvalidClassException("Deserialization of class " + sClass + " was rejected");
}
}

value = (ExternalizableLite)clz.newInstance();
} catch (InstantiationException var7) {
throw new IOException("Unable to instantiate an instance of class '" + sClass + "'; this is most likely due to a missing public no-args constructor: " + var7 + "\n" + getStackTrace(var7) + "\nClass: " + sClass + "\nClassLoader: " + loader + "\nContextClassLoader: " + getContextClassLoader());
} catch (Exception var8) {
throw new IOException("Class initialization failed: " + var8 + "\n" + getStackTrace(var8) + "\nClass: " + sClass + "\nClassLoader: " + loader + "\nContextClassLoader: " + getContextClassLoader(), var8);
}

if (loader != null) {
if (inWrapper == null) {
in = new WrapperDataInputStream((DataInput)in, loader);
} else if (loader != inWrapper.getClassLoader()) {
inWrapper.setClassLoader(loader);
}
}

value.readExternal((DataInput)in);
if (value instanceof SerializerAware) {
((SerializerAware)value).setContextSerializer(ensureSerializer(loader));
}

首先是一段黑名单判断,这应该是之前某个二次反序列化话的补丁。黑名单后就会newInstance,然后会生成一个新的WrapperDataInputStream对象,这看起来像是对流进行了转化,但其实只是一层封装,在实际的readObject过程中还是使用的原始流。

1
2
3
4
5
6
7
if (loader != null) {
if (inWrapper == null) {
in = new WrapperDataInputStream((DataInput)in, loader);
} else if (loader != inWrapper.getClassLoader()) {
inWrapper.setClassLoader(loader);
}
}

因此重点需要关注第二个readSerializable方法了。这个方法是对常规的序列化的封装。在执行readObject前存在这样一段代码:

1
2
ObjectInput streamObj = getObjectInput(in, loader);

在高版本的Weblogic中,最终是执行下面这段代码:

1
2
3
4
5
6
7
8
9
public ObjectInput getObjectInput(DataInput in, ClassLoader loader, boolean fForceNew) throws IOException {
if (!fForceNew && in instanceof WLSObjectInputStream) {
return (ObjectInput)in;
} else {
InputStream inStream = this.getInputStream(in, fForceNew);
loader = loader == null && in instanceof WrapperDataInputStream ? ((WrapperDataInputStream)in).getClassLoader() : loader;
return (ObjectInput)(loader == null && in instanceof FilteringObjectInputStream ? (ObjectInput)in : new WLSObjectInputStream(inStream, RemoteObjectReplacer.getReplacer(), new ClassLoaderResolver(loader), loader, this.setFilter));
}
}

在T3反序列化中,会生成一个新的WLSObjectInputStream对象作为流,从而摆脱了白名单。

下面是测试的调用栈。

在进入WLSObjectInputStream的readObject后,构建符合要求的数据流,即可正常反序列化,在这一步的实操中,我利用程序自身的序列化方法进行序列化的数据,在反序列化时都失败了,可能需要对字节码的一些字段进行手动修改,本文只提出T3协议的绕过方法,后续的WLSObjectInputStream的readObject实现,有兴趣的师傅可以自行尝试。