反序列化类加载
反序列化的类加载
前面关于虚拟机类加载机制只是参考《深入理解Java虚拟机》这本书理解了个大概。具体的细节比如双亲委派的操作也没有进入代码中调试。为什么需要整理关于Java反序列化时的类加载,因为有一些CTF考题,以及一些反序列化漏洞都会涉及到类加载时的各种变形,比如:shiro...
ObjectInputStream#readObject
值得注意的地方:
- 由于JDK版本的不同readObject()方法代码可能会发生变化,这里使用的是 JDK 1.8.0_311
序列化类无重写readObject
test.java
package ReadObject;
import java.io.*;
public class test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Student student = new Student();
Serialize(student);
UnSerialize("test.ser");
}
public static void Serialize(Object obj) throws IOException {
FileOutputStream fileOutputStream = new FileOutputStream("test.ser");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
objectOutputStream.writeObject(obj);
}
public static void UnSerialize(String filename) throws IOException, ClassNotFoundException {
FileInputStream fileInputStream = new FileInputStream(filename);
ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
objectInputStream.readObject();
}
}
Student.java
package ReadObject;
import java.io.Serializable;
public class Student implements Serializable {
String name = "BUTLER";
int id = 0;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
}
在objectInputStream.readObject()代码处下断点,然后进入ObjectInputSteam#readObject()方法
private final Object readObject(Class<?> type)
throws IOException, ClassNotFoundException
{
if (enableOverride) { //这里不用管,继承ObjectInputStream的类并重写了readObjectOveriide方法才会进入
return readObjectOverride();
}
if (! (type == Object.class || type == String.class))
throw new AssertionError("internal error");
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(type, false);
handles.markDependency(outerHandle, passHandle);
ClassNotFoundException ex = handles.lookupException(passHandle);
if (ex != null) {
throw ex;
}
if (depth == 0) {
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
然后就是主要的方法调用栈,最后会到达Object的resolveClass(ObjectStreamClass desc)方法
-ObjectInputStream#readObject()
-ObjectInputStream#readObject(Class<?> type)
-ObjectInputStream#readObject0(Class<?> type, boolean unshared)
-ObjectInputStream#readOrdinaryObject(boolean unshared)
-ObjectInputStream#readClassDesc(boolean unshared)
-ObjectInputStream#readNonProxyDesc(boolean unshared)
-ObjectInputStream#resolveClass(ObjectStreamClass desc)
这里的ObjectInputStream#resolveClass(ObjectStreamClass desc)
方法特别关注一下,这里会使用java.lang.Class#forName(name, false, latestUserDefinedLoader())
来加载这个类最后返回一个类对象,然后返回ReadObject.Student的类对象
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}
在这里其实才完成了类加载中的加载阶段。P神说过Java反序列化是为了还原一个完整的类实例,所以下面还有进行还原完整的类实例的操作
-ObjectInputStream#readOrdinaryObject(boolean unshared)
-ObjectInputStream#readSerialData(Object obj, ObjectStreamClass desc)
ObjectInputStream#readOrdinaryObject(boolean unshared)
ObjectInputStream#readSerialData(Object obj, ObjectStreamClass desc)
private void readSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
if (slots[i].hasData) {
if (obj == null || handles.lookupException(passHandle) != null) {
defaultReadFields(null, slotDesc); // skip field values
} else if (slotDesc.hasReadObjectMethod()) { //这个判断比较重要
ThreadDeath t = null;
boolean reset = false;
SerialCallbackContext oldContext = curContext;
if (oldContext != null)
oldContext.check();
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bin.setBlockDataMode(true);
slotDesc.invokeReadObject(obj, this);
} catch (ClassNotFoundException ex) {
/*
* In most cases, the handle table has already
* propagated a CNFException to passHandle at this
* point; this mark call is included to address cases
* where the custom readObject method has cons'ed and
* thrown a new CNFException of its own.
*/
handles.markException(passHandle, ex);
} finally {
do {
try {
curContext.setUsed();
if (oldContext!= null)
oldContext.check();
curContext = oldContext;
reset = true;
} catch (ThreadDeath x) {
t = x; // defer until reset is true
}
} while (!reset);
if (t != null)
throw t;
}
/*
* defaultDataEnd may have been set indirectly by custom
* readObject() method when calling defaultReadObject() or
* readFields(); clear it to restore normal read behavior.
*/
defaultDataEnd = false;
} else {
defaultReadFields(obj, slotDesc);
}
if (slotDesc.hasWriteObjectData()) {
skipCustomData();
} else {
bin.setBlockDataMode(false);
}
} else {
if (obj != null &&
slotDesc.hasReadObjectNoDataMethod() &&
handles.lookupException(passHandle) == null)
{
slotDesc.invokeReadObjectNoData(obj);
}
}
}
}
这里注意if (slotDesc.hasReadObjectMethod())
判断比较重要,代码的意思简单来说就是序列化的类有没有重写readObject()方法,如果重写了进入if,反之进入else,这里序列化类没有重写readObject()方法会进入else部分即ObjectInputStream#defaultReadFields()
。在ObjectInputStream#defaultReadFields()
里面会有恢复属性的方法desc.setPrimFieldValues(obj, primVals)
和desc.setObjFieldValues(obj, objVals)
。具体里面的操作就不跟进了,以后有时间在详细分析
private void defaultReadFields(Object obj, ObjectStreamClass desc)
throws IOException
{
Class<?> cl = desc.forClass();
if (cl != null && obj != null && !cl.isInstance(obj)) {
throw new ClassCastException();
}
int primDataSize = desc.getPrimDataSize();
if (primVals == null || primVals.length < primDataSize) {
primVals = new byte[primDataSize];
}
bin.readFully(primVals, 0, primDataSize, false);
if (obj != null) {
desc.setPrimFieldValues(obj, primVals); //恢复属性1
}
int objHandle = passHandle;
ObjectStreamField[] fields = desc.getFields(false);
Object[] objVals = new Object[desc.getNumObjFields()];
int numPrimFields = fields.length - objVals.length;
for (int i = 0; i < objVals.length; i++) {
ObjectStreamField f = fields[numPrimFields + i];
objVals[i] = readObject0(Object.class, f.isUnshared());
if (f.getField() != null) {
handles.markDependency(objHandle, passHandle);
}
}
if (obj != null) {
desc.setObjFieldValues(obj, objVals); //恢复属性2
}
passHandle = objHandle;
}
该方法里面具体又会调用ObjectInputStream#defaultReadFields()
方法执行完成以后可以看到已经有"真正属性值的实例"了
之后就是返回调用栈就OK了
序列化类有重写readObject
这部分相比于上部分Student类重写了readObject()方法
package ReadObject;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
public class Student implements Serializable {
String name = "BUTLER";
int id = 0;
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", id=" + id +
'}';
}
private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
objectInputStream.defaultReadObject();
System.out.println("Student defaultReadObject");
}
}
前面类加载中的加载阶段就不多少了,直接看ObjectInputStream#readSerialData(Object obj, ObjectStreamClass desc)
部分的代码,上一节的if (slotDesc.hasReadObjectMethod())
判断为假没有进入if,这一部分判断为真会进入if语句中
private void readSerialData(Object obj, ObjectStreamClass desc)
throws IOException
{
ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
for (int i = 0; i < slots.length; i++) {
ObjectStreamClass slotDesc = slots[i].desc;
if (slots[i].hasData) {
if (obj == null || handles.lookupException(passHandle) != null) {
defaultReadFields(null, slotDesc); // skip field values
} else if (slotDesc.hasReadObjectMethod()) {
ThreadDeath t = null;
boolean reset = false;
SerialCallbackContext oldContext = curContext;
if (oldContext != null)
oldContext.check();
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bin.setBlockDataMode(true);
slotDesc.invokeReadObject(obj, this);
} catch (ClassNotFoundException ex) {
/*
* In most cases, the handle table has already
* propagated a CNFException to passHandle at this
* point; this mark call is included to address cases
* where the custom readObject method has cons'ed and
* thrown a new CNFException of its own.
*/
handles.markException(passHandle, ex);
} finally {
do {
try {
curContext.setUsed();
if (oldContext!= null)
oldContext.check();
curContext = oldContext;
reset = true;
} catch (ThreadDeath x) {
t = x; // defer until reset is true
}
} while (!reset);
if (t != null)
throw t;
}
/*
* defaultDataEnd may have been set indirectly by custom
* readObject() method when calling defaultReadObject() or
* readFields(); clear it to restore normal read behavior.
*/
defaultDataEnd = false;
} else {
defaultReadFields(obj, slotDesc);
}
if (slotDesc.hasWriteObjectData()) {
skipCustomData();
} else {
bin.setBlockDataMode(false);
}
} else {
if (obj != null &&
slotDesc.hasReadObjectNoDataMethod() &&
handles.lookupException(passHandle) == null)
{
slotDesc.invokeReadObjectNoData(obj);
}
}
}
}
然后进入ObjectInputStream#invokeReadObjcet()方法,该方法中将会使用readObjectMethod.invoke(obj, new Object[]{ in })
代码激活Student#readObject()方法,之后便会进入Student#readObject()方法
void invokeReadObject(Object obj, ObjectInputStream in)
throws ClassNotFoundException, IOException,
UnsupportedOperationException
{
requireInitialized();
if (readObjectMethod != null) {
try {
readObjectMethod.invoke(obj, new Object[]{ in }); //激活Student#readObject()方法
} catch (InvocationTargetException ex) {
Throwable th = ex.getTargetException();
if (th instanceof ClassNotFoundException) {
throw (ClassNotFoundException) th;
} else if (th instanceof IOException) {
throw (IOException) th;
} else {
throwMiscException(th);
}
} catch (IllegalAccessException ex) {
// should not occur, as access checks have been suppressed
throw new InternalError(ex);
}
} else {
throw new UnsupportedOperationException();
}
}
来到Student#Object方法,可以看到readObject方法里面调用了ObjectInputStream#defaultReadObject()
方法。这个方法是做什么的?其实是和ObjectInputStream#defaultReadFields()
方法是一样的都是还原一个完整的"真正属性值的实例。也就是P神说的在反序列化时将TC_OBJECT的classdata数组中对象非静态、非transient的属性全部读取出来。然后再实例化成一个对象。
private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
objectInputStream.defaultReadObject();
System.out.println("Student defaultReadObject");
}
序列化类重写readResolve
网上查了查一般重写readResolve方法单例模式有关。如果我们反序列化的类有重写readResolve方法,在ObjectInputStream#readObject反序列化的时候会调用到该类的readResolve 简单看看
-ObjectInputStream#readObject()
-ObjectInputStream#readObject(Class<?> type)
-ObjectInputStream#readObject0(Class<?> type, boolean unshared)
-ObjectInputStream#readOrdinaryObject(boolean unshared)
-ObjectInputStream#readClassDesc(boolean unshared)
-ObjectInputStream#readNonProxyDesc(boolean unshared)
-ObjectInputStream#resolveClass(ObjectStreamClass desc)
-ObjectInputStream#hasReadResolveMethod() //true
-ObjectInputStream#invokeReadResolve(Object obj)
ObjectInputStream#readOrdinaryObject(boolean unshared)
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
......
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
继承ObjectInputStream类
这一节根据几个例子来看,一个是shiro550漏洞,另外是一个0ctf的buggyloader题目,Searilkiller的类加载。我见过的继承ObjectInputStream
类的,它们都重写了resolveClass
方法,它们的区别都在于获取类对象的地方是URLloadClass.loadClass
还是Class.forName
....
以及还有继承ObjectInputStream
类的,它们都重写了resolveProxyClass
方法,这个在weblogic中遇到的多
反序列化的类是普通对象
ObjectInpuStream#resolveClass:根据类描述符返回相应的类
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException
{
String name = desc.getName();
try {
return Class.forName(name, false, latestUserDefinedLoader());
} catch (ClassNotFoundException ex) {
Class<?> cl = primClasses.get(name);
if (cl != null) {
return cl;
} else {
throw ex;
}
}
}
shiro550的resolveClass
这里前面的就不说了,直接看反序列部分
这里有一个shiro自己实现的反序列化流ClassResolvingObjectInputStream,该流继承了ObjectInputStream并且重写了resolveClass()方法。resolveClass方法里面会使用org.apache.shiro.util.ClassUtils#forName来完成加载阶段得到类对象。因为这里的改变shiro这里是不支持反序列化数组类型的(具体的原因我也不清楚,可以参考这篇文章https://blog.zsxsoft.com/post/35) 所以之前在shiro笔记中使用CC6漏洞利用失败,之后使用CB和CC6+CC2的结合才成功
package org.apache.shiro.io;
import org.apache.shiro.util.ClassUtils;
import org.apache.shiro.util.UnknownClassException;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
/**
* Enables correct ClassLoader lookup in various environments (e.g. JEE Servers, etc).
*
* @since 1.2
* @see <a href="https://issues.apache.org/jira/browse/SHIRO-334">SHIRO-334</a>
*/
public class ClassResolvingObjectInputStream extends ObjectInputStream {
public ClassResolvingObjectInputStream(InputStream inputStream) throws IOException {
super(inputStream);
}
/**
* Resolves an {@link ObjectStreamClass} by delegating to Shiro's
* {@link ClassUtils#forName(String)} utility method, which is known to work in all ClassLoader environments.
*
* @param osc the ObjectStreamClass to resolve the class name.
* @return the discovered class
* @throws IOException never - declaration retained for subclass consistency
* @throws ClassNotFoundException if the class could not be found in any known ClassLoader
*/
@Override
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
try {
return ClassUtils.forName(osc.getName());
} catch (UnknownClassException e) {
throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);
}
}
}
buggyloader的resolveClass
URLClassLoader#loadClass
获取类对象
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.net.URL;
import java.net.URLClassLoader;
import org.apache.commons.collections.Transformer;
public class MyObjectInputStream extends ObjectInputStream {
private ClassLoader classLoader;
public MyObjectInputStream(InputStream inputStream) throws Exception {
super(inputStream);
URL[] urls = ((URLClassLoader)Transformer.class.getClassLoader()).getURLs();
this.classLoader = new URLClassLoader(urls);
}
protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
Class<?> clazz = this.classLoader.loadClass(desc.getName());
return clazz;
}
}
Searialkiller的resolveClass
Class#forName
获取类对象
反序列化是动态代理对象
ObjectInputStream#resolveProxyClass 根据代理类描述符中所有接口的代理类
-ObjectInputStream#readObject()
-ObjectInputStream#readObject(Class<?> type)
-ObjectInputStream#readObject0(Class<?> type, boolean unshared)
-ObjectInputStream#readOrdinaryObject(boolean unshared)
-ObjectInputStream#readClassDesc(boolean unshared)
-ObjectInputStream#readProxyDesc(boolean unshared)
-ObjectInputStream#resolveProxyClass(String[] interfaces)
参数:interfaces -- 接口名称被反序列化的代理类描述符列表
返回值:此方法返回一个代理类指定的接口
protected Class<?> resolveProxyClass(String[] interfaces)
throws IOException, ClassNotFoundException
{
ClassLoader latestLoader = latestUserDefinedLoader();
ClassLoader nonPublicLoader = null;
boolean hasNonPublicInterface = false;
// define proxy in class loader of non-public interface(s), if any
Class<?>[] classObjs = new Class<?>[interfaces.length];
for (int i = 0; i < interfaces.length; i++) {
Class<?> cl = Class.forName(interfaces[i], false, latestLoader);
if ((cl.getModifiers() & Modifier.PUBLIC) == 0) {
if (hasNonPublicInterface) {
if (nonPublicLoader != cl.getClassLoader()) {
throw new IllegalAccessError(
"conflicting non-public interface class loaders");
}
} else {
nonPublicLoader = cl.getClassLoader();
hasNonPublicInterface = true;
}
}
classObjs[i] = cl;
}
try {
return Proxy.getProxyClass(
hasNonPublicInterface ? nonPublicLoader : latestLoader,
classObjs);
} catch (IllegalArgumentException e) {
throw new ClassNotFoundException(null, e);
}
}
比如说我的代理类是这样写的
public static void main(String[] args) throws Exception {
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint("127.0.0.1",1099);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(test.class.getClassLoader(), new Class[] {Registry.class}, obj);
serialize(proxy);
unserialize();
}
当执行到ObjectInputStream#resolveProxyClass
方法的时候 参数和返回值是:
返回值:
ServerChannelInputStream的resolveProxyClass
不同的版本补丁此处有不同的变化,CVE-2017-3248的补丁:
protected Class<?> resolveProxyClass(String[] interfaces) throws IOException, ClassNotFoundException {
String[] arr$ = interfaces;
int len$ = interfaces.length;
for(int i$ = 0; i$ < len$; ++i$) {
String intf = arr$[i$];
if (intf.equals("java.rmi.registry.Registry")) {
throw new InvalidObjectException("Unauthorized proxy deserialization");
}
}
return super.resolveProxyClass(interfaces);
}
加载类对象
这篇文章不错推荐阅读https://www.anquanke.com/post/id/260902#h2-7
Class#forName
Class.forName
不能加载原生类型,但其他类型都是支持的。跟踪Class#forName最后发现调用Class#forName0获取类对象。native修饰的方法最后由C/C++来实现,根据资料显示这个底层也遵循双亲委派机制
private static native Class<?> forName0(String name, boolean initialize,
ClassLoader loader,
Class<?> caller)
throws ClassNotFoundException;
ClassLoader#loadClass
Classloader.loadclass
不能加载原生类型和数组类型,其他类型都是支持的。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) { //判断是否已经加载类
long t0 = System.nanoTime();
try {
if (parent != null) { //如果有父类,尝试让父类加载
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
//如果没有父类,尝试使用根装载器加载
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); //应用类加载器可以重写该类实现寻找类对象的逻辑
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
这里的resolveClass
注意不要和ObjectInputStream#resolveClass
混淆,它们是不同的概念
ClassLoader#findClass
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
URLClassLoader#loadClass
URLCLassLoader继承SecureClassLoader,SecureClassLoader继承ClassLoader。
- 这里的loadClass里进行了判断然后使用
ClassLoader#loadClass
获取类对象
public final Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First check if we have permission to access the package. This
// should go away once we've added support for exported packages.
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
int i = name.lastIndexOf('.');
if (i != -1) {
sm.checkPackageAccess(name.substring(0, i));
}
}
return super.loadClass(name, resolve);
}
并且URLClassLoader
还重写了findClass
方法
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
破坏双亲为派机制
如果我们继承ClassLoasder并重写loadClass会破坏双亲委派机制
在Java中常见的几种破坏双亲委派场景
- Tomcat类加载机制
- OSGI模块化类加载
- JDBC类加载机制