java安全学习(一)


  少年先疯队队长      90   
  2021-01-13      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接口的类都有一个表示序列化版本标识符的静态变量
    如果没有指定序列化版本号时,会出现如下警告提示:

    java安全学习(一) - 第1张

    那么 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

java安全学习(一) - 第2张

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

java安全学习(一) - 第3张

意思就是说,文件流中的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);

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

java安全学习(一) - 第4张

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

java安全学习(一) - 第5张

继续看:

java安全学习(一) - 第6张

发现在整个过程中,会通过反射的形式来调用重写的 privatereadObject 方法,如果是设置为 public 时,再检测是否重写 read Object 方法时就已经返回 false ,接下来就是直接调用 defaultReadObject方法 ,至于为什么这样设计,这个解释我感觉非常恰当:
关于readObject()/ writeObject()是私有的,这里是交易:如果你的类Bar扩展了一些类Foo; Foo还实现了readObject()/ writeObject(),而Bar也实现了readObject()/ writeObject().
现在,当Bar对象被序列化或反序列化时,JVM需要自动为Foo和Bar调用readObject()/ writeObject()(即,不需要显式调用这些超类方法).但是,如果这些方法不是私有的,那么它将成为方法重写,并且JVM不能再调用子类对象上的超类方法.因此他们必须是私人的!

最后来一个效果图:

java安全学习(一) - 第7张

java反射等基础知识也会慢慢记录上
作者:Crispr-bupt

ps:以上是java安全学习(一)全部内容,希望文章能够帮你解决java安全学习(一)所遇到的游戏开发问题。
本文收录在 游戏编程 🕹️ - 学习Java专题,分享走一走~

猜你喜欢 全系列


您可以在登录后,发表评论




    关于作者
    游戏开发者 - 103
  • 少年先疯队队长
  • 码神
  • 498 文章  √   5 提问  ?
    此作者缺少注释。


    目录