0x00 前言
前一篇关于RMI的文章写的稍微粗糙了些,写这一篇再巩固一下RMI的内容并做一些延伸。
0x01 一些细节
网上关于RMI服务端和客户端编写的文章方法很多,我上一篇是分为接口、服务端、客户端三个部分编写的,当时是将rmi registry和rmi server都写到了服务端类中:
package com.Anchor;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer implements Hello {
public static void main(String[] args) {
try {
RMIServer obj = new RMIServer();
Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 8888);//该语句执行后会运行一个 rmi server,监听本地8888端口等待client的请求
Registry registry = LocateRegistry.createRegistry(1099); // 该语句执行后会创建并启动 rmi registry,监听在本地 1099 端口
//exportObject() 方法返回结果为 remote object stub (代理对象,实现了与 Hello 接口同样的方法,包含 rmi server 的 host、port 信息)
registry.bind("Hello", stub); // 将 stub 注册到 registry,并与 name Hello 绑定
} catch (Exception e) {
System.out.println("Server Exception: " + e.toString());
e.printStackTrace();
}
}
public String sayHello() throws RemoteException {
return "Hello, World";
}
}
因为是在本地环境下,为了避免麻烦,就直接写到了一起。但是在低版本的jdk中,注册中心rmi registry与rmi server是可以分离的,甚至可以是运行在不同的主机上。但官方文档中是这样说:
出于安全原因,应用程序只能绑定或取消绑定到在同一主机上运行的注册中心。这样可以防止客户端删除或覆盖服务器的远程注册表中的条目。但是,查找操作是任意主机都可以进行的。
也就是说不建议将rmi registry和rmi server分离到不同主机来运行,我们后文会提到为什么会有这样的安全考虑。
另外该段代码是使用UnicastRemoteObject的exportObject方法来导出远程对象,并监听8888端口运行一个 rmi server。
Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 8888)
接着是通过registry.bind("Hello", stub)
将”Hello”与stub的映射关系绑定到rmi registry上,这种绑定方式不需要书写完整的RMI URL,只需写对象名称”Hello”即可。
这是我参考的官网上的做法,但网上的大部分文章更多是利用下面的方式:
package com.Anchor;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer2 {
public class RemoteHello extends UnicastRemoteObject implements Hello{
protected RemoteHello() throws RemoteException{
super();
}
public String sayHello() throws RemoteException{
return "Hello, World";
}
}
private void start() throws Exception{
RemoteHello rh = new RemoteHello();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/Hello", rh);
}
public static void main(String[] args) throws Exception{
RMIServer2 rs2 = new RMIServer2();
rs2.start();
}
}
这里使用了java.rmi.Naming
类,该类又叫做命名服务,提供了一种方便的方式来绑定和查找远程对象。可以看一下它的bind方法的源码,发现它是通过解析URI绑定远程对象,将URI拆分成主机、端口和远程对象名称,最后使用的仍是Registry
类的bind
来进行操作的:
public static void bind(String name, Remote obj)
throws AlreadyBoundException,
java.net.MalformedURLException,
RemoteException
{
ParsedNamingURL parsed = parseURL(name);
Registry registry = getRegistry(parsed);
if (obj == null)
throw new NullPointerException("cannot bind to null");
registry.bind(parsed.name, obj);
}
我们在rmi client
中也可以使用Naming
类的lookup
方法来查找远程对象(可以看到client端简洁了许多):
package com.Anchor;
import java.rmi.Naming;
public class RMIClient {
public static void main(String[] args) throws Exception {
Hello hello = (Hello)
Naming.lookup("rmi://192.168.0.107:1099/Hello");
System.out.println(hello.sayHello());
}
}
上面说到,相比于直接使用Registry类, Naming类在绑定和查找操作时需要的是一个URL,需要说明的时,很多文章都指出该URL的形式一定要下面这样:
rmi://host:port/objName
但其实并不是。首先,其中的rmi:
是可以省略的,也就是可以写成这样://host:port/objName
(但不要少了前面的双斜杠,否则会报错);其次,如果服务端或者客户端和RMI registry在同一主机上运行,那么host
和port
也可以省略,也就是直接远程对象名objName
。
另外,对于绑定方法,在
Naming
类和Registry
类,除了bind
还要rebind
,两者区别如下:
rebind
是指“重绑定”,如果“重绑定”时rmi registry
已经有了这个服务name的存在,则之前所绑定的Remote Object将会被替换;而bind
在执行时如果“绑定”时rmi registry
已经有这个服务name的存在,则系统会抛出错误。所以除非有特别的业务要求,一般建议使用rebind方法进行Remote Object绑定。
0x02 RMI利用
讲了这么多,只是涉及了rmi具体通信原理以及如何实现,下面来从利用的层面讲讲。
关于利用的方法,可以分别从攻击rmi registry、攻击rmi server、攻击rmi client角度来实现,由于篇幅和本人水平有限(对java反序列化的知识掌握不深),本文就略带介绍一下利用方式,具体实现在以后的文章中进行。
攻击rmi registry
也许你也注意到上文中提到的,rmi server可以通过bind、rebind等方法申请在rmi registry上进行绑定注册,那么想想是否可以伪造成一个远程的rmi server注册一个恶意远程服务上去呢?
前文也说过,在低版本的jdk本中(8u121),rmi registry和rmi server可以不在一台服务器上,此时是可以利用这种方法攻击注册中心的。但是通常情况下,我们遇到的场景都是高版本的jdk,此时这种攻击注册中心的方式就不太适用。
由于我没能下载到低版本的jdk,部分源码片段我就截取了网上的文章进行说明。
我们先来看前面贴上的java.rmi.Naming#bind
方法的源码,其中它会将URL拆解后的结果作为参数放到getRegistry
方法中从而得到一个registry
:
Registry registry = getRegistry(parsed);
那么查看这个getRegistry
方法源码:
private static Registry getRegistry(ParsedNamingURL parsed)
throws RemoteException
{
return LocateRegistry.getRegistry(parsed.host, parsed.port);
}
可以看到这里归根到底调用的还是LocateRegistry类的getRegistry方法,继续跟踪进去:
public static Registry getRegistry(String host, int port,
RMIClientSocketFactory csf)
throws RemoteException
{
Registry registry = null;
if (port <= 0)
port = Registry.REGISTRY_PORT;
if (host == null || host.length() == 0) {
// If host is blank (as returned by "file:" URL in 1.0.2 used in
// java.rmi.Naming), try to convert to real local host name so
// that the RegistryImpl's checkAccess will not fail.
try {
host = java.net.InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
// If that failed, at least try "" (localhost) anyway...
host = "";
}
}
/*
* Create a proxy for the registry with the given host, port, and
* client socket factory. If the supplied client socket factory is
* null, then the ref type is a UnicastRef, otherwise the ref type
* is a UnicastRef2. If the property
* java.rmi.server.ignoreStubClasses is true, then the proxy
* returned is an instance of a dynamic proxy class that implements
* the Registry interface; otherwise the proxy returned is an
* instance of the pregenerated stub class for RegistryImpl.
**/
LiveRef liveRef =
new LiveRef(new ObjID(ObjID.REGISTRY_ID),
new TCPEndpoint(host, port, csf, null),
false);
RemoteRef ref =
(csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);
return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
}
这里getRegistry方法有好几种重载方式,前面所说的传入不同形式的URL,正好对应了这几种重载方式,我这里粘贴的是最终调用的这个核心的getRegistry方法。前面的if判断是处理port和host的,然后分别实例化了liveRef和RemoteRef对象,这一段过程应该是用过来处理网络通信相关的,不用管,直接定位最后的返回语句,调用了createProxy方法。关于这个方法,要涉及到动态代理的内容(给自己挖个坑,以后研究),它会根据传入的class对象,返回一个代理,也就是说,这里传入RegistryImpl.class
后,得到的其实是一个RegistryImpl_Stub对象,即远程的Registry接口在本地的代理。
我们再回到Naming类的bind方法中,接下来就是执行registry.bind(parsed.name, obj);
语句,这里调用了registry
的bind方法
,其实是调用的RegistryImpl_Stub类中的bind方法,我们定位到该类的bind方法:
public void bind(String var1, Remote var2) throws AccessException, AlreadyBoundException, RemoteException {
try {
StreamRemoteCall var3 = (StreamRemoteCall)this.ref.newCall(this, operations, 0, 4905912898345647071L);
try {
ObjectOutput var4 = var3.getOutputStream();
var4.writeObject(var1);
var4.writeObject(var2);
} catch (IOException var5) {
throw new MarshalException("error marshalling arguments", var5);
}
this.ref.invoke(var3);
this.ref.done(var3);
} catch (RuntimeException var6) {
throw var6;
} catch (RemoteException var7) {
throw var7;
} catch (AlreadyBoundException var8) {
throw var8;
} catch (Exception var9) {
throw new UnexpectedException("undeclared checked exception", var9);
}
}
前面处理一些约定好的数据,比如host、port以及要绑定的对象和对象名称,我们直接定位到invoke那一句,invoke这里会把请求发出去,这里会调用Skeleton代理即sun.rmi.registry.RegistryImpl_Skel
类中的dispatch
方法来处理请求,需要注意的是,在这里之后是无法通过断点进入到sun.rmi.registry.RegistryImpl_Skel#dispatch
方法中的,看一篇文章说是在bind函数执行处sun.rmi.registry.RegistryImpl#bind
下一个断点,直接运行就可以得到调用栈,再回去找就行了。
我们定位查看重要逻辑代码:
switch(var3) {
case 0:
try { //bind方法
var11 = var2.getInputStream();
// readObject反序列化触发
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: //list()方法
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 { // lookup()方法
var10 = var2.getInputStream();
// readObject反序列化触发
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 { // rebind()方法
var11 = var2.getInputStream();
//readObject反序列化触发
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 { //unbind()方法
var10 = var2.getInputStream();
//readObject反序列化触发
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");
}
这里var3类型是int型,取值从0到4,依次对应bind/rebind/unbind/lookup/list等请求。从这个switch语句我们可以得知,Registry注册中心能够接收bind/rebind/unbind/lookup/list等请求,而在接收五类请求方法的时候,只有我们bind,rebind,unbind和lookup方法进行了反序列化数据调用readObject函数(再给自己挖个坑,后面补个java反序列化的内容),可能导致直接触发了反序列化漏洞产生。
需要说明的是lookup是从rmi client角度攻击rmi registry, 而bind/rebind/unbind是从rmi server角度攻击rmi registry
由于无法下载到对应的jdk版本,这里我无法演示,可以具体看看这个文章是如何利用该处反序列化进行实际操作的。
高版本的jdk中,进行了如下修复:
在8u121之后,在bind方法里面增加了一个checkAccess方法,该方法会检查是否为localhost,不是则会抛出下面这个异常:
catch (PrivilegedActionException var4) { throw new AccessException(var0 + " disallowed; origin " + var2 + " is non-local host"); }
不过我们看源码就知道反序列化语句readObject其实在bind调用之前就执行了,并没有什么用。
然后在8u141修改为在RegistryImpl_Skel中执行readObject之前就执行了checkAccess方法,这样bind,rebind,unbind就没失效了。
以我现在手上的java为例(java版本为1.8.0_391),
sun.rmi.registry.RegistryImpl_Skel
类的dispatch
方法中的switch语句对应处理bind请求的片段如下:case 0: RegistryImpl.checkAccess("Registry.bind"); try { var10 = (ObjectInputStream)var7.getInputStream(); var8 = SharedSecrets.getJavaObjectInputStreamReadString().readString(var10); var81 = (Remote)var10.readObject(); } catch (IOException | ClassNotFoundException | ClassCastException var78) { var7.discardPendingRefs(); throw new UnmarshalException("error unmarshalling arguments", var78); } finally { var7.releaseInputStream(); } var6.bind(var8, var81); try { var7.getResultStream(true); break; } catch (IOException var77) { throw new MarshalException("error marshalling return", var77); }
可以看到第一句就调用了checkAccess进行了限制。
0x03 小结
写这篇时我结合了网上的文章再看的源码,但是还是感觉其中的过程有些绕,对于rmi利用这块,有很多有待去学习。
参考文章:
《java安全漫谈》