java安全学习(一)

java安全学习

现在要开启java安全学习的坑了,想法是先从java最常见的安全漏洞入手,先把java漏洞成因和偏底层的原理掌握,再去跟一些主流框架的洞,最后尝试去分析等,也正好借此机会拜读一下p牛的java安全漫谈

java反序列化初步

基本概念

什么是java序列化和反序列化

Java 序列化(Serialization)是指把Java对象保存为二进制字节码的过程,是把 Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的 writeObject()方法可以实现序列化。

Java 反序列化(deserialization)是指把二进制码重新转换成Java对象的过程。把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject()方法用于反序列化。

什么时候需要用到java反序列化
当 Java 对象需要在网络上传输或者持久化存储到文件中时,就需要对 Java 对象进行序列化处理。

  • 当想把的内存中的对象保存到一个文件中或者数据库中时候

  • 当想用套接字在网络上传送对象的时候

  • 当想通过RMI传输对象的时候

如何实现java的序列化与反序列化
实现序列化其实非常简单,只需要将需要序列化的类实现java.io.serializable接口即可,而该接口没有任何方法需要重写,我认为可以把它理解成为一个标记,一旦实现这个接口,代表该类是可以进行反序列化的。但是需要注意的是,并不是任何一个类只要实现了该接口就能实现反序列化的,总结一些不能进行反序列化的情况:
Transient 关键字
transient修饰符仅适用于变量,不适用于方法和类。在序列化时,如果我们不想序列化特定变量以满足安全约束,那么我们应该将该变量声明为transient。执行序列化时,JVM会忽略transient变量的原始值并将默认值保存到文件中。因此,transient意味着不要序列化
Static
静态变量不是对象状态的一部分,因此它不参与序列化。所以将静态变量声明为transient变量是没有用处的。
serialVersionUID
关于serialVersionUID,需要深入理解一下:指序列化的版本号,凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量
如果没有指定序列化版本号时,会出现如下警告提示:

那么serialVersionUID起到一个什么样的作用呢,下面通过一个例子来进行说明。

//Students.java
package java_learn;

import java.io.Serializable;

public class Students implements Serializable{
	private String name;
	private String sno;
	private String sex;
	private int height;
	public transient int grade;
	public Students(String name,String sno,String sex,int grade) {
		this.name = name;
		this.sno = sno;
		this.sex = sex;
		this.grade = grade;
	}
	public void getInformation() {
		System.out.print("name: " + this.name +" StudentID: "+ this.sno + " grade: " + this.grade);
	}
	
}

Serialize.java:

package java_learn;

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

public class Serialize {
	public static void main(String args[]) {
		Students student = new Students("Crispr", "2019111111", "male", 2);
		try {
			FileOutputStream fileOut = new FileOutputStream("student.ser");
			ObjectOutputStream oos = new ObjectOutputStream(fileOut);
			oos.writeObject(student);
			oos.close();
			fileOut.close();
			System.out.print("Data is serialized successfully!");
		}catch (Exception e) {
			// TODO: handle exception
			System.out.print(e.toString());
		}
	}
}

Unserialize.java:

package java_learn;

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

public class unserialize {
	public static void main(String args[]) {
		try {
			FileInputStream fileIn = new FileInputStream("Student.ser");
			ObjectInputStream ois = new ObjectInputStream(fileIn);
			Students Stu = (Students)ois.readObject();
			ois.close();
			fileIn.close();
			System.out.println("data is unserialized successfully!");
			Stu.getInformation();
		}catch (Exception e) {
			// TODO: handle exception
			System.out.print(e.toString());
		}
		
	}
}

当先进行serialize在执行unserialize时,我们可以发现反序列化成功,但由于garde是临时的,并不会存入序列化数据中,因此反反序列化时默认值为0

而当我们在没有添加serialVersionUID时,如果添加Students类的属性(不管是私有还是public)或者是添加一个类方法等,而直接利用之前序列化的数据再进行反序列化时,便会出现如下错误:

意思就是说,文件流中的class和classpath中的class,也就是修改过后的class,不兼容了,处于安全机制考虑,程序抛出了错误,并且拒绝载入。那么如果我们真的有需求要在序列化后添加一个字段或者方法呢?应该怎么办?那就是自己去指定serialVersionUID。在例子中,没有指定Students类的serialVersionUID的,那么java编译器会自动给这个class进行一个摘要算法,类似于指纹算法,只要这个文件多一个空格,得到的UID就会截然不同的,可以保证在这么多类中,这个编号是唯一的。所以,添加了一个字段后,由于没有显指定 serialVersionUID,编译器又为我们生成了一个UID,当然和前面保存在文件中的那个不会一样了,于是就出现了2个序列化版本号不一致的错误。因此,只要我们自己指定了serialVersionUID,就可以在序列化后,去添加一个字段,或者方法,而不会影响到后期的还原,还原后的对象照样可以使用,而且还多了方法或者属性可以用。

在当我设置serialVersionUID后,重新执行序列化操作,再增加一个私有属性和类方法后,再次进行反序列化时,此时因为已经显示声明了serialVersionUID因此反序列化时解析了该UID便不会在生成一个UID,此时得到的类还是Students类。

不能序列化场景备注
没有添加serialVersionUID1)添加或者删除成员,改变成员的修饰符,类型2)添加或者删除方法,改变方法的修饰符,返回类型java编译器会根据类的成员,方法生成一个serialVersionUID如果修改了方法实现,是可以进行反序列化的
添加serialVersionUID只要serialVersionUID不一致,肯定不能被序列化

显式地定义serialVersionUID有两种用途:

  • 1、在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID;
  • 2、 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。

自定义序列化与反序列化

其实自定义序列化与反序列化的过程,也就是对readObjectwriteObject方法重写的过程,在重新方法中加入需要的逻辑,下面通过一个例子来自定义readObject方法达到执行代码的目的:

package java_learn;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class eval_ser implements Serializable{
	private static final long serialVersionUID = -5215701594592700115L;
	private int id;
	public eval_ser(int id) {
		this.id = id;
	}
	
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		String str = "toString function is overridden";
		return str;
	}
	
	/*重写readObject方法来实现命令执行,注意重写方法时参数和返回类型以及方法的属性必须和被重写的方法保持一致*/
	private void readObject(ObjectInputStream in) throws ClassNotFoundException, IOException {
		/*使用原生的readObject方法*/
		in.defaultReadObject();
		Runtime.getRuntime().exec("calc.exe");
		System.out.println("eval_ser.readObject() is overridden");
	}
	
	
	public static void main(String args[]) throws FileNotFoundException, IOException, ClassNotFoundException {
		eval_ser test = new eval_ser(1);
		test.unserialize();
		
	}
	
	public void serialize() throws IOException,FileNotFoundException{
		try {
			FileOutputStream fileOut = new FileOutputStream("eval.ser");
			ObjectOutputStream oos = new ObjectOutputStream(fileOut);
			oos.writeObject(this);
			oos.close();
			fileOut.close();
			System.out.println("successful serialize");
		}catch (FileNotFoundException e) {
			// TODO: handle exception
			e.printStackTrace();
		}catch (IOException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
	
	public void unserialize() throws IOException,FileNotFoundException, ClassNotFoundException{
		try {
			FileInputStream fileIn = new FileInputStream("eval.ser");
			ObjectInputStream ois = new ObjectInputStream(fileIn);
			Object obj = ois.readObject();
			System.out.println(obj);
			System.out.println("successful unserialize");
			fileIn.close();
			ois.close();
		}catch (IOException e) {
			// TODO: handle exception
			e.printStackTrace();
		}catch (ClassNotFoundException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
	}
	
}

这里还是会存在一个小的疑惑,就是重写readObject方法时必须是private才会进入到重写的readObject中,否则不会进入重写方法,为此我们需要一探ObjectInputStream的源码:

 public ObjectInputStream(InputStream in) throws IOException {
        verifySubclass();
        bin = new BlockDataInputStream(in);
        handles = new HandleTable(10);
        vlist = new ValidationList();
        enableOverride = false;
        readStreamHeader();
        bin.setBlockDataMode(true);
    }

protected ObjectInputStream() throws IOException, SecurityException {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
        bin = null;
        handles = null;
        vlist = null;
        enableOverride = true;
    }

存在两个构造方法,如果构造方法为空,则enableOverride=true否则为false,因为后续会根据这个属性的值来选择readObject方法,一般情况下该构造方法都有参数,因此我们重点看readObject0方法

public final Object readObject()
        throws IOException, ClassNotFoundException
    {
        if (enableOverride) {
            return readObjectOverride();
        }

        // if nested read, passHandle contains handle of enclosing object
        int outerHandle = passHandle;
        try {
            Object obj = readObject0(false);
            handles.markDependency(outerHandle, passHandle);
            ClassNotFoundException ex = handles.lookupException(passHandle);
            if (ex != null) {
                throw ex;
            }
            if (depth == 0) {
                vlist.doCallbacks();
            }
            return obj;
        } finally {
            passHandle = outerHandle;
            if (closed && depth == 0) {
                clear();
            }
        }
    }

readObject0方法:

private Object readObject0(boolean unshared) throws IOException {
    boolean oldMode = bin.getBlockDataMode();
    if (oldMode) {
        int remain = bin.currentBlockRemaining();
        if (remain > 0) {
            throw new OptionalDataException(remain);
        } else if (defaultDataEnd) {
            /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
            throw new OptionalDataException(true);
        }
        // 这里将BlockDataMode置false
        bin.setBlockDataMode(false);
    }

    byte tc;
    // 从序列化信息中获取第一个字节
    while ((tc = bin.peekByte()) == TC_RESET) {
        bin.readByte();
        handleReset();
    }

    depth++;
    totalObjectRefs++;
    // 如果是对象的反序列化,这里tc=115,即0x73,所以走下面的TC_OBJECT
    try {
        switch (tc) {
            case TC_NULL:
                return readNull();

            case TC_REFERENCE:
                return readHandle(unshared);

            case TC_CLASS:
                return readClass(unshared);

            case TC_CLASSDESC:
            case TC_PROXYCLASSDESC:
                return readClassDesc(unshared);

            case TC_STRING:
            case TC_LONGSTRING:
                return checkResolve(readString(unshared));

            case TC_ARRAY:
                return checkResolve(readArray(unshared));

            case TC_ENUM:
                return checkResolve(readEnum(unshared));

            case TC_OBJECT:
                return checkResolve(readOrdinaryObject(unshared));

            case TC_EXCEPTION:
                IOException ex = readFatalException();
                throw new WriteAbortedException("writing aborted", ex);

            case TC_BLOCKDATA:
            case TC_BLOCKDATALONG:
                if (oldMode) {
                    bin.setBlockDataMode(true);
                    bin.peek();             // force header read
                    throw new OptionalDataException(
                        bin.currentBlockRemaining());
                } else {
                    throw new StreamCorruptedException(
                        "unexpected block data");
                }

            case TC_ENDBLOCKDATA:
                if (oldMode) {
                    throw new OptionalDataException(true);
                } else {
                    throw new StreamCorruptedException(
                        "unexpected end of block data");
                }

            default:
                throw new StreamCorruptedException(
                    String.format("invalid type code: %02X", tc));
        }
    } finally {
        depth--;
        bin.setBlockDataMode(oldMode);
    }
}

再进入readOrdinaryObject:

private Object readOrdinaryObject(boolean unshared)
    throws IOException
{
    if (bin.readByte() != TC_OBJECT) {
        throw new InternalError();
    }
    // name = com.xxx.xxx.xxx.User
    // suid = 1
    // filed = User中的属性名及类型
    ObjectStreamClass desc = readClassDesc(false);
    desc.checkDeserialize();

    Class<?> cl = desc.forClass();
    if (cl == String.class || cl == Class.class
            || cl == ObjectStreamClass.class) {
        throw new InvalidClassException("invalid class descriptor");
    }

    Object obj;
    try {
        obj = desc.isInstantiable() ? desc.newInstance() : null;
    } catch (Exception ex) {
        throw (IOException) new InvalidClassException(
            desc.forClass().getName(),
            "unable to create instance").initCause(ex);
    }

    passHandle = handles.assign(unshared ? unsharedMarker : obj);
    ClassNotFoundException resolveEx = desc.getResolveException();
    if (resolveEx != null) {
        handles.markException(passHandle, resolveEx);
    }

    if (desc.isExternalizable()) {
        readExternalData((Externalizable) obj, desc);
    } else {
        // 除非实现Externalizable接口,否则走这个分支去反序列化obj对象
        readSerialData(obj, desc);
    }

    handles.finish(passHandle);

    if (obj != null &&
        handles.lookupException(passHandle) == null &&
        desc.hasReadResolveMethod())
    {
        Object rep = desc.invokeReadResolve(obj);
        if (unshared && rep.getClass().isArray()) {
            rep = cloneArray(rep);
        }
        if (rep != obj) {
            // Filter the replacement object
            if (rep != null) {
                if (rep.getClass().isArray()) {
                    filterCheck(rep.getClass(), Array.getLength(rep));
                } else {
                    filterCheck(rep.getClass(), -1);
                }
            }
            handles.setObject(passHandle, obj = rep);
        }
    }

    return obj;
}

再进入到readSerialData这个函数里面:

private void readSerialData(Object obj, ObjectStreamClass desc)
    throws IOException
{
    //从父类开始
    ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
    for (int i = 0; i < slots.length; i++) {
        ObjectStreamClass slotDesc = slots[i].desc;

        if (slots[i].hasData) {
            if (obj != null &&
                slotDesc.hasReadObjectMethod() &&
                handles.lookupException(passHandle) == null)
            {
                ...
                    //如果有readObject()执行
                    slotDesc.invokeReadObject(obj, this);
                ...
            } else {
                //如果没有的话就执行默认的反序列化,与序列化类似
                defaultReadFields(obj, slotDesc);
            }
            if (slotDesc.hasWriteObjectData()) {
                skipCustomData();
            } else {
                bin.setBlockDataMode(false);
            }
        } else {
            if (obj != null &&
                slotDesc.hasReadObjectNoDataMethod() &&
                handles.lookupException(passHandle) == null)
            {
                slotDesc.invokeReadObjectNoData(obj);
            }
        }
    }
}

readSerialData中比较关键的是:

if(slotDesc.hasReadObjectMethod())

slotDesc.hasReadObjectMethod()获取的是readObjectMethod这个属性,如果反序列化的类没有重写readobject(),那么readObjectMethod这个属性就是空,如果这个类重写了readobject(),那么就会进入到if之中的

slotDesc.invokeReadObject(obj, this);

通过一张图进行完整说明:

虽然写到这里,流程是清晰了不少,但是还是看了个寂寞,自己动手丰衣足食,跟着debug了一遍,这才明白为啥需要使用private来修饰:
程序整个调用链如下图:

继续看:

发现在整个过程中,会通过反射的形式来调用重写的privatereadObject方法,如果是设置为public时,再检测是否重写read Object方法时就已经返回false,接下来就是直接调用defaultReadObject方法,至于为什么这样设计,这个解释我感觉非常恰当:

关于readObject()/ writeObject()是私有的,这里是交易:如果你的类Bar扩展了一些类Foo; Foo还实现了readObject()/ writeObject(),而Bar也实现了readObject()/ writeObject().
现在,当Bar对象被序列化或反序列化时,JVM需要自动为Foo和Bar调用readObject()/ writeObject()(即,不需要显式调用这些超类方法).但是,如果这些方法不是私有的,那么它将成为方法重写,并且JVM不能再调用子类对象上的超类方法.因此他们必须是私人的!

最后来一个效果图:

java反射等基础知识也会慢慢记录上

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值