作者:Lucifaer
原文鏈接:https://lucifaer.com/2020/11/25/WebLogic one GET request RCE分析(CVE-2020-14882+CVE-2020-14883)/
0x01 漏洞概述


Weblogic官方在10月補丁中修復了CVE-2020-14882及CVE-2020-14883兩個漏洞,這兩個漏洞都位于Weblogic Console及控制臺組件中,兩個漏洞組合利用允許遠程攻擊者通過http進行網絡請求,從而攻擊Weblogic服務器,最終遠程攻擊者可以利用該漏洞在未授權的情況下完全接管Weblogic服務器。
在經過diff后,可以定位到漏洞觸發點:

CVE-2020-14883:com.bea.console.handles.HandleFactory

CVE-2020-14882:com.bea.console.utils.MBeanUtilsInitSingleFileServlet

這里先把結論放出來:
- CVE-2020-14882:這個漏洞起到的作用可以簡單理解為目錄穿越使
Netuix渲染后臺頁面 - CVE-2020-14883:為登錄后的一處代碼執行點
0x02 漏洞分析
該漏洞分為三部分:
- 路由鑒權
- Netuix框架完成執行流轉換
- HandleFactory完成代碼執行
前兩部分為CVE-2020-14882,后面一部分為CVE-2020-14883。本文將從上而下將三部分進行串流分析,主要采用動態跟蹤。
2.1 路由鑒權
在具體分析路由鑒權前,需要先要尋找一下處理路由的servlet是哪個。
2.1.1 尋找處理路由的servlet
Weblogicconsole組件對應著Weblogic Server啟動后的管理平臺(即/console路由所對應的組件),其對應著一個webapp,所以想要理清路由所對應的servlet映射關系,就需要去看一下相關的配置文件。配置文件為wlserver/server/lib/consoleapp/webapp/WEB-INF/web.xml。
正常登錄后的路由情況為:

會訪問一個console.portal文件,對應在web.xml中看一下相關的路由處理情況:

可以看到對應的servlet為AppManagerServlet:

所以先在AppManagerServlet下斷調試一下路徑鑒權或者說是權限鑒定的流程。
跟進一下初始化流程:
weblogic.servlet.AsyncInitServlet#init ->
weblogic.servlet.AsyncInitServlet#initDelegate ->
weblogic.servlet.AsyncInitServlet#createDelegate


這里的this.SERVLET_CLASS_NAME也就是xml中的:

所以初始化過程實際上是實例化了com.bea.console.utils.MBeanUtilsInitSingleFileServlet,并調用其init()方法,跟進看一下其所對應的處理方法:

注意紅框所標識的內容,oracle針對CVE-2020-14882的修補也是在這里針對url加了一個黑名單,并過了一遍黑名單:

繼續跟進父類SingleFileServlet的server中:

在完成AppContext初始化后,即進入真的處理請求的UIServlet:

在此處完成后續的請求處理。
2.1.2 路由映射及路由權限校驗
在這里我們先不向后跟進,在此處下個斷點向上跟蹤一下,看一下Weblogic路由映射及路由鑒權在哪里觸發。調用棧如下:

可以看到在weblogic.servlet.internal.WebAppServletContext中完成的權限校驗。跟進具體看一下:

在weblogic.servlet.internal.WebAppServletContext#doSecuredExecute方法的流程中會調用checkAccess方法來進行權限校驗,跟進看一下:

當首次請求進入后checkAllResources變量為false,所以跟進getConstraint方法:

這里的constraintsMap中保存著一份路由表:

這份路由表對應的是web.xml中的security-constraint:

注意在針對/的路由處理是限定了需要經過認證的,而針對:
/images/*/common/*/css/*
路徑的訪問是沒有認證約束的。對應到代碼中,就是說當訪問的路由符合該路由映射表中的情況時,將根據配置設置rcForAllMethods變量,也就是最終返回的resourceConstraint:

這里的unrestricted變量代表該路由是否為非受限路由,在后續鑒權時該變量會起關鍵性作用。當請求的路由是路由表中的路由時,該變量都為true。當完成resourceConstraint設置后,就會進入isAuthorized方法進行權限鑒定:


這里將執行流轉換到CertSecurityModule#checkUserPerm方法中:

首先會根據session來確定是否需要重新登錄,之后會判斷是否為指定路由,如果是未指定的路由,則保護資源,由于我們這里訪問的路由為/css,在指定的路由表中,所以這里是false。重點看hasPermission方法,這里會用到resourceConstraint中的unrestricted:

這里首先會判斷當前的賬戶是否為Admin賬戶,當前應用是否為內部引用等,若都不滿足,則會判斷是否設置了完整安全路由選項,這里是false。接下來會判斷該路由是否為非受限的路由,如果是,則返回true。由于我們根據路由表返回設置的unrestricted變量為true,即為非受限的路由,所以這樣就通過了路由鑒權,導致了未授權訪問相關資源。
2.1.3 請求分派
當完成了路由鑒權后,會根據web.xml中的設置,將訪問的路由映射到相應的servlet進行請求處理:


因為我們后續的流程在UIServlet中進行,所以可以用于繞過路由鑒權的路由即為:
/css/*/images/*
當checkAccess方法返回為true后,會根據配置返回對應的servlet并調用service方法。
首先會初始化ServletInvocationAction對象:

從subject.run(action)一路向下跟,在weblogic.security.acl.internal.AuthenticatedSubject#doAs中調用action的run方法,即跟進ServletInvocationAction#run:

在調用execute方法前,會首先判斷是否存在攔截器及請求監聽器,若存在則執行對應的攔截器執行鏈,否則執行stub.execute()方法。跟進stub.execute()方法,即weblogic.servlet.internal.ServletStubImpl#execute:

這里會調用getServlet()方法返回對應的servlet:



2.1.3 總結
從上面的分析可知,想要訪問非受限的資源,就需要構造符合路由表中的路由。從此我們也可以看出這里并非一個權限繞過操作,而是一個正常的訪問非受限資源(如css文件這類資源)的操作,想要搞清楚為什么能因此而觸發一個登陸后代碼執行操作,就需要跟進UIServlet的具體處理流程中。
2.2 Netuix框架完成執行流轉換
weblogic.servlet.AsyncInitServlet為處理Netuix相關請求的servlet,根據2.1.1中的分析,我們可以知道其真實的處理邏輯是在com.bea.netuix.servlets.manager.UIServlet中完成的:

對于UIServlet來說,處理GET請求的邏輯最終也會在doPost方法中。上圖紅框中所標明的兩處即為UIServlet的核心功能:
- 建立
UIContext,或者說是通過解析.portal文件建立渲染模板的上下文 - 完成模板渲染的生命周期
接下來也會以這兩點為核心具體敘述Netuix框架是如何完成執行流的轉換的。
2.2.1 建立UIContext
建立UIContext的主要流程在createUIContext方法中:

紅框所標注的兩行為關鍵流程。首先跟進UIContextFactory.createUIContext,這里主要完成了UIContext的初始化:

在執行setServletRequest方法時,會根據請求的參數對postback成員變量進行設置:

可以看到:
- 請求類型為POST請求,會將
postback設置為true - 存在
_nfpb參數的GET請求,會根據參數的值設置postback的值
postback變量在后續執行UIContext生命周期時會對流程產生影響。這里先記一下。
完成UIContext的初始化過程后,接下來就是解析.portal文件,將解析結果填充到UIContext中。這一部分的流程在getTree()方法中:

這里有一個需要注意的點,這里會對請求的路徑進行二次URLDecode,這也就是為什么構造的poc是需要二次URL編碼的原因。
跟進processStream()方法,具體的解析邏輯就在這里:

可以看到經過二次URLDecode后的請求路徑在此造成了目錄穿越的效果。
com.bea.netuix.servlets.manager.SingleFileProcessor#getMergedControlFromFile中首先會初始化SAX解析器,然后根據傳入的文件路徑獲取到對應的.portal文件,并利用SAX解析器解析該.portal文件:

getMergedControlFromFile()方法最終會調用getSourceFromDisk()方法根據傳入的路徑獲取consoleapp/webapp目錄下相應的文件即:
- console.portal
- consolejndi.portal
在利用SAX解析器解析完該portal文件后,生成語法樹,也就是getTree()返回的ControlTreeRoot對象,并將語法樹置入UIContext中。
至此就完成了UIContext的初始化流程。
2.2.2 完成模板渲染的生命周期
在完成了UIContext初始化流程之后,便會調用runLifecycle()方法運行生命周期,開始根據請求參數完成模板渲染。
跟進runLifecycle():


在com.bea.netuix.nf.Lifecycle#run中,需要注意這個條件判斷,這里會影響到后面的流程調用。
根據2.2.1中的分析我們知道當GET請求存在_nfpb參數時,會根據參數的值設置postback的值,outbound值默認為false。
而postback值只會影響是否會執行runInbound()流程。在具體跟蹤了runInbound()流程后,可以發現其處理邏輯是相同的:

而關鍵點就在其VisitorType是不同的,這會在processLifecycles()流程中影響具體的節點遍歷順序:

在com.bea.netuix.nf.Lifecycle中,我們可以看到inbound與outbound的區別:

各VisitorType具體配置為:

所以由postback會衍生出兩種不同的執行流。
2.2.3 Netuix生命周期及控件間的關系
在具體跟進兩種執行流前,首先介紹一下Netuix的解析流程,在其官方介紹頁面上有對生命周期方法執行順序及netuix控件解析流程的詳細描述,這里將其內容簡要總結一下。
Netuix控件樹的生命周期其實就是按順序所執行的一組方法,這組方法的執行順序如下:
init()
loadState()
handlePostbackData()
raiseChangeEvents()
preRender()
saveState()
render()
dispose()
其中的方法與上面所看到的inbound與outbound相同。這些方法在節點間是以深度優先的方式執行,即按照順序會執行所有控件的init(),之后才會重新遍歷執行loadState()方法。
在說完了每個控件的生命周期后,再來說一下控件間的關系:

根據這張表我們來對應看一下consolejndi.portal:

紅框所標注的區域是完美符合上表所描述的關系的。向上尋找Portlet,看加載了哪些外部控件:

跟進該文件:

這里調用了strutsContent控件,同時標注了具體的action為MessagesAction。可以通過該action在struts-config.xml中找到其所對應的類:

2.2.4 總結
通過上面的分析,可以看到Netuix將執行流從模板渲染轉換到其各個組件的渲染之中。所以最終觸發代碼執行的只和組件的生命周期有關,即只和節點有關。
在經過分析后,我列舉三個最通用的組件:
strutsConentPagePortlet
由于無論postback為何值,最終都會執行outbound流程,所以接下來對于組件生命周期的分析,我都以outbound流程來說明。
2.3 條條大路通羅馬——HandleFactory完成代碼執行
根據上面的分析,outbound的生命周期為:
preRender()
saveState()
render()
所以首先執行的方法是preRender,跟進看一下com.bea.netuix.nf.ControlTreeWalker#walk:



ControlVisitor visit = root.getVisitorForLifecycle(vt);這里將獲取ControlLifecycle.preRenderVisitor以深度優先的方式遍歷所有節點,并調用visit()方法。跟進看一下com.bea.netuix.nf.ControlVisitor#visit:

就如上面所說,關鍵邏輯還是調用傳入控件的preRender()方法。接下來就會按照2.2.3中的所介紹的控件間關系進行深度遍歷,在遍歷到不同組件時會利用不同的方式觸發代碼執行流程。
2.3.1 strutsContent
以consolejndi.portal為例,當節點為portletInstance時,會觸發外部組件調用,及會跟進該文件,解析Content節點:



此處處理的節點為strutsContent,即control為strutsContent。跟進com.bea.netuix.servlets.controls.content.StrutsContent#preRender方法:

沒有相關的方法,跟進其父類com.bea.netuix.servlets.controls.content.NetuiContent#preRender:

this.getScopedContentStub()調用棧如下:
com.bea.netuix.servlets.controls.content.StrutsContent#getScopedContentStub ->
com.bea.netuix.servlets.controls.content.StrutsContent.StrutsContentUrlRewriter初始化 ->
com.bea.portlet.adapter.scopedcontent.AdapterFactory#getInstance(com.bea.struts.adapter.util.rewriter.StrutsURLRewriter)
最終通過適配工廠返回一個StrutsStubImpl:

所以跟進com.bea.portlet.adapter.scopedcontent.StrutsStubImpl#render:

在renderInternal()方法中,完成內部渲染的工作,包括:
- 初始化
Action及其servlet,并設置解析器,最終調用executeAction執行 - 初始化并設置請求監聽器,完成請求接收


跟進executeAction()方法:

這里會調用PageFlowUtils#strutsLookup方法,該方法最終將會觸發負責處理針對Action請求的servlet的doGet方法,調用鏈如下:
com.bea.portlet.adapter.scopedcontent.framework.PageFlowUtils#strutsLookup
com.bea.portlet.adapter.scopedcontent.framework.PageFlowUtils#getInstance
com.bea.portlet.adapter.scopedcontent.framework.PageFlowUtils#instantiateStrutsDelegate
com.bea.portlet.adapter.scopedcontent.framework.internal.PageFlowUtilsBeehiveDelegate#strutsLookupInternal
org.apache.beehive.netui.pageflow.PageFlowUtils#strutsLookup(javax.servlet.ServletContext, javax.servlet.ServletRequest, javax.servlet.http.HttpServletResponse, java.lang.String, java.lang.String[])
org.apache.beehive.netui.pageflow.PageFlowUtils#strutsLookup(javax.servlet.ServletContext, javax.servlet.ServletRequest, javax.servlet.http.HttpServletResponse, java.lang.String, java.lang.String[], boolean)

這里有兩個點需要注意,第一個點是獲取ActionServlet的過程,這一部分其實并不需要去跟蹤代碼,可以通過直接看web.xml找到:

關于AsyncInitServlet的初始化流程在2.1.1中有詳細的跟蹤,這里就不贅述了。這里可以看出真正的處理邏輯在com.bea.console.internal.ConsoleActionServlet中,直接跟進看com.bea.console.internal.ConsoleActionServlet#doGet:

一路向下跟進,調用棧如下:
org.apache.struts.action.ActionServlet#process ->
com.bea.console.internal.ConsoleActionServlet#process ->
org.apache.beehive.netui.pageflow.PageFlowActionServlet#process ->
org.apache.beehive.netui.pageflow.AutoRegisterActionServlet#process ->
org.apache.beehive.netui.pageflow.PageFlowRequestProcessor#process ->
org.apache.beehive.netui.pageflow.PageFlowRequestProcessor#processInternal ->
org.apache.struts.action.RequestProcessor#process ->
com.bea.console.internal.ConsolePageFlowRequestProcessor#processActionPerform ->
com.bea.console.utils.HandleUtils#getHandleContextFromRequest ->
com.bea.console.utils.HandleUtils#handleFromQueryString
重點看一下com.bea.console.utils.HandleUtils#handleFromQueryString:

首先會將請求的參數進行解析,并映射到Map中,之后遍歷所有的參數,當參數以handle結尾,則將其轉換為Handle類型的對象。所以跟蹤流程到com.bea.console.handles.HandleConverter#convert:

這里會將請求中以handle結尾的參數值作為local,直接傳入HandleFactory.getHandle()方法中,在該方法中將傳入的參數值進行處理,直接完成反射實例化操作:

2.3.2 page
當解析Page組件時,control.preRender()實際將會調用com.bea.netuix.servlets.controls.page.Page#preRender:

接下來就是一路向上,調用父類的preRender方法,調用棧如下:
com.bea.netuix.servlets.controls.page.Page#preRender ->
com.bea.netuix.servlets.controls.window.Window#preRender ->
com.bea.netuix.servlets.controls.AdministeredBackableControl#preRender ->
com.bea.netuix.servlets.controls.Backable.Impl#preRender
在com.bea.netuix.servlets.controls.Backable.Impl#preRender中將會獲取jspbacking,并調用其preRender方法:

以consolejndi.portal為例,其中的一個page組件描述如下:

此處會根據book組件中所定義的title獲取其backingFile的具體引用,在這里為com.bea.console.utils.JndiViewerBackingFile:

接下來的調用棧為:
com.bea.console.utils.GeneralBackingFile#preRender ->
com.bea.console.utils.GeneralBackingFile#localizeTitle(com.bea.netuix.servlets.controls.window.backing.WindowBackingContext, javax.servlet.http.HttpServletRequest) ->
com.bea.console.utils.GeneralBackingFile#getDisplayName ->
com.bea.console.utils.HandleUtils#getHandleContextFromRequest
調用至此已經和2.3.1中提到的調用路徑相同了,在此不再贅述。
2.3.3 portlet
portlet組件執行流與page組件基本完全相同,唯一區別點在于backingFile不同。以consolejndi.portal為例:

引用外部組件,跟進jnditree.portlet:

跟進看一下:

調用父類com.bea.console.utils.PortletBackingFile#preRender,同樣,都會調用父類的localizeTitle()方法:

這里也會調用com.bea.console.utils.GeneralBackingFile#localizeTitle,之后的流程與2.3.2中的流程完全相同。
2.3.4 總結
根據以上分析,我們可以看到除了strutsContent外,其他幾種組件的應用方式都比較類似,關鍵點為兩個:
- 組件的
preRender流程中會調用到Backable#preRender方法 backingFile為GeneralBackingFile子類,同時其preRender方法會調用父類localizeTitle方法
想要尋找其他的組件可以看一下繼承樹:

紅框所標注的即為2.3.2與2.3.3中所分析到的調用過程。
0x03 漏洞利用
經過0x02的分析,我們不難看出該漏洞和其他傳統的越權漏洞是有很大區別的:
- 所謂的認證繞過是通過請求原本無需認證的資源路徑
- 在1的基礎上利用
../造成目錄穿越,使Netuix在初始化語法樹時讀取對應的后臺模板文件 - 在
Netuix生命周期中通過組件對應的處理流程觸發Handle流程 - 組件處理流程中會將請求中以
handle結尾的參數的值作為參數傳入HandleFactory#getHandle方法中,完成反射調用
所以利用方式也顯而易見,這里利用@77ca1k1k1的poc做展示:

poc:
com.tangosol.coherence.mvel2.sh.ShellSession('weblogic.work.ExecuteThread currentThread = (weblogic.work.ExecuteThread)Thread.currentThread(); weblogic.work.WorkAdapter adapter = currentThread.getCurrentWork(); java.lang.reflect.Field field = adapter.getClass().getDeclaredField("connectionHandler");field.setAccessible(true);Object obj = field.get(adapter);weblogic.servlet.internal.ServletRequestImpl req = (weblogic.servlet.internal.ServletRequestImpl)obj.getClass().getMethod("getServletRequest").invoke(obj); String cmd = req.getHeader("cmd");String[] cmds = System.getProperty("os.name").toLowerCase().contains("window") ? new String[]{"cmd.exe", "/c", cmd} : new String[]{"/bin/sh", "-c", cmd};if(cmd != null ){ String result = new java.util.Scanner(new java.lang.ProcessBuilder(cmds).start().getInputStream()).useDelimiter("\\A").next(); weblogic.servlet.internal.ServletResponseImpl res = (weblogic.servlet.internal.ServletResponseImpl)req.getClass().getMethod("getResponse").invoke(req);res.getServletOutputStream().writeStream(new weblogic.xml.util.StringInputStream(result));res.getServletOutputStream().flush();} currentThread.interrupt();')
0x04 Reference
https://docs.oracle.com/cd/E13218_01/wlp/docs81/whitepapers/netix/body.html
@77ca1k1k1關于回顯的研究
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1411/
暫無評論