Java安全之反序列化(一)--基础篇
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(); } } }
接下来恢复这个对象:
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关键字标记的属性并没有被序列化写入。
那么反序列化漏洞是如何产生的呢?
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");//执行命令 } }
可以看见,反序列化的流程是正常执行的,命令也正常执行了。不过这只是java反序列化漏洞的一个demo,正常情况下,没有人会这样写代码。
讲真正的反序列化漏洞之前,再简单讲一下java的反射机制,这个机制很重要,是绝大部分java大型框架的核心,比如spring,strust2,hibernate,mybatis等。咱们简单了解一下反射机制。
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远程调用机制的基础知识。
先定义一个远程接口
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(); } } }
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远程协议簇。