Java反序列化基础
Java反序列化基础
1.扫盲
-
常用单词
source:入口,gadget chain:调用链,sink:目标方法,RMI:remote method invocation远程方法调用
-
反序列化漏洞的理解
反序列化漏洞的挖掘,本质上就是已知source和sink,如何走通整个流程的问题
这里source包括:
- Java原生的反序列化,,即通过objectInputStream.readObject(),处理二进制格式的内容得到java对象
- 专有格式的反序列化,列如通过Fastjson、Xstream等第三方库,处理json、xml等格式内容,得到java对象
而要执行的sink包括:
- Runtime.exec()直接在目标环境中执行命令
- Method.Invoke()这种需要选择合适的方法和参数,通过反射执行方法
- RMI/JNDI/JRMP等,通过引用远程对象,间接命令执行
2.ObjectOutputStream序列化分析
示例(来自https://y4er.com/post/java-deserialization-1/)
import java.io.*;
interface Animal {
public void eat();
}
class Ani implements Serializable {
public String name;
private void writeObject(java.io.ObjectOutputStream out) throws IOException, ClassNotFoundException{
out.defaultWriteObject();
System.out.println("重新writeObject");
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("calc");
}
}
class Cat extends Ani implements Animal {
@Override
public void eat() {
System.out.println("cat eat.");
}
}
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Ani cat = new Cat();
cat.name = "tom";
FileOutputStream fos = new FileOutputStream("obj");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(cat);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("obj");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
Cat objectFromDisk = (Cat) ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}
ObjectOutputStream的writeObject调用栈:
writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->defaultWriteFields或者invokeWriteObject
PS:如果对象重写了writeObject则会通过invokeWriteObject反射调用重写的writeObject,反之直接调用defaultWriteFields进行序列化
writeObject:
public final void writeObject(Object obj) throws IOException {
if (enableOverride) {
writeObjectOverride(obj);
return;
}
try {
writeObject0(obj, false);
} catch (IOException ex) {
if (depth == 0) {
writeFatalException(ex);
}
throw ex;
}
}
如果ObjectOutputStream是有参构造器的话enableOverride = false;
writeObject0关键代码
代码1
// check for replacement object
Object orig = obj;
Class<?> cl = obj.getClass();
ObjectStreamClass desc;
for (;;) {
// REMIND: skip this check for strings/arrays?
Class<?> repCl;
desc = ObjectStreamClass.lookup(cl, true);
if (!desc.hasWriteReplaceMethod() ||
(obj = desc.invokeWriteReplace(obj)) == null ||
(repCl = obj.getClass()) == cl)
{
break;
}
cl = repCl;
}
if (enableReplace) {
Object rep = replaceObject(obj);
if (rep != obj && rep != null) {
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}
obj = rep;
}
desc存放的是当前类的描述信息
代码2
if (obj instanceof String) {
writeString((String) obj, unshared);
} else if (cl.isArray()) {
writeArray(obj, desc, unshared);
} else if (obj instanceof Enum) {
writeEnum((Enum<?>) obj, desc, unshared);
} else if (obj instanceof Serializable) {
writeOrdinaryObject(obj, desc, unshared);
} else {
if (extendedDebugInfo) {
throw new NotSerializableException(
cl.getName() + "\n" + debugInfoStack.toString());
} else {
throw new NotSerializableException(cl.getName());
}
}
} finally {
depth--;
bout.setBlockDataMode(oldMode);
}
如果对象是Serializable则进入writeOrdinaryObject
writeOrdinaryObject
private void writeOrdinaryObject(Object obj,
ObjectStreamClass desc,
boolean unshared)
throws IOException
{
if (extendedDebugInfo) {
debugInfoStack.push(
(depth == 1 ? "root " : "") + "object (class \"" +
obj.getClass().getName() + "\", " + obj.toString() + ")");
}
try {
desc.checkSerialize();
bout.writeByte(TC_OBJECT);
writeClassDesc(desc, false);
handles.assign(unshared ? null : obj);
if (desc.isExternalizable() && !desc.isProxy()) {
writeExternalData((Externalizable) obj);
} else {
writeSerialData(obj, desc);
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
writeSerialData
private void writeSerialData(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 (slotDesc.hasWriteObjectMethod()) {
PutFieldImpl oldPut = curPut;
curPut = null;
SerialCallbackContext oldContext = curContext;
if (extendedDebugInfo) {
debugInfoStack.push(
"custom writeObject data (class \"" +
slotDesc.getName() + "\")");
}
try {
curContext = new SerialCallbackContext(obj, slotDesc);
bout.setBlockDataMode(true);
slotDesc.invokeWriteObject(obj, this);
bout.setBlockDataMode(false);
bout.writeByte(TC_ENDBLOCKDATA);
} finally {
curContext.setUsed();
curContext = oldContext;
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
curPut = oldPut;
} else {
defaultWriteFields(obj, slotDesc);
}
}
}
这里面slotDesc.hasWriteObjectMethod()判断了是否重写writeObject,重写之后通过反射调用重写的writeObject,否则直接进入defaultWriteFields
示例运行结果
成功调用了我们重写的writeObject
3.ObjectInputStream反序列化分析
示例(来自https://y4er.com/post/java-deserialization-1/)
import java.io.*;
interface Animal {
public void eat();
}
class Ani implements Serializable {
public String name;
private void writeObject(java.io.ObjectOutputStream out) throws IOException, ClassNotFoundException{
out.defaultWriteObject();
System.out.println("重新writeObject");
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("calc");
}
}
class Cat extends Ani implements Animal {
@Override
public void eat() {
System.out.println("cat eat.");
}
}
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Ani cat = new Cat();
cat.name = "tom";
FileOutputStream fos = new FileOutputStream("obj");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(cat);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("obj");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
Cat objectFromDisk = (Cat) ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}
反序列化调用栈
已示例为例
readObject--->readObject0--->readOrdinaryObject--->readSerialData--->invokeReadObject
readObject代码
public final Object readObject()
throws IOException, ClassNotFoundException
{
if (enableOverride) {
return readObjectOverride();
}
// if nested read, passHandle contains handle of enclosing object
int outerHandle = passHandle;
try {
Object obj = readObject0(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();
}
}
}
readObject0代码
private Object readObject0(boolean unshared) throws IOException {
boolean oldMode = bin.getBlockDataMode();
if (oldMode) {
int remain = bin.currentBlockRemaining();
if (remain > 0) {
throw new OptionalDataException(remain);
} else if (defaultDataEnd) {
/*
* Fix for 4360508: stream is currently at the end of a field
* value block written via default serialization; since there
* is no terminating TC_ENDBLOCKDATA tag, simulate
* end-of-custom-data behavior explicitly.
*/
throw new OptionalDataException(true);
}
bin.setBlockDataMode(false);
}
byte tc;
while ((tc = bin.peekByte()) == TC_RESET) {
bin.readByte();
handleReset();
}
depth++;
totalObjectRefs++;
try {
switch (tc) {
case TC_NULL:
return readNull();
case TC_REFERENCE:
return readHandle(unshared);
case TC_CLASS:
return readClass(unshared);
case TC_CLASSDESC:
case TC_PROXYCLASSDESC:
return readClassDesc(unshared);
case TC_STRING:
case TC_LONGSTRING:
return checkResolve(readString(unshared));
case TC_ARRAY:
return checkResolve(readArray(unshared));
case TC_ENUM:
return checkResolve(readEnum(unshared));
case TC_OBJECT:
return checkResolve(readOrdinaryObject(unshared));
case TC_EXCEPTION:
IOException ex = readFatalException();
throw new WriteAbortedException("writing aborted", ex);
case TC_BLOCKDATA:
case TC_BLOCKDATALONG:
if (oldMode) {
bin.setBlockDataMode(true);
bin.peek(); // force header read
throw new OptionalDataException(
bin.currentBlockRemaining());
} else {
throw new StreamCorruptedException(
"unexpected block data");
}
case TC_ENDBLOCKDATA:
if (oldMode) {
throw new OptionalDataException(true);
} else {
throw new StreamCorruptedException(
"unexpected end of block data");
}
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--;
bin.setBlockDataMode(oldMode);
}
}
readObject0中根据对象不同类型调用不同的方法,这里调用的是readOrdinaryObject
readOrdinaryObject关键代码
Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}
handles.finish(passHandle);
从desc中获取对象实例后调用readSerialData
readSerialData中判断了是否重写readObject方法,如果重写则使用反射调用重写的readObject,反之使用默认的方法进行反序列化
readSerialData
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);
}
}
}
}
之前的疑问
- 为什么示例中的命令执行不能直接拿来利用?
通过对Java反序列化的JDK代码分析可知,反序列化首先会获取利用对象的描述(desc),通过反射的方式执行readObject。这里反射执行的是当前环境的那个利用类和readObject,不是直接执行序列化对象的readObject,而开发者的代码中不会这样去写readObject,所以在真实攻击中无法利用成功。