Hibernate 是 Java 的一个对象关系映射(ORM)框架,使用 GNU 开源协议。该利用链以 HashMap 为入口点,通过 hibernate-core 包中的多个类及其方法构成调用路径,最终执行事先写入到 TemplatesImpl 类中的命令。
动态字节码
ysoserial 在构造 payload 时,利用了 Java 的动态字节码生成的技术,所以我们先稍微了解一下。
众所周知,Java 是一门需要编译的语言,JVM 读取并运行的是编译后的 .class
字节码文件。但有时候我们想要在程序运行中动态修改一些代码,比如热补丁、接口升级、IDE 在调试时读取或修改变量等等需求。如果每次都要把程序停掉再重新编译,显然不太现实,而通过 Java 动态字节码技术,就可以直接修改字节码文件,再通过相关接口加载到 JVM 中,实现运行时的代码修改。
常见的动态字节码修改有两种方式:
- ASM,可以直接操作字节码,执行效率高,相对的门槛也比较高,需要对 Java 的字节码文件有所了解,熟悉 JVM 的编译指令。
- Javassit,由东京技术学院的 Shigeru Chiba 创作的开源类库,提供了更抽象的 API 来对字节码进行操作。Hibernate1 利用链中也正是使用这个类库来构造 payload。
Javassit demo
下面的示例代码通过 Javassit 构造了一个 Someone 类,并添加了方法和属性。然后将该类实例化,并调用其 toString 方法。
示例代码
import javassist.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class demo {
public static void main(String[] args) {
ClassPool classPool = ClassPool.getDefault();
//定义Someone类
CtClass ccSomeone = classPool.makeClass("Someone");
try {
//定义成员变量name
CtClass fieldType = classPool.get("java.lang.String");
CtField cfName = new CtField(fieldType, "name", ccSomeone);
cfName.setModifiers(Modifier.PRIVATE); //用private修饰name
ccSomeone.addField(cfName, CtField.Initializer.constant("init")); //添加name到Someone中,初始值为init
//定义构造方法
CtClass[] parameters = new CtClass[]{classPool.get("java.lang.String")}; //参数为String类型
CtConstructor constructor = new CtConstructor(parameters, ccSomeone);
String body = "{this.name=$1;}"; //方法体,$1表示的第一个参数
constructor.setBody(body);
ccSomeone.addConstructor(constructor); //设置为Someone的构造方法
//setName和getName方法
ccSomeone.addMethod(CtNewMethod.setter("setName",cfName));
ccSomeone.addMethod(CtNewMethod.getter("getName",cfName));
//toString 方法
CtClass returnType = classPool.get("java.lang.String"); //返回类型为 String
String methodName = "toString";
CtMethod cmToString = new CtMethod(returnType, methodName, null, ccSomeone);
cmToString.setModifiers(Modifier.PUBLIC); //用 public 修饰
String methodBody = "{return \"My name is \"+$0.name;}"; //方法体,$0 表示 this
cmToString.setBody(methodBody);
ccSomeone.addMethod(cmToString);
//获取 Someone 类的实例,设置 name 属性,调用 toString 方法
Class clazz = ccSomeone.toClass();
Constructor cons = clazz.getConstructor(String.class);
Object someone = cons.newInstance("zrquan"); //通过构造方法设置 name
Method toString = clazz.getMethod("toString");
System.out.println(toString.invoke(someone));
} catch (NotFoundException | CannotCompileException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
构造 payload
我们先看一下最主要的 getObject 方法,这个方法完成了对要执行的命令的“包装”,并返回包含该命令的对象。
public Object getObject ( String command ) throws Exception {
Object tpl = Gadgets.createTemplatesImpl(command);
Object getters = makeGetter(tpl.getClass(), "getOutputProperties");
return makeCaller(tpl, getters);
}
其中 makeGetter 和 makeCaller 都是内部函数,而 makeGetter 其实只起到路由作用,真正的逻辑代码在内部函数 makeHibernate4Getter 和 makeHibernate5Getter 中。
首先跟进一下 createTemplatesImpl 方法:
该方法先对系统属性 properXalan 进行了判断——如果返回 false,则通过反射机制获取 TemplatesImpl、AbstractTranslet、TransformerFactoryImpl 三个类的全局限定类名,将其作为参数,调用重载的 createTemplatesImpl 方法;如果返回 true,就使用上述三个类的非全局限定类名作为参数调用重载方法。
在重载的 createTemplatesImpl 方法中,通过 Javassit 库创建一个 StubTransletPayload 类,并将我们要执行的命令添加到这个类的静态代码块,当这个类被加载时,我们写入的命令就会执行。
可以看到在 116 行,调用 CtClass.makeClassInitializer()
创建一个空的静态代码块,再通过 insertAfter()
将命令插入到静态代码块末尾。
代码的 119 和 120 行给 StubTransletPayload 设置了父类 AbstractTranslet,但其实没有必要,因为 StubTransletPayload 本身就继承了 AbstractTranslet。
方法的最后将 StubTransletPayload 实例转换成 byte 数组,赋给 templates 变量的 _bytecodes
属性,并设置了 _name
和 _tfactory
属性。
返回的 templates 变量是 TemplatesImpl 类的实例,执行完 createTemplatesImpl 后,它的结构大致如下图所示:
接下来执行的代码是 makeGetter 方法:
Object getters = makeGetter(tpl.getClass(), "getOutputProperties");
首先通过系统属性判断当前的 hibernate 版本,ysoserial 使用的是 4.3 版本,所以直接跳到 makeHibernate4Getter 函数。
这一部分比较简单,通过反射机制创建了一个 BasicPropertyAccessor$BasicGetter
实例,并赋值了 clazz、method、propertyName 属性,然后放到 Getter 数组中。类图大致如下:
最后执行的是 makeCaller(tpl, getters)
,通过反射机制进行一系列生成实例、赋值属性的操作,然后返回一个 HashMap 对象。
Reflections 类封装了一些反射机制的操作,注意到生成 PojoComponentTuplizer 实例时调用的是 createWithoutConstructor 方法,实际上会使用 Object 类的构造方法,所以实例中的属性基本都是 null。接着将之前构造的 BasicPropertyAccessor$BasicGetter
赋值给 getters 属性。
生成 ComponentType 实例时,使用的是其父类 AbstractType 的默认构造方法,然后赋值了 componentTuplizer、propertySpan、propertyTypes 三个属性。
Gadgets.makeMap(v1, v2)
将两个相同的 TypedValue 实例写入到 HashMap 中,payload 的构造到此就完成了,当目标应用反序列化这个 HashMap 对象时,我们写入的命令就会执行。
HashMap 的类图大致如下:
反序列化过程
使用 jdk 反序列化 HashMap 对象,自然会调用其 readObject 方法,所以以该方法作为入口点,调试分析 HashMap 对象反序列化的过程。
如下图,在 readObject 方法中,局部变量 mappings 的值为 size 属性的值 2:
一直执行到方法最后的 for 循环,从注释可以知道在这个循环中取出所有 key 和 value,并保存到 HashMap 对象中。这里用到了上面的 mappings 变量,如果 mappings 变量是 0,就无法进入循环了。
跟进最后一行的 hash(key)
,这里的 key 是 TypedValue 类的对象。
调用 TypedValue.hashCode
方法:
public int hashCode() {
return (Integer)this.hashcode.getValue();
}
其中 this.hashcode
属性是一个匿名内部类:
private void initTransients() {
this.hashcode = new ValueHolder(new DeferredInitializer<Integer>() {
public Integer initialize() {
return TypedValue.this.value == null ? 0 : TypedValue.this.type.getHashCode(TypedValue.this.value);
}
});
}
可以看到匿名内部类中有一个 initialize 方法,这个方法会在 getValue 方法中被调用。从上面的类图可以很清楚地看到,TypedValue 的 value 属性和 type 属性分别是 TemplatesImpl 对象和 ComponentType 对象。
继续跟进代码的执行过程:
执行到 PojoComponentTuplizer 的 getPropertyValue 方法,并获取我们之前构造的 BasicPropertyAccessor$BasicGetter
对象,执行其 get 方法,参数是我们传入的 TemplatesImpl 对象。
通过反射机制调用 TemplatesImpl.getOutputProperties
方法:
跟进 newTransformer 方法:
然后进入我们的最终的目标方法 getTransletInstance:
之所以在开始设置 TemplatesImpl 的 _name
属性,就是为了通过这里的 if 判断。
此时 _class
的值是 null,调用 defineTransletClasses 方法将 _bytecodes
中的每个 byte[]
数组还原成一个 Class 对象,写到 _class
中。
可见此时的 _class[0]
是我们通过 Javassit 库构造的 StubTransletPayload 类,它的静态代码块保存着我们要执行的命令。
紧接着实例化了这个类,成功执行命令。
这里还留意到 _class[1]
也是我们构造的 Foo 类,然而这个类似乎对过程没什么影响,不太明白构造这个类的用意🤔