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 exceptionSystem.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 exceptionSystem.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类。

不能序列化场景 备注
没有添加serialVersionUID 1)添加或者删除成员,改变成员的修饰符,类型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;}@Overridepublic String toString() {// TODO Auto-generated method stubString 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 exceptione.printStackTrace();}catch (IOException e) {// TODO: handle exceptione.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 exceptione.printStackTrace();}catch (ClassNotFoundException e) {// TODO: handle exceptione.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 objectint 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置falsebin.setBlockDataMode(false);}byte tc;// 从序列化信息中获取第一个字节while ((tc = bin.peekByte()) == TC_RESET) {bin.readByte();handleReset();}depth++;totalObjectRefs++;// 如果是对象的反序列化,这里tc=115,即0x73,所以走下面的TC_OBJECTtry {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 readthrow 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 objectif (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反射等基础知识也会慢慢记录上

java安全学习(一)相关推荐

  1. Java EE学习心得

    –Java EE学习心得   1.    称为编程专家的秘诀是: 思考-----编程--------思考------编程--.. 编程不能一步到位,不能一上来就编,必须先思考如何写,怎样写?然后再编程 ...

  2. java web学习项目20套源码完整版

    java web学习项目20套源码完整版 自己收集的各行各业的都有,这一套源码吃遍所有作业项目! 1.BBS论坛系统(jsp+sql) 2.ERP管理系统(jsp+servlet) 3.OA办公自动化 ...

  3. 初级java开发学习路线_成为初级全栈Web开发人员的10分钟路线图

    初级java开发学习路线 So you have started your journey into the world of web development. But what do you lea ...

  4. Java培训学习步骤有哪些

    最近几年,有很多学习java技术的同学都有过半途而废的想法,认为java零基础是很难学会的,其实出现这样的问题,最主要的原因就是学习方法有问题,下面小编整理的Java培训学习步骤,希望能够帮助大家更有 ...

  5. Java入门学习注意事项有哪些?

    想要学好java技术,做好学习规划路线和注意事项是非常重要的,尤其是零基础学员,Java涉及到的知识点非常多,我们需要制定合理的Java学习路线图,这样会事半功倍,下面小编和大家总结一下Java入门学 ...

  6. java培训学习阶段步骤讲解

    目前的培训机构行业比较热门的IT技术就是java技术,java技术在近几年广受关注,java所涉及的技术知识也比较广泛,下面小编就为大家详细的介绍一下java培训学习多有哪几个阶段? java培训学习 ...

  7. Java多线程学习处理高并发问题

    在程序的应用程序中,用户或请求的数量达到一定数量,并且无法避免并发请求.由于对接口的每次调用都必须在返回时终止,因此,如果接口的业务相对复杂,则可能会有多个用户.调用接口时,该用户将冻结. 以下内容将 ...

  8. 超硬核全套Java视频教程(学习路线+免费视频+配套资料)

    文内福利,扫码免费领取 Hello,各位锋迷们,我是小千.很多学习Java的小伙伴都在找的全套免费java视频教程,这里全都有,资料齐全,拿来吧你! 零基础学Java的学习路线图是怎样的?! 曾经写过 ...

  9. java正则表达式 1,Java正则表达式学习(1)

    Java正则表达式学习(一) 1.什么是正则表达式: 正则表达式(regular expressions) 是一种描述字符串集的方法,它是以字符串集中各种字符串的公有特征为依据的. 正则表达式可以用于 ...

  10. Java并发学习三:银行转账的死锁问题解决及示例

    Java并发学习系列文章:Java并发学习-博客专栏 今天在学习极客时间专栏:<Java并发编程实战> 从03 | 互斥锁(上):解决原子性问题到06 | 用"等待-通知&quo ...

最新文章

  1. virtualC++打开汇编语言代码
  2. UINavigationController 返回到各级目录
  3. 汽车全景标定(拼接)效果的检验方法
  4. 【杂谈】一招,同时可视化18个开源框架的网络模型结构和权重
  5. python漏洞检测脚本_URL重定向漏洞,python打造URL重定向漏洞检测脚本
  6. SQL Server之索引
  7. linux批量去掉文件名前缀,linux 批量删除某个前缀文件
  8. MySQL源码编译与初始化
  9. windows7原版iso镜像_一定收藏,常用操作系统原版下载地址整理,Win7 Win10 Deepin...
  10. SHELL脚本-猜数字游戏
  11. STM32标准外设库(标准库)官网下载方法,附带2021最新标准固件库下载链接
  12. 教你怎么学习arcgis软件①
  13. 如何快速统计多条线段的长度和?
  14. vue-Observe、Dep、Watcher
  15. android viewgroup点击变色,Android ViewGroup点击效果(背景色)
  16. Windows 2008 Server R2 桌面体验
  17. 11210怎么等于24_算24点
  18. python列表生成器语法_Python 列表生成式\生成器
  19. 2020-09-22
  20. 小博老师解析经典Java面试题-redirect和forward的区别

热门文章

  1. 山东初级消防设施操作员考前必背考点,模拟真题及答案
  2. AI顶级专家沈春华回国加盟浙大!
  3. ES学习笔记十-数据建模
  4. 用Python写一个量化交易策略
  5. Qt窗口设置成透明色方法(窗口设置成透明色结果显示成黑色的解决办法)
  6. hiredis的各种windows版本
  7. 以太坊区块链入门之实现简单DApp开发
  8. 面向对象程序设计课程设计:利用决策树方法判定西瓜质量
  9. GYM 101128H Sheldon Numbers
  10. 评中药:就这样被慢慢毒死