java反序列化入门_Java安全之反序列化(一)--基础篇

本文介绍了Java反序列化的基本操作,包括序列化和反序列化的过程。通过示例展示了如何序列化一个Employee对象,并在反序列化时执行自定义的readObject方法,从而执行命令。文章还提到了反序列化漏洞的产生,即当readObject方法存在风险时。此外,文章简述了Java反射机制,强调其在大型框架中的核心地位,并简单讲解了Java远程调用的基础,包括RMI和JNDI的概念及其基本使用。最后提到了LDAP协议在安全场景中的应用。
摘要由CSDN通过智能技术生成

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

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文件看看

452f3b4ccea06f941f41e60b081f9641.png

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);

}

}

ce077e323dfd769faebed4e6706538f2.png

可以看见对象被成功恢复了,而且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序列化过后再执行反序列化操作

fea6a2fecfe2913eacaef85f2fe04435.png

可以看见,反序列化的流程是正常执行的,命令也正常执行了。不过这只是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();

}

}

}

2425f368e01dc5e46fc640b8bef6db9c.png

在运行服务端程序后,再运行客户端程序向服务端请求远程对象。我们可以直接看见运行结果,成功调用了远程对象的方法。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远程协议簇。

14031068.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值