JDK7u21反序列化Gadgets
原文https://l3yx.github.io/2020/02/22/JDK7u21%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Gadgets/#more
一开始是学习FastJson反序列化的POC,然后发现欠缺不少知识,遂来补一下,收获良多,总结下笔记
所谓JDK反序列化Gadgets就是不同于利用Apache-CommonsCollections这种外部库,而是只利用JDK自带的类所构造的
先下载并配置好JDK7u21
Javassist
为了理解POC构造过程,还需要学习一些前置知识,Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含 Java 类或接口,Javassist 就是一个用来 处理 Java 字节码的类库
pom.xml
1
|
<dependency>
|
例如
1
|
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
|
将D:/Evil/Evil.calss拖入IDEA即可反编译,可以看见javassist动态构建出了如下类
至于为什么要继承AbstractTranslet,和构造函数中写命令执行的payload就涉及到下面POC的构造,暂时只需要了解javassist的大概功能
TemplatesImpl
之前参考网上分析文章的POC是从ysoserial中修改得来的,代码中使用了ysoserial的一些类,我修改了一下将POC核心部分单独提取出来方便理解
1
|
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
|
主要就是利用了TemplatesImpl
,向其中的_bytecodes
属性赋值了一个恶意类,最终该恶意类被实例化并且调用了构造函数中的命令执行payload。javassist在这里的作用呢其实主要就是构建这么一个恶意类,并且得到其字节码用以给TemplatesImpl
相关属性赋值,所以可以自行编译一个恶意类并读入字节码来使用
但会发现这里其实反序列化TemplatesImpl
后还需要调用getOutputProperties()
方法才能触发,不过在FastJson中已经可以形成完整利用链
在getOutputProperties()
函数下断点,跟踪一下执行过程
强制进入该函数
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getOutputProperties
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#newTransformer
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#getTransletInstance
这里得到POC中两项属性的构造条件,即_name
不能为null,_class
为null,然后进入defineTransletClasses()
其实最终的触发点就在380行_class[_transletIndex].newInstance()
,defineTransletClasses()
是对_class
和_transletIndex
赋值
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl#defineTransletClasses
代码比较长这里就直接复制出来了
1
|
private void defineTransletClasses()
|
defineTransletClasses()
执行完以后,回到getTransletInstance()
,此时_class[_transletIndex]
已经为Evil
类的一个类对象,调用newInstance()
实例化Evil
即可触发该类构造函数或者静态代码块中的代码
所以总结以上条件,便可理解TemplatesImpl
的构造
1
|
setFieldValue(templates,"_bytecodes",targetByteCodes);
|
动态代理
以上POC是需要反序列化TemplatesImpl
类并调用其getOutputProperties()
方法才能触发,即可以放入FastJson的反序列化处,但若没有触发getOutputProperties()
的点,就需要寻找其他手段
代理是为了在不改变目标对象方法的情况下对方法进行增强,比如,我们希望对方法的调用增加日志记录,或者对方法的调用进行拦截
假设有一个Person
类实现了IPerson
接口中的say
方法,但现在要在say
方法前后实现一些逻辑,那么借助动态代理实现如下
1
|
import java.lang.reflect.InvocationHandler;
|
AnnotationInvocationHandler
AnnotationInvocationHandler
就是一个InvocationHandler
的实现类,也在下面的POC中起到关键作用
先贴出整理好的POC
1
|
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
|
可见最后unserialize(obj)
只是反序列化了一个LinkedHashSet
类就触发了命令执行
Java在反序列化的时候会调用ObjectInputStream
类的readObject()
方法,如果被反序列化的类重写了readObject()
,那么该类在进行反序列化时,Java会优先调用重写的readObject()
方法
LinkedHashSet
没有readObject()
但是继承自HashSet
HashSet
实现了Serializable
接口并且有readObject()
方法,所以在反序列化LinkedHashSet
时会调用其父类HashSet
的readObject()
,可以在该函数处下断点运行POC进一步跟踪调试
java.util.HashSet#readObject
到309行的逻辑是将POC中add到set
的templates
和proxy
加入到map
中,
PRESENT
是一个常量,就是一个新的object对象
继续跟进put
方法,会在第二次调用map.put
时进入下面的475行的位置,即现在传入的key
是proxy
java.util.HashMap#put
这段代码本意是判断最新的元素是否已经存在的元素,如果不是已经存在的元素,就插入到table中,e.key为前一个元素即templates
,key为当前元素proxy
table[i]
就是一个键为我们构造的templates
的Map
当前的e.key
和key
,一个是templates
,另一个是POC中的proxy
,显然不同,(k = e.key) == key
为false
这条链想要完成是需要进入key.equals(k)
的,依据短路特性,那么必须要e.hash == hash
为true,也就是需要满足 hash(templates)== hash(proxy)
,看起来貌似不可能,但漏洞作者确实做到了(大写的佩服)
这里hash的绕过方法就暂时放在下面,先接着跟踪key.equals(k)
由于POC中使用动态代理,这里调用Templates.equals()
就会进入handler
的invoke
sun.reflect.annotation.AnnotationInvocationHandler#invoke
var1
就是上图中的key
,var2
是equals
方法对象,var3
是传入的参数数组,即上图中的k
(TemplatesImpl)
继续跟入equalsImpl
sun.reflect.annotation.AnnotationInvocationHandler#equalsImpl
分析之前先看一下这个类的相关方法和属性
首先是构造函数
sun.reflect.annotation.AnnotationInvocationHandler#AnnotationInvocationHandler
在构造handler时ctor.newInstance(Templates.class, map)
即这里的this.type
和this.memberValues
分别是Templates.class
和map
sun.reflect.annotation.AnnotationInvocationHandler#getMemberMethods
并未对this.memberMethods
赋值,所以这里进入if分支,最后返回的是this.type
的所有方法,即Templates
的所有方法
sun.reflect.annotation.AnnotationInvocationHandler#asOneOfUs
判断var1
对象若是一个AnnotationInvocationHandler
实例的话则转换为AnnotationInvocationHandler
然后接着看equalsImpl
1
|
private Boolean equalsImpl(Object var1) {//var1是POC中构造的templates
|
既然调用了Templates
中的所有方法,自然包括getOutputProperties()
,即完成了命令执行
Hash绕过
java.util.HashMap#hash
hash()
中调用了对象本身的hashCode()
调用hash(templates)
的时候,这个类没有重写,调用的是templates
默认的hashCode()
方法
当调用hash(proxy)
的时候,则会跳到AnnotationInvocationHandler.invoke()
sun.reflect.annotation.AnnotationInvocationHandler#invoke
sun.reflect.annotation.AnnotationInvocationHandler#hashCodeImpl
该方法会从memberValues
中进行遍历,并且依次计算key.hashCode()
,而这个memberValues
是我们在初始化AnnotationInvocationHandler
的时候传入的map
sun.reflect.annotation.AnnotationInvocationHandler#memberValueHashCode
所以
var1=0; var1 += 127 * ((String)var3.getKey()).hashCode() ^ memberValueHashCode(var3.getValue())
相当于
var1 = 127 * map中键的hashCode ^ map中值的hashCode
POC中构造map.put("f5a5a608", templates)
,而字符串的hashCode为0
所以
var1 = 127 * 0 ^ templates的hashCode
var1 = templates的hashCode
map.put的位置问题
仔细观察POC会发现,并没有在创建一个HashMap后就立即插入数据,而是把map.put("f5a5a608", templates)
放在了set.add
之后
如果放在set.add
之前会直接在本地触发命令执行,并且得到的序列化之后的数据不能反序列化成功
java.util.HashSet#add
这是因为add方法中会直接调用map.put,然后后面的过程就同之前分析的一致了