报告编号: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
内容中可以得出:
MBeanUtilsInitSingleFileServlet
是AppManagerServlet
的servlet-class-name
初始化的值。- 访问
*.portal
会经过AppManagerServlet
的分派处理(通过认证后访问console
的路径是/console/console.portal
)。weblogic
所有的请求都会经过weblogic.servlet.internal.ServletRequestImpl
的预处理。跟到doSecuredExecute
方法。
这里会调用WebAppSecurity#checkAccess
进行权限的校验。
第一次请求的时候checkAllResources
为false
,于是调用getConstraint
方法。
传入参数为请求的/console
下的资源的路径和请求的方法。这里以请求/console.portal
为例。
静态资源列表获取
看下前面的逻辑:
- 获取一个
mapping
如下
- 以当前请求方法作为
key
值匹配value
,而第一行获取到的map
的key
是""
,所以匹配结果为null
。
- 判断当前请求的
url
是否匹配mapping
里的路径,如果匹配不到,那么返回默认的rcForAllMethods
(注意unrestricted
为false
,unrestricted
是权限验证的一个关键点),也就是:
现在的结果就是rcForAllMethods
为默认值,而rcForOneMethod
为null
。所以返回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#runInternal
的finally
分支。
这里会调用send
方法,在该方法会将没有分派的请求重定向到login
界面。
请求分派
如果checkAcess
方法返回true
。进入后续请求的分派,经过几个filterchain
的分派,最终调用ServletStubImpl
的excute
方法。这里会根据web.xml
的配置来获取对应的具体的Servlet
。
注意,根据web.xml
,请求如下路径所对应的servlet
不一样,因为几个路径都是之前所提到的静态路径,没有身份验证,但是我们需要利用到AsyncInitServlet
来处理,因为我们diff到的修补点在MBeanUtilsInitSingleFileServlet
,这个类是通过AsyncInitServlet
来设置的。
servlet
对应关系部分如下:
/framework/skeletons/wlsconsole/js/* -> FileDefault
/css/* -> AsyncInitServlet
/images/* -> AsyncInitServlet
/common/* -> JSPCServlet
...
于是,请求/css/*
会调用AsyncInitServlet
的service
方法,
这里的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
,而接下来:
然后,深度优先遍历子节点。
当然,这个顺序也就是console.portal
文件里的xml
嵌套顺序。
因为是深度优先,在console.portal
里的所有引用的portal
文件也会按顺序解析,比如
直到所有标签解析完。
CVE-2020-14883
接下来也就是造成代码执行的点,com.bea.console.handles.HandleFactory
要触发getHandle
方法有两个触发点
触发点一
回到之前创建UIContext
的时候,有一个setServletRequest
方法。
如果请求中存在_nfpb=true
的时候,会把postback
选项设置为true
。
那么,之后在运行树的生命周期时,由于outbound
选项默认false
,而postback
为true
进入判断。
会调用runInbound
方法,因为runInbound
会把types
设置为_inboundLifecycle
。
_inboundLifecycle
如下,注意不同的type对应了不同的静态类
当然,如果没有_nfpb=true
,会调用runOutbound
,type
设置为_outboundNewLifecycle
。
这决定了在深度遍历的时候先调用的方法,上面说过生命周期方法,于是这里就会先调用所有节点的init
方法。因为在运行生命周期的时候,这里会调用ControlTreeWalker#walk
方法,第一个参数,也就是type[0]
,是init
。
继续跟入walkRecursive
方法
注意两处:
- 如果当前是
Root
节点,那么调用visitRoot
,这个方法只会调用一次,如果不是,则调用当前visit
的visit
方法,当前visit
也就是type
里提到的静态类。init
是ControlLifecycle$1
,也就是第一个静态类,而这里的control就是当前节点。也就是说,如果当前type
是init
,深度解析所有节点的时候,都会把init
方法调用一次。也就有了漏洞触发点Portlet#init
。
- 调用完之后,如果深度遍历发现还有子节点,那么继续调用
walkRecursive
,重复1
的步骤,直到所有节点解析完。
当调用到Portlet
节点的init
方法时,会一直调用super.init
。
调用栈:
直到AdministeredBackableControl#init
,会调用initializeBackingFile
最终会调用到BreadcrumbBacking#init
,而这里会获取请求中的handle
参数,调用getHandle
方法。
触发点二
在调用完init
之后,会根据type
里的顺序,继续调用生命周期方法(都对应着ControlLifecycle
里的visitor
)。
如果是_nfpb=true
,调用完runInbound
->runOutbound
由于postback
为true
。
之后流程类似,不过调用的visitor
最开始是ControlLifecycle#preRenderVisitor
在调用到StrutsContet
节点的时候,这个是在解析到引用PortalConfig/contentheader/ContentHeader_messages.portlet
的时候。
这时候会调用preRenderVisitor#preRender
,preRenderVisitor
没有该方法,去父类NetuiContent#preRender
。
并且在文件里会设置action/refreshAction
为MessagesAction
。
后续调用栈:
当然,不止StrutsContet
节点会调用到这里,还有Book
,Portlet
节点,而在深度遍历的时候,会有很多Book
,Portlet
,StrutsContet
的子节点,于是就会执行getHandle
很多次,这也是为什么在使用计算器进行poc测试的时候,会多次弹出的原因。
最终的利用结果如下:
总结
- 通过静态资源来绕过权限验证,防止被重定向到登陆界面。
- 通过请求
.portal
,控制处理的Servlet
是渲染UI
的MBeanUtilsInitSingleFileServlet
。 - 通过编码后的
../
,让最终渲染的模版是console.portal
。
综合起来,才造成了最终的未授权访问。
0x05 时间线
2020-10-21 360CERT发布Oracle补丁日通告
2020-10-28 360CERT监测到POC公开
2020-10-29 360CERT发布通告
2020-11-05 360CERT发布分析