去年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 + "\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过程中还是使用的原始流。

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实现,有兴趣的师傅可以自行尝试,期待你的分享文章。

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖