CVE-2020-14882:Weblogic Console 权限绕过深入解析
2020-11-05 14:55

报告编号:B6-2020-110501

报告来源:360CERT

报告作者:Hu3sky

更新日期:2020-11-05

0x01 漏洞简述

2020年10月29日,360CERT监测发现 Weblogic ConSole HTTP 协议代码执行漏洞 相关 POC已经公开,相关漏洞编号为 CVE-2020-14882,CVE-2020-14883 ,漏洞等级:严重,漏洞评分:9.8

远程攻击者可以构造特殊的HTTP请求,在未经身份验证的情况下接管 WebLogic Server Console ,并执行任意代码。

对此,360CERT建议广大用户及时将 Weblogic 升级到最新版本。与此同时,请做好资产自查以及预防工作,以免遭受黑客攻击。

0x02 风险等级

360CERT对该漏洞的评定结果如下

评定方式 等级
威胁等级 严重
影响面 广泛
360CERT评分 9.8

0x03 影响版本

Oracle:Weblogic:

  • 10.3.6.0.0
  • 12.1.3.0.0
  • 12.2.1.3.0
  • 12.2.1.4.0
  • 14.1.1.0.0

0x04 漏洞详情

目前网上的分析都没有说清楚权限绕过具体是怎么访问到console.portal路径并且触发handle执行的,在与@Lucifaer的共同深入研究下,大概掌握了原理,于是有了此文。

CVE-2020-14882 是一个 Console 的未授权访问,而 CVE-2020-14882 是在利用未授权访问的前提下,在 Console 进行代码执行,于是远程攻击者可以构造特殊的 HTTP 请求,在未经身份验证的情况下接管 WebLogic Server Console ,并在 WebLogic Server Console 执行任意代码。

通过diff补丁,console.jar 里主要修改有两个类,能够定位到漏洞触发点。

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

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

下面对漏洞进行逐个分析。

CVE-2020-14882

首先要明白,漏洞的触发是在 console 组件,而console对应着webapp服务,路径:wlserver/server/lib/consoleapp/webapp。并且存在web.xml,于是查看与MBeanUtilsInitSingleFileServlet相关的web.xml信息:

  <servlet>
    <servlet-name>AppManagerServlet</servlet-name>
    <servlet-class>weblogic.servlet.AsyncInitServlet</servlet-class>

    <init-param>
      <param-name>weblogic.servlet.AsyncInitServlet.servlet-class-name</param-name>
      <param-value>com.bea.console.utils.MBeanUtilsInitSingleFileServlet</param-value>
    </init-param>
    ...

  <servlet-mapping>
    <servlet-name>AppManagerServlet</servlet-name>
    <url-pattern>*.portal</url-pattern>
</servlet-mapping>

Request处理

从上面的 web.xml 内容中可以得出:

  1. MBeanUtilsInitSingleFileServletAppManagerServletservlet-class-name初始化的值。
  2. 访问*.portal会经过AppManagerServlet的分派处理(通过认证后访问console的路径是/console/console.portal)。 weblogic所有的请求都会经过weblogic.servlet.internal.ServletRequestImpl的预处理。跟到doSecuredExecute方法。

这里会调用WebAppSecurity#checkAccess进行权限的校验。

第一次请求的时候checkAllResourcesfalse,于是调用getConstraint方法。 传入参数为请求的/console下的资源的路径和请求的方法。这里以请求/console.portal为例。

静态资源列表获取

看下前面的逻辑:

  1. 获取一个mapping如下

  1. 以当前请求方法作为key值匹配value,而第一行获取到的mapkey"",所以匹配结果为null

  1. 判断当前请求的url是否匹配mapping里的路径,如果匹配不到,那么返回默认的rcForAllMethods注意unrestrictedfalseunrestricted是权限验证的一个关键点),也就是:

现在的结果就是rcForAllMethods为默认值,而rcForOneMethodnull。所以返回rcForAllMethods

执行到这里可以得出的是,如果请求的路径在matchMap列表里,那么unrestricted值就为true,这些是属于静态资源,没有做资源的限制和身份校验。

接着做if判断,resourceConstraint不为null

接着进入else,调用SecurityModule#isAuthorized

继续调用ChainedSecurityModule#checkAccess

权限校验

然后调用hasPermission开始判断是否有权限。

hasPermission方法首先判断unrestricted,这里我们通过修改请求/console/css/console.protal访问静态资源使值为true

然后 checkAcess方法返回 true

重定向登陆界面

如果checkAcess方法返回false。那么不会进入后续的分派,会结束doSecuredExecute方法的执行。一路return到执行ServletRequestImpl#runInternalfinally分支。

这里会调用send方法,在该方法会将没有分派的请求重定向到login界面。

请求分派

如果checkAcess方法返回true。进入后续请求的分派,经过几个filterchain的分派,最终调用ServletStubImplexcute方法。这里会根据web.xml的配置来获取对应的具体的Servlet

注意,根据web.xml,请求如下路径所对应的servlet不一样,因为几个路径都是之前所提到的静态路径,没有身份验证,但是我们需要利用到AsyncInitServlet来处理,因为我们diff到的修补点在MBeanUtilsInitSingleFileServlet,这个类是通过AsyncInitServlet来设置的。

servlet对应关系部分如下:

/framework/skeletons/wlsconsole/js/* -> FileDefault
/css/* -> AsyncInitServlet
/images/* -> AsyncInitServlet
/common/* -> JSPCServlet
...

于是,请求/css/*会调用AsyncInitServletservice方法,

这里的delegate就是在web.xml被初始化的MBeanUtilsInitSingleFileServlet。 接下来以漏洞URL为例。

/css/%252E%252E%252Fconsole.portal

这里要二次编码的原因是,发过去的时候http会解一次码,也就是说如果我们传的是/css/%2E%2E%2Fconsole.portal,那么解码后就是/css/../console.portal,这样发到服务端就没办法匹配到静态资源了,直接处理成了/console.portal

如果http解码后的url里没有;,那么就会继续调用super.service,而官方的补丁修复也是在这,通过一个黑名单列表检测路径里的非法字符,不过官方给出的黑名单字符不够完善,能够被绕过。

一路到达UIServlet#service,根据请求method调用不同的方法,doGet最终也会调用到doPost

url解码

doPost里调用createUIContext

UIContext会根据请求中的参数作对应属性值的设置,比如后面会说到的_nfpb

创建完之后,会返回一个UIContext对象。 这里的tree也就是createUIContext传入的第三个参数,初始值为null

跟入UIServletInternal#getTree,这里会对requestPattern解码。

解码后。

请求portal文件,构建控件树

将解码后的url传入processStream方法。

然后SingleFileProcessor#getMergedControlFromFile

关于.portal的加载方式singleFile:简单来说,在访问.portal时,是从文件系统加载的而不是数据库中,解析.portal文件的XML,并将呈现的.portal返回到浏览器。

将请求路径同时当作file路径传入,接着创建了SAXParser,准备将文件解析。

接着调用下方getControlFactoryFromFile,一直跟进,会从本地获取请求的文件.

在这里目录穿越起了效果,获取到的文件也就是webapp下的console.portal

并且以WarSource对象存入缓存

之后调用sax解析xml文件console.portal,并从中生成控制树,也就是getTree返回的ControlTreeRoot对象,然后存入UIContext

树的生命周期

控件树被构建后,就会进入生命周期的运行,回到UIServlet#doPost,调用runLifecycle,运行生命周期。

这里会根据UIContext里的两个值来判断执行runInbound还是runOutbound,后面细说

生命周期可以看作是控件上的一组方法,这些方法按定义的顺序调用。生命周期方法如下

init()
loadState()
handlePostbackData()
raiseChangeEvents()
preRender() 
saveState()
render()  
dispose() 

控件的具体解析流程如下

对应了调用栈里的调用,从ROOT开始,第一个子节点是Desktop,而接下来:

enter description here 然后,深度优先遍历子节点。

当然,这个顺序也就是console.portal文件里的xml嵌套顺序。

因为是深度优先,在console.portal里的所有引用的portal文件也会按顺序解析,比如

直到所有标签解析完。

CVE-2020-14883

接下来也就是造成代码执行的点,com.bea.console.handles.HandleFactory 要触发getHandle方法有两个触发点

触发点一

回到之前创建UIContext的时候,有一个setServletRequest方法。

如果请求中存在_nfpb=true的时候,会把postback选项设置为true

那么,之后在运行树的生命周期时,由于outbound选项默认false,而postbacktrue进入判断。

会调用runInbound方法,因为runInbound会把types设置为_inboundLifecycle

_inboundLifecycle如下,注意不同的type对应了不同的静态类

当然,如果没有_nfpb=true,会调用runOutboundtype设置为_outboundNewLifecycle

这决定了在深度遍历的时候先调用的方法,上面说过生命周期方法,于是这里就会先调用所有节点的init方法。因为在运行生命周期的时候,这里会调用ControlTreeWalker#walk方法,第一个参数,也就是type[0],是init

继续跟入walkRecursive方法

注意两处:

  1. 如果当前是Root节点,那么调用visitRoot,这个方法只会调用一次,如果不是,则调用当前visitvisit方法,当前visit也就是type里提到的静态类。initControlLifecycle$1,也就是第一个静态类,而这里的control就是当前节点。也就是说,如果当前typeinit,深度解析所有节点的时候,都会把init方法调用一次。也就有了漏洞触发点Portlet#init

  1. 调用完之后,如果深度遍历发现还有子节点,那么继续调用walkRecursive,重复1的步骤,直到所有节点解析完。

当调用到Portlet节点的init方法时,会一直调用super.init

调用栈:

直到AdministeredBackableControl#init,会调用initializeBackingFile

最终会调用到BreadcrumbBacking#init,而这里会获取请求中的handle参数,调用getHandle方法。

触发点二

在调用完init之后,会根据type里的顺序,继续调用生命周期方法(都对应着ControlLifecycle里的visitor)。 如果是_nfpb=true,调用完runInbound->runOutbound

由于postbacktrue

之后流程类似,不过调用的visitor最开始是ControlLifecycle#preRenderVisitor 在调用到StrutsContet节点的时候,这个是在解析到引用PortalConfig/contentheader/ContentHeader_messages.portlet的时候。

这时候会调用preRenderVisitor#preRenderpreRenderVisitor没有该方法,去父类NetuiContent#preRender

并且在文件里会设置action/refreshActionMessagesAction

后续调用栈:

当然,不止StrutsContet节点会调用到这里,还有Book,Portlet节点,而在深度遍历的时候,会有很多Book,Portlet,StrutsContet的子节点,于是就会执行getHandle很多次,这也是为什么在使用计算器进行poc测试的时候,会多次弹出的原因。

最终的利用结果如下:

总结

  1. 通过静态资源来绕过权限验证,防止被重定向到登陆界面。
  2. 通过请求.portal,控制处理的Servlet是渲染UIMBeanUtilsInitSingleFileServlet
  3. 通过编码后的../,让最终渲染的模版是console.portal

综合起来,才造成了最终的未授权访问。

0x05 时间线

2020-10-21 360CERT发布Oracle补丁日通告

2020-10-28 360CERT监测到POC公开

2020-10-29 360CERT发布通告

2020-11-05 360CERT发布分析

0x06 参考链接

  1. White Paper: WebLogic Portal Framework
  2. CVE-2020-14882/14883: Weblogic ConSole HTTP 协议代码执行漏洞POC公开通告