去年weblogic出白名單時研究了下怎么繞過,總結出了下面的思路,本想再找找有無新的攻擊面的思路,但是找了幾次都沒找到,后來就擱置了。昨天看見https://xz.aliyun.com/t/11037這篇文章,提到了T3協議繞過,才想起自己也搞過這塊的研究,遂將研究的內容分享出來拋磚引玉,希望能看到大佬們更多的分析文章。
T3協議交互流程
0x01 協商
Weblogic處理T3基礎信息協商的類如下
weblogic.rjvm.t3.MuxableSocketT3#readIncomingConnectionBootstrapMessage
客戶端發送:
t3 10.3.6 AS:255 HL:19
服務端發送:
HELO:12.2.1.4.false AS:2048 HL:19 MS:10000000 PN:DOMAIN
客戶端發送的都是這種鍵值對的形式,存在以下可用鍵:

其中,AS和HL是兩種常用的頭信息,這里和我們利用的關系不大,就不分析了,需要注意的時候AS需要設置成01,盡可能小。
0x02 信息發送
信息處理的主要代碼在weblogic.rjvm.MsgAbbrevInputStream#init函數
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
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);讀取信息。
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值的不同,進入不同的分支。
if (!this.header.getFlag(8)) {
this.read81Contexts();
} else {
this.readExtendedContexts();
}
根據需求設定flag值,可以進入如下函數

該函數的關鍵代碼如下:
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方法如下:
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為例
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接口的類。
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 + "" + getStackTrace(var7) + "Class: " + sClass + "ClassLoader: " + loader + "ContextClassLoader: " + getContextClassLoader());
} catch (Exception var8) {
throw new IOException("Class initialization failed: " + var8 + "" + getStackTrace(var8) + "Class: " + sClass + "ClassLoader: " + loader + "ContextClassLoader: " + 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過程中還是使用的原始流。
if (loader != null) {
if (inWrapper == null) {
in = new WrapperDataInputStream((DataInput)in, loader);
} else if (loader != inWrapper.getClassLoader()) {
inWrapper.setClassLoader(loader);
}
}
因此重點需要關注第二個readSerializable方法了。這個方法是對常規的序列化的封裝。在執行readObject前存在這樣一段代碼:
ObjectInput streamObj = getObjectInput(in, loader);
在高版本的Weblogic中,最終是執行下面這段代碼:
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實現,有興趣的師傅可以自行嘗試,期待你的分享文章。
安全圈
安全圈
商密君
系統安全運維
系統安全運維
HACK學習呀
GoUpSec
商密君
HACK學習呀
合天網安實驗室
合天網安實驗室