Java安全之反序列化(一)--基础篇

Java序列化基础:
 
   java是纯面向对象语言,在java的世界里所有东西都是对象。有些情况下,我们需要保存某一刻某个对象的信息来进行一些操作。因此才需要序列化机制,序列化机制将对象状态以二进制形式存储于文件系统中,然后可以在另一个jvm程序中用反序列化将对象再次恢复,可以有效地实现多平台之间的对象通信以及对象持久化存储。
  先直接来看菜鸟教程提供的简单demo:
  Employee是将被序列化的类
package fanxuliehua;

public class Employee implements java.io.Serializable{//所有java可序列化对象都必须直接或间接实现Serializable接口
    public String name;
    public String address;
    public transient int SSN;//transient关键字代表该属性不需要被序列化,不会被写进二进制文件中
    public int number;
    public void mailCheck()
    {
        System.out.println("Mailing a check to " + name + " " + address);
    }
}

  接下来执行序列化操作操作,将对象写入文件:

package fanxuliehua;

import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main {
    public static void main(String [] args)
       {
          Employee e = new Employee();
          e.name = "小鬼";
          e.address = "成都";
          e.SSN = 2333;
          e.number = 110;
          try
          {
             FileOutputStream fileOut =
             new FileOutputStream("D:\\/employee.ser");//序列化文件路径
             ObjectOutputStream out = new ObjectOutputStream(fileOut);
             out.writeObject(e);//序列化写入对象
             out.close();
             fileOut.close();
          }catch(IOException i)
          {
              i.printStackTrace();
          }
       }
}
  然后我们去打开这个employee.ser文件看看

  aced 0005是16进制流中java序列化对象的标志,通常会在TCP流量中出现,如果是base64传输java序列化对象的标志则是rO0AB开头(aced0005经过base64编码的结果)或者是YWNlZCA开头(fastjson的POC中就可以看见这种base64编码方式),如果是字节码数据,则数据有可能是sr java.xxx.xxx 的样子。

  接下来恢复这个对象:

package fanxuliehua;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;

public class Main {
    public static void main(String [] args)
       {
          Employee e = null;
          try
          {
             FileInputStream fileIn = new FileInputStream("D:\\/employee.ser");
             ObjectInputStream in = new ObjectInputStream(fileIn);
             e = (Employee) in.readObject();//readObject是从文件中读取对象的方法
             in.close();
             fileIn.close();
          }catch(IOException i)
          {
             i.printStackTrace();
             return;
          }catch(ClassNotFoundException c)
          {
             c.printStackTrace();
             return;
          }
          System.out.println("Deserialized Employee...");
          System.out.println("Name: " + e.name);
          System.out.println("Address: " + e.address);
          System.out.println("SSN: " + e.SSN);
          System.out.println("Number: " + e.number);
        }
}

  可以看见对象被成功恢复了,而且transitent关键字标记的属性并没有被序列化写入。

  那么反序列化漏洞是如何产生的呢?

  在java的反序列化机制中,会调用被反序列化的对象的readObject方法,当readObject方法写法存在风险的时候就会产生漏洞。除了readObject()方法可以读取被序列化的对象以外,还有readUnshared()方法可以读取对象,不过readUnshared()方法是不共享的,不允许后续的readObject和readUnshared方法调用这次反序列化得到的对象,因此使用情况比readObject方法少很多。
  修改一下Employee对象的readObject方法代码,被重写的readObject()方法会被优先调用。
package fanxuliehua;

import java.io.IOException;

public class Employee implements java.io.Serializable{//所有java可序列化对象都必须直接或间接实现Serializable接口
    public String name;
    public String address;
    public transient int SSN;//transient关键字代表该属性不需要被序列化,不会被写进二进制文件中
    public int number;
    public void mailCheck()
    {
        System.out.println("Mailing a check to " + name + " " + address);
    }
    public void readObject(java.io.ObjectInputStream in) throws ClassNotFoundException, IOException {//重写Serializable接口的readObject方法
        in.defaultReadObject();
        Runtime.getRuntime().exec("calc.exe");//执行命令
    }
}
  将Employee序列化过后再执行反序列化操作

  可以看见,反序列化的流程是正常执行的,命令也正常执行了。不过这只是java反序列化漏洞的一个demo,正常情况下,没有人会这样写代码。

Java反射机制基础:
 

  讲真正的反序列化漏洞之前,再简单讲一下java的反射机制,这个机制很重要,是绝大部分java大型框架的核心,比如spring,strust2,hibernate,mybatis等。咱们简单了解一下反射机制。

  反射 (Reflection) 是 Java 的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。通过反射,我们可以直接创建对象,即使这个对象的类型在编译期是未知的。
package fanxuliehua;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main {
    public static void main(String[] args) throws Exception{
        //最常见的加载数据库连接驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        //通过反射创建对象
        Class<?> cls=String.class;
        Object str=cls.newInstance();
        //通过反射获取一个带String参数的构造器
        Class<?> cls2=String.class;
        Constructor<?> con=cls2.getConstructor(String.class);
        Object str2=con.newInstance("123");
        //获取一个类的所有方法
        Method[] methods=cls2.getMethods();
        Method method=cls2.getMethod("toString");//获取特定的方法
        //获取所有成员变量
        Field[] fields=cls2.getFields();
        //调用toString方法
        Object obj2=method.invoke(str2, new Object[]{});
        System.out.println(obj2);
    }
}

  java反射机制非常强大,这里只是简单介绍一下,方便后续阅读POC,有兴趣的可以看看网上的详解。

Java远程基础:
 

   除了上面的java序列化基础知识和java反射机制基础知识以外,我们还需要学习一下java远程调用机制的基础知识。

Java RMI:
  java远程方法调用(Remote Method Invocation),一种用于实现远程过程调用(RPC)的java API,能直接传输序列化后的java对象。java RMI的目的就是要使运行在不同jvm中的对象之间的调用更加方便,客户机上运行的程序可以调用远程服务器上的对象。直接看demo。

  先定义一个远程接口

package rmitest;

import java.rmi.Remote;
//定义一个远程接口
public interface IHello extends Remote{
    /* 
     * 在java中,只要继承了Remote接口,即可成为存在于服务器端的远程对象,
     * 供客户端访问并提供一定服务,任何远程对象都必须直接或间接实现这个接口
     * 并且只有在继承了Remote接口的接口中定义的方法才可以被远程调用
     */
    public String sayHello(String name) throws java.rmi.RemoteException;
    public String showInfo() throws java.rmi.RemoteException;
}

  定义一个远程接口的实现类,用于创建对象并传输。

package rmitest;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
/*
 * 远程对象必须继承UnicastRemoteObject类
 */
public class HelloImpl extends UnicastRemoteObject implements IHello{
    private String info;
    protected HelloImpl(String msg) throws RemoteException{
        this.info=msg;
        System.out.println("初始化远程类");
    }
    
    private static final long serialVersionUID=4077329331699640331L;
    public String sayHello(String name) throws RemoteException{
        return "Hello "+name;
    }
    public String showInfo() throws RemoteException{
        return info;
    }
}
  编写服务端程序,绑定远程对象提供访问端口
package rmitest;

import java.rmi.registry.LocateRegistry;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
/*
 * 注册远程对象,向客户端提供远程对象服务
 * 远程对象是在远程服务器上注册的,客户端无法明确地知道远程服务器上的对象名称
 * 但是将远程对象注册到RMI service之后,客户端就可以访问到远程对象了
 */
public class HelloServer {
    public static void main(String[] args) {
        try {
            IHello hello=new HelloImpl("我是远程对象一号");
            LocateRegistry.createRegistry(1099);//RMI服务默认情况下会使用1099
            //将hello对象绑定到Registry服务的URL上
            java.rmi.Naming.rebind("rmi://localhost:1099/hello", hello);
            System.out.println("RMI is ready");
        } catch(Exception e){
            e.printStackTrace();
        }
    }
}

  编写客户端程序,请求远程对象

package rmitest;

import java.rmi.Naming;
import java.util.Hashtable;
import java.util.Properties;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

import fastjsontest.User;
//客户端向服务端请求远程对象服务
public class RMIClient {
    public static void main(String[] agrs) {
        try {
            //请求RMI Service上的远程对象
            IHello hello=(IHello) Naming.lookup("rmi://localhost:1099/hello");
            System.out.println(hello.sayHello("小鬼"));//调用远程对象的方法
            System.out.println(hello.showInfo());
        }catch(Exception e) {
            e.printStackTrace();
        }
    }
}

  在运行服务端程序后,再运行客户端程序向服务端请求远程对象。我们可以直接看见运行结果,成功调用了远程对象的方法。RMI有很多实现方式,这里也不详细介绍了,只需要知道在RMI的帮助下,能够实现跨jvm调用远程对象就行了。

JNDI:

   Java 命名与目录接口(Java Naming and Directory Interface)是J2EE(java web规范)中重要的规范之一。为开发人员提供了查找和访问各种命名和目录服务的通用、统一的接口。其实就相当于一个表或者索引,将名称和对象联系在了一起,并且可以通过指定的名称找到相对应的对象。

  JNDI和RMI常常集成到一起,用上面的例子,修改一下服务端和客户端代码,集成JNDI。

package rmitest;

import java.rmi.registry.LocateRegistry;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
//RMI+JDNI服务端代码
public class HelloServer {
    public static void main(String[] args) {
        try {
            //创建远程对象
            IHello hello=new HelloImpl("我是远程对象二号");
            //注册RMI服务端口
            LocateRegistry.createRegistry(1099);
            //设置JNDI属性
            Properties properties=new Properties();
            properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
            //设置RMI服务访问地址
            properties.setProperty(Context.PROVIDER_URL, "rmi://localhost:1099");
            //根据已设置的JNDI属性创建上下文
            InitialContext ctx = new InitialContext(properties);
            //将对象与命名绑定
            ctx.bind("hello", hello);
            System.out.println("Server is allready");
        } catch(Exception e){
            e.printStackTrace();
        }
    }
}
  客户端调用JNDI上下文,使用lookup函数直接查询与命名对应的远程对象
package rmitest;

import java.rmi.registry.LocateRegistry;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
//RMI+JDNI服务端代码
public class HelloServer {
    public static void main(String[] args) {
        try {
            //创建远程对象
            IHello hello=new HelloImpl("我是远程对象二号");
            //注册RMI服务端口
            LocateRegistry.createRegistry(1099);
            //设置JNDI属性
            Properties properties=new Properties();
            properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
            //设置RMI服务访问地址
            properties.setProperty(Context.PROVIDER_URL, "rmi://localhost:1099");
            //根据已设置的JNDI属性创建上下文
            InitialContext ctx = new InitialContext(properties);
            //将对象与命名绑定
            ctx.bind("hello", hello);
            System.out.println("Server is allready");
        } catch(Exception e){
            e.printStackTrace();
        }
    }
}

  JNDI也有很多实现方式,我们只是简单介绍了一下JNDI是什么作用,方便后续理解。

LDAP:

   LDAP全称为轻型目录访问协议(lightweight dirctory access protocol),在作用上跟RMI服务器类似,都是为了绑定资源和目录并且向客户端提供访问接口。

  测试过程中为了方便我们可以直接使用marshalsec简单创建一个LDAP服务器,来为易受攻击的JNDI的lookup方法提供一个绝对路径的LDAP url;

  将Poc.class放在本地8080端口的网站根目录下,再利用marshalsec转发,创建一个LDAP服务器

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8080/#Poc"

  marshalsec默认开启端口为1389,因此我们提供的ldap url应该是ldap://127.0.0.1:1389/Poc。

 

 

  以上的基础内容是为了方便后续理解Java安全问题,并没有深入研究这些协议,如果有兴趣的话可以再深入研究一下Java远程协议簇。

 

 

 

 

 

 

 

 

 

 

 

 

 

posted @ 2020-11-24 16:38  学安全的小鬼  阅读(297)  评论(0编辑  收藏  举报